目錄
- 一、概述
- 二、項目目標與技術架構
- 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.2的Widget框架進行開發。這種架構不僅厘清了職責,也極大地便利了團隊協作開發與后期的功能維護。
二、項目目標與技術架構
2.1 核心目標
開發一個桌面端AOI應用程序,該程序需具備以下核心功能:
- 圖像加載與顯示:支持用戶從本地加載待檢測的產品圖像。
- 交互式ROI定義:允許質檢員在圖像上通過鼠標拖拽,靈活地繪制一個或多個感興趣區域(ROI)。
- 一鍵式智能檢測:點擊按鈕后,程序調用后端AI算法,對每個ROI區域進行獨立的目標檢測與邏輯判斷。
- 可視化結果呈現:在原始圖像上,直觀地展示所有檢測到的目標(邊界框、類別、置信度),并高亮標記出判定為“異常”的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 軟件架構
項目采用前后端分離的設計理念,具體分為兩個核心模塊:
-
AI推理動態鏈接庫 (DLL):
- 職責:封裝所有與計算機視覺和AI推理相關的復雜邏輯。這包括模型加載/釋放、圖像數據預處理、ONNX模型推理、結果后處理以及核心的業務邏輯判斷。
- 開發工具:使用Visual Studio C++進行開發和編譯。
- 接口設計:提供純C語言風格的函數接口,不暴露任何OpenCV或特定庫的數據類型。這種設計確保了接口的穩定與通用性,使得UI開發者無需關心底層算法實現細節。
-
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的包含目錄、庫目錄和鏈接器輸入:
- C/C++ -> 常規 -> 附加包含目錄:
D:\toolplace\opencv\build\include
- 鏈接器 -> 常規 -> 附加庫目錄:
D:\toolplace\opencv\build\x64\vc16\lib
- 鏈接器 -> 輸入 -> 附加依賴項:
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.dll
和DetectorAPI.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與我們新代碼關聯起來的關鍵一步。
- 打開
mainwindow.ui
文件。 - 在界面上右鍵單擊
imageLabel
控件。 - 從菜單中選擇 “Promote to…” (提升為…)。
- 在彈出的對話框中,將 “Promoted class name” 設置為
ROILabel
,“Header file” 設置為roilabel.h
。 - 點擊 “Add”,然后點擊 “Promote”。
- 保存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();}
}
五、編譯與部署
- 編譯DLL:在Visual Studio中,選擇Release配置,編譯
DetectorAPI
項目,生成DetectorAPI.dll
和DetectorAPI.lib
。 - 編譯GUI:在Qt Creator中,選擇Release配置,構建
qtDemo
項目。 - 部署:創建一個部署文件夾,并將以下文件放入:
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來實現。