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

一、概述

在追求高效與精密的現代制造業中,自動化光學檢測(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/bicheng/98072.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/98072.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/98072.shtml

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

相關文章

【目標檢測】metrice_curve和loss_curve對比圖可視化

代碼如下&#xff1a; import warnings warnings.filterwarnings(ignore)import os import pandas as pd import numpy as np import matplotlib.pylab as pltpwd os.getcwd()names [model1, model2, model3,ours]plt.figure(figsize(10, 10))plt.subplot(2, 2, 1) for i in …

【LeetCode hot100|Week2】滑動窗口,子串

筆記用于個人復習和鞏固&#xff0c;題解非原創&#xff0c;參考LeetCode官方題解以及各個大佬的解法&#xff0c;希望給大家帶來幫助&#xff0c;同時筆記也能督促我學習進步 這周主要把滑動窗口和子串的題目刷了一遍 文章目錄Week2D1 滑動窗口209. 長度最小的子數組713. 乘積…

vue2純前端對接海康威視攝像頭實現實時視頻預覽

vue2純前端對接海康威視攝像頭實現實時視頻預覽一、環境準備二、代碼集成1.1 準備webrtcstreamer.js&#xff0c;粘貼即用&#xff0c;不用做任何修改1.2 封裝視頻組件&#xff0c;在需要視頻的地方引入此封裝的視頻組件即可&#xff0c;也是粘貼即用&#xff0c;注意其中impor…

Android 設置禁止截圖和禁止長截圖

1.禁止截圖 在 Activity 代碼中 , 可以在調用 setContentView 函數之前 ,為 Window 窗口對象 設置 LayoutParams.FLAG_SECURE 標志位 , 可以禁止對本界面進行截屏 ,Window 窗口對象 , 可通過 getWindow 方法獲取 ,核心代碼如下 :getWindow().setFlags(LayoutParams.FLAG_SECUR…

AR 巡檢在工業的應用|阿法龍XR云平臺

AR 巡檢的應用覆蓋電力、石油化工、智能制造、軌道交通、冶金等對設備可靠性和安全性要求極高的行業&#xff0c;具體場景包括&#xff1a;電力行業變電站內設備的狀態檢查&#xff1a;通過 AR 眼鏡掃描設備&#xff0c;實時顯示設備額定參數、歷史故障記錄、實時傳感器數據&am…

【C++】STL詳解(七)—stack和queue的介紹及使用

? 堅持用 清晰易懂的圖解 代碼語言&#xff0c; 讓每個知識點都 簡單直觀 &#xff01; &#x1f680; 個人主頁 &#xff1a;不呆頭 CSDN &#x1f331; 代碼倉庫 &#xff1a;不呆頭 Gitee &#x1f4cc; 專欄系列 &#xff1a; &#x1f4d6; 《C語言》&#x1f9e9; 《…

深度學習周報(9.8~9.14)

目錄 摘要 Abstract 1 LSTM相關網絡總結與對比 1.1 理論總結 1.2 代碼運行對比 2 量子計算入門 3 總結 摘要 本周首先總結了LSTM、Bi-LSTM與GRU的區別與優缺點&#xff0c;對比了三者實戰的代碼與效果&#xff0c;還另外拓展了一些循環神經網絡變體&#xff08;包括窺視…

Quat 四元數庫使用教程:應用場景概述

基礎概念 四元數是一個包含四個元素的數組 [x, y, z, w]&#xff0c;其中 x,y,z表示虛部&#xff0c;w 表示實部。單位四元數常用于表示3D空間中的旋轉。 1. 創建和初始化函數 create() - 創建單位四元數 應用場景&#xff1a;初始化一個新的四元數對象&#xff0c;通常作為其他…

【Java后端】Spring Boot 多模塊項目實戰:從零搭建父工程與子模塊

如何用 Spring Boot 搭建一個父工程 (Parent Project)&#xff0c;并在其中包含多個子模塊 (Module)&#xff0c;適合企業級項目或者需要分模塊管理的場景。Spring Boot 多模塊項目實戰&#xff1a;從零搭建父工程與子模塊在日常開發中&#xff0c;我們經常會遇到這樣的需求&am…

企業級AI會議系統技術實現:快鷺如何用AI重構會議全流程

摘要 本文深度解析快鷺AI會議系統的核心技術架構&#xff0c;重點探討其在語音識別、自然語言處理、數據集成和安全防護等方面的技術實現。通過對比傳統會議系統的技術痛點&#xff0c;分析快鷺AI如何通過技術創新實現會議籌備時間減少67%、數據調取速度提升100倍的顯著效果。…

【CSS學習筆記3】css特性

1css三大特性 1.1層疊性&#xff1a;就近原則&#xff0c;最新定義的樣式 1.2繼承性&#xff1a;子標簽集成父標簽的樣式&#xff0c;如文本和字號 行高的繼承&#xff1a;不加單位指的是當前文字大小的倍數 body {font: 12px/1.5 Microsoft YaHei;color: #be1313;} div {…

[C語言]常見排序算法①

1.排序的概念及常見的排序算法排序在咱們日常生活中十分的常見&#xff0c;就好比是網上購物的時候通常能夠選擇按照什么排序&#xff0c;比如價格、評論數量、銷量等。那么接下來咱們就來了解一些關于排序的概念。排序&#xff1a;所謂排序&#xff0c;就是使一串記錄&#xf…

文獻閱讀筆記:RS電子戰測試與測量技術文檔

信息來源&#xff1a;羅德與施瓦茨&#xff08;Rohde & Schwarz&#xff09;公司關于電子戰&#xff08;Electronic Warfare, EW&#xff09;測試與測量解決方案專業技術文檔。 該文檔由臺灣地區應用工程師Mike Wu撰寫&#xff0c;核心圍繞電子戰基礎、雷達系統、實戰應用及…

別再糾結 Postman 和 Apifox 了!這款開源神器讓 API 測試更簡單

別再糾結 Postman 和 Apifox 了&#xff01;這款開源神器讓 API 測試更簡單&#x1f525; 作為一名開發者&#xff0c;你是否還在為選擇 API 測試工具而糾結&#xff1f;Postman 太重、Apifox 要聯網、付費功能限制多&#xff1f;今天給大家推薦一款完全免費的開源替代方案 ——…

微調神器LLaMA-Factory官方保姆級教程來了,從環境搭建到模型訓練評估全覆蓋

1. 項目背景 開源大模型如LLaMA&#xff0c;Qwen&#xff0c;Baichuan等主要都是使用通用數據進行訓練而來&#xff0c;其對于不同下游的使用場景和垂直領域的效果有待進一步提升&#xff0c;衍生出了微調訓練相關的需求&#xff0c;包含預訓練&#xff08;pt&#xff09;&…

創建其他服務器賬號

? 在 /home74 下創建新用戶的完整步驟1. 創建用戶并指定 home 目錄和 shellsudo useradd -m -d /home74/USERNAME -s /bin/bash USERNAME-m&#xff1a;自動創建目錄并復制 /etc/skel 默認配置文件&#xff08;.bashrc 等&#xff09;。-d&#xff1a;指定用戶 home 路徑&…

【WebGIS】Vue3使用 VueLeaflet + 天地圖 搭建地圖可視化平臺(基礎用法)

初始化 創建項目 nodejs 18.0.6npm 9.5.1 引入地圖服務 VueLeaflet GitHub - vue-leaflet/vue-leaflet&#xff1a; vue-leaflet 與 vue3 兼容 Vue Leaflet (vue2-leaflet) package.josn安裝版本 直接添加四個依賴 {// ..."scripts": {// ...},"depen…

OpenCV 開發 -- 圖像閾值處理

文章目錄[toc]1 基本概念2 簡單閾值處理cv2.threshold3 自適應閾值處理cv2.adaptiveThreshold更多精彩內容&#x1f449;內容導航 &#x1f448;&#x1f449;OpenCV開發 &#x1f448;1 基本概念 圖像閾值處理&#xff08;Thresholding&#xff09;是圖像處理中的一種基本技術…

單串口服務器-工業級串口聯網解決方案

在工業自動化、智能電網、環境監測等領域&#xff0c;傳統串口設備&#xff08;如PLC、傳感器、儀表等&#xff09;的網絡化升級需求日益增長。博為智能單串口服務器憑借高性能硬件架構、多協議支持和工業級可靠性&#xff0c;為RS485設備提供穩定、高效的TCP/IP網絡接入能力&a…

第 9 篇:深入淺出學 Java 語言(JDK8 版)—— 吃透泛型機制,筑牢 Java 類型安全防線

簡介&#xff1a;聚焦 Java 泛型這一“類型安全保障”核心技術&#xff0c;從泛型解決的核心痛點&#xff08;非泛型代碼的運行時類型錯誤、強制類型轉換冗余&#xff09;切入&#xff0c;詳解泛型的本質&#xff08;參數化類型&#xff09;、核心用法&#xff08;泛型類/接口/…