目录

  • 一、概述
    • 1.1 背景介绍:从“训练”到“部署”
    • 1.2 学习目标
  • 二、在C++中集成ONNX模型
    • 2.1 准备模型文件
    • 2.2 修改`Backend`以加载和运行模型
  • 三、关键一步:输出结果的后处理
  • 四、运行与验证
  • 五、总结与展望

一、概述

1.1 背景介绍:从“训练”到“部署”

在上一篇文章中,我们成功地跨入了Python的世界,完整地经历了一次AI模型从数据标注到训练、再到导出的全过程。我们最终的产出是一个名为best.onnx的模型文件——这是AI算法工程师工作的结晶。

然而,这个模型本身还只是一个静态的权重文件,无法独立工作。本篇文章的核心任务,就是完成从“算法研发”到“软件部署”这至关重要的一跃。我们将回归C++的主战场,学习如何使用OpenCV强大的DNN(深度神经网络)模块,在我们的Qt应用程序中加载并运行这个ONNX模型。这个过程,我们称之为模型推理(Inference)

1.2 学习目标

通过本篇的学习,读者将能够:

  1. 在C++中加载ONNX模型,并对输入的图像进行预处理,使其符合模型的输入要求。
  2. 执行模型的前向传播(推理),获取模型的原始输出。
  3. 掌握关键的后处理技术:解析YOLOv8复杂的输出张量,提取出边界框、置信度和类别信息。
  4. 将识别出的瑕疵信息,可视化地绘制在QML界面显示的图像上,让AI的结果“看得见”。

二、在C++中集成ONNX模型

2.1 准备模型文件

首先,将上一章生成的best.onnx模型文件,从runs/detect/train/weights目录中,拷贝到项目根目录下,方便程序访问。

2.2 修改Backend以加载和运行模型

我们将为Backend类增加一个YOLOv8的封装,用于处理所有与模型推理相关的逻辑。

1. 编写代码 (backend.h)

// backend.h
#ifndef BACKEND_H
#define BACKEND_H#include <QObject>
#include <QImage>
#include <opencv2/dnn.hpp> // 1. 包含OpenCV DNN模块头文件class ImageProvider;class Backend : public QObject
{Q_OBJECT
public:explicit Backend(ImageProvider *provider, QObject *parent = nullptr);Q_INVOKABLE void startScan();signals:void imageReady(const QString &imageId);void statusMessageChanged(const QString &message);private:// 2. 添加一个私有方法用于AI推理cv::Mat runInference(const cv::Mat &inputImage);ImageProvider *m_imageProvider;cv::dnn::Net m_net; // 3. 添加一个Net对象,用于表示我们的神经网络std::vector<std::string> m_classNames; // 4. 用于存储类别名称
};#endif // BACKEND_H

2. 编写代码 (backend.cpp)
这是本章的核心。我们将修改构造函数以加载模型,并实现runInference方法。

// backend.cpp
#include "backend.h"
#include "imageprovider.h"
#include <QDebug>
#include <QDir>
#include <opencv2/imgcodecs.hpp>// ... (matToQImage辅助函数保持不变)Backend::Backend(ImageProvider *provider, QObject *parent): QObject(parent), m_imageProvider(provider)
{// --- 1. 加载ONNX模型 ---QString modelPath = QDir::currentPath() + "/../../best.onnx";try {m_net = cv::dnn::readNetFromONNX(modelPath.toStdString());if (m_net.empty()) {qWarning() << "Failed to load ONNX model!";} else {qDebug() << "ONNX model loaded successfully.";// 设置计算后端。CPU是默认选项,但可以显式指定m_net.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV);m_net.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);}} catch (const cv::Exception& e) {qWarning() << "Error loading model:" << e.what();}// --- 2. 加载类别名称 ---// 这个顺序必须与训练时的.yaml文件严格一致!m_classNames = {"neck_defect", "thread_defect", "head_defect"};
}cv::Mat Backend::runInference(const cv::Mat &inputImage)
{if (m_net.empty()) {qDebug() << "Network not loaded.";return inputImage;}// --- 3. 图像预处理 ---// YOLOv8需要一个640x640的方形输入const int inputWidth = 640;const int inputHeight = 640;cv::Mat blob;// 将图像转换为blob格式:调整尺寸、归一化(像素值/255)、通道重排(BGR->RGB)cv::dnn::blobFromImage(inputImage, blob, 1./255., cv::Size(inputWidth, inputHeight), cv::Scalar(), true, false);// --- 4. 执行推理 ---m_net.setInput(blob);std::vector<cv::Mat> outputs;m_net.forward(outputs, m_net.getUnconnectedOutLayersNames());// outputs[0]是模型的原始输出,我们需要对其进行后处理// ... 后处理代码将在下一节添加 ...// 暂时返回原始图像return inputImage;
}void Backend::startScan()
{// ... (加载图像的代码保持不变)QString imagePath = QDir::currentPath() + "/../../dataset/screw/test/scratch_head/000.png";cv::Mat sourceMat = cv::imread(imagePath.toStdString());if (sourceMat.empty()) { /* ... */ return; }// --- 5. 调用推理函数 ---cv::Mat resultMat = runInference(sourceMat);QImage imageQ = matToQImage(resultMat);if (imageQ.isNull()){ /* ... */ return; }m_imageProvider->updateImage(imageQ);emit imageReady("screw_processed");emit statusMessageChanged("AI推理完成!");
}

关键代码分析:
(1) cv::dnn::readNetFromONNX(...): OpenCV DNN模块中用于从ONNX文件加载模型的函数。加载成功后会返回一个cv::dnn::Net对象。
(2) m_classNames: 我们手动定义了一个std::vector<std::string>来存储类别名称。注意:这里的顺序必须与训练时dataset.yaml文件中的names顺序严格一致,因为模型输出的类别ID是基于这个顺序的。
(3) cv::dnn::blobFromImage(...): 这是一个强大的预处理函数,它能一步到位地完成YOLOv8所需的几项操作:
- 1./255.:归一化因子,将像素值从0-255范围缩放到0-1范围。
- cv::Size(640, 640):将图像缩放或填充到640x640的尺寸。
- cv::Scalar(): 减去均值,此处不减。
- true: 交换R和B通道(BGR -> RGB),因为YOLOv8是在RGB图像上训练的。
- false: 不裁剪。
(4) m_net.forward(...): 执行网络的前向传播,即推理。推理结果会存放在outputs这个std::vector<cv::Mat>中。

三、关键一步:输出结果的后处理

YOLOv8的原始输出是一个cv::Mat,其维度通常是1 x (4 + num_classes) x 8400。我们需要编写代码来解析这个复杂的张量,提取出我们真正需要的信息。

【核心概念:解析YOLOv8输出】
对于输出矩阵的每一列(共8400列,代表8400个可能的检测框):

  • 前4行是边界框的坐标(中心x, 中心y, 宽, 高)。
  • 后面的N行(N是类别数)是该框属于每个类别的置信度分数。

我们的后处理流程是:

  1. 遍历所有8400个可能的检测框。
  2. 找到每个框置信度最高的类别。
  3. 如果这个最高置信度大于一个阈值(例如0.5),则认为这是一个有效的检测。
  4. 将所有有效的检测框及其信息收集起来。
  5. 由于同一个物体可能被多个框检测到,最后使用**非极大值抑制(NMS)**来剔除重叠的多余框。

【例8-1】 实现后处理并可视化结果。

1. 编写代码 (backend.cpp)
我们将用完整的后处理逻辑来替换runInference函数中的注释部分。

// backend.cppcv::Mat Backend::runInference(const cv::Mat &inputImage)
{if (m_net.empty()) { /* ... */ return inputImage; }cv::Mat blob;// --- 图像预处理 (代码同上) ---cv::dnn::blobFromImage(inputImage, blob, 1./255., cv::Size(640, 640), cv::Scalar(), true, false);m_net.setInput(blob);std::vector<cv::Mat> outputs;m_net.forward(outputs, m_net.getUnconnectedOutLayersNames());cv::Mat output_buffer = outputs[0]; // [1, num_classes + 4, 8400]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]// --- 1. 后处理 ---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;float x_factor = (float)inputImage.cols / 640.f;float y_factor = (float)inputImage.rows / 640.f;for (int i = 0; i < output_buffer.rows; i++) {cv::Mat row = output_buffer.row(i);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));}}// --- 2. 非极大值抑制 (NMS) ---std::vector<int> indices;cv::dnn::NMSBoxes(boxes, confidences, conf_threshold, nms_threshold, indices);// --- 3. 结果可视化 ---cv::Mat resultImage = inputImage.clone();for (int idx : indices) {cv::Rect box = boxes[idx];int class_id = class_ids[idx];// 绘制边界框cv::rectangle(resultImage, box, cv::Scalar(0, 255, 0), 2);// 绘制标签std::string label = cv::format("%s: %.2f", m_classNames[class_id].c_str(), confidences[idx]);cv::putText(resultImage, label, cv::Point(box.x, box.y - 10), cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(0, 255, 0), 2);}return resultImage;
}

关键代码分析:
(1) 坐标还原: 模型的输出是基于640x640输入的归一化坐标,我们必须乘以x_factory_factor将其还原到原始图像的坐标系。
(2) cv::minMaxLoc(...): 一个方便的函数,用于在单行/列的Mat中快速找到最大值(置信度)及其位置(类别ID)。
(3) cv::dnn::NMSBoxes(...): OpenCV DNN模块内置的非极大值抑制函数。它接收原始的框和置信度,返回一个indices向量,其中包含了最终保留下来的框的索引。
(4) cv::rectangle(...)cv::putText(...): OpenCV的绘图函数,用于在最终结果图上画出边界框和带有类别、置信度的标签文本。

四、运行与验证

现在,一切准备就绪。重新编译并运行ScrewDetector项目。

1. 运行结果
点击“开始检测”按钮。稍等片刻,界面上将会显示出带有绿色边界框和标签的螺丝图像。程序成功地识别出了图片中的head_defect(头部瑕疵)!同时,状态栏也会更新为“AI推理完成!”。
在这里插入图片描述

2. 尝试其他图片
可以尝试修改Backend::startScan()中的imagePath,换成数据集中其他的图片(例如good文件夹下的图片),重新运行,观察AI模型是否会误检。运行效果如下:
在这里插入图片描述
可以看到,对于没有瑕疵的图片,AI模型不会检测出瑕疵,验证了模型的有效性。

五、总结与展望

在本篇文章中,我们成功地跨越了Python与C++之间的鸿沟,将上一章训练的AI模型部署到了我们的Qt应用程序中。我们掌握了使用OpenCV DNN模块加载ONNX模型对输入图像进行预处理以及最关键的YOLOv8输出后处理技术。

至此,我们的应用程序已经拥有了真正的“AI大脑”,能够对静态图片进行智能瑕疵检测。然而,在真实的工业场景中,产品是连续不断地在传送带上移动的。如何处理来自摄像头的实时视频流?如何保证处理速度?

这将是我们下一篇文章【《使用Qt Quick从零构建AI螺丝瑕疵检测系统》——9. 接入真实硬件:驱动USB摄像头】的核心主题。我们将让程序从处理单张图片,升级为处理实时动态视频。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/91270.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/91270.shtml
英文地址,请注明出处:http://en.pswp.cn/bicheng/91270.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【动态规划 | 多状态问题】动态规划求解多状态问题

算法相关知识点可以通过点击以下链接进行学习一起加油&#xff01;斐波那契数列模型路径问题多状态问题通常涉及多个决策点和状态转换&#xff0c;解决起来复杂且计算量大。动态规划作为一种强大的算法工具&#xff0c;能够通过将问题分解为子问题并逐步求解&#xff0c;显著提…

【HTTP】防XSS+SQL注入:自定义HttpMessageConverter过滤链深度解决方案

防XSSSQL注入&#xff1a;自定义HttpMessageConverter过滤链深度解决方案一、安全威胁模型分析二、自定义HttpMessageConverter架构设计2.1 技术栈组成三、完整实现代码3.1 安全过滤工具类3.2 自定义HttpMessageConverter3.3 Spring安全配置四、深度防御增强方案4.1 SQL注入参数…

学习游戏制作记录(冻结敌人时间与黑洞技能)7.30

1.实现剑击中敌人时冻结敌人时间Enemy脚本&#xff1a;public float defaultMoveSpeed;//默认速度defaultMoveSpeed moveSpeed;//Awake&#xff08;&#xff09;中设置public virtual void FreezeTime(bool _timeFreeze)//冻结设置函数{if (_timeFreeze){moveSpeed 0;anim.sp…

【数据结构】真题 2016

待补充已知表头元素为c的单链表在内存中的存储状态如下表所示地址元素链接地址1000Ha1010H1004Hb100CH1008Hc1000H100CHdNULL1010He1004H1014H现将f存放于1014H处并插入到单链表中&#xff0c;若f在逻辑上位于a和e之间&#xff0c;则a, e, f的“链接地址”依次是&#xff08; &…

双线串行的 “跨界对话”:I2C 与 MDIO 的异同解析

在电子系统设计中,串行总线凭借其精简的信号线数量和灵活的拓扑结构,成为芯片间通信的主流选择。I2C(Inter-Integrated Circuit)和 MDIO(Management Data Input/Output)作为两种典型的双线串行总线,虽同属低速信号范畴,却在各自的应用领域扮演着不可替代的角色。本文将…

算法精讲:二分查找(二)—— 变形技巧

&#x1f3af; 算法精讲&#xff1a;二分查找&#xff08;二&#xff09;—— 变形技巧 &#x1f50d; 友情提示&#xff1a;&#xff1a;本小节含高能代码片段 &#x1f964; 阅读前请确保已掌握基础二分原理与实现代码片段可能包含不同程度的变形&#xff0c;请根据实际情况选…

两个程序配合实现了基于共享内存和信号量的进程间通信,具体说明如下:

第一个程序&#xff1a;共享内存读取程序&#xff08;消费者&#xff09;该程序作为消费者&#xff0c;从共享内存中读取数据&#xff0c;通过信号量保证只有当生产者写入数据后才能读取。/*4 - 读共享内存*/ #include<stdio.h> // 标准输入输出库 #inc…

JeecgBoot(1):前后台环境搭建

1 项目介绍 JeecgBoot 是一款基于 Java 的 AI 低代码平台&#xff0c;它采用了 SpringBoot、SpringCloud、Ant Design Vue3、Mybatis 等技术栈&#xff0c;并集成了代码生成器、AI 对话助手、AI 建表、AI 写文章等功能。JeecgBoot 的设计宗旨是实现简单功能零代码开发&#xf…

Nestjs框架: 关于 OOP / FP / FRP 编程

概述 在软件开发过程中&#xff0c;不同的编程范式为我们提供了多样化的思维方式与实现路径它们不仅影响着代码的结构和逻辑组织方式&#xff0c;也深刻影响着项目的可维护性、可扩展性以及团队协作效率 什么是 OOP、FP 和 FRP&#xff1f;首先从三个术语的含义入手 1 &#xf…

elememtor 添加分页功能

各位看官好&#xff0c;最近在忙着使用elementor搭建自己的网站&#xff0c;由于我不是专业的程序员和前端&#xff0c;又没有很多钱去找外包公司实现自己的设计&#xff0c;所以选择了elementor. 总的来说这是一个不错的wordpress 插件&#xff0c;也让我们这种非专业的网站设…

关于“PromptPilot” 之2 -目标系统:Prompt构造器

目标系统&#xff1a;Prompt构造器想法首先&#xff0c;在抽象层对PromptPilot进行封装给出提示词形成过程的全部环节。然后&#xff0c;在 形成一套确定的提示词后再为 小规模试点方案生成一整套开发工具并配套集成开发环境和指南。最后&#xff0c;在小规模试点成功后进行拓展…

短剧小程序系统开发:重塑影视内容消费格局

在数字化浪潮的推动下&#xff0c;影视内容消费正经历着深刻的变革。短剧小程序系统开发作为这一变革的重要力量&#xff0c;正在重塑影视内容消费的格局&#xff0c;为用户带来更加个性化、便捷化的观影体验。传统影视内容消费往往受到时间和空间的限制&#xff0c;用户需要前…

一文掌握最新版本Monocle3单细胞轨迹(拟时序)分析

许多大佬的软件想要构建一个大而美的生态&#xff0c;从 monocle2 开始就能做单细胞的质控、降维、分群、注释这一系列的分析&#xff0c;但不幸的是我们只知道 monocle 系列还是主要做拟时序分析&#xff0c;一方面是因为 Seurat 有先发优势&#xff0c;出名要趁早&#xff0c…

spark入门-helloword

我们学习编程语言的时候&#xff0c;第一个程序就是打印一下 “hello world” &#xff0c;对于大数据领域的第一个任务则是wordcount。那我们就开始我们的第一个spark任务吧&#xff01; 下载spark 官方下载地址&#xff1a;Apache Download Mirrors 下载完毕以后&#xff0c…

雷达系统设计学习:自制6GHz FMCW Radar

国外大神自制6GHZ FMCW Radar开源项目: https://github.com/Ttl/fmcw3 引言 之前我做过一个简单的调频连续波&#xff08;FMCW&#xff09;雷达&#xff0c;能够探测到100米范围内人体大小的物体。虽然它确实能用&#xff0c;但由于预算有限&#xff0c;还有很大的改进空间。 …

系统选择菜单(ubuntu grub)介绍

好的&#xff0c;我们来详细解释一下什么是Ubuntu的GRUB菜单。 简单来说&#xff0c;GRUB菜单是您电脑启动时看到的第一个交互界面&#xff0c;它就像一个“系统选择”菜单&#xff0c;让您决定接下来要启动哪个操作系统或进入哪种模式。详细解释 1. GRUB是什么&#xff1f; GR…

方案C,version2

实现一个简单的Helloworld网页,并通过GitHub Actions自动构建并推送到公开仓库的gh-pages分支。同时,使用PAT进行认证,确保源码在私有仓库中,构建后的静态文件在公开仓库中。 重新设计deploy.yml内容如下(针对纯静态文件,无需构建过程): 步骤: 检出私有仓库源码。 由于…

R 语言科研绘图 --- 其他绘图-汇总1

在发表科研论文的过程中&#xff0c;科研绘图是必不可少的&#xff0c;一张好看的图形会是文章很大的加分项。 为了便于使用&#xff0c;本系列文章介绍的所有绘图都已收录到了 sciRplot 项目中&#xff0c;获取方式&#xff1a; R 语言科研绘图模板 --- sciRplothttps://mp.…

webpack 原理及使用

【点赞收藏加关注,前端技术不迷路~】 一、webpack基础 1.核心概念 1)entry:定义入口,webpack构建的第一步 module.exports ={entry:./src/xxx.js } 2)output:出口(输出) 3)loader:模块加载器,用户将模块的原内容按照需求加载成新内容 比如文本加载器raw-loade…

「日拱一码」039 机器学习-训练时间VS精确度

目录 时间-精度权衡曲线&#xff08;不同模型复杂度&#xff09; 训练与验证损失对比 帕累托前沿分析&#xff08;3D&#xff09; 在机器学习实践中&#xff0c;理解模型收敛所需时间及其与精度的关系至关重要。下面介绍如何分析模型收敛时间与精度之间的权衡&#xff0c;并…