企業級實戰:構建基于Qt、C++與YOLOv8的模塊化工業視覺檢測系統(基于QWidget)

目錄

    • 一、概述
    • 二、項目目標與技術架構
      • 2.1 核心目標
      • 2.2 技術選型
      • 2.3 軟件架構
    • 三、AI推理DLL的開發 (Visual Studio 2019)
      • 3.1 定義DLL接口 (`DetectorAPI.h`)
      • 3.2 實現核心功能 (`DetectorAPI.cpp`)
    • 四、Qt Widget GUI應用程序的開發
      • 4.1 項目配置 (`.pro` 文件)
      • 4.2 UI設計 (`mainwindow.ui`)
    • 4.3 交互邏輯實現
      • 4.3.1 自定義ROI繪制標簽 (`roilabel.h` & `roilabel.cpp`)
      • 4.3.2 在UI設計器中提升控件
      • 4.3.3 主窗口邏輯實現 (`mainwindow.h` & `mainwindow.cpp`)
    • 五、編譯與部署
    • 六、結語

一、概述

在追求高效與精密的現代制造業中,自動化光學檢測(AOI)已成為保障產品質量的核心技術。傳統的質檢流程往往受限于人工效率與主觀判斷,難以滿足大規模、高精度的生產需求。本文旨在研發一套完整的、企業級的工業視覺異常檢測解決方案,通過構建一個功能強大的桌面應用程序,實現對金屬沖壓件關鍵特征的自動化、高精度檢測。

該項目將采用模塊化的軟件工程思想,將核心的AI算法邏輯與前端用戶界面徹底分離。算法部分將封裝為一個獨立的C++動態鏈接庫(DLL),而用戶交互界面則使用Qt 5.15.2Widget框架進行開發。這種架構不僅厘清了職責,也極大地便利了團隊協作開發與后期的功能維護。

二、項目目標與技術架構

2.1 核心目標

開發一個桌面端AOI應用程序,該程序需具備以下核心功能:

  1. 圖像加載與顯示:支持用戶從本地加載待檢測的產品圖像。
  2. 交互式ROI定義:允許質檢員在圖像上通過鼠標拖拽,靈活地繪制一個或多個感興趣區域(ROI)。
  3. 一鍵式智能檢測:點擊按鈕后,程序調用后端AI算法,對每個ROI區域進行獨立的目標檢測與邏輯判斷。
  4. 可視化結果呈現:在原始圖像上,直觀地展示所有檢測到的目標(邊界框、類別、置信度),并高亮標記出判定為“異常”的ROI區域。

2.2 技術選型

  • UI框架Qt 5.15.2 Widgets。選用此版本因為它對Windows 7等傳統工業環境保持著良好的兼容性,且其成熟穩定的Widgets模塊非常適合開發傳統的桌面應用程序。
  • 開發環境Qt Creator 17.0.1,其集成的Copilot AI輔助編程功能可以顯著提升開發效率。
  • AI推理引擎OpenCV 4.12.0 DNN。利用其強大的DNN模塊,直接在CPU上對ONNX格式的YOLOv8模型進行高效推理。
  • 算法模型:基于Ultralytics框架訓練的YOLOv8模型,并已轉換為跨平臺兼容的ONNX格式。關于模型訓練與轉換的具體方法,可參考我的另一篇技術文章:https://blog.csdn.net/qianbin3200896/article/details/149663222。
  • 檢測類別:模型可識別四個類別:chongdian (沖壓點), baoxiansi (保險絲), dianpian (墊片), chaxiao (插銷)。

2.3 軟件架構

項目采用前后端分離的設計理念,具體分為兩個核心模塊:

  1. AI推理動態鏈接庫 (DLL)

    • 職責:封裝所有與計算機視覺和AI推理相關的復雜邏輯。這包括模型加載/釋放、圖像數據預處理、ONNX模型推理、結果后處理以及核心的業務邏輯判斷。
    • 開發工具:使用Visual Studio C++進行開發和編譯。
    • 接口設計:提供純C語言風格的函數接口,不暴露任何OpenCV或特定庫的數據類型。這種設計確保了接口的穩定與通用性,使得UI開發者無需關心底層算法實現細節。
  2. Qt GUI應用程序

    • 職責:負責所有用戶交互。包括窗口、按鈕、圖像顯示控件的創建,響應用戶加載圖像、繪制ROI的操作,調用DLL執行檢測,以及將返回的結果進行可視化展示。
    • 開發工具:使用Qt Creator進行開發。
    • DLL集成:采用動態鏈接的方式,在項目的.pro文件中直接配置DLL的頭文件(.h)和庫文件(.lib),實現對DLL函數的調用。

三、AI推理DLL的開發 (Visual Studio 2019)

首先,在Visual Studio 2019 中創建一個新的“動態鏈接庫(DLL)”項目,配置工程生成屬性為 (Release x64),同時配置好OpenCV 4.12.0的包含目錄、庫目錄和鏈接器輸入:

  1. C/C++ -> 常規 -> 附加包含目錄:
D:\toolplace\opencv\build\include
  1. 鏈接器 -> 常規 -> 附加庫目錄:
D:\toolplace\opencv\build\x64\vc16\lib
  1. 鏈接器 -> 輸入 -> 附加依賴項:
opencv_world4120.lib 

3.1 定義DLL接口 (DetectorAPI.h)

創建一個頭文件,用于聲明將從DLL中導出的函數和數據結構。采用extern "C"確保C風格的函數命名,避免C++的名稱修飾問題,增強兼容性。

#ifndef DETECTOR_API_H
#define DETECTOR_API_H#ifdef DETECTOR_EXPORTS
#define DETECTOR_API __declspec(dllexport)
#else
#define DETECTOR_API __declspec(dllimport)
#endif// 定義檢測對象的類別
enum ObjectType {CHONGDIAN = 0,BAOXIANSI = 1,DIANPIAN = 2,CHAXIAO = 3,UNKNOWN = 4
};// 定義傳入的ROI信息結構體
struct ROIInfo {int x;int y;int width;int height;
};// 定義返回的單個ROI的檢測結果
struct ROIResult {bool is_abnormal; // true表示異常,false表示正常
};extern "C" {/*** @brief 初始化檢測模型* @param model_path ONNX模型文件的絕對或相對路徑* @return 0表示成功,-1表示失敗*/DETECTOR_API int InitializeModel(const char* model_path);/*** @brief 釋放模型資源*/DETECTOR_API void ReleaseModel();/*** @brief 執行檢測* @param in_image_data 輸入的圖像數據 (BGR格式)* @param width 圖像寬度* @param height 圖像高度* @param rois ROI信息數組* @param roi_count ROI的數量* @param out_image_data 輸出的帶有繪制結果的圖像數據 (BGR格式,由DLL內部分配內存,調用方需使用ReleaseImageData釋放)* @param out_width 輸出圖像的寬度* @param out_height 輸出圖像的高度* @param results 每個ROI的檢測結果數組 (由調用方分配內存)* @return 0表示成功,-1表示失敗*/DETECTOR_API int PerformDetection(const unsigned char* in_image_data, int width, int height,const ROIInfo* rois, int roi_count,unsigned char** out_image_data, int* out_width, int* out_height,ROIResult* results);/*** @brief 釋放由PerformDetection函數分配的圖像數據內存* @param image_data 指向圖像數據的指針*/DETECTOR_API void ReleaseImageData(unsigned char* image_data);
}#endif // DETECTOR_API_H

3.2 實現核心功能 (DetectorAPI.cpp)

這是DLL的核心實現。它包含了模型加載、圖像處理、推理和邏輯判斷的全部代碼。

#include "pch.h" // VS項目預編譯頭
#include "DetectorAPI.h"
#include <opencv2/opencv.hpp>
#include <vector>
#include <string>// 全局變量,用于持有模型和類別名稱
static cv::dnn::Net net;
static std::vector<std::string> classNames;int InitializeModel(const char* model_path) {try {net = cv::dnn::readNetFromONNX(model_path);net.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV);net.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);// 初始化類別名稱classNames = { "chongdian", "baoxiansi", "dianpian", "chaxiao" };return 0; // 成功}catch (const cv::Exception& e) {// 在實際項目中,應使用更完善的日志系統記錄錯誤return -1; // 失敗}
}void ReleaseModel() {// 清理資源net.~Net();classNames.clear();
}void ReleaseImageData(unsigned char* image_data) {if (image_data) {delete[] image_data;}
}int PerformDetection(const unsigned char* in_image_data, int width, int height,const ROIInfo* rois, int roi_count,unsigned char** out_image_data, int* out_width, int* out_height,ROIResult* results
) {if (net.empty() || in_image_data == nullptr || rois == nullptr || roi_count == 0) {return -1;}// 1. 將輸入數據轉換為OpenCV的Mat格式cv::Mat source_image(height, width, CV_8UC3, (void*)in_image_data);cv::Mat result_image = source_image.clone(); // 復制一份用于繪制結果// 2. 遍歷每個ROI進行處理for (int i = 0; i < roi_count; ++i) {ROIInfo roi = rois[i];cv::Rect roi_rect(roi.x, roi.y, roi.width, roi.height);// 安全檢查,確保ROI在圖像范圍內roi_rect &= cv::Rect(0, 0, width, height);if (roi_rect.width <= 0 || roi_rect.height <= 0) {results[i] = { true }; // 無效ROI視為異常continue;}cv::Mat roi_image = source_image(roi_rect);// 3. 圖像預處理和模型推理cv::Mat blob;cv::dnn::blobFromImage(roi_image, blob, 1.0 / 255.0, cv::Size(640, 640), cv::Scalar(), true, false); //倒數第二個參數表明進行通道轉換  BGR轉RGBnet.setInput(blob);std::vector<cv::Mat> outs;net.forward(outs, net.getUnconnectedOutLayersNames());// 4. 后處理cv::Mat output_buffer = outs[0];output_buffer = output_buffer.reshape(1, { output_buffer.size[1], output_buffer.size[2] });cv::transpose(output_buffer, output_buffer);float conf_threshold = 0.5f;float nms_threshold = 0.4f;std::vector<int> class_ids;std::vector<float> confidences;std::vector<cv::Rect> boxes;float x_factor = (float)roi_image.cols / 640.f;float y_factor = (float)roi_image.rows / 640.f;for (int j = 0; j < output_buffer.rows; j++) {cv::Mat row = output_buffer.row(j);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));}}std::vector<int> indices;cv::dnn::NMSBoxes(boxes, confidences, conf_threshold, nms_threshold, indices);// 5. 業務邏輯判斷int counts[4] = { 0, 0, 0, 0 }; // chongdian, baoxiansi, dianpian, chaxiaobool object_found = !indices.empty();for (int idx : indices) {int class_id = class_ids[idx];if (class_id >= 0 && class_id < 4) {counts[class_id]++;}}bool is_abnormal = false;if (counts[CHONGDIAN] + counts[BAOXIANSI] + counts[DIANPIAN] + counts[CHAXIAO] == 0)is_abnormal = true;else{if(counts[CHONGDIAN]>0 && counts[CHONGDIAN]!=2)is_abnormal = true;}results[i] = { is_abnormal };// 6. 繪制檢測結果到大圖上cv::Scalar color = is_abnormal ? cv::Scalar(0, 0, 255) : cv::Scalar(0, 255, 0); // 異常紅色,正常綠色cv::rectangle(result_image, roi_rect, color, 2);for (int idx : indices) {cv::Rect box = boxes[idx];// 坐標轉換:從ROI內部坐標轉換到大圖坐標box.x += roi_rect.x;box.y += roi_rect.y;cv::rectangle(result_image, box, cv::Scalar(255, 178, 50), 2);std::string label = cv::format("%.2f", confidences[idx]);label = classNames[class_ids[idx]] + ":" + label;cv::putText(result_image, label, cv::Point(box.x, box.y - 10), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(255, 178, 50), 2);}}// 7. 準備輸出數據*out_width = result_image.cols;*out_height = result_image.rows;size_t data_size = result_image.total() * result_image.elemSize();*out_image_data = new unsigned char[data_size];memcpy(*out_image_data, result_image.data, data_size);return 0;
}

編譯此項目,會生成DetectorAPI.dllDetectorAPI.lib文件。


四、Qt Widget GUI應用程序的開發

現在,切換到Qt Creator,創建一個新的“Qt Widgets Application”項目。項目使用qmake編譯器,并且選擇visual studio 2019 Release 64 bit套件。

4.1 項目配置 (.pro 文件)

為了讓Qt項目能夠找到并使用之前創建的DLL,需要修改.pro文件,指定頭文件路徑、庫文件路徑和要鏈接的庫。

QT += core gui widgetsCONFIG += c++11TARGET = IndustrialDetectorGUI
TEMPLATE = appSOURCES += main.cpp\mainwindow.cppHEADERS  += mainwindow.hFORMS    += mainwindow.ui# 鏈接AI推理DLL,路徑需要根據實際位置進行修改
INCLUDEPATH += $$PWD/../SDK/ # 指向DetectorAPI.h所在的目錄
LIBS += -L$$PWD/../SDK/ -lDetectorAPI # 指向DetectorAPI.lib所在的目錄# 在文件最后添加編譯選項,防止報錯
QMAKE_PROJECT_DEPTH = 0

4.2 UI設計 (mainwindow.ui)

使用Qt Designer拖拽控件,設計一個簡單的界面:

  • 一個QLabel (imageLabel) 用于顯示圖像。
  • 一個QPushButton (loadButton) 用于加載圖像。
  • 一個QPushButton (detectButton) 用于執行檢測。
  • 一個QPushButton (clearButton) 用于清除已繪制的ROIs。

好的,遵照您的要求,我將根據我們最終確定的正確方案(子類化QLabel),為您完整地重寫整個4.3節。這個版本將包含所有必要的代碼,無任何省略,并整合了正確的架構說明。


4.3 交互邏輯實現

這是GUI應用程序的核心。為了解決在QLabel上正確、高效地繪制圖形(如ROI矩形框)的難題,我們采用最符合Qt框架設計思想的方案:創建QLabel的子類

這個自定義的Label將專門負責繪制圖像和其上層的ROI矩形框,而MainWindow則退居二線,只負責處理用戶輸入、管理ROI數據和調用AI算法。這種職責分離的架構使得代碼更清晰、更健壯。

4.3.1 自定義ROI繪制標簽 (roilabel.h & roilabel.cpp)

首先,我們需要在項目中創建一個新的C++類,命名為ROILabel,并使其繼承自QLabel

文件: roilabel.h

這個頭文件定義了ROILabel的接口。它重寫了paintEvent以實現自定義繪制,并提供了一個公共方法setRois,用于從MainWindow接收需要繪制的矩形數據。

#ifndef ROILABEL_H
#define ROILABEL_H#include <QLabel>
#include <QList>
#include <QRect>
#include <QPainter>class ROILabel : public QLabel
{Q_OBJECTpublic:explicit ROILabel(QWidget *parent = nullptr);/*** @brief 設置需要繪制的ROI矩形列表* @param rois 已確定的ROI列表 (已轉換為視圖坐標)* @param currentRoi 當前正在繪制的ROI (已轉換為視圖坐標)*/void setRois(const QList<QRect>& rois, const QRect& currentRoi);protected:// 重寫父類的 paintEvent 來繪制矩形void paintEvent(QPaintEvent *event) override;private:QList<QRect> m_roisToDraw;      // 存儲要繪制的已確定ROIQRect m_currentRoiToDraw; // 存儲要繪制的當前ROI
};#endif // ROILABEL_H

文件: roilabel.cpp

這是ROILabel的實現。setRois函數接收數據后,立即調用update()來觸發一次重繪請求。在paintEvent中,我們首先調用基類QLabel::paintEvent來確保背景圖像被正確繪制,然后在其上層繪制我們自己的矩形。

#include "roilabel.h"ROILabel::ROILabel(QWidget *parent) : QLabel(parent)
{// 構造函數可以保持為空
}void ROILabel::setRois(const QList<QRect>& rois, const QRect& currentRoi)
{m_roisToDraw = rois;m_currentRoiToDraw = currentRoi;// 請求Qt在下一個事件循環中重繪此控件,這將自動調用paintEventthis->update();
}void ROILabel::paintEvent(QPaintEvent *event)
{// 1. 必須首先調用基類的paintEvent,這會負責繪制QLabel本身的內容(如pixmap)QLabel::paintEvent(event);// 2. 在圖像之上,為這個控件自身創建一個QPainterQPainter painter(this);// 3. 繪制所有已經確定的ROI(藍色實線)painter.setPen(QPen(Qt::blue, 2));for (const QRect& roi : m_roisToDraw) {painter.drawRect(roi);}// 4. 如果當前正在繪制ROI,則實時顯示它(紅色虛線)if (!m_currentRoiToDraw.isNull()) {painter.setPen(QPen(Qt::red, 2, Qt::DashLine));painter.drawRect(m_currentRoiToDraw);}
}

4.3.2 在UI設計器中提升控件

這是將UI與我們新代碼關聯起來的關鍵一步。

  1. 打開mainwindow.ui文件。
  2. 在界面上右鍵單擊imageLabel控件。
  3. 從菜單中選擇 “Promote to…” (提升為…)。
  4. 在彈出的對話框中,將 “Promoted class name” 設置為 ROILabel,“Header file” 設置為 roilabel.h
  5. 點擊 “Add”,然后點擊 “Promote”。
  6. 保存UI文件。現在,ui->imageLabel在代碼中的類型將自動變為ROILabel*

4.3.3 主窗口邏輯實現 (mainwindow.h & mainwindow.cpp)

現在,我們更新MainWindow的代碼。它不再處理任何paintEvent,而是專注于管理數據和響應用戶操作,并在數據變化時通知ROILabel進行重繪。

頭文件 mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include <QMainWindow>
#include <QImage>
#include <QRect>
#include <QList>
#include <QMouseEvent>#include "DetectorAPI.h" // 包含檢測SDK的接口頭文件
#include "roilabel.h"    // 包含我們自定義Label的頭文件QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACEclass MainWindow : public QMainWindow
{Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();protected:// 重寫事件處理函數以實現ROI繪制void mousePressEvent(QMouseEvent *event) override;void mouseMoveEvent(QMouseEvent *event) override;void mouseReleaseEvent(QMouseEvent *event) override;void resizeEvent(QResizeEvent* event) override;// paintEvent 已被移除private slots:// 按鈕的槽函數void on_loadButton_clicked();void on_detectButton_clicked();void on_clearButton_clicked();private:// 將QImage轉換為DLL所需的BGR格式數據unsigned char* convertQImageToBGR(const QImage& image);// 更新圖像在Label中的顯示void updateImageDisplay();// 通知ROILabel更新其繪制內容void updateLabelRois();// 坐標映射函數QPoint mapPointToImage(const QPoint& viewPoint); // 將視圖(Label)坐標點映射到原始圖像坐標點QRect mapRectFromImage(const QRect& imageRect);  // 將原始圖像矩形映射到視圖(Label)矩形Ui::MainWindow *ui;QImage m_originalImage;   // 用于存儲原始的、未被修改的圖像QImage m_image;           // 存儲加載的原始圖像QPixmap m_pixmap;         // 存儲用于顯示的縮放后圖像// 注意:m_rois 和 m_currentRoi 存儲的都是【原始圖像】坐標系下的矩形QList<QRect> m_rois;QRect m_currentRoi;bool m_isDrawing;QPoint m_startPoint;      // 存儲鼠標按下時在【視圖】坐標系下的點// 用于坐標轉換的參數double m_scaleFactor;     // 圖像縮放比例QPoint m_pixmapOffset;    // 縮放后圖像在Label內的偏移量
};
#endif // MAINWINDOW_H

實現文件 mainwindow.cpp

新增了一個輔助函數updateLabelRois(),它的作用是將在MainWindow中以圖像坐標存儲的ROI,轉換為視圖坐標,然后傳遞給ROILabel去繪制。所有修改了ROI數據的操作(鼠標事件、清空按鈕)都會調用這個函數來確保界面同步刷新。

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QFileDialog>
#include <QMessageBox>
#include <QDebug>
#include <vector>MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow), m_isDrawing(false), m_scaleFactor(1.0)
{ui->setupUi(this);// 讓Label能夠響應鼠標事件,并讓坐標計算更精確ui->imageLabel->setMouseTracking(true);ui->imageLabel->setAlignment(Qt::AlignCenter); // 讓圖像居中顯示// 在程序啟動時初始化模型const char* model_path = "best.onnx"; // 假設模型文件在程序運行目錄下if (InitializeModel(model_path) != 0) {QMessageBox::critical(this, "Error", "Failed to initialize AI model. Make sure 'best.onnx' is in the correct path.");QApplication::quit();}
}MainWindow::~MainWindow()
{// 在程序退出前釋放模型ReleaseModel();delete ui;
}void MainWindow::on_loadButton_clicked()
{QString fileName = QFileDialog::getOpenFileName(this, "Open Image", "", "Image Files (*.png *.jpg *.bmp)");if (!fileName.isEmpty()) {// 加載圖像到兩個變量中if (m_originalImage.load(fileName)) {m_image = m_originalImage; // m_image也設為原始圖像m_rois.clear();updateImageDisplay();updateLabelRois();}}
}void MainWindow::on_clearButton_clicked()
{// 如果沒有加載過原始圖像,則不執行任何操作if (m_originalImage.isNull()) {return;}// 1. 將當前顯示圖像恢復為原始的干凈圖像m_image = m_originalImage;// 2. 清空數據模型中的所有ROIm_rois.clear();m_currentRoi = QRect();// 3. 更新圖像顯示,此時會使用干凈的m_imageupdateImageDisplay();// 4. 通知ROILabel清除其上層繪制的所有矩形updateLabelRois();
}void MainWindow::on_detectButton_clicked()
{if (m_image.isNull() || m_rois.isEmpty()) {QMessageBox::warning(this, "Warning", "Please load an image and draw at least one ROI first.");return;}// 1. 將QImage轉換為DLL期望的BGR格式unsigned char* bgr_data = convertQImageToBGR(m_image);if (!bgr_data) return;// 2. 將QList<QRect>轉換為ROIInfo數組std::vector<ROIInfo> roi_infos;for (const QRect& rect : m_rois) {roi_infos.push_back({rect.x(), rect.y(), rect.width(), rect.height()});}// 3. 準備接收結果的變量unsigned char* out_image_data = nullptr;int out_width = 0, out_height = 0;std::vector<ROIResult> results(roi_infos.size());// 4. 調用DLL執行檢測int status = PerformDetection(bgr_data, m_image.width(), m_image.height(),roi_infos.data(), roi_infos.size(),&out_image_data, &out_width, &out_height,results.data());delete[] bgr_data; // 釋放轉換時分配的內存// 5. 處理結果if (status == 0 && out_image_data != nullptr) {// 將返回的BGR數據轉換為QImage并更新QImage resultImage(out_image_data, out_width, out_height, QImage::Format_RGB888);m_image = resultImage.rgbSwapped(); // 更新底圖為結果圖m_rois.clear(); // 清除ROI,因為結果已繪制在圖上updateImageDisplay();updateLabelRois(); // 清除label上的ROI// 釋放DLL分配的內存ReleaseImageData(out_image_data);// 可選:顯示每個ROI的邏輯判斷結果QString result_summary = "Detection Results:\n";for (size_t i = 0; i < results.size(); ++i) {result_summary += QString("ROI %1: %2\n").arg(i + 1).arg(results[i].is_abnormal ? "Abnormal" : "Normal");}QMessageBox::information(this, "Detection Complete", result_summary);} else {QMessageBox::critical(this, "Error", "Detection failed.");}
}unsigned char* MainWindow::convertQImageToBGR(const QImage& image)
{if (image.isNull()) return nullptr;QImage convertedImage = image.convertToFormat(QImage::Format_RGB888);int width = convertedImage.width();int height = convertedImage.height();size_t data_size = width * height * 3;unsigned char* bgr_data = new unsigned char[data_size];for (int y = 0; y < height; ++y) {const uchar* line = convertedImage.scanLine(y);for (int x = 0; x < width; ++x) {bgr_data[(y * width + x) * 3 + 0] = line[x * 3 + 2]; // Bluebgr_data[(y * width + x) * 3 + 1] = line[x * 3 + 1]; // Greenbgr_data[(y * width + x) * 3 + 2] = line[x * 3 + 0]; // Red}}return bgr_data;
}void MainWindow::updateImageDisplay()
{if (m_image.isNull()) {ui->imageLabel->clear();return;}QPixmap pixmap = QPixmap::fromImage(m_image);m_pixmap = pixmap.scaled(ui->imageLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);double scaleX = (double)m_pixmap.width() / m_image.width();double scaleY = (double)m_pixmap.height() / m_image.height();m_scaleFactor = 1.0 / scaleX; // 更新為正確的比例因子m_pixmapOffset.setX((ui->imageLabel->width() - m_pixmap.width()) / 2);m_pixmapOffset.setY((ui->imageLabel->height() - m_pixmap.height()) / 2);ui->imageLabel->setPixmap(m_pixmap);
}void MainWindow::updateLabelRois()
{QList<QRect> view_rois;for(const QRect& img_roi : m_rois) {view_rois.append(mapRectFromImage(img_roi));}QRect view_current_roi;if(!m_currentRoi.isNull()) {view_current_roi = mapRectFromImage(m_currentRoi);}// 調用 ROILabel 的公共接口來傳遞轉換后的視圖坐標矩形ui->imageLabel->setRois(view_rois, view_current_roi);
}void MainWindow::resizeEvent(QResizeEvent* event)
{QMainWindow::resizeEvent(event);updateImageDisplay();updateLabelRois(); // 窗口變化時也要更新矩形位置
}QPoint MainWindow::mapPointToImage(const QPoint& viewPoint)
{QPoint parentPoint = viewPoint - m_pixmapOffset;return QPoint(parentPoint.x() * m_scaleFactor, parentPoint.y() * m_scaleFactor);
}QRect MainWindow::mapRectFromImage(const QRect& imageRect)
{QPoint topLeft = QPoint(imageRect.left() / m_scaleFactor, imageRect.top() / m_scaleFactor);QPoint bottomRight = QPoint(imageRect.right() / m_scaleFactor, imageRect.bottom() / m_scaleFactor);return QRect(topLeft, bottomRight).translated(m_pixmapOffset);
}void MainWindow::mousePressEvent(QMouseEvent *event)
{QPoint localPos = ui->imageLabel->mapFrom(this, event->pos());QRect pixmapRect(m_pixmapOffset, m_pixmap.size());if (pixmapRect.contains(localPos) && event->button() == Qt::LeftButton) {m_isDrawing = true;m_startPoint = localPos;m_currentRoi = QRect(mapPointToImage(localPos), QSize());updateLabelRois();}
}void MainWindow::mouseMoveEvent(QMouseEvent *event)
{if (m_isDrawing) {QPoint localPos = ui->imageLabel->mapFrom(this, event->pos());QPoint imageEndPoint = mapPointToImage(localPos);m_currentRoi.setBottomRight(imageEndPoint);updateLabelRois();}
}void MainWindow::mouseReleaseEvent(QMouseEvent *event)
{if (m_isDrawing && event->button() == Qt::LeftButton) {m_isDrawing = false;m_currentRoi = m_currentRoi.normalized();if (m_currentRoi.width() > 5 && m_currentRoi.height() > 5) {m_rois.append(m_currentRoi);}m_currentRoi = QRect(); // 清空當前正在繪制的ROIupdateLabelRois();}
}

五、編譯與部署

  1. 編譯DLL:在Visual Studio中,選擇Release配置,編譯DetectorAPI項目,生成DetectorAPI.dllDetectorAPI.lib
  2. 編譯GUI:在Qt Creator中,選擇Release配置,構建qtDemo項目。
  3. 部署:創建一個部署文件夾,并將以下文件放入:
    • qtDemo.exe (Qt程序)
    • DetectorAPI.dll (AI推理庫)
    • opencv_world4120.dll (OpenCV運行庫)
    • best.onnx (模型文件)
    • 使用Qt官方的windeployqt.exe工具,將所有Qt相關的依賴庫(platforms, imageformats等插件)自動復制到部署文件夾中。代碼如下所示:
windeployqt qtDemo.exe

最終部署文件夾的結構應如下:

Deployment/
├── qtDemo.exe
├── DetectorAPI.dll
├── opencv_world4120.dll
├── best.onnx
├── platforms/
│   └── qwindows.dll
├── ... (其他Qt依賴項)

六、結語

通過將AI推理邏輯封裝到獨立的C++ DLL中,并由Qt Widgets應用程序進行調用,成功構建了一個模塊化、易于維護和擴展的工業視覺檢測系統。該架構充分利用了Visual Studio在C++和OpenCV開發上的優勢,以及Qt在跨平臺GUI開發上的強大能力,為開發復雜的企業級桌面應用提供了一個清晰且高效的范例。

此項目框架不僅可以應對當前的檢測需求,也為未來的功能升級奠定了堅實的基礎,例如集成更復雜的算法、連接生產數據庫、生成詳細的質量報告等,都可以在不改動UI代碼的情況下,通過升級DLL來實現。

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

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

相關文章

SVN自動化部署工具 腳本

SVN自動化部署工具 功能概述 這是一個自動化部署SVN倉庫的bash腳本&#xff0c;主要功能包括&#xff1a; 自動安裝SVN服務&#xff08;如未安裝&#xff09; 創建SVN項目倉庫 配置多用戶權限 設置自動同步到網站目錄 提供初始檢出功能 下載地址 https://url07.ctfile…

Facebook主頁變現功能被封?跨境玩家該如何申訴和預防

不少跨境玩家在運營Facebook公共主頁時&#xff0c;最期待的就是通過變現工具獲得穩定收入。但現實中&#xff0c;經常會遇到一個扎心的問題&#xff1a;主頁好不容易做起來&#xff0c;卻突然收到提示——“你的變現功能已被停用”。這意味著收入中斷&#xff0c;甚至可能導致…

安裝es、kibana、logstash

下載 elk 下載地址 elasticsearch地址: https://www.elastic.co/cn/downloads/elasticsearch kibana地址: https://www.elastic.co/cn/downloads/kibana logstash地址: https://www.elastic.co/cn/downloads/logstash 解壓elk 創建es全家桶文件夾 cd /usr/local mkdir elk …

Django admin 后臺開發案例【字段/圖片】

這是一個簡單的django admin 管理后臺,這個應用案例主要是給運營人員進行填寫數據 主要功能包括: 上傳圖片功能【選擇上傳時可以預覽】【替換已有數據中的圖片時可以預覽新舊圖片】 每條數據都將會記錄操作歷史。記錄操作人是誰?修改內容是什么?并且定位責任到某一員。 …

【C++】const和static的用法

目錄&#x1f680;前言&#x1f4bb;const&#xff1a;“只讀”的守護者&#x1f4af;修飾普通變量&#x1f4af;修飾指針&#x1f4af;修飾函數&#x1f4af;修飾類成員&#x1f4af;修飾對象&#x1f31f;static&#xff1a;“靜態存儲”與“作用域控制”&#x1f4af;修飾全…

F019 vue+flask海外購商品推薦可視化分析系統一帶一路【三種推薦算法】

文章結尾部分有CSDN官方提供的學長 聯系方式名片 B站up&#xff1a; 麥麥大數據 關注B站&#xff0c;有好處&#xff01; 編號: F019 關鍵詞&#xff1a;海外購 推薦系統 一帶一路 python 視頻 VueFlask 海外購電商大數據推薦系統源碼 &#xff08;三種推薦算法 全新界面布局…

【大數據專欄】流式處理框架-Apache Fink

Apache Fink 1 前言 1.1 功能 1.2 用戶 國際 國內 1.3 特點 ◆ 結合Java、Scala兩種語言 ◆ 從基礎到實戰 ◆ 系統學習Flink的核心知識 ◆ 快速完成從入門到上手企業開發的能力提升 1.4 安排 ◆ 初識Flink ◆ 編程模型及核心概念 ◆ DataSet API編程 ◆ Data…

向內核社區提交補丁

一、背景 內核的版本一直以來一直在持續迭代&#xff0c;離不開眾多開發者的貢獻。有時候我們會根據項目要求基于現有的內核版本開發一些新的功能或者修復掉一些特定場下的問題&#xff0c;我們是可以將其提交給社區的。 一般提交社區有兩個基本原則&#xff0c;一是提交的補…

TENGJUN-USB TYPE-C 24PIN測插雙貼連接器(H14.3,4腳插板帶柱):USB4.0高速傳輸時代的精密連接方案解析

在高速數據傳輸與多設備互聯需求日益增長的當下&#xff0c;USB TYPE-C接口憑借其可逆插拔、高兼容性的優勢成為主流&#xff0c;而TENGJUN推出的USB TYPE-C 24PIN測插雙貼連接器&#xff08;規格&#xff1a;H14.3&#xff0c;4腳插板帶柱&#xff09; &#xff0c;以對USB4.0…

企業級 Docker 應用:部署、倉庫與安全加固

1 Docker簡介及部署方法 1.1 Docker簡介 Docker之父Solomon Hykes&#xff1a;Docker就好比傳統的貨運集裝箱 Note 2008 年LXC(LinuX Contiainer)發布&#xff0c;但是沒有行業標準&#xff0c;兼容性非常差 docker2013年首次發布&#xff0c;由Docker, Inc開發1.1.1 什么是do…

rust語言 (1.88) 學習筆記:客戶端和服務器端同在一個項目中

同一項目下多個可執行文件&#xff0c;多個子項目參照以下&#xff1a; 一、項目目錄 項目/|-- client/|-- main.rs|-- Cargo.toml|-- server/|-- main.rs|-- Cargo.toml|-- Cargo.toml二、項目公共 Cargo.toml [workspace] # 定義Rust工作區配置 members …

mac本地安裝mysql

本人環境 macOs 14.5 1.下載安裝mysql https://dev.mysql.com/downloads/mysql/ 配置環境變量&#xff0c;打開terminal vim ~/.bash_profile 添加MYSQL_HOME/usr/local/mysql 在PATH中添加 通過mysql --version命令查看版本 2.開啟mysql 打開終端teminal,輸入命令 sudo…

面試前端遇到的問題

面試官讓我寫一個delay函數然后這是我寫的代碼async function delay(){setTimeout(function() {}, 3000); }面試官就和我說不是這個&#xff0c;用promise當時就蒙了&#xff0c;什么東西&#xff0c;為什么要用promise然后問豆包說Promise 是 JavaScript 中用于處理異步操作的…

Ubuntu Desktop 22.04.5 LTS 使用默認的 VNC 遠程桌面

1. 打開 VNC 打開設置 - 分享 - 遠程桌面2. 配置 VNC 打開遠程桌面 啟用vnc 選擇vnc密碼訪問 配置密碼3. 固定密碼 遠程桌面的訪問密碼在每次開機后會刷新一次&#xff0c;可以通過以下方式固定 打開【應用程序】&#xff0d;【附件】&#xff0d;密碼和加密密鑰&#xff08;或…

【無線安全實驗4】基于偽隨機數的WPS PIN碼逆向(精靈塵埃/仙塵攻擊)

文章目錄1 原理分析1.1 WPS連接過程1.1.1 初始階段1.1.2 注冊階段1.2 WPS攻擊原理1.2.1 在線攻擊1.2.2 離線攻擊1.2.2.1 Ralink模式1.2.2.2 eCos模式2 實驗過程3 參考資料在2011年 Stefan Viehbck 演示過WPS的在線暴力攻擊&#xff0c;由于PIN碼猜測最多只需11000種組合&#x…

IDEA開發過程中經常使用到的快捷鍵

IntelliJ IDEA 開發 Java 時常用的快捷鍵列表 代碼編輯與行操作快捷鍵功能描述Ctrl Y刪除當前行。Ctrl D復制當前行到下一行。Shift Alt ↑將當前行&#xff08;或選中塊&#xff09;向上移動。Shift Alt ↓將當前行&#xff08;或選中塊&#xff09;向下移動。Ctrl /注…

ubuntu使用webrtc庫開發一個webrtc推拉流程序

目錄 一. 前言 二. 整體交互流程 三. 類實現說明 1. WebRtcClient 2. SignalPeerClient 3. WebRTCStream 4. 視頻源類 5. 拉流渲染 四. 使用示例 1. 推流代碼示例 2. 拉流代碼示例 一. 前言 在 《ubuntu編譯webrtc庫》我們介紹了如何在 ubuntu 上使用 webrtc 源代碼…

【Block總結】ConverseNet:神經網絡中的反向卷積算子

1. 論文信息 標題:Reverse Convolution and Its Applications to Image Restoration 發布平臺:arXiv 論文鏈接:https://arxiv.org/pdf/2508.09824 代碼倉庫:https://github.com/cszn/converseNet 任務領域:圖像恢復(去噪、超分辨率、去模糊) 核心貢獻:提出了一種新的反…

優化瀏覽體驗:4個設置讓Google Chrome更好用!

想要更流暢、更快速的瀏覽體驗嗎&#xff1f;本文章將向大家展示Google Chrome中你應該立即更改的4個重要設置&#xff0c;設置調整將幫助您提升性能&#xff0c;讓你的瀏覽更高效。1、打開瀏覽器&#xff0c;在地址欄輸入“chrome://flags"確定&#xff0c;在搜索標志中輸…

【Git】一篇文章帶你入門Git

1. 初識 Git 1.1 Git 是什么&#xff1f; Git 是一個開源的分布式版本控制系統&#xff0c;用于高效地跟蹤和管理項目代碼的變更歷史&#xff08;不僅僅是代碼&#xff0c;還有其它格式也是可以的~&#xff09; 1.2 為什么要有 Git 在學習或者是工作的時候&#xff0c;比如…