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

《使用Qt Quick从零构建AI螺丝瑕疵检测系统》——9. 接入真实硬件:驱动USB摄像头

目录

  • 一、概述
    • 1.1 背景介绍:从“静态”到“动态”
    • 1.2 学习目标
  • 二、多线程摄像头处理框架
    • 2.1 引入Qt Multimedia模块
    • 2.2 创建`CameraWorker`类
  • 三、主线程与工作线程的协同
  • 四、运行与验证
  • 五、走向工业级应用:性能与实践
    • 5.1 关于工业相机SDK
    • 5.2 CPU vs GPU:推理性能的选择
  • 六、总结与展望

一、概述

1.1 背景介绍:从“静态”到“动态”

在此前的文章中,我们已经成功地将一个训练好的YOLOv8模型部署到了C++后端,并实现了对单张静态图片的智能瑕疵检测。这验证了我们AI算法流程的正确性。然而,在真实的工业场景中,产品是在流水线上连续不断地移动的,我们需要处理的是来自摄像头的实时视频流,而不是单张图片。

本篇文章的核心任务,就是完成从**“静态图像处理”“动态视频处理”的关键升级。我们将使用Qt官方提供的Qt Multimedia**模块来驱动真实的USB摄像头,捕获视频帧,并将其源源不断地送入我们已经构建好的AI推理引擎中,最终实现对动态画面的实时瑕疵检测。

1.2 学习目标

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

  1. 在Qt项目中配置并使用Qt Multimedia模块。
  2. 学习如何查找、初始化并控制一个USB摄像头。
  3. 掌握从摄像头捕获视频帧(QVideoFrame),并将其高效转换为OpenCV cv::Mat格式进行处理的关键技术。
  4. 实践多线程编程:将耗时的摄像头捕获与AI推理任务放到一个独立的工作线程中,以确保主UI线程的绝对流畅,避免界面卡顿。

二、多线程摄像头处理框架

直接在主线程(UI线程)中操作摄像头和进行AI推理,会长时间占用CPU,导致界面冻结,这是桌面应用开发的大忌。因此,我们必须采用多线程方案。

【核心概念:UI线程与工作线程】

  • UI线程(主线程): 专门负责渲染界面、响应用户输入(如点击、拖动)。它必须时刻保持流畅。
  • 工作线程(子线程): 专门负责执行所有耗时的后台任务,如摄像头数据捕获、AI推理、文件读写等。

具体设计思路如下:

  1. 创建一个CameraWorker类,它将在一个独立的工作线程中运行,负责所有与摄像头硬件的交互和AI推理。
  2. Backend类(运行在UI线程)作为总指挥,负责创建并管理CameraWorker线程。
  3. CameraWorker通过信号,将处理好的、带有检测结果的图像帧,安全地传递回Backend,再由Backend更新到QML界面。

2.1 引入Qt Multimedia模块

要使用摄像头功能,我们必须告诉CMake链接Qt Multimedia库。
1. 编写代码 (CMakeLists.txt)
打开项目的CMakeLists.txt文件,在find_packagetarget_link_libraries指令中分别添加Multimedia

# ...
find_package(Qt6 REQUIRED COMPONENTS Multimedia) # 添加Multimedia
# ...
target_link_libraries(appScrewDetector PRIVATE# ...PRIVATE Qt6::Multimedia # 链接Multimedia库
)
# ...

2.2 创建CameraWorker

【例9-1】 创建一个负责后台处理的CameraWorker类。

在Qt Creator中,添加新文件... -> C/C++ -> C++ Class

  • 类名: CameraWorker
  • 基类: QObject

1. 编写头文件代码 (cameraworker.h)

#ifndef CAMERAWORKER_H
#define CAMERAWORKER_H#include <QObject>
#include <QVideoFrame>
#include <QCamera>
#include <QMediaCaptureSession>
#include <QVideoSink>
#include <opencv2/dnn.hpp>
#include <vector>
#include <string>class QCamera;
class QMediaCaptureSession;
class QVideoSink;class CameraWorker : public QObject
{Q_OBJECT
public:explicit CameraWorker(QObject *parent = nullptr);~CameraWorker();signals:// 信号:一帧新的、处理完毕的图像已准备好void newFrameReady(const QImage &frame);// 信号:用于向UI传递状态或错误信息void statusMessageChanged(const QString &message);public slots:// 槽:启动摄像头void startCamera();// 槽:停止摄像头void stopCamera();private slots:// 槽:当摄像头捕获到新的一帧时被调用void processFrame(const QVideoFrame &frame);private:cv::Mat runInference(const cv::Mat &inputImage);QCamera *m_camera = nullptr;QMediaCaptureSession *m_captureSession = nullptr;QVideoSink *m_videoSink = nullptr;cv::dnn::Net m_net;std::vector<std::string> m_classNames;
};#endif // CAMERAWORKER_H

2. 编写实现文件代码 (cameraworker.cpp)
这是本章的核心逻辑,包括摄像头的初始化、视频帧的格式转换和AI推理。

#include "cameraworker.h"
#include <QCameraDevice>
#include <QMediaDevices>
#include <QDebug>
#include <QDir>
#include <opencv2/imgproc.hpp>// 辅助函数:将QVideoFrame转换为cv::Mat,通过QImage作为中介
static cv::Mat videoFrameToMat(const QVideoFrame &frame) {if (!frame.isValid()) {return cv::Mat();}// 1. 将QVideoFrame转换为QImage// QImage的构造函数已经为我们处理了复杂的内存映射和格式问题QImage image = frame.toImage();if (image.isNull()) {return cv::Mat();}// 2. 将QImage转换为cv::Mat// 确保图像是3通道或4通道的if (image.format() != QImage::Format_RGB888 && image.format() != QImage::Format_ARGB32) {image = image.convertToFormat(QImage::Format_RGB888);}// 根据QImage的格式创建对应的cv::Matcv::Mat mat(image.height(), image.width(), CV_8UC3, (void*)image.constBits(), image.bytesPerLine());// 返回一个深拷贝,确保数据独立和线程安全return mat.clone();
}CameraWorker::CameraWorker(QObject *parent) : QObject(parent)
{// ... (加载ONNX模型和类别名称的代码与上一章Backend的构造函数完全相同)QString modelPath = QDir::currentPath() + "/../../best.onnx";m_net = cv::dnn::readNetFromONNX(modelPath.toStdString());m_classNames = {"neck_defect", "thread_defect", "head_defect"};// ... (错误检查和设置后端)
}CameraWorker::~CameraWorker()
{stopCamera();
}void CameraWorker::startCamera()
{if (m_camera && m_camera->isActive()) {return; // 如果已在运行,则不执行任何操作}const QList<QCameraDevice> cameras = QMediaDevices::videoInputs();if (cameras.isEmpty()) {emit statusMessageChanged("错误: 未找到可用的摄像头");return;}m_camera = new QCamera(cameras.first(), this);m_captureSession = new QMediaCaptureSession(this);m_videoSink = new QVideoSink(this);m_captureSession->setCamera(m_camera);m_captureSession->setVideoSink(m_videoSink);// 关键连接:当VideoSink接收到新的一帧时,触发我们的处理槽函数connect(m_videoSink, &QVideoSink::videoFrameChanged, this, &CameraWorker::processFrame);m_camera->start();emit statusMessageChanged("摄像头已启动");
}void CameraWorker::stopCamera()
{if (m_camera) {m_camera->stop();// 释放资源delete m_camera; m_camera = nullptr;delete m_captureSession; m_captureSession = nullptr;delete m_videoSink; m_videoSink = nullptr;emit statusMessageChanged("摄像头已停止");}
}void CameraWorker::processFrame(const QVideoFrame &frame)
{if (!frame.isValid()) return;// 1. 格式转换cv::Mat mat = videoFrameToMat(frame);if (mat.empty()) return;// QVideoFrame通常是BGRA格式,我们需要BGRcv::cvtColor(mat, mat, cv::COLOR_BGRA2BGR);// 2. AI推理 (复用上一章的runInference函数)cv::Mat resultMat = runInference(mat);// 3. 将结果转换为QImage并发送信号QImage resultImage(resultMat.data, resultMat.cols, resultMat.rows, resultMat.step, QImage::Format_RGB888);emit newFrameReady(resultImage.copy()); // 使用copy确保线程安全
}cv::Mat CameraWorker::runInference(const cv::Mat &inputImage)
{// 这个函数的全部内容与上一章Backend中的runInference完全相同// 包括预处理、前向传播、后处理和绘制结果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);// --- 执行推理 ---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]// --- 后处理 ---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));}}// --- 非极大值抑制 (NMS) ---std::vector<int> indices;cv::dnn::NMSBoxes(boxes, confidences, conf_threshold, nms_threshold, indices);// --- 结果可视化 ---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) QCamera: 代表一个物理摄像头设备。
(2) QMediaCaptureSession: 管理捕获会话,将摄像头(输入)和“接收器”(输出)连接起来。
(3) QVideoSink: 一个视频“接收器”,它的作用是从摄像头会话中接收视频帧。它有一个核心信号videoFrameChanged
(4) videoFrameToMat(...): 这是一个关键的辅助函数。QVideoFrame是Qt多媒体框架中的视频帧格式,我们需要将其内存数据映射出来,并包装成一个cv::Mat对象,以便OpenCV能够处理。
(5) processFrame(...): 这是我们的实时处理循环。每当摄像头有新的一帧图像,这个槽函数就会被触发,执行“格式转换 -> AI推理 -> 发送结果”的流程。

三、主线程与工作线程的协同

现在,Backend需要扮演好“总指挥”的角色,负责创建CameraWorker并将其移动到新线程,并充当CameraWorker和QML之间的信号中介。

1. 修改Backend (backend.h)

// backend.h
#ifndef BACKEND_H
#define BACKEND_H#include <QObject>
#include <QThread> // 包含QThread头文件
#include "cameraworker.h" //包含定义摄像头工作类
class ImageProvider;
class CameraWorker; // 前向声明class Backend : public QObject
{Q_OBJECT
public:explicit Backend(ImageProvider *provider, QObject *parent = nullptr);~Backend();Q_INVOKABLE void startCamera();Q_INVOKABLE void stopCamera();signals:// 这两个信号的用途不变,但现在由Backend转发void imageReady(const QString &imageId);void statusMessageChanged(const QString &message);// 内部信号,用于安全地与工作线程通信void startCameraRequested();void stopCameraRequested();private slots:// 槽,用于接收来自工作线程的图像void handleNewFrame(const QImage &frame);private:ImageProvider *m_imageProvider;QThread *m_cameraThread;CameraWorker *m_cameraWorker;
};
#endif // BACKEND_H

2. 修改Backend (backend.cpp)

// backend.cpp
#include "backend.h"
#include "imageprovider.h"
#include <QDebug>Backend::Backend(ImageProvider *provider, QObject *parent): QObject(parent), m_imageProvider(provider)
{m_cameraThread = new QThread(this);m_cameraWorker = new CameraWorker();// 关键一步:将worker对象移动到新线程m_cameraWorker->moveToThread(m_cameraThread);// --- 设置线程间的通信 ---// UI线程 -> 工作线程connect(this, &Backend::startCameraRequested, m_cameraWorker, &CameraWorker::startCamera);connect(this, &Backend::stopCameraRequested, m_cameraWorker, &CameraWorker::stopCamera);// 工作线程 -> UI线程connect(m_cameraWorker, &CameraWorker::newFrameReady, this, &Backend::handleNewFrame);connect(m_cameraWorker, &CameraWorker::statusMessageChanged, this, &Backend::statusMessageChanged);// 确保线程退出时,worker对象能被安全删除connect(m_cameraThread, &QThread::finished, m_cameraWorker, &QObject::deleteLater);m_cameraThread->start(); // 启动线程的事件循环
}Backend::~Backend()
{// 退出线程m_cameraThread->quit();
}void Backend::startCamera() { emit startCameraRequested(); }
void Backend::stopCamera() { emit stopCameraRequested(); }void Backend::handleNewFrame(const QImage &frame)
{m_imageProvider->updateImage(frame);// 发射带有时间戳的信号,强制QML刷新emit imageReady("live_frame?" + QString::number(QDateTime::currentMSecsSinceEpoch()));
}

3. 修改QML (Main.qml)
UI端的修改非常简单,只需将按钮的onClicked处理器指向新的Backend方法即可。

// Main.qml
// ...
RowLayout {// ...Button {id: startButtontext: "启动摄像头"onClicked: {backend.startCamera();}}Button {id: stopButtontext: "停止摄像头"onClicked: {backend.stopCamera();}}
}
// ...

四、运行与验证

现在,确保有一个USB摄像头连接到电脑。编译并运行ScrewDetector项目。

1. 运行结果

  • 点击“启动摄像头”按钮,状态栏会显示“摄像头已启动”。
  • 主界面的图像显示区将不再是静态图片,而是来自摄像头的实时视频画面!
  • 将一个螺丝(或任何物体)放到摄像头前,会看到AI模型正在实时地对它进行检测,并画出边界框。
  • 点击“停止摄像头”,视频流停止,状态栏更新。
    在这里插入图片描述
    2. 验证UI流畅性
    在摄像头运行和AI推理的同时,尝试拖动窗口、点击按钮。会发现UI界面依然保持着极高的响应速度,没有任何卡顿。这证明了我们的多线程架构是成功的!

五、走向工业级应用:性能与实践

我们已经成功地在桌面USB摄像头上实现了实时AI检测,这对于学习和验证算法流程来说是完美的。然而,当我们将目光投向真实的工业生产环境时,还需要考虑两个核心问题:相机选型推理性能

5.1 关于工业相机SDK

在本教程中,我们使用了Qt Multimedia模块来驱动标准的USB摄像头。这种方法通用性好,无需额外配置,非常适合教学和快速原型开发。

但在实际的工业项目中,通常会选用专业的工业相机(如海康、大华、Basler、FLIR等品牌)。这些相机相比消费级摄像头,在稳定性、帧率、曝光控制、触发模式等方面都更为出色。更重要的是,它们都提供了专属的软件开发工具包(SDK)

  • 为何使用SDK?

    1. 极致性能: SDK直接与相机硬件底层通信,绕过了操作系统的通用驱动层,能够最大限度地发挥相机的性能,实现更高帧率、更低延迟的图像采集。
    2. 高级功能: 只有通过SDK,才能访问到工业相机独有的高级功能,例如硬件触发(同步产线信号)、精准的曝光时间控制、自定义ROI(感兴趣区域)等。
    3. 稳定性: 官方SDK经过了严格测试,能保证在7x24小时不间断的工业环境中稳定运行。
  • 如何在项目中集成?
    集成SDK通常涉及:

    1. CMakeLists.txt中添加厂商提供的头文件目录和库文件(.lib, .dll, .so)。
    2. 在我们的CameraWorker类中,将调用QCamera的代码,替换为调用SDK提供的API函数(如MV_CC_CreateHandle, MV_CC_StartGrabbing等)。
    3. SDK通常会提供回调函数或事件机制来获取图像数据,我们需要在这些回调中,将厂商自定义的图像帧格式转换为cv::Mat

虽然具体API因厂商而异,但其底层的多线程处理思想与本章所介绍的框架是完全一致的。

5.2 CPU vs GPU:推理性能的选择

为了方便所有读者都能复现本教程,我们的AI推理过程完全在CPU上运行。读者在运行程序时可能会注意到,虽然UI是流畅的,但视频画面本身可能会有明显的卡顿或延迟。这是因为对于YOLOv8这类复杂的AI模型,CPU进行推理计算确实有些“力不从心”。

在实际项目中,选择CPU还是GPU进行推理,是一个需要根据项目需求进行权衡的决策:

  • 成本: CPU方案成本低,无需额外硬件。GPU方案则需要配备一块性能合适的NVIDIA显卡。
  • 模型大小: 对于一些轻量化的模型(如MobileNet等),CPU或许能满足实时性要求。
  • 产线速度(FPS要求): 这是最重要的考量因素。如果产线速度要求每秒检测30个产品(30 FPS),而CPU推理一帧需要100毫秒(即10 FPS),那么CPU方案显然是不可行的。

对于本文使用的YOLOv8n模型,要在640x640的输入尺寸下达到实时推理(例如 > 25 FPS),使用一块支持CUDA的NVIDIA GPU几乎是必需的。

  • 如何切换到GPU推理?
    OpenCV的DNN模块对CUDA提供了良好的支持。切换到GPU推理通常只需要两步:
    1. 编译支持CUDA的OpenCV: 官方预编译的OpenCV通常不包含CUDA支持,需要从源码自行编译,并在CMake配置中开启WITH_CUDA选项。
    2. 修改C++代码: 在加载网络时,指定计算后端为CUDA。
      // 在CameraWorker的构造函数中
      m_net.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);
      m_net.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA);
      
    完成这两步后,所有的推理计算将自动在GPU上执行,速度会得到质的飞跃,视频流卡顿的问题也将迎刃而解。

通过以上讨论,我们明确了从教学原型迈向工业级应用的技术升级路径。现在,我们的程序不仅功能可用,更具备了向更高性能、更强稳定性演进的清晰蓝图。

六、总结与展望

在本篇文章中,我们完成了从处理静态图片到分析动态视频流的重大升级。通过引入Qt Multimedia模块和多线程编程,我们不仅成功地驱动了真实硬件,还保证了复杂后台任务不对用户体验造成任何影响。

至此,我们的应用程序已经具备了工业视觉软件的核心雏形:一个美观的UI、一个强大的AI推理引擎,以及与硬件实时交互的能力。

然而,在真实的工业环境中,软件还需要与生产线上的其他控制单元(如PLC)进行联动。在下一篇文章【《使用Qt Quick从零构建AI螺S瑕疵检测系统》——10. 模拟PLC通信:玩转串口】中,我们将学习如何使用Qt的串口模块,实现与外部控制设备的通信。

http://www.dtcms.com/a/313096.html

相关文章:

  • LeetCode 分类刷题:2824. 统计和小于目标的下标对数目
  • Go语言--语法基础7--函数定义与调用--自定义函数
  • Go语言实战案例:TCP服务器与客户端通信
  • HoloLens+vuforia打包后遇到的问题
  • 图像、视频、音频多模态大模型中长上下文token压缩方法综述
  • Connection refused: no further information: localhost/127.0.0.1:2375
  • Git的安装和配置
  • JavaWeb开发
  • XSS-DOM 2
  • [硬件电路-150]:数字电路 - 数字电路与模拟电路的异同
  • 洛谷 B3841:[GESP202306 二级] 自幂数判断
  • 当Windows远程桌面出现“身份验证错误。要求的函数不受支持”的问题
  • 方差 协方差矩阵是什么
  • java的隐式类型转换和强制转换类型
  • 科威特塔观测指南:412米高空俯瞰石油城变迁
  • 在AI技术快速迭代的背景下,如何通过RAG技术提升模型的实时性和准确性?从Naive RAG到Modular RAG:AI技术进化的关键路径
  • 生成式人工智能展望报告-欧盟-04-社会影响与挑战
  • 86、信息系统建设原则
  • Java 中的多态性及其实现方式
  • AI + 云原生:正在引爆下一代应用的技术革命
  • 中国计算机学会杭州分部副主席朱霖潮:多模态大模型的研究进展与未来
  • k8s+isulad 国产化技术栈云原生技术栈搭建4-添加worker节点
  • Java函数式编程之【Stream终止操作】【上】【简单约简】
  • ethtool,lspci,iperf工具常用命令总结
  • 前端面试手撕题目全解析
  • CXGrId中按回车控制
  • 微店所有店铺内的商品数据API接口
  • 宝马集团与SAP联合打造生产物流数字化新标杆
  • 达梦数据库备份与还原终极指南:从基础到增量策略实战
  • [leetcode] 位运算