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

基于Qt/QML 5.14和YOLOv8的工业异常检测Demo:冲压点智能识别

目录

  • 一、概述
  • 二、目标
    • 2.1基本要求
    • 2.2技术架构
  • 三、详细实现步骤
    • 3.1 项目初始化与 C++ 后端搭建
    • 3.2 构建 QML 用户界面
    • 3.3 项目配置和运行
  • 四、工作流程演示
  • 五、为程序添加图标
    • 5.1 准备图标文件 (.ico)
    • 5.2 设置可执行文件 (.exe) 的图标
        • 第 1 步:创建资源文件 (`appicon.rc`)
        • 第 2 步:修改项目配置文件 (`.pro`)
    • 5.3 设置窗口及任务栏图标
        • 第 1 步:将图标添加到 Qt 资源系统
        • 第 2 步:在 QML 中设置窗口图标
    • 5.4 查看效果
  • 六、程序部署 (Windows)
    • 6.1 部署原理
    • 6.2 准备工作
    • 6.3 部署步骤
        • 第 1 步:创建部署文件夹
        • 第 2 步:复制可执行文件 (.exe)
        • 第 3 步:使用 `windeployqt` 自动部署 Qt 依赖
        • 第 4 步:手动复制非 Qt 依赖项
        • 第 5 步:最终检查与测试
    • 6.4 常见问题排查
  • 七、结语

一、概述

在现代工业生产,特别是金属成型和冲压领域,保证产品质量是至关重要的环节。传统的质检方法依赖于人工目视检查,不仅效率低下,而且容易因疲劳、疏忽等因素导致漏检或误判。随着人工智能技术的发展,利用计算机视觉进行自动化质量检测(AOI)已成为大势所趋。

本文将分享一个完整的异常检测 Demo 项目,旨在展示如何利用现代化的软件和 AI 技术,解决一个典型的工业质检场景:自动检测金属件上的两个冲压点

二、目标

我们的目标是开发一个桌面应用程序,它允许质检员加载一张包含多个产品的巨幅图像。操作员只需在图像上框选出感兴趣的单个产品区域,然后单击一个按钮,程序即可自动、快速地识别出该产品上的两个关键冲压点。

这个 Demo 不仅是技术展示,更是面向工业领域客户的一个概念验证(PoC),证明了 AI 在提升质检效率和准确性方面的巨大潜力。

2.1基本要求

  1. 用户界面 (UI):使用 Qt 5.14 QML 技术构建现代化、流畅的用户界面。
  2. 图像交互:支持从本地加载大尺寸图像,并允许用户通过鼠标在图像上拖拽出一个矩形选框。
  3. AI 推理:截取用户框选的子图,调用预先训练好的 YOLOv8 ONNX 模型进行推理。
  4. 结果展示:在原图的框选区域内,精确地标记出检测到的两个冲压点的位置。
  5. 跨平台兼容性:项目需能在 Windows 7 64位等传统工业计算机环境中稳定运行,因此我们选用兼容性更佳的 Qt 5.14版本,同时选择MSVC 2019 64bit编译器。编译工具采用qmake

由于国内网络IP限制,直接从官网下载Qt5.14的安装包可能会下载不下来。这里可以使用迅雷直接下载Qt 5.14:

https://download.qt.io/archive/qt/5.14/5.14.2/qt-opensource-windows-x86-5.14.2.exe

2.2技术架构

我们的 Demo 采用前后端分离的设计思想:

  • 前端 (QML):负责所有用户交互,如图像显示、选框绘制、按钮点击等。QML 的声明式语法和强大的图形渲染能力非常适合快速构建美观的 UI。
  • 后端 (C++):负责核心逻辑,包括图像处理、调用 OpenCV 进行 ONNX 模型推理、以及与 QML 的数据通信。

这种架构使得 UI 设计与核心算法逻辑解耦,便于维护和未来扩展。


三、详细实现步骤

现在,让我们一步步从零开始,用 Qt 5.14 和 C++ 构建这个 Demo。

3.1 项目初始化与 C++ 后端搭建

首先,在 Qt Creator 中创建一个新的 Qt Quick Application 项目,选择 Qt 5.14.x 版本。

接下来,我们需要一个 C++ 类作为 QML 与后端逻辑的桥梁。我们将其命名为 ImageProcessor。这个类需要继承自 QObject 以便利用 Qt 的信号和槽机制。

imageprocessor.h

#ifndef IMAGEPROCESSOR_H
#define IMAGEPROCESSOR_H#include <QObject>
#include <QString>
#include <QImage>
#include <QRect>
#include <QVariantList>
#include <QUrl>
#include <QVariantMap>// 引入OpenCV头文件
#include <opencv2/opencv.hpp>
#include <opencv2/core.hpp>
#include <opencv2/dnn.hpp>
#include <opencv2/imgproc.hpp>class ImageProcessor : public QObject
{Q_OBJECT
public:explicit ImageProcessor(QObject *parent = nullptr);// 定义一个信号,用于将检测结果(点的坐标)发送回 QMLsignals:void detectionComplete(QVariantList points);void testSignal(int count);public slots:// 定义一个槽函数,QML 将调用它来开始检测// 参数: 图像的 QImage 对象,以及用户框选的矩形区域void startDetection(const QUrl &imageUrl, const QRect &cropRect, const QSize &containerSize);private:cv::dnn::Net net; // ONNX 推理网络QImage cropImage(const QImage &source, const QRect &rect);cv::Mat qImageToCvMat(const QImage &inImage);std::vector<std::string> m_classNames; // 存储类别名称
};#endif // IMAGEPROCESSOR_H

imageprocessor.cpp

#include "imageprocessor.h"
#include <QDebug>ImageProcessor::ImageProcessor(QObject *parent) : QObject(parent)
{// 在构造函数中加载 ONNX 模型std::string modelPath = "./models/best.onnx";try {net = cv::dnn::readNetFromONNX(modelPath);net.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV);net.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);qDebug() << "ONNX model loaded successfully.";} catch (const cv::Exception& e) {qWarning() << "Error loading ONNX model:" << e.what();}m_classNames = {"neck_defect", "thread_defect", "head_defect"};
}void ImageProcessor::startDetection(const QUrl &imageUrl, const QRect &viewRect,const QSize &containerSize)
{// 检查 URL 是否有效if (!imageUrl.isValid() || !imageUrl.isLocalFile()) {qWarning() << "Invalid or non-local image URL:" << imageUrl;return;}// 根据 URL 加载 QImageQImage image(imageUrl.toLocalFile());if (image.isNull() || !viewRect.isValid()) {qWarning() << "Image is null or crop rectangle is invalid.";return;}// 通道标准化if (image.format() != QImage::Format_RGB888) {image = image.convertToFormat(QImage::Format_RGB888);}// 坐标映射QSize sourceSize = image.size(); // 获取原始图像尺寸// 计算图片被 PreserveAspectFit 缩放后的实际尺寸 (scaledSize)QSize scaledSize = sourceSize;scaledSize.scale(containerSize, Qt::KeepAspectRatio);// 计算图片在容器中的偏移量 (黑边的大小)double offsetX = (containerSize.width() - scaledSize.width()) / 2.0;double offsetY = (containerSize.height() - scaledSize.height()) / 2.0;// 计算从 "视图坐标" 到 "源图像坐标" 的缩放比例double scaleFactor = (double)sourceSize.width() / (double)scaledSize.width();// 将 QML 传来的视图矩形 (viewRect) 映射为源图像矩形 (sourceRect)//    a. 首先减去黑边偏移//    b. 然后乘以缩放比例,还原到原始尺寸QRect sourceRect(static_cast<int>((viewRect.x() - offsetX) * scaleFactor),static_cast<int>((viewRect.y() - offsetY) * scaleFactor),static_cast<int>(viewRect.width() * scaleFactor),static_cast<int>(viewRect.height() * scaleFactor));// 安全检查,防止计算出的矩形超出原始图像边界sourceRect = sourceRect.intersected(image.rect());// 裁剪图像QImage croppedQImage = image.copy(sourceRect);if(croppedQImage.isNull()) {qWarning() << "Cropping failed.";return;}// QImage 转换为 cv::Matcv::Mat frame = qImageToCvMat(croppedQImage);cv::imwrite("temp.jpg",frame);// 图像预处理、推理、后处理cv::Mat blob;cv::dnn::blobFromImage(frame, blob, 1.0 / 255.0, cv::Size(640, 640), cv::Scalar(), true, false);net.setInput(blob);std::vector<cv::Mat> outs;net.forward(outs, net.getUnconnectedOutLayersNames());// 解析和重塑模型输出// YOLOv8 输出尺寸为 [1, num_classes + 4, 8400]cv::Mat output_buffer = outs[0];output_buffer = output_buffer.reshape(1, {output_buffer.size[1], output_buffer.size[2]}); // 变为 [num_classes + 4, 8400]cv::transpose(output_buffer, output_buffer); // 变为 [8400, num_classes + 4]// 4. 后处理 - 筛选候选框float conf_threshold = 0.5f; // 置信度阈值float nms_threshold = 0.4f;  // NMS阈值std::vector<int> class_ids;std::vector<float> confidences;std::vector<cv::Rect> boxes;// 计算缩放因子,将模型坐标(640x640)映射回裁剪后的图像坐标float x_factor = (float)frame.cols / 640.f;float y_factor = (float)frame.rows / 640.f;for (int i = 0; i < output_buffer.rows; i++) {cv::Mat row = output_buffer.row(i);// 前4个值是 cx, cy, w, h。之后的是类别分数。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);// 从中心点+宽高 (cx,cy,w,h) 计算左上角坐标 (x,y)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));}}// 非极大值抑制 (NMS) - 消除重叠的框std::vector<int> indices;cv::dnn::NMSBoxes(boxes, confidences, conf_threshold, nms_threshold, indices);// 将最终结果打包成 QVariantList,准备发送给 QMLQVariantList detectedBoxes;for (int idx : indices) {cv::Rect box = boxes[idx]; // 'box' 的坐标是相对于 croppedQImage 的int class_id = class_ids[idx];float confidence = confidences[idx];QVariantMap boxMap;// --- 核心反向映射逻辑 ---// a. 将 box 的坐标从 "裁剪图坐标系" 转换回 "源图坐标系"double absolute_source_x = sourceRect.x() + box.x;double absolute_source_y = sourceRect.y() + box.y;// b. 将 "源图坐标系" 下的坐标和尺寸,反向映射回 QML 的 "视图坐标系"//    - 除以缩放因子,得到在按比例缩放后的图像上的坐标/尺寸//    - 加上偏移量,得到在整个 imageContainer 容器中的最终坐标double final_view_x = (absolute_source_x / scaleFactor) + offsetX;double final_view_y = (absolute_source_y / scaleFactor) + offsetY;double final_view_w = (double)box.width / scaleFactor;double final_view_h = (double)box.height / scaleFactor;// 将计算出的最终视图坐标和尺寸放入 MapboxMap.insert("x", final_view_x);boxMap.insert("y", final_view_y);boxMap.insert("width", final_view_w);boxMap.insert("height", final_view_h);// 标签和置信度保持不变boxMap.insert("label", QString::fromStdString(m_classNames[class_id]));boxMap.insert("confidence", confidence);detectedBoxes.append(boxMap);}// ======================================================================// 7. 发送包含所有检测框信息的信号qDebug() << "C++ SIDE: Emitting" << detectedBoxes.size() << "detected boxes to be drawn on QML.";emit detectionComplete(detectedBoxes);
}// 工具函数:QImage 转 cv::Mat
cv::Mat ImageProcessor::qImageToCvMat(const QImage &inImage)
{cv::Mat mat;switch (inImage.format()){case QImage::Format_ARGB32:case QImage::Format_RGB32:case QImage::Format_ARGB32_Premultiplied:mat = cv::Mat(inImage.height(), inImage.width(), CV_8UC4, (void*)inImage.constBits(), inImage.bytesPerLine());break;case QImage::Format_RGB888:mat = cv::Mat(inImage.height(), inImage.width(), CV_8UC3, (void*)inImage.constBits(), inImage.bytesPerLine());cv::cvtColor(mat, mat, cv::COLOR_RGB2BGR);break;case QImage::Format_Grayscale8:mat = cv::Mat(inImage.height(), inImage.width(), CV_8UC1, (void*)inImage.constBits(), inImage.bytesPerLine());break;}return mat;
}

最后,在 main.cpp 中,我们需要将 ImageProcessor 注册到 QML 引擎中,这样 QML 才能访问它。

main.cpp

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext> // 需要包含这个头文件
#include <QMetaType>
#include "imageprocessor.h" // 包含我们的 C++ 类头文件int main(int argc, char *argv[])
{
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endifQGuiApplication app(argc, argv);// 注册特定的数据传递类型qRegisterMetaType<QVariantList>("QVariantList");QQmlApplicationEngine engine;// 实例化我们的 C++ 后端类ImageProcessor imageProcessor;// 将 C++ 对象暴露给 QML,命名为 "imageProcessor"engine.rootContext()->setContextProperty("imageProcessor", &imageProcessor);const QUrl url(QStringLiteral("qrc:/main.qml"));QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,&app, [url](QObject *obj, const QUrl &objUrl) {if (!obj && url == objUrl)QCoreApplication::exit(-1);}, Qt::QueuedConnection);engine.load(url);return app.exec();
}

3.2 构建 QML 用户界面

现在,我们来编写 main.qml 文件。UI 将包含一个用于显示大图像的区域,一个用于选择文件的按钮,一个用于触发检测的按钮,以及用于绘制选择框和结果的元素。

main.qml

import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Dialogs 1.2ApplicationWindow {id: windowobjectName: "rootWindow" // <<<<<<<< 1. 添加 objectNamevisible: truewidth: 800height: 600title: qsTr("冲压点异常检测 Demo")// 用于存储检测结果的点property var detectedPoints: []// C++ 信号的连接Connections {target: imageProcessorfunction onDetectionComplete(points) {console.log("Detection complete. Points received:", JSON.stringify(points))window.detectedPoints = points}}// 主界面布局Column {anchors.fill: parentspacing: 10 // 可以适当增加间距// 顶部工具栏Row {anchors.horizontalCenter: parent.horizontalCenterheight: 50spacing: 20 // 可以增加按钮间的间距padding: 10Button {text: "加载图像"onClicked: fileDialog.open()// 设置按钮的首选尺寸implicitWidth: 120implicitHeight: 40// 自定义内容 (文字)contentItem: Text {text: parent.text // 使用父级 Button 的 text 属性font: parent.fontcolor: "white" // 文字颜色始终为白色horizontalAlignment: Text.AlignHCenterverticalAlignment: Text.AlignVCenter}// 自定义背景background: Rectangle {// 使用三元运算符根据按钮状态改变颜色// 检查顺序很重要:先检查 pressed,再检查 hoveredcolor: parent.pressed ? "#004085" : (parent.hovered ? "#0056b3" : "#007BFF")radius: 5 // 圆角// 添加平滑的颜色过渡动画Behavior on color { ColorAnimation { duration: 150 } }}}Button {id: detectButtontext: "开始识别"enabled: selectionRect.visible && mainImage.source !== ""  // 只有在画了框之后才可用onClicked: {// 调用 C++ 槽函数,传递 Image 对象和选框的 QRectimageProcessor.startDetection(mainImage.source,Qt.rect(selectionRect.x, selectionRect.y, selectionRect.width, selectionRect.height),Qt.size(imageContainer.width, imageContainer.height))}// 设置按钮的首选尺寸implicitWidth: 120implicitHeight: 40// 自定义内容 (文字)contentItem: Text {text: parent.textfont: parent.font// 当按钮被禁用时,文字颜色变灰color: parent.enabled ? "white" : "#AAAAAA"horizontalAlignment: Text.AlignHCenterverticalAlignment: Text.AlignVCenter}// 自定义背景background: Rectangle {// 当按钮被禁用时,显示灰色;否则根据状态改变颜色color: parent.enabled ? (parent.pressed ? "#004085" : (parent.hovered ? "#0056b3" : "#007BFF")) : "#5A5A5A"radius: 5Behavior on color { ColorAnimation { duration: 150 } }}}}// 图像显示和交互区域Item {id: imageContainerclip: true // 裁剪超出边界的内容width: 750height: 500anchors.horizontalCenter: parent.horizontalCenter// 纯色占位背景Rectangle {anchors.fill: parentcolor: "#F0F0F0"}// 图像显示层Image {id: mainImagesource: ""fillMode: Image.PreserveAspectFitanchors.fill: parent}// 用于绘制选择框的 MouseAreaMouseArea {anchors.fill: parentproperty var startPoint: Qt.point(0, 0)onPressed: (mouse) => {// 按下鼠标时,清空上次结果,记录起点,并显示矩形window.detectedPoints = []startPoint = Qt.point(mouse.x, mouse.y)selectionRect.x = mouse.xselectionRect.y = mouse.yselectionRect.width = 0selectionRect.height = 0selectionRect.visible = true}onPositionChanged: (mouse) => {// 拖动时更新矩形的尺寸selectionRect.width = mouse.x - startPoint.xselectionRect.height = mouse.y - startPoint.y}}// 用户绘制的选择框Rectangle {id: selectionRectcolor: "#401E90FF" // 半透明蓝色border.color: "#FF1E90FF" // 不透明蓝色边框border.width: 2visible: false}// 使用 Repeater 动态显示检测结果Repeater {model: window.detectedPoints // 模型名称不变// 委托:一个包含矩形框和文字标签的组合项delegate: Item {// Item 的位置和尺寸与检测框完全一致x: modelData.xy: modelData.ywidth: modelData.widthheight: modelData.height// 绘制检测框的边框Rectangle {anchors.fill: parentcolor: "transparent" // 矩形本身透明border.color: "lime" // 使用鲜艳的绿色作为边框border.width: 2}// 绘制标签背景Rectangle {// 将标签放在检测框的左上角,并稍微向上偏移一点x: 0y: -labelText.implicitHeight - 2 // 向上偏移文字高度+2像素// 尺寸根据文字内容自适应width: labelText.implicitWidth + 8height: labelText.implicitHeight + 4color: "lime" // 背景色与边框色一致// 标签文字Text {id: labelTextanchors.centerIn: parent// 组合显示类别和置信度(保留两位小数)text: `${modelData.label}: ${modelData.confidence.toFixed(2)}`color: "black" // 黑色文字font.pixelSize: 12 // 可以调整字体大小}}}}}// 文件选择对话框FileDialog {id: fileDialogtitle: "请选择一张图片"folder: shortcuts.picturesnameFilters: ["Image files (*.jpg *.png *.bmp)"]onAccepted: {mainImage.source = fileDialog.fileUrlwindow.detectedPoints = [] // 加载新图片时清空旧结果}}}
}

3.3 项目配置和运行

  1. 配置 .pro 文件: 确保你的 .pro 文件包含了 networkopencv 的库。
QT += quick core guiCONFIG += c++11# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0HEADERS += \imageprocessor.hSOURCES += \main.cpp \imageprocessor.cppRESOURCES += qml.qrc# Additional import path used to resolve QML modules in Qt Creator's code model
QML_IMPORT_PATH =# Additional import path used to resolve QML modules just for Qt Quick Designer
QML_DESIGNER_IMPORT_PATH =# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += targetHEADERS += \imageprocessor.h# 引入 OpenCV 库 (路径请根据你的实际安装位置修改)
INCLUDEPATH += C:/opencv/build/include
LIBS += -LC:/opencv/build/x64/vc16/lib \-lopencv_world4120 

注意:OpenCV 库的版本号 (例如 4.12.0) 和路径需要与你的环境匹配。

  1. 放置 ONNX 模型: 将你训练好的 best.onnx 模型文件放到 imageprocessor.cpp 中指定的路径,或者一个可以被程序访问的位置。

  2. 编译和运行: 现在,你可以编译并运行你的项目了!

四、工作流程演示

  1. 启动程序:程序启动后,显示主界面。
  2. 加载图像:点击“加载图像”按钮,从本地文件系统中选择一张待检测的大图。
  3. 框选区域:在加载的图像上,按住鼠标左键并拖动,会形成一个蓝色的半透明矩形框。
  4. 执行检测:松开鼠标后,选框确定。“开始识别”按钮变为可用状态。点击该按钮。
  5. 后台处理
    • QML 将原始 QImage 和选框的 QRect 传递给 C++ 的 startDetection 函数。
    • C++ 代码根据 QRectQImage 中裁剪出子图。
    • 子图被转换为 cv::Mat 格式,并经过预处理后送入 YOLOv8 ONNX 模型。
    • 模型推理完成后,C++ 代码解析输出,得到冲压点的坐标。
  6. 结果显示
    • C++ 通过 detectionComplete 信号将坐标列表发送回 QML。
    • QML 中的 Connections 对象接收到信号,更新 detectedPoints 属性。
    • Repeater 检测到 model 的变化,自动在图像上对应的坐标处创建矩形标记。

在这里插入图片描述
在这里插入图片描述

五、为程序添加图标

一个专业的应用程序,离不开一个醒目的图标。它不仅是品牌的象征,也是用户在桌面和任务栏上识别程序的关键。在 Qt 中,为程序添加图标分为两步:

  1. 设置可执行文件 (.exe) 的图标:这是您在 Windows 文件资源管理器中看到的图标。
  2. 设置窗口及任务栏的图标:这是程序运行时,在窗口左上角和 Windows 任务栏上显示的图标。
    在这里插入图片描述

5.1 准备图标文件 (.ico)

要同时满足上述两个要求,最标准的做法是使用一个 .ico 格式的图标文件。.ico 文件可以内嵌多种不同尺寸的位图,以适应不同的显示场景(例如大图标、小图标、列表视图等)。

  1. 设计图标:首先,您需要一个正方形的图标设计图,例如 PNG 格式,尺寸建议为 256x256 或 512x512 像素,这样可以保证高质量显示。
  2. 转换为 .ico:您可以使用在线工具或专用软件将您的 PNG 图片转换为 .ico 文件。只需搜索 “PNG to ICO converter” 即可找到很多免费的在线服务。
  3. 命名和存放:将转换后的图标文件命名为 appicon.ico,并将其放置在您的项目源码根目录中(与 .pro 文件同级)。

资源推荐:可以从免费的图标素材网站 https://icons8.com/ 寻找合适的图标。

5.2 设置可执行文件 (.exe) 的图标

Windows 程序通过一个“资源文件” (.rc) 来将图标嵌入到 .exe 文件中。

第 1 步:创建资源文件 (appicon.rc)

在您的项目根目录中,新建一个文本文档,将其命名为 appicon.rc,并用任意文本编辑器打开,在里面输入以下一行内容

IDI_ICON1               ICON                    "appicon.ico"

这行代码告诉编译器:将 appicon.ico 文件作为应用程序的默认图标资源。

第 2 步:修改项目配置文件 (.pro)

现在,我们需要告诉 qmake 在编译时处理这个 .rc 文件。打开您的 .pro 文件,在末尾添加下面这行代码:

# For Windows: link the resource file to embed the icon in the .exe
win32: RC_FILE = appicon.rc

win32: 作用域可以确保这行配置只在编译 Windows 平台时生效,保持了项目的跨平台兼容性。

5.3 设置窗口及任务栏图标

第 1 步:将图标添加到 Qt 资源系统
  1. 在 Qt Creator 中,右键 qml.qrc 文件。
  2. 单击 添加 -> 添加现有文件
  3. 找到并选择您之前准备好的 appicon.ico 文件。
第 2 步:在 QML 中设置窗口图标

打开 pro 文件,在最后添加代码如下:

RC_ICONS = myico.ico

5.4 查看效果

完成以上所有修改后,重新构建项目,构建完成后,您会发现:

  • release 文件夹中,您的 .exe 文件已经换上了新图标。
  • 运行程序后,窗口的标题栏和 Windows 任务栏上的图标也已经变成了您的新设计。

六、程序部署 (Windows)

完成了程序的开发和调试,最后一步就是将其打包,使其能够在任何一台没有安装 Qt 和 OpenCV 的 Windows 7电脑上独立运行。这个过程我们称之为“部署”。

6.1 部署原理

我们的程序之所以能在开发电脑上运行,是因为它能找到所需的 Qt 库文件(例如 Qt5Core.dll, Qt5Quick.dll)、OpenCV 库文件(opencv_worldxxxx.dll)以及 C++ 运行库(VCRUNTIME140.dll 等)。部署的目标就是创建一个包含我们 .exe 文件和所有这些依赖项的文件夹,实现“绿色运行”,即无需安装。

6.2 准备工作

  1. Release 模式编译

    • 在 Qt Creator 左侧的构建套件选择器中,确保选择的是 Release 模式,而不是 Debug。
    • Release 模式会进行代码优化,生成的程序体积更小,运行速度更快,并且不依赖于调试库。
    • 点击“构建”->“构建项目”,生成 Release 版本的 .exe 文件。
  2. 找到 windeployqt.exe

    • 这是 Qt 官方提供的部署工具,它能自动扫描 .exe 文件并复制大部分所需的 Qt 依赖项。
    • 它的位置在您的 Qt 安装目录中,例如:C:\Qt\5.14.2\msvc2019_64\bin\windeployqt.exe
  3. 找到 OpenCV 的 DLL

    • 您需要找到您在 .pro 文件中链接的那个 OpenCV 库文件。根据您的配置,它应该是 opencv_world4120.dll
    • 它的位置在您的 OpenCV 安装目录中,例如:C:\opencv\build\x64\vc16\bin\

6.3 部署步骤

第 1 步:创建部署文件夹

在您电脑的任意位置(例如桌面)创建一个新的、干净的文件夹,用来存放所有部署文件。我们将其命名为 Stamp_Detector_Release

第 2 步:复制可执行文件 (.exe)

进入您项目的构建目录(例如 build-YourProjectName-Desktop_Qt_5_14_2_MSVC2019_64bit-Release),在其中的 release 文件夹下,找到您的程序 .exe 文件(例如 YourProjectName.exe),并将其复制到刚刚创建的 Stamp_Detector_Release 文件夹中。

第 3 步:使用 windeployqt 自动部署 Qt 依赖

这是最关键的一步。

  1. 从 Windows 的“开始”菜单中,找到并打开 “Qt 5.14.2 (MSVC 2019 64-bit)” 这个特殊的命令行终端。

    注意: 必须使用这个 Qt 提供的终端,因为它已经为您配置好了所有必要的环境变量。不要使用普通的 cmd.exe

  2. 在打开的命令行终端中,首先用 cd 命令切换到您创建的部署文件夹:

    cd C:\Users\YourUsername\Desktop\Stamp_Detector_Release
    
  3. 运行 windeployqt 命令。由于您的项目是 QML 应用,必须使用 --qmldir 参数指向 QML 源文件所在的目录,这样工具才能正确地复制 QML 相关的依赖项。

    windeployqt --qmldir C:\path\to\your\project\source_code YourProjectName.exe
    ```*   请将 `C:\path\to\your\project\source_code` 替换为您存放 `main.qml` 的真实项目源码路径。
    *   请将 `YourProjectName.exe` 替换为您 `.exe` 文件的真实名称。
  4. 命令执行后,您会看到终端输出大量“Copying …”信息。完成后,您的部署文件夹中会自动出现很多 Qt 相关的 DLL 文件和 pluginsqml 等子文件夹。

第 4 步:手动复制非 Qt 依赖项

windeployqt 只认识 Qt 的东西,它不认识 OpenCV 和您的 ONNX 模型。我们需要手动将它们复制过来。

  1. 复制 OpenCV DLL

    • 从您的 OpenCV bin 目录(例如 C:\opencv\build\x64\vc16\bin\)中,找到 opencv_world4120.dll 文件。
    • 将其复制Stamp_Detector_Release 文件夹中,与您的 .exe 文件放在一起。
  2. 复制 ONNX 模型

    • 在您的 C++ 代码中,模型路径是 ./models/best.onnx。这意味着程序运行时会从当前目录下寻找一个名为 models 的文件夹。
    • 因此,在 Stamp_Detector_Release 文件夹中,手动创建一个名为 models 的新文件夹
    • 然后将您的 best.onnx 模型文件复制到这个新的 models 文件夹中。
第 5 步:最终检查与测试

此时,您的 Stamp_Detector_Release 文件夹结构应该如下所示:

Stamp_Detector_Release/
├── YourProjectName.exe         <-- 你的程序
├── opencv_world4120.dll        <-- OpenCV 库
├── Qt5Core.dll                 <-- 以下由 windeployqt 生成
├── Qt5Gui.dll
├── Qt5Qml.dll
├── Qt5Quick.dll
├── ... (其他 Qt DLLs) ...
├── models/                     <-- 你手动创建的文件夹
│   └── best.onnx               <-- 你的模型文件
├── platforms/
│   └── qwindows.dll
├── qml/
│   ├── QtQuick/
│   ├── QtQuick.2/
│   └── QtQuick/Controls.2/
└── ... (其他由 windeployqt 生成的文件夹和文件) ...

现在,您可以尝试双击 Stamp_Detector_Release 文件夹中的 YourProjectName.exe。如果程序能够正常启动并使用所有功能,恭喜您,部署成功!

您可以将整个 Stamp_Detector_Release 文件夹压缩成一个 .zip 文件,发送给其他人,他们解压后即可直接运行。

6.4 常见问题排查

  • “无法定位程序输入点 …” 或 “缺少 VCRUNTIME140.dll”

    • windeployqt 通常会自动复制所需的 C++ 运行库。如果失败了,您可能需要在目标电脑上安装“Visual C++ Redistributable for Visual Studio 2019”。
  • “This application failed to start because it could not find or load the Qt platform plugin “windows””

    • 这几乎总是因为没有正确运行 windeployqt,或者没有在 Qt 提供的专用命令行中运行它,导致 platforms/qwindows.dll 文件没有被正确复制。
  • 程序能启动,但点击“识别”按钮后无反应或闪退

    • 检查 opencv_world4120.dll 是否已复制到 .exe 同级目录。
    • 检查 models/best.onnx 的路径和文件名是否完全正确。

七、结语

通过这个 Demo,我们完整地展示了如何将强大的 QML 前端与高性能的 C++、OpenCV 后端相结合,构建、调试并最终成功部署一个满足特定工业需求的智能视觉检测应用。Qt 5.14 的稳定性和 windeployqt 工具的便捷性,使其成为在多样化的工业环境中开发和交付此类应用的理想选择。

这个项目仅仅是一个起点。基于这个框架,我们可以轻松扩展更多功能,例如:

  • 支持多种缺陷类型的检测。
  • 集成数据库,记录和追溯检测结果。
  • 提供更详细的报表和数据分析功能。

希望本文能为正在探索工业 AI 视觉应用的开发者和企业提供有价值的参考和启发。


http://www.lryc.cn/news/616226.html

相关文章:

  • 线程池的核心线程数与最大线程数怎么设置
  • 基于FFmpeg的B站视频下载处理
  • 简要介绍交叉编译工具arm-none-eabi、arm-linux-gnueabi与arm-linux-gnueabihf
  • 【iOS】JSONModel源码学习
  • 2025.8.10总结
  • mpv core_thread pipeline
  • 第16届蓝桥杯Scratch选拔赛初级及中级(STEMA)2025年4月13日真题
  • ARM保留的标准中断处理程序入口和外设中断处理程序入口介绍
  • Python设计模式 - 装饰模式
  • 双亲委派机制是什么?
  • 亚麻云之轻云直上EC2
  • 硬件开发_基于STM32单片机的智能电梯系统
  • 关键基础设施中的新兴技术如何扩大网络风险
  • Java .class文件反编译成 .java文件
  • LeetCode 括号生成
  • 机器学习数学基础:46.Mann-Kendall 序贯检验(Sequential MK Test)
  • AtomicStampedReference解决方案
  • QT常用控件三
  • 浏览器CEFSharp88+X86+win7 之js交互开启(五)
  • 深入理解C语言一维数组的本质:数组名、指针常量与访问细节
  • 女子试穿4条裤子留下血渍赔50元引争议:消费责任边界在哪?
  • 无须炮解,打开即是Pro版
  • (LeetCode 每日一题) 869. 重新排序得到 2 的幂 (哈希表+枚举)
  • Python中随机化列表元素的详细方法
  • LintCode第604题-滑动窗口内数的和
  • DAY36打卡
  • 自创论述类文本阅读:论温泉
  • ubuntu 安装内核模块驱动 DKMS 介绍
  • 基于Ubuntu20.04的环境,编译QT5.15.17源码
  • ubuntu22.04+samba