基于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基本要求
- 用户界面 (UI):使用 Qt 5.14 QML 技术构建现代化、流畅的用户界面。
- 图像交互:支持从本地加载大尺寸图像,并允许用户通过鼠标在图像上拖拽出一个矩形选框。
- AI 推理:截取用户框选的子图,调用预先训练好的 YOLOv8 ONNX 模型进行推理。
- 结果展示:在原图的框选区域内,精确地标记出检测到的两个冲压点的位置。
- 跨平台兼容性:项目需能在 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 项目配置和运行
- 配置
.pro
文件: 确保你的.pro
文件包含了network
和opencv
的库。
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) 和路径需要与你的环境匹配。
-
放置 ONNX 模型: 将你训练好的
best.onnx
模型文件放到imageprocessor.cpp
中指定的路径,或者一个可以被程序访问的位置。 -
编译和运行: 现在,你可以编译并运行你的项目了!
四、工作流程演示
- 启动程序:程序启动后,显示主界面。
- 加载图像:点击“加载图像”按钮,从本地文件系统中选择一张待检测的大图。
- 框选区域:在加载的图像上,按住鼠标左键并拖动,会形成一个蓝色的半透明矩形框。
- 执行检测:松开鼠标后,选框确定。“开始识别”按钮变为可用状态。点击该按钮。
- 后台处理:
- QML 将原始
QImage
和选框的QRect
传递给 C++ 的startDetection
函数。 - C++ 代码根据
QRect
从QImage
中裁剪出子图。 - 子图被转换为
cv::Mat
格式,并经过预处理后送入 YOLOv8 ONNX 模型。 - 模型推理完成后,C++ 代码解析输出,得到冲压点的坐标。
- QML 将原始
- 结果显示:
- C++ 通过
detectionComplete
信号将坐标列表发送回 QML。 - QML 中的
Connections
对象接收到信号,更新detectedPoints
属性。 Repeater
检测到model
的变化,自动在图像上对应的坐标处创建矩形标记。
- C++ 通过
五、为程序添加图标
一个专业的应用程序,离不开一个醒目的图标。它不仅是品牌的象征,也是用户在桌面和任务栏上识别程序的关键。在 Qt 中,为程序添加图标分为两步:
- 设置可执行文件 (.exe) 的图标:这是您在 Windows 文件资源管理器中看到的图标。
- 设置窗口及任务栏的图标:这是程序运行时,在窗口左上角和 Windows 任务栏上显示的图标。
5.1 准备图标文件 (.ico)
要同时满足上述两个要求,最标准的做法是使用一个 .ico
格式的图标文件。.ico
文件可以内嵌多种不同尺寸的位图,以适应不同的显示场景(例如大图标、小图标、列表视图等)。
- 设计图标:首先,您需要一个正方形的图标设计图,例如 PNG 格式,尺寸建议为 256x256 或 512x512 像素,这样可以保证高质量显示。
- 转换为 .ico:您可以使用在线工具或专用软件将您的 PNG 图片转换为
.ico
文件。只需搜索 “PNG to ICO converter” 即可找到很多免费的在线服务。 - 命名和存放:将转换后的图标文件命名为
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 资源系统
- 在 Qt Creator 中,右键
qml.qrc
文件。 - 单击
添加
->添加现有文件
。 - 找到并选择您之前准备好的
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 准备工作
-
Release 模式编译:
- 在 Qt Creator 左侧的构建套件选择器中,确保选择的是 Release 模式,而不是 Debug。
- Release 模式会进行代码优化,生成的程序体积更小,运行速度更快,并且不依赖于调试库。
- 点击“构建”->“构建项目”,生成 Release 版本的
.exe
文件。
-
找到
windeployqt.exe
:- 这是 Qt 官方提供的部署工具,它能自动扫描
.exe
文件并复制大部分所需的 Qt 依赖项。 - 它的位置在您的 Qt 安装目录中,例如:
C:\Qt\5.14.2\msvc2019_64\bin\windeployqt.exe
。
- 这是 Qt 官方提供的部署工具,它能自动扫描
-
找到 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 依赖
这是最关键的一步。
-
从 Windows 的“开始”菜单中,找到并打开 “Qt 5.14.2 (MSVC 2019 64-bit)” 这个特殊的命令行终端。
注意: 必须使用这个 Qt 提供的终端,因为它已经为您配置好了所有必要的环境变量。不要使用普通的
cmd.exe
。 -
在打开的命令行终端中,首先用
cd
命令切换到您创建的部署文件夹:cd C:\Users\YourUsername\Desktop\Stamp_Detector_Release
-
运行
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` 文件的真实名称。
-
命令执行后,您会看到终端输出大量“Copying …”信息。完成后,您的部署文件夹中会自动出现很多 Qt 相关的 DLL 文件和
plugins
、qml
等子文件夹。
第 4 步:手动复制非 Qt 依赖项
windeployqt
只认识 Qt 的东西,它不认识 OpenCV 和您的 ONNX 模型。我们需要手动将它们复制过来。
-
复制 OpenCV DLL:
- 从您的 OpenCV bin 目录(例如
C:\opencv\build\x64\vc16\bin\
)中,找到opencv_world4120.dll
文件。 - 将其复制到
Stamp_Detector_Release
文件夹中,与您的.exe
文件放在一起。
- 从您的 OpenCV bin 目录(例如
-
复制 ONNX 模型:
- 在您的 C++ 代码中,模型路径是
./models/best.onnx
。这意味着程序运行时会从当前目录下寻找一个名为models
的文件夹。 - 因此,在
Stamp_Detector_Release
文件夹中,手动创建一个名为models
的新文件夹。 - 然后将您的
best.onnx
模型文件复制到这个新的models
文件夹中。
- 在您的 C++ 代码中,模型路径是
第 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 视觉应用的开发者和企业提供有价值的参考和启发。