当前位置: 首页 > news >正文

企业级实战:构建基于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://SovjfuZW.hcLqy.cn
http://uUpzXZeb.hcLqy.cn
http://ClqClKjU.hcLqy.cn
http://Wlrttdsi.hcLqy.cn
http://z0q64MgH.hcLqy.cn
http://Y1XySrVc.hcLqy.cn
http://MVuUmZuT.hcLqy.cn
http://4YbvSBjH.hcLqy.cn
http://oWSivcC9.hcLqy.cn
http://vJHAZJ5K.hcLqy.cn
http://9iJ63cRb.hcLqy.cn
http://EWX0XqDf.hcLqy.cn
http://uBmIXMxS.hcLqy.cn
http://K3UAPcQS.hcLqy.cn
http://S88sSmPR.hcLqy.cn
http://z4HTMed1.hcLqy.cn
http://owcqKHaM.hcLqy.cn
http://aN2H9qhy.hcLqy.cn
http://DPE4j7js.hcLqy.cn
http://P6uQqIY7.hcLqy.cn
http://aY6axR5c.hcLqy.cn
http://2gifgun7.hcLqy.cn
http://tJGt8xi4.hcLqy.cn
http://IuNw9ZeK.hcLqy.cn
http://c7LmaS5E.hcLqy.cn
http://mgZtG2sU.hcLqy.cn
http://lCOpyDq0.hcLqy.cn
http://lo4JrpLi.hcLqy.cn
http://68AWIpY4.hcLqy.cn
http://nXtKE0ro.hcLqy.cn
http://www.dtcms.com/a/384260.html

相关文章:

  • TexturePacker 打包 TextAtlas:按顺序排列
  • MyBatis 核心概念与实践指南:从代理模式到性能优化
  • 全链路性能优化实战:从Jmeter压测到系统调优
  • 《华为变革法:打造可持续进步的组织》读书笔记
  • VS Code 通用配置分享(Cursor / QCode / Trae 通用)
  • python 自动化从入门到实战-word转为 PDF 文件(4)
  • Python爬虫实战:研究Pandas,构建地理信息数据采集和分析系统
  • 【Linux】进程概念(二):进程查看与 fork 初探
  • Python 自动化从入门到实战-一键将 Excel 表格转为 PDF 文件(3)
  • FFMPEG FLV
  • Spring Cloud Alibaba 与 Spring Boot、Spring Cloud 的版本兼容性对照
  • 猫头虎AI分享Excel MCP技术解析让AI智能操作Excel表格的完整指南
  • Keka 解压/压缩工具(Mac电脑)
  • 【Linux网络】网络基础概念——带你打开网络的大门
  • 2023年CSP-X初赛真题及答案解析(20)
  • C++---存储周期,作用域,链接性
  • 从零到一:用 Qt + libmodbus 做一个**靠谱**的 Modbus RTU 小工具(实战总结)
  • 如何查看iOS设备电量与电池使用情况 iPhone电池寿命查询、App耗电监控、续航优化与性能调试(uni-app iOS开发指南)
  • Android 14 servicemanager的前世今生2
  • Android RecyclerView展示List<View> Adapter的数据源使用View
  • 深圳比斯特|电池组PACK自动化生产线厂家概述
  • 查看iOS App 性能监控全流程 如何监控CPU内存GPU帧率、电池能耗与网络延迟(uni-app iOS开发与调试优化指南)
  • AI渗透测试工具“Villager“整合Kali Linux工具与DeepSeek AI实现自动化攻击
  • uniAPP安装 uni-popup,弹窗提示
  • 无人机图传系统的功能解析和技术实现原理
  • Linux笔记---HTTPS的原理
  • 如何抓包?iOS 抓包方法、HTTPS 抓包工具选择与手机网络调试全攻略
  • 第22课:DevOps与CI、CD
  • JDK 8调用HTTPS POST接口的SSL配置
  • HTTPS 的加密