企业级实战:构建基于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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈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;泛型类/接口/…