《使用Qt Quick從零構建AI螺絲瑕疵檢測系統》——8. AI賦能(下):在Qt中部署YOLOv8模型

目錄

  • 一、概述
    • 1.1 背景介紹:從“訓練”到“部署”
    • 1.2 學習目標
  • 二、在C++中集成ONNX模型
    • 2.1 準備模型文件
    • 2.2 修改`Backend`以加載和運行模型
  • 三、關鍵一步:輸出結果的后處理
  • 四、運行與驗證
  • 五、總結與展望

一、概述

1.1 背景介紹:從“訓練”到“部署”

在上一篇文章中,我們成功地跨入了Python的世界,完整地經歷了一次AI模型從數據標注到訓練、再到導出的全過程。我們最終的產出是一個名為best.onnx的模型文件——這是AI算法工程師工作的結晶。

然而,這個模型本身還只是一個靜態的權重文件,無法獨立工作。本篇文章的核心任務,就是完成從“算法研發”到“軟件部署”這至關重要的一躍。我們將回歸C++的主戰場,學習如何使用OpenCV強大的DNN(深度神經網絡)模塊,在我們的Qt應用程序中加載并運行這個ONNX模型。這個過程,我們稱之為模型推理(Inference)

1.2 學習目標

通過本篇的學習,讀者將能夠:

  1. 在C++中加載ONNX模型,并對輸入的圖像進行預處理,使其符合模型的輸入要求。
  2. 執行模型的前向傳播(推理),獲取模型的原始輸出。
  3. 掌握關鍵的后處理技術:解析YOLOv8復雜的輸出張量,提取出邊界框、置信度和類別信息。
  4. 將識別出的瑕疵信息,可視化地繪制在QML界面顯示的圖像上,讓AI的結果“看得見”。

二、在C++中集成ONNX模型

2.1 準備模型文件

首先,將上一章生成的best.onnx模型文件,從runs/detect/train/weights目錄中,拷貝到項目根目錄下,方便程序訪問。

2.2 修改Backend以加載和運行模型

我們將為Backend類增加一個YOLOv8的封裝,用于處理所有與模型推理相關的邏輯。

1. 編寫代碼 (backend.h)

// backend.h
#ifndef BACKEND_H
#define BACKEND_H#include <QObject>
#include <QImage>
#include <opencv2/dnn.hpp> // 1. 包含OpenCV DNN模塊頭文件class ImageProvider;class Backend : public QObject
{Q_OBJECT
public:explicit Backend(ImageProvider *provider, QObject *parent = nullptr);Q_INVOKABLE void startScan();signals:void imageReady(const QString &imageId);void statusMessageChanged(const QString &message);private:// 2. 添加一個私有方法用于AI推理cv::Mat runInference(const cv::Mat &inputImage);ImageProvider *m_imageProvider;cv::dnn::Net m_net; // 3. 添加一個Net對象,用于表示我們的神經網絡std::vector<std::string> m_classNames; // 4. 用于存儲類別名稱
};#endif // BACKEND_H

2. 編寫代碼 (backend.cpp)
這是本章的核心。我們將修改構造函數以加載模型,并實現runInference方法。

// backend.cpp
#include "backend.h"
#include "imageprovider.h"
#include <QDebug>
#include <QDir>
#include <opencv2/imgcodecs.hpp>// ... (matToQImage輔助函數保持不變)Backend::Backend(ImageProvider *provider, QObject *parent): QObject(parent), m_imageProvider(provider)
{// --- 1. 加載ONNX模型 ---QString modelPath = QDir::currentPath() + "/../../best.onnx";try {m_net = cv::dnn::readNetFromONNX(modelPath.toStdString());if (m_net.empty()) {qWarning() << "Failed to load ONNX model!";} else {qDebug() << "ONNX model loaded successfully.";// 設置計算后端。CPU是默認選項,但可以顯式指定m_net.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV);m_net.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);}} catch (const cv::Exception& e) {qWarning() << "Error loading model:" << e.what();}// --- 2. 加載類別名稱 ---// 這個順序必須與訓練時的.yaml文件嚴格一致!m_classNames = {"neck_defect", "thread_defect", "head_defect"};
}cv::Mat Backend::runInference(const cv::Mat &inputImage)
{if (m_net.empty()) {qDebug() << "Network not loaded.";return inputImage;}// --- 3. 圖像預處理 ---// YOLOv8需要一個640x640的方形輸入const int inputWidth = 640;const int inputHeight = 640;cv::Mat blob;// 將圖像轉換為blob格式:調整尺寸、歸一化(像素值/255)、通道重排(BGR->RGB)cv::dnn::blobFromImage(inputImage, blob, 1./255., cv::Size(inputWidth, inputHeight), cv::Scalar(), true, false);// --- 4. 執行推理 ---m_net.setInput(blob);std::vector<cv::Mat> outputs;m_net.forward(outputs, m_net.getUnconnectedOutLayersNames());// outputs[0]是模型的原始輸出,我們需要對其進行后處理// ... 后處理代碼將在下一節添加 ...// 暫時返回原始圖像return inputImage;
}void Backend::startScan()
{// ... (加載圖像的代碼保持不變)QString imagePath = QDir::currentPath() + "/../../dataset/screw/test/scratch_head/000.png";cv::Mat sourceMat = cv::imread(imagePath.toStdString());if (sourceMat.empty()) { /* ... */ return; }// --- 5. 調用推理函數 ---cv::Mat resultMat = runInference(sourceMat);QImage imageQ = matToQImage(resultMat);if (imageQ.isNull()){ /* ... */ return; }m_imageProvider->updateImage(imageQ);emit imageReady("screw_processed");emit statusMessageChanged("AI推理完成!");
}

關鍵代碼分析:
(1) cv::dnn::readNetFromONNX(...): OpenCV DNN模塊中用于從ONNX文件加載模型的函數。加載成功后會返回一個cv::dnn::Net對象。
(2) m_classNames: 我們手動定義了一個std::vector<std::string>來存儲類別名稱。注意:這里的順序必須與訓練時dataset.yaml文件中的names順序嚴格一致,因為模型輸出的類別ID是基于這個順序的。
(3) cv::dnn::blobFromImage(...): 這是一個強大的預處理函數,它能一步到位地完成YOLOv8所需的幾項操作:
- 1./255.:歸一化因子,將像素值從0-255范圍縮放到0-1范圍。
- cv::Size(640, 640):將圖像縮放或填充到640x640的尺寸。
- cv::Scalar(): 減去均值,此處不減。
- true: 交換R和B通道(BGR -> RGB),因為YOLOv8是在RGB圖像上訓練的。
- false: 不裁剪。
(4) m_net.forward(...): 執行網絡的前向傳播,即推理。推理結果會存放在outputs這個std::vector<cv::Mat>中。

三、關鍵一步:輸出結果的后處理

YOLOv8的原始輸出是一個cv::Mat,其維度通常是1 x (4 + num_classes) x 8400。我們需要編寫代碼來解析這個復雜的張量,提取出我們真正需要的信息。

【核心概念:解析YOLOv8輸出】
對于輸出矩陣的每一列(共8400列,代表8400個可能的檢測框):

  • 前4行是邊界框的坐標(中心x, 中心y, 寬, 高)。
  • 后面的N行(N是類別數)是該框屬于每個類別的置信度分數。

我們的后處理流程是:

  1. 遍歷所有8400個可能的檢測框。
  2. 找到每個框置信度最高的類別。
  3. 如果這個最高置信度大于一個閾值(例如0.5),則認為這是一個有效的檢測。
  4. 將所有有效的檢測框及其信息收集起來。
  5. 由于同一個物體可能被多個框檢測到,最后使用**非極大值抑制(NMS)**來剔除重疊的多余框。

【例8-1】 實現后處理并可視化結果。

1. 編寫代碼 (backend.cpp)
我們將用完整的后處理邏輯來替換runInference函數中的注釋部分。

// backend.cppcv::Mat Backend::runInference(const cv::Mat &inputImage)
{if (m_net.empty()) { /* ... */ return inputImage; }cv::Mat blob;// --- 圖像預處理 (代碼同上) ---cv::dnn::blobFromImage(inputImage, blob, 1./255., cv::Size(640, 640), cv::Scalar(), true, false);m_net.setInput(blob);std::vector<cv::Mat> outputs;m_net.forward(outputs, m_net.getUnconnectedOutLayersNames());cv::Mat output_buffer = outputs[0]; // [1, num_classes + 4, 8400]output_buffer = output_buffer.reshape(1, {output_buffer.size[1], output_buffer.size[2]}); // [num_classes + 4, 8400]cv::transpose(output_buffer, output_buffer); // [8400, num_classes + 4]// --- 1. 后處理 ---float conf_threshold = 0.5f; // 置信度閾值float nms_threshold = 0.4f;  // NMS閾值std::vector<int> class_ids;std::vector<float> confidences;std::vector<cv::Rect> boxes;float x_factor = (float)inputImage.cols / 640.f;float y_factor = (float)inputImage.rows / 640.f;for (int i = 0; i < output_buffer.rows; i++) {cv::Mat row = output_buffer.row(i);cv::Mat scores = row.colRange(4, output_buffer.cols);double confidence;cv::Point class_id_point;cv::minMaxLoc(scores, nullptr, &confidence, nullptr, &class_id_point);if (confidence > conf_threshold) {confidences.push_back(confidence);class_ids.push_back(class_id_point.x);float cx = row.at<float>(0,0);float cy = row.at<float>(0,1);float w = row.at<float>(0,2);float h = row.at<float>(0,3);int left = (int)((cx - 0.5 * w) * x_factor);int top = (int)((cy - 0.5 * h) * y_factor);int width = (int)(w * x_factor);int height = (int)(h * y_factor);boxes.push_back(cv::Rect(left, top, width, height));}}// --- 2. 非極大值抑制 (NMS) ---std::vector<int> indices;cv::dnn::NMSBoxes(boxes, confidences, conf_threshold, nms_threshold, indices);// --- 3. 結果可視化 ---cv::Mat resultImage = inputImage.clone();for (int idx : indices) {cv::Rect box = boxes[idx];int class_id = class_ids[idx];// 繪制邊界框cv::rectangle(resultImage, box, cv::Scalar(0, 255, 0), 2);// 繪制標簽std::string label = cv::format("%s: %.2f", m_classNames[class_id].c_str(), confidences[idx]);cv::putText(resultImage, label, cv::Point(box.x, box.y - 10), cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(0, 255, 0), 2);}return resultImage;
}

關鍵代碼分析:
(1) 坐標還原: 模型的輸出是基于640x640輸入的歸一化坐標,我們必須乘以x_factory_factor將其還原到原始圖像的坐標系。
(2) cv::minMaxLoc(...): 一個方便的函數,用于在單行/列的Mat中快速找到最大值(置信度)及其位置(類別ID)。
(3) cv::dnn::NMSBoxes(...): OpenCV DNN模塊內置的非極大值抑制函數。它接收原始的框和置信度,返回一個indices向量,其中包含了最終保留下來的框的索引。
(4) cv::rectangle(...)cv::putText(...): OpenCV的繪圖函數,用于在最終結果圖上畫出邊界框和帶有類別、置信度的標簽文本。

四、運行與驗證

現在,一切準備就緒。重新編譯并運行ScrewDetector項目。

1. 運行結果
點擊“開始檢測”按鈕。稍等片刻,界面上將會顯示出帶有綠色邊界框和標簽的螺絲圖像。程序成功地識別出了圖片中的head_defect(頭部瑕疵)!同時,狀態欄也會更新為“AI推理完成!”。
在這里插入圖片描述

2. 嘗試其他圖片
可以嘗試修改Backend::startScan()中的imagePath,換成數據集中其他的圖片(例如good文件夾下的圖片),重新運行,觀察AI模型是否會誤檢。運行效果如下:
在這里插入圖片描述
可以看到,對于沒有瑕疵的圖片,AI模型不會檢測出瑕疵,驗證了模型的有效性。

五、總結與展望

在本篇文章中,我們成功地跨越了Python與C++之間的鴻溝,將上一章訓練的AI模型部署到了我們的Qt應用程序中。我們掌握了使用OpenCV DNN模塊加載ONNX模型對輸入圖像進行預處理以及最關鍵的YOLOv8輸出后處理技術。

至此,我們的應用程序已經擁有了真正的“AI大腦”,能夠對靜態圖片進行智能瑕疵檢測。然而,在真實的工業場景中,產品是連續不斷地在傳送帶上移動的。如何處理來自攝像頭的實時視頻流?如何保證處理速度?

這將是我們下一篇文章【《使用Qt Quick從零構建AI螺絲瑕疵檢測系統》——9. 接入真實硬件:驅動USB攝像頭】的核心主題。我們將讓程序從處理單張圖片,升級為處理實時動態視頻。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/91270.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/91270.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/91270.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【動態規劃 | 多狀態問題】動態規劃求解多狀態問題

算法相關知識點可以通過點擊以下鏈接進行學習一起加油&#xff01;斐波那契數列模型路徑問題多狀態問題通常涉及多個決策點和狀態轉換&#xff0c;解決起來復雜且計算量大。動態規劃作為一種強大的算法工具&#xff0c;能夠通過將問題分解為子問題并逐步求解&#xff0c;顯著提…

【HTTP】防XSS+SQL注入:自定義HttpMessageConverter過濾鏈深度解決方案

防XSSSQL注入&#xff1a;自定義HttpMessageConverter過濾鏈深度解決方案一、安全威脅模型分析二、自定義HttpMessageConverter架構設計2.1 技術棧組成三、完整實現代碼3.1 安全過濾工具類3.2 自定義HttpMessageConverter3.3 Spring安全配置四、深度防御增強方案4.1 SQL注入參數…

學習游戲制作記錄(凍結敵人時間與黑洞技能)7.30

1.實現劍擊中敵人時凍結敵人時間Enemy腳本&#xff1a;public float defaultMoveSpeed;//默認速度defaultMoveSpeed moveSpeed;//Awake&#xff08;&#xff09;中設置public virtual void FreezeTime(bool _timeFreeze)//凍結設置函數{if (_timeFreeze){moveSpeed 0;anim.sp…

【數據結構】真題 2016

待補充已知表頭元素為c的單鏈表在內存中的存儲狀態如下表所示地址元素鏈接地址1000Ha1010H1004Hb100CH1008Hc1000H100CHdNULL1010He1004H1014H現將f存放于1014H處并插入到單鏈表中&#xff0c;若f在邏輯上位于a和e之間&#xff0c;則a, e, f的“鏈接地址”依次是&#xff08; &…

雙線串行的 “跨界對話”:I2C 與 MDIO 的異同解析

在電子系統設計中,串行總線憑借其精簡的信號線數量和靈活的拓撲結構,成為芯片間通信的主流選擇。I2C(Inter-Integrated Circuit)和 MDIO(Management Data Input/Output)作為兩種典型的雙線串行總線,雖同屬低速信號范疇,卻在各自的應用領域扮演著不可替代的角色。本文將…

算法精講:二分查找(二)—— 變形技巧

&#x1f3af; 算法精講&#xff1a;二分查找&#xff08;二&#xff09;—— 變形技巧 &#x1f50d; 友情提示&#xff1a;&#xff1a;本小節含高能代碼片段 &#x1f964; 閱讀前請確保已掌握基礎二分原理與實現代碼片段可能包含不同程度的變形&#xff0c;請根據實際情況選…

兩個程序配合實現了基于共享內存和信號量的進程間通信,具體說明如下:

第一個程序&#xff1a;共享內存讀取程序&#xff08;消費者&#xff09;該程序作為消費者&#xff0c;從共享內存中讀取數據&#xff0c;通過信號量保證只有當生產者寫入數據后才能讀取。/*4 - 讀共享內存*/ #include<stdio.h> // 標準輸入輸出庫 #inc…

JeecgBoot(1):前后臺環境搭建

1 項目介紹 JeecgBoot 是一款基于 Java 的 AI 低代碼平臺&#xff0c;它采用了 SpringBoot、SpringCloud、Ant Design Vue3、Mybatis 等技術棧&#xff0c;并集成了代碼生成器、AI 對話助手、AI 建表、AI 寫文章等功能。JeecgBoot 的設計宗旨是實現簡單功能零代碼開發&#xf…

Nestjs框架: 關于 OOP / FP / FRP 編程

概述 在軟件開發過程中&#xff0c;不同的編程范式為我們提供了多樣化的思維方式與實現路徑它們不僅影響著代碼的結構和邏輯組織方式&#xff0c;也深刻影響著項目的可維護性、可擴展性以及團隊協作效率 什么是 OOP、FP 和 FRP&#xff1f;首先從三個術語的含義入手 1 &#xf…

elememtor 添加分頁功能

各位看官好&#xff0c;最近在忙著使用elementor搭建自己的網站&#xff0c;由于我不是專業的程序員和前端&#xff0c;又沒有很多錢去找外包公司實現自己的設計&#xff0c;所以選擇了elementor. 總的來說這是一個不錯的wordpress 插件&#xff0c;也讓我們這種非專業的網站設…

關于“PromptPilot” 之2 -目標系統:Prompt構造器

目標系統&#xff1a;Prompt構造器想法首先&#xff0c;在抽象層對PromptPilot進行封裝給出提示詞形成過程的全部環節。然后&#xff0c;在 形成一套確定的提示詞后再為 小規模試點方案生成一整套開發工具并配套集成開發環境和指南。最后&#xff0c;在小規模試點成功后進行拓展…

短劇小程序系統開發:重塑影視內容消費格局

在數字化浪潮的推動下&#xff0c;影視內容消費正經歷著深刻的變革。短劇小程序系統開發作為這一變革的重要力量&#xff0c;正在重塑影視內容消費的格局&#xff0c;為用戶帶來更加個性化、便捷化的觀影體驗。傳統影視內容消費往往受到時間和空間的限制&#xff0c;用戶需要前…

一文掌握最新版本Monocle3單細胞軌跡(擬時序)分析

許多大佬的軟件想要構建一個大而美的生態&#xff0c;從 monocle2 開始就能做單細胞的質控、降維、分群、注釋這一系列的分析&#xff0c;但不幸的是我們只知道 monocle 系列還是主要做擬時序分析&#xff0c;一方面是因為 Seurat 有先發優勢&#xff0c;出名要趁早&#xff0c…

spark入門-helloword

我們學習編程語言的時候&#xff0c;第一個程序就是打印一下 “hello world” &#xff0c;對于大數據領域的第一個任務則是wordcount。那我們就開始我們的第一個spark任務吧&#xff01; 下載spark 官方下載地址&#xff1a;Apache Download Mirrors 下載完畢以后&#xff0c…

雷達系統設計學習:自制6GHz FMCW Radar

國外大神自制6GHZ FMCW Radar開源項目: https://github.com/Ttl/fmcw3 引言 之前我做過一個簡單的調頻連續波&#xff08;FMCW&#xff09;雷達&#xff0c;能夠探測到100米范圍內人體大小的物體。雖然它確實能用&#xff0c;但由于預算有限&#xff0c;還有很大的改進空間。 …

系統選擇菜單(ubuntu grub)介紹

好的&#xff0c;我們來詳細解釋一下什么是Ubuntu的GRUB菜單。 簡單來說&#xff0c;GRUB菜單是您電腦啟動時看到的第一個交互界面&#xff0c;它就像一個“系統選擇”菜單&#xff0c;讓您決定接下來要啟動哪個操作系統或進入哪種模式。詳細解釋 1. GRUB是什么&#xff1f; GR…

方案C,version2

實現一個簡單的Helloworld網頁,并通過GitHub Actions自動構建并推送到公開倉庫的gh-pages分支。同時,使用PAT進行認證,確保源碼在私有倉庫中,構建后的靜態文件在公開倉庫中。 重新設計deploy.yml內容如下(針對純靜態文件,無需構建過程): 步驟: 檢出私有倉庫源碼。 由于…

R 語言科研繪圖 --- 其他繪圖-匯總1

在發表科研論文的過程中&#xff0c;科研繪圖是必不可少的&#xff0c;一張好看的圖形會是文章很大的加分項。 為了便于使用&#xff0c;本系列文章介紹的所有繪圖都已收錄到了 sciRplot 項目中&#xff0c;獲取方式&#xff1a; R 語言科研繪圖模板 --- sciRplothttps://mp.…

webpack 原理及使用

【點贊收藏加關注,前端技術不迷路~】 一、webpack基礎 1.核心概念 1)entry:定義入口,webpack構建的第一步 module.exports ={entry:./src/xxx.js } 2)output:出口(輸出) 3)loader:模塊加載器,用戶將模塊的原內容按照需求加載成新內容 比如文本加載器raw-loade…

「日拱一碼」039 機器學習-訓練時間VS精確度

目錄 時間-精度權衡曲線&#xff08;不同模型復雜度&#xff09; 訓練與驗證損失對比 帕累托前沿分析&#xff08;3D&#xff09; 在機器學習實踐中&#xff0c;理解模型收斂所需時間及其與精度的關系至關重要。下面介紹如何分析模型收斂時間與精度之間的權衡&#xff0c;并…