简介:一套基于Qt部署YOLOv5、并通过OpenCV DNN模块与CUDA实现加速推理的完整项目资料,面向正在准备毕业设计、课程设计或期末大作业的计算机相关专业学生。源码结构完整,涵盖界面设计、推理封装与模型调用等模块,配合文档说明可快速理解Qt与YOLOv5的集成思路,即使基础偏弱也能按指引完成部署与调试。压缩包共454个文件,以hpp/h头文件、xml配置、dll动态库、a静态库为主,并包含ui界面文件、pro工程文件、jpg示例图片、mp4演示录像及onnx模型文件等,整体约54.16MB,目录分类明确,便于按需查阅和二次开发。目前已有112人学习下载,适合需要实际项目参考或快速搭建目标检测演示环境的学习者。资料附带导师认可的高分项目文档,能够提供从环境配置、代码结构说明到推理效果演示的完整支持,帮助读者少走弯路,直接聚焦核心功能实现与优化。
1. 为什么是 Qt + YOLOv5 + OpenCV DNN(CUDA)这条路线
做目标检测落地,最常遇到的不是模型训练不出来,而是训练好的模型怎么装进一个能被客户双击打开的桌面程序里。YOLOv5 的 Python 推理脚本只适合原型验证,交付时要么打包 PyInstaller 被杀毒软件误报,要么让对方装 Python 环境直接劝退。用 Qt 写界面、用 OpenCV 的 DNN 模块加载 YOLOv5 导出的 ONNX 模型,再通过 CUDA 让推理跑在 GPU 上,是当前把检测能力嵌进 Windows/Linux 桌面客户端最省事的一条路径:OpenCV DNN 的 C++ API 不依赖 PyTorch,模型文件只有一个 .onnx,拷贝即用。这套方案的适合人群很明确——手里有 YOLOv5 权重(自己训练的或官方预训练的),需要做带界面的本地推理工具,且不想引入 TensorRT 或 ONNX Runtime 那套依赖链;前提是你愿意接受它在吞吐量上比 TensorRT 低一些,换来的则是零额外运行时和极低的集成复杂度。
2. 环境装配:CUDA 版本匹配与 Qt 下载安装的硬件前提
2.1 CUDA 与工具链的版本对应关系
OpenCV DNN 的 CUDA 加速是在编译期决定的,不是运行时自动开启。你要么直接下载官方预编译的 opencv-world,但带 CUDA 的官方包基本不存在,所以常见做法是自己编译 OpenCV,或者找第三方维护的预编译包。自己编译时,CUDA 版本和 OpenCV 版本的对应是第一个坑。OpenCV 4.8 官方文档明确支持的 CUDA 版本是 11.8,OpenCV 4.10 支持到 CUDA 12.4,但实测 4.8 在 CUDA 12.x 下也能编译通过,只是部分算子可能走不到 GPU。建议先核对一下你手里的显卡驱动支持的最高 CUDA 版本,用nvidia-smi看右上角的 CUDA Version,这个值表示驱动能向下兼容的版本上限,不是已安装的运行时版本。
如果之前已经装过其他深度学习框架,不要急着卸。CUDA 的安装本质上是把nvcc编译器和 CUDA 运行时库解压到一个目录,系统里同时存在多个 CUDA 版本是正常的。我在 Windows 上一般是装完 CUDA 后用环境变量CUDA_PATH指到当前需要的那份,比如C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.8。请特别留意:OpenCV 编译时认的是CUDA_TOOLKIT_ROOT_DIR,如果你在 CMake 配置阶段没指定这个变量,而系统里恰好装了多个 CUDA,CMake 会按版本号自己挑一个,经常挑到新版,但新版如果超出 OpenCV 的默认支持范围,编译能过但运行时 DNN 后端的 kernel 可能加载失败。因此,在 CMake 里显式设一次:
cmake -DCMAKE_BUILD_TYPE=RELEASE \ -DCMAKE_INSTALL_PREFIX=/usr/local/opencv \ -DWITH_CUDA=ON \ -DCUDA_TOOLKIT_ROOT_DIR=/usr/local/cuda-11.8 \ -DWITH_CUDNN=ON \ -DOPENCV_DNN_CUDA=ON \ -DWITH_CUBLAS=ON \ -DOPENCV_EXTRA_MODULES_PATH=../opencv_contrib/modules \ ../opencv-4.8.0编译完必须在 C++ 代码里确认 OpenCV 真的带 CUDA 后端,这一步不能省。你可以在 Qt 工程的 main.cpp 里先打印一行std::cout << cv::getBuildInformation(),搜NVIDIA CUDA段,找到DNN_CUDA: YES字样。如果这里显示 NO,后面cv::dnn::Net::setPreferableBackend怎么设都会在运行时抛出OpenCV(4.8.0) Error: The function/feature is not implemented。
2.2 Qt 下载与 MSVC 构建套件的选择
Qt 的安装本身没有太多玄学,关键是编译器套件和 OpenCV 的编译工具链要对齐。用 MSVC 编译的 OpenCV 库就配 MSVC 的 Qt 构建套件,用 MinGW 编译的 OpenCV 就只能配 MinGW 套件,混用的典型症状是一堆unresolved external symbol链接错误。Qt 5.15.2 是个稳定选择,用 Qt 在线安装器勾选 MSVC 2019 64-bit 组件即可。如果你是自己编译 OpenCV,直接用 Qt 自带的 MinGW 也行,但 OpenCV 在 MinGW 下编译比 MSVC 慢不少,而且第三方预编译的带 CUDA 的 OpenCV 基本都是 MSVC 版,各人按自己习惯选,我用 MSVC 2019 居多。
一点经验:OpenCV 的安装目录确认后,把C:\opencv\build\x64\vc16\bin加进系统 PATH。否则 Qt 程序点击运行后会报缺opencv_world480.dll。另外,Qt 的qmake和 CMake 的路径不要有中文和空格,OpenCV 的路径也一样,否则 CMake 解析路径时容易出怪问题。
2.3 验证 CUDA 与 cuDNN 的组合是否匹配
编译 OpenCV 之前先确认版本组合能吃上 cuDNN。OpenCV 的 DNN 模块在 CUDA 模式下有两种后端:DNN_BACKEND_CUDA配合DNN_TARGET_CUDA是纯 CUDA kernel 实现,DNN_BACKEND_CUDA配合DNN_TARGET_CUDA_FP16则使用半精度计算。cuDNN 不是必需的,但建议装上。下表是常见的版本组合,来自 OpenCV 社区的编译反馈:
| OpenCV 版本 | CUDA 版本 | cuDNN 版本 | 实测结论 |
|---|---|---|---|
| 4.5.5 | 11.2 | 8.2.x | 稳定,社区使用最广 |
| 4.7.0 | 11.7 | 8.5.x | 稳定,支持更多新算子 |
| 4.8.0 | 11.8 | 8.9.x | 稳定,YOLOv5 导出 ONNX 无压力 |
| 4.9.0 | 12.x | 9.x | 可编译,DNN 部分 kernel 不完整 |
同一个 OpenCV 版本对 cuDNN 的版本没有严格限制,只要 CUDA 主版本匹配。查看当前环境的 CUDA 和 cuDNN 版本,Windows 下可以直接看C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.8\include\cudnn_version.h里的CUDNN_MAJOR和CUDNN_MINOR宏。Linux 下用nvcc --version看 CUDA,cuDNN 则用cat /usr/local/cuda/include/cudnn_version.h | grep CUDNN_MAJOR。别记命令,记住一句话:编译时报 cuDNN 相关错误,十有八九是头文件和库文件路径没指对地方,检查CUDNN_INCLUDE_DIR和CUDNN_LIBRARY两个 CMake 变量,它们指向的目录层级必须分别包含cudnn.h和cudnn.lib。
3. 模型转换:把 YOLOv5 的 pt 权重导出为 ONNX 再接入 DNN
3.1 DNN 输入要求与 YOLOv5 输出的差异
YOLOv5 训练时输出的检测头是三个尺度的特征图,每个尺度对应三种 anchor 尺寸。直接用 Python 加载 .pt 做推理,后处理里包含 anchor 解码、NMS、类别过滤,是一套完整逻辑。而 ONNX 导出时通常把 anchor 解码和前两个维度的 reshape 放进图里,输出变成[1, 25200, 85]这种形状,其中 25200 = 3 个尺度 × (80×80 + 40×40 + 20×20),85 = 5(x,y,w,h,obj_conf)+ 80(COCO 类别数)。DNN 模块的forward()拿到这个输出后,NMS 得自己写。另一个坑是 YOLOv5 的预处理和后处理都涉及对图像像素的操作,导出前必须明确你的预处理方式(letterbox + 归一化)要么自包含在 ONNX 图里,要么在 C++ 侧手动做。常见做法是让 C++ 侧做 letterbox,把归一化交给 DNN 的blobFromImage的scalefactor参数,不要在导出时把归一化写进图里,这样同样一份 ONNX 还能继续给 TensorRT 用。
3.2 用 ultralytics 或者 YOLOv5 仓库导出 ONNX
如果你用的是ultralytics库(YOLOv5 官方后来并入了这个仓库),导出命令是:
yolo export model=yolov5s.pt format=onnx opset=12 simplify=True如果你用的是原ultralytics/yolov5仓库,则用:
python export.py --weights yolov5s.pt --include onnx --opset 12 --simplify关键参数解释:opset=12对应 ONNX IR 版本,OpenCV DNN 对 opset 的支持上限一般到 11 或 12,opset 13+ 的某些算子如Split的改动会导致 DNN 加载失败;simplify用 onnx-simplifier 消除一些冗余的 shape 运算,这能避免 DNN 模块里常见的Unsupported layer报错。导出成功后,用 Python 验证 ONNX 的输出形状:
import onnx import onnxruntime as ort import numpy as np import cv2 model = onnx.load("yolov5s.onnx") onnx.checker.check_model(model) sess = ort.InferenceSession("yolov5s.onnx") x = np.random.rand(1, 3, 640, 640).astype(np.float32) outs = sess.run(None, {sess.get_inputs()[0].name: x}) print([o.shape for o in outs])输出应该是[(1, 25200, 85)],这是单输出形态。有些导出配置会输出三个尺度的分离张量,DNN 处理起来更复杂,建议统一为单输出。
3.3 在 Python 侧先用 OpenCV DNN 做一次完整推理对照
模型导出完成后,不要急着进 Qt,先在 Python 里用cv2.dnn.readNetFromONNX跑通一遍,排除「OpenCV 根本不认这个模型」的风险。这一步非常快,也是排查问题成本最低的环节:
import cv2 import numpy as np net = cv2.dnn.readNetFromONNX("yolov5s.onnx") img = cv2.imread("bus.jpg") # letterbox 调整到模型输入尺寸 h, w = img.shape[:2] ratio = min(640 / w, 640 / h) new_w, new_h = int(w * ratio), int(h * ratio) padded = np.full((640, 640, 3), 114, dtype=np.uint8) padded[:new_h, :new_w] = cv2.resize(img, (new_w, new_h)) blob = cv2.dnn.blobFromImage(padded, 1 / 255.0, (640, 640), (0, 0, 0), swapRB=True) net.setInput(blob) outs = net.forward() print(outs.shape)注意blobFromImage里swapRB=True是因为 OpenCV 读图是 BGR,而 YOLOv5 训练时用的是 RGB。这个参数写错的话,检测精度会显著下降但不会报错,属于比较隐蔽的问题。1/255.0是归一化系数,对应训练时的像素缩放。这两点要和你导出模型前在 Python 侧跑测试时保持一致。
4. 在 Qt 工程里用 C++ 接住 OpenCV DNN 并把画面画出来
4.1 CMake 工程配置与 Qt + OpenCV 的链接
到了 Qt 工程这一步,核心结构是一个普通的 QMainWindow,左边放 QLabel 显示图像,右边放检测结果列表。工程文件用 CMake 比 qmake 更直观,尤其是在指定 OpenCV 库路径时。下面是一份最小可用的 CMakeLists.txt:
cmake_minimum_required(VERSION 3.16) project(DetectorApp) set(CMAKE_CXX_STANDARD 17) find_package(Qt5 COMPONENTS Widgets REQUIRED) set(OpenCV_DIR "C:/opencv/build") find_package(OpenCV REQUIRED COMPONENTS core imgproc imgcodecs dnn) add_executable(DetectorApp main.cpp MainWindow.cpp Detector.cpp ) target_link_libraries(DetectorApp Qt5::Widgets ${OpenCV_LIBS} )find_package(OpenCV)通过OpenCV_DIR定位到 OpenCV 的 CMake 配置目录,${OpenCV_LIBS}会展开为所有库的绝对路径加链接选项。注意 Windows 下调试和发布版本的库是分开的,CMake 会自动按配置切换。如果链接时报一堆LNK2019,优先检查当前构建套件是 Debug 还是 Release,OpenCV 的 Debug 库带d后缀,和 Release 混用的第一反应就是这类错误。
4.2 推理线程的封装:阻断 UI 是最大误区
Qt 的 GUI 线程必须保持响应,而 OpenCV DNN 的推理是同步阻塞调用。直接在按钮槽函数里跑推理,测试图片时可能体感不明显,一旦接摄像头或视频流,界面立刻卡到拖不动。推荐做法是把推理扔进std::thread或者QThread,完成后用信号把结果传回主线程。
// Detector.h #pragma once #include <opencv2/dnn.hpp> #include <opencv2/opencv.hpp> #include <QObject> #include <QMutex> class Detector : public QObject { Q_OBJECT public: explicit Detector(QObject *parent = nullptr); bool loadModel(const QString &onnxPath); cv::Mat detect(const cv::Mat &frame); signals: void inferenceFinished(cv::Mat annotated, QVector<QRect> boxes); private: cv::dnn::Net net; float confThreshold = 0.25; float nmsThreshold = 0.45; int inputSize = 640; QMutex mutex; };// Detector.cpp 关键实现 bool Detector::loadModel(const QString &onnxPath) { QMutexLocker locker(&mutex); try { net = cv::dnn::readNetFromONNX(onnxPath.toStdString()); net.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA); net.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA); return !net.empty(); } catch (const cv::Exception &e) { qCritical() << "模型加载失败:" << e.what(); return false; } } cv::Mat Detector::detect(const cv::Mat &frame) { QMutexLocker locker(&mutex); cv::Mat blob = cv::dnn::blobFromImage(frame, 1.0 / 255.0, cv::Size(inputSize, inputSize), cv::Scalar(), true, false); net.setInput(blob); cv::Mat outs = net.forward(); // 形状: [1, 25200, 85] // 后处理: 解码 + NMS + 画框 return frame; }DNN_BACKEND_CUDA和DNN_TARGET_CUDA这两个枚举值配合出现,告诉 OpenCV 用 CUDA 作为计算后端,并且目标设备是 GPU。blobFromImage的第三个参数Size(640, 640)强制缩放,这会改变图像宽高比,导致检测框位置偏移,正确做法是先做 letterbox 再用cv::dnn::blobFromImage处理,这里为了代码简洁先直接缩放,正式实现必须补上 padding 的逻辑和坐标还原。
4.3 后处理:从原始输出到画面上的框
forward 拿到的输出是个[1, 25200, 85]的 Mat,在 C++ 里需要把它解析成候选框数组。YOLOv5 的输出布局是每行一个候选框:前 4 个是 x_center, y_center, w, h(已除以输入尺寸,即 0~1 的归一化坐标),第 5 个是 objectness,后面 80 个是类别概率。后处理核心步骤是先过滤 objectness 低于阈值的行,再把坐标从中心点格式转换成 OpenCV 喜欢的左上角+宽高格式,最后做一次 NMS。
std::vector<cv::Rect> boxes; std::vector<float> scores; std::vector<int> classIds; float *data = (float *)outs.data; for (int i = 0; i < 25200; ++i) { float objConf = data[i * 85 + 4]; if (objConf < confThreshold) continue; float *classScores = data + i * 85 + 5; cv::Point maxLoc; double maxVal; cv::minMaxLoc(cv::Mat(1, 80, CV_32F, classScores), nullptr, &maxVal, nullptr, &maxLoc); float score = objConf * maxVal; if (score < confThreshold) continue; float cx = data[i * 85 + 0]; float cy = data[i * 85 + 1]; float w = data[i * 85 + 2]; float h = data[i * 85 + 3]; int left = (cx - w / 2) * frame.cols; int top = (cy - h / 2) * frame.rows; boxes.push_back(cv::Rect(left, top, (int)(w * frame.cols), (int)(h * frame.rows))); classIds.push_back(maxLoc.x); scores.push_back(score); } std::vector<int> indices; cv::dnn::NMSBoxes(boxes, scores, confThreshold, nmsThreshold, indices);这里的NMSBoxes是 OpenCV 4.5.2 之后的推荐接口,返回值是保留框的索引,不用自己实现 NMS。注意输出的坐标已经按原图尺寸换算过了,如果之前做了 letterbox,这里需要按 scale 和 pad 反算回去,不少新手在这里直接漏掉导致框的位置偏移。
4.4 Qt 界面上显示检测结果
推理完成后要把结果画到界面上,注意 QLabel 显示图片时要用QPixmap,而 OpenCV 的cv::Mat是 BGR 格式,需要转换:
// 把带标注框的 cv::Mat 转成 QPixmap 显示 cv::Mat rgb; cv::cvtColor(annotated, rgb, cv::COLOR_BGR2RGB); QImage qimg(rgb.data, rgb.cols, rgb.rows, rgb.step, QImage::Format_RGB888); QPixmap pixmap = QPixmap::fromImage(qimg.copy()); ui->labelImage->setPixmap(pixmap.scaled(ui->labelImage->size(), Qt::KeepAspectRatio));qimg.copy()这一行比较关键,因为QImage构造时不复制像素数据,而cv::Mat离开作用域后会释放内存,如果不 copy,显示区域会出现花屏或者随机色块。实际画面到达 UI 线程后,用异步信号inferenceFinished把annotated图传过去,千万不要在detect()里直接操作 UI 组件。
5. 模型精度保持与 Qt 部署的边界问题
5.1 动态输入尺寸还是固定 640
YOLOv5 原版支持任意 32 的倍数作为输入尺寸。ONNX 导出时默认固定了 640×640,这意味着你在 Qt 里只能用这个尺寸,除非在导出时指定--dynamic参数:
python export.py --weights yolov5s.pt --include onnx --opset 12 --dynamic动态输入意味着网络里的 reshape 算子会变成动态 shape,OpenCV DNN 对动态 shape 的支持很有限,部分版本直接不支持。我的建议是放弃动态输入。理由很简单:检测框的精度和输入尺寸的关系不是线性的,640 输入对大多数桌面端应用已经够用,硬上动态尺寸往往在net.forward()阶段报TypeError: only size-1 arrays can be converted to Python scalars或者类似 C++ 侧的 shape 错误。如果你在 Qt 界面里想要高精度模式,就准备两份 ONNX,一份 640 一份 1280,运行时切换模型而不是切换输入尺寸。
5.2 多模型切换时的显存管理
OpenCV 的 DNN 模块在 CUDA 模式下会缓存中间计算结果,如果你在一个程序里先后加载多个模型并切换推理,显存不会自动释放。cv::dnn::Net的析构不保证立即释放显存,需要手动清理:
void Detector::releaseModel() { QMutexLocker locker(&mutex); net = cv::dnn::Net(); // 赋值空 Net 触发内部释放 cv::cuda::resetDevice(); }cv::cuda::resetDevice()这个调用会重置当前 CUDA 设备的上下文,相当于释放了 OpenCV 在显存里暂存的所有 buffer。但是注意,如果程序里其他模块也在用 CUDA,这个调用会把人家也干掉。所以这个函数只在完全切换模型且确定没有其他 CUDA 消费者时调用。实测在 Windows 上连续切换 10 次模型不动态释放的话,显存占用量会线性增长直到cv::Exception: OpenCV(4.8.0) Error: CUDA error (2) out of memory。
5.3 Qt 5.15 的 MSVC2019 版本衔接问题
Qt 5.15.2 的官方在线安装包默认提供 MSVC2019 64-bit 套件,但是如果你系统装了 VS2022,直接用这个套件会报Please check your Compiler toolchain之类的错误。解决方式是在 Qt Creator 的构建套件页面,把编译器手动指定为 VS2022 的cl.exe,或者在 Qt 安装器里额外勾选 MSVC2019_64 组件并单独安装 VS2019 Build Tools。我自己处理是把 Qt 5.15.2 的qmake.exe路径加进 PATH,然后让 CMake 自己找编译器,避免 Qt Creator 的套件检测机制介入。反正共用同一个 qmake 生成的 Makefile 在 VS2022 下编译没问题,需要保证 Windows SDK 版本不低于 10.0.19041。
5.4 不同场景下该调的后处理参数
表格整理一下 Qt 界面里常见的几个旋钮,它们通常放在设置面板上可以运行时修改:
| 参数名 | 默认值 | 影响 | 调优建议 |
|---|---|---|---|
| confThreshold | 0.25 | 误检数量 | 工厂质检场景调到 0.4 以上 |
| nmsThreshold | 0.45 | 重叠框保留 | 人群密集场景调到 0.3 |
| inputSize | 640 | 小目标检出率 | 小目标多就改用 1280 模型 |
| cuda 是否开启 | 开启 | 延迟和吞吐 | CPU 部署时注意耗电发热 |
confThreshold和nmsThreshold的关系不是独立的,调低 conf 会导致候选框变多,NMS 的压力变大,如果发现帧率明显下降,先看是不是阈值太低产生了几千个候选框。另外,setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV)是默认的 CPU 后端,改到 CUDA 后不太容易验证 DNN 是真正走了 GPU,一个可靠的方法是在 CUDA 后端下运行时调用nvidia-smi看显存占用是否上升。如果在 Qt 里看不到准确状态,用下面这行代码:
size_t freeMem, totalMem; cv::cuda::DeviceInfo dev(0); dev.queryMemory(freeMem, totalMem); qDebug() << "显存使用率:" << (totalMem - freeMem) * 100.0 / totalMem << "%";5.5 一个实用的 Qt 绘图技巧:自定义置信度进度条
界面里的检测结果列表往往会显示每个目标的类别和置信度,用 QTableWidget 或者 QListView 都能做。但如果想让置信度可视化更直观,可以用一个自定义 QProgressBar 放在表格里。这不算新东西,关键是 QProgressBar 的文字格式。在 Qt 里给检测结果做置信度显示,一个更自然的方案是直接在图片上绘制半透明蒙版配合置信度标签,这比任何表格都直观:
cv::putText(annotated, classNames[classIds[i]] + " " + cv::format("%.2f", scores[i]), cv::Point(box.x, box.y - 5), cv::FONT_HERSHEY_SIMPLEX, 0.6, cv::Scalar(0, 255, 0), 2);如果不想引入中文字体在 Qt 里显示乱码的问题,检测框标签保持英文就好,识别结果的汉化可以放在左边列表里用 QString 处理。用cv::putText画中文会乱码,原生方案是自己维护一份字体映射表,但投入产出比太低,建议直接避开。
Qt 里处理耗时操作的完整思路是:工作线程执行检测、信号槽传递数据回主线程、主线程只做绘制和交互。我在实际项目里会用QThread派生一个工作类,把摄像头读帧、推理、后处理全封装进去,主线程只等readyFrame信号来刷新 QLabel 和结果表,这也是 Qt 程序在连续视频流下不卡界面的基本盘。
本文还有配套的精品资源,点击获取