企业级实战:构建基于Qt、C++与YOLOv8的模块化工业视觉检测系统
一、概述
在追求高效与精密的现代制造业中,自动化光学检测(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来实现。