简介:这是一套面向计算机视觉初学者与嵌入式/桌面端开发者的目标检测入门源码,基于 OpenCV、Qt 与 YOLO 组合实现,帮助读者快速搭建可运行的实时检测界面,省去从零搭建框架的成本。资源包共 27 个文件,约 1.94MB,包含 5 个 cpp 与 4 个 h 源文件承载推理与检测线程逻辑,1 个 ui 界面文件与 qrc 资源文件负责窗口布局,另有 png、jpg、gif 等示例图片与图标、CMakeLists 构建脚本及 README 说明,结构清晰便于二次开发。使用时需注意:导入 onnx 模型时须同时导入同名 txt 类别文件,模型训练输入尺寸应为 640x640,且检测文件路径避免中文。目前已有 350 人学习下载,适合希望理解 YOLO 推理流程、Qt 界面与 OpenCV 图像处理如何协同工作的读者参考借鉴。
1. 从一堆散装脚本到能跑起来的桌面检测器:这套源码到底省了哪几步
很多人第一次做视觉检测,卡住的地方不是 YOLO 推理本身,而是把「模型能跑」变成「人能点」。命令行里detect.py一敲,框是画出来了,可一旦要交给产线同事、实验室同学或者课程答辩老师,对方第一句话往往是:界面呢?于是你开始翻 OpenCV 的imshow窗口,发现它连个像样的按钮都没有,想加个「打开图片」「切换摄像头」「保存结果」得自己从零写 Qt 信号槽,再回头处理 YOLO 的输入输出格式,最后还要把 OpenCV 的 BGR 和 Qt 的 QImage 对齐。这套「基于 OpenCV + Qt + YOLO 的简单检测系统」整套源码,解决的就是这段从算法到桌面的胶水工程。它把检测线程、界面刷新、模型加载、结果绘制这几件事拆成了可读的模块,适合刚学完 YOLO 想做个完整 demo 的人,也适合需要快速搭一个内部标注/抽检工具的一线开发者。你拿到手不是一堆散装脚本,而是一个能编译、能换模型、能接着改的起点。
2. 拆开这套源码:Qt 界面、OpenCV 图像链路和 YOLO 推理是怎么串起来的
2.1 三个技术栈各自负责哪一段,别混着用
先把这个系统的职责边界划清楚,后面改代码才不容易翻车。Qt 负责的是「人机交互层」:窗口、按钮、文件对话框、视频显示控件、状态栏。OpenCV 负责的是「图像 I/O 与预处理层」:读图、读摄像头、缩放、颜色空间转换、画框、写结果。YOLO 负责的是「推理层」:把一张张图送进网络,拿回类别、置信度、坐标。三者之间靠数据流串起来,而不是互相调用内部函数。
常见做法是:Qt 主线程只碰界面,检测逻辑放到独立线程或QTimer里跑,避免界面卡死。OpenCV 的Mat在进入 Qt 显示前必须转成QImage,这里有个血泪经验——Mat的内存是它自己管理的,如果你直接把mat.data塞给QImage而不做深拷贝,等Mat析构后界面就会花屏或者直接崩。YOLO 的输出通常是[x, y, w, h, conf, class]这样的张量,需要先做 NMS,再映射回原图坐标,最后交给 OpenCV 画框。
这套源码的价值就在于它已经把这条链路走通了,你不需要再纠结「到底谁先谁后」。下面这张表是我拆完后整理的模块职责,照着看代码会快很多。
| 模块 | 技术栈 | 输入 | 输出 | 常见改动点 |
|---|---|---|---|---|
| 主窗口 | Qt Widgets | 用户点击 | 信号 | 加按钮、改布局 |
| 图像读取 | OpenCV VideoCapture | 路径/摄像头索引 | Mat | 换视频源、加 RTSP |
| 预处理 | OpenCV + NumPy | Mat | blob | 改输入尺寸、归一化 |
| 推理 | YOLO (ONNX/Darknet) | blob | 检测框 | 换模型、调阈值 |
| 后处理 | NumPy | 原始输出 | NMS 后框 | 改 IoU、置信度 |
| 显示 | Qt QLabel/QGraphicsView | QImage | 屏幕 | 改缩放、加叠加层 |
2.2 环境搭建:OpenCV、Qt、YOLO 三件套的版本对齐
环境这一步最容易出玄学问题,尤其是 Qt 和 OpenCV 的版本组合。我一般会先把 Qt 装好,再编译或安装 OpenCV,最后单独把 YOLO 的推理库拉进来。Qt 推荐用 Qt 5.15.2 或 Qt 6 的 LTS 版本,安装时勾选 MSVC 或 MinGW 对应的套件。OpenCV 如果用预编译包,注意它自带的 Qt 支持可能和你系统里的 Qt 版本不一致,导致could not find the qt platform plugin这类报错。
下面是我在 Windows + MSVC 下常用的环境检查脚本,跑一遍能确认关键路径是否就绪。
# 检查 Qt 安装路径下的平台插件是否存在 ls $QT_DIR/plugins/platforms/ # 预期看到 qwindows.dll (Windows) 或 libqxcb.so (Linux) # 检查 OpenCV 是否带 Qt 支持 python -c "import cv2; print(cv2.getBuildInformation())" | grep -i qt # 检查 YOLO 推理库能否加载 python -c "import onnxruntime; print(onnxruntime.get_device())"逻辑说明:第一条命令确认 Qt 平台插件目录完整,缺了它程序启动就会报could not find the qt platform plugin。第二条看 OpenCV 编译时是否启用了 Qt,如果显示 NO,那cv2.imshow可能还能用,但和 Qt 窗口混用会有事件循环冲突。第三条确认 ONNX Runtime 能识别设备,CPU 版返回CPU,GPU 版要确认 CUDA 和 cuDNN 版本匹配。
参数上,QT_DIR要指向你的 Qt 安装根目录,比如C:\Qt\5.15.2\msvc2019_64。如果你用的是 MinGW 套件,OpenCV 也必须用 MinGW 编译,MSVC 和 MinGW 的 ABI 不兼容,混用会在链接阶段报一堆未定义符号。这是新手最容易踩的坑之一。
2.3 模型加载与推理线程:别让界面卡成 PPT
YOLO 模型加载有两种常见方式:一种是直接用 Darknet 的.weights+.cfg,另一种是转成 ONNX 后用 ONNX Runtime 推理。这套源码如果用的是 ONNX,加载逻辑大概是下面这样。
import cv2 import numpy as np import onnxruntime as ort class YoloDetector: def __init__(self, model_path, conf_thres=0.5, iou_thres=0.45): self.session = ort.InferenceSession(model_path, providers=['CPUExecutionProvider']) self.input_name = self.session.get_inputs()[0].name self.conf_thres = conf_thres self.iou_thres = iou_thres def preprocess(self, img, input_size=(640, 640)): # 保持长宽比的 letterbox 缩放,避免形变影响检测 h, w = img.shape[:2] scale = min(input_size[0] / h, input_size[1] / w) new_h, new_w = int(h * scale), int(w * scale) resized = cv2.resize(img, (new_w, new_h)) canvas = np.full((input_size[0], input_size[1], 3), 114, dtype=np.uint8) canvas[:new_h, :new_w] = resized blob = canvas[:, :, ::-1].transpose(2, 0, 1).astype(np.float32) / 255.0 return np.expand_dims(blob, axis=0), scale def infer(self, blob): outputs = self.session.run(None, {self.input_name: blob}) return outputs[0]逻辑说明:preprocess里的 letterbox 是关键,直接resize到 640x640 会让宽高比失真的图检测精度下降。填充值 114 是 YOLO 系列常用的灰度填充。blob的维度顺序从 HWC 转成 CHW,再除以 255 归一化,这是 ONNX 模型的常见输入要求。infer只负责跑一次前向,后处理 NMS 单独写,方便你替换成别的模型。
参数上,conf_thres控制置信度阈值,调高会漏检,调低会误检,一般从 0.5 起步。iou_thres控制 NMS 的重叠阈值,0.45 是通用值,密集场景可以降到 0.3。input_size要和模型导出时一致,YOLOv5/v8 常见的是 640,YOLOv3 可能是 416。改错尺寸不会报错,但检测框会整体偏移,这是很隐蔽的坑。
线程方面,Qt 里我一般用QThread把检测循环包起来,通过信号把QImage发回主线程刷新。不要在主线程里写while True读摄像头,否则按钮点不动、窗口拖不动,用户以为程序死了。
3. 从零跑通第一个检测窗口:编译、换模型、接摄像头的完整操作
3.1 编译与首次运行:把工程导入 Qt Creator
拿到源码后,第一步不是急着改代码,而是先确认它能编译通过。用 Qt Creator 打开.pro文件,选择对应的 Kit,然后执行qmake和构建。如果报错找不到 OpenCV 头文件,需要在.pro里补上INCLUDEPATH和LIBS。
# 在 .pro 文件中追加 OpenCV 路径 INCLUDEPATH += $$PWD/third_party/opencv/include LIBS += -L$$PWD/third_party/opencv/lib \ -lopencv_core \ -lopencv_imgproc \ -lopencv_highgui \ -lopencv_videoio逻辑说明:INCLUDEPATH告诉编译器去哪找opencv2/opencv.hpp,LIBS告诉链接器去哪找.lib或.so。Windows 下如果用的是动态库,还要把对应的.dll拷到可执行文件同级目录,否则运行时报「找不到 opencv_core450.dll」这类错误。Linux 下则是LD_LIBRARY_PATH的问题。
首次运行建议先用一张静态图片测试,不要一上来就接摄像头。图片路径不要带中文,OpenCV 在 Windows 下对中文路径的支持一直不太稳定,常见做法是先用QFileDialog拿到路径,再用cv::imdecode读字节流,绕开编码问题。
3.2 换模型:把预训练权重替换成你自己的
这套源码默认带的模型大概率是 YOLO 的通用预训练权重,能检测 COCO 的 80 类。如果你想检测自己的目标,比如螺丝、瓶盖、电路板缺陷,就需要替换模型。步骤是:先用自己的数据集训练,导出 ONNX,再改源码里的模型路径和类别名。
# 替换模型路径和类别名 detector = YoloDetector( model_path="models/my_yolov8n.onnx", conf_thres=0.4, iou_thres=0.5 ) # 类别名要和训练时的 data.yaml 顺序一致 class_names = ["screw", "cap", "pcb_defect"]逻辑说明:model_path指向你导出的 ONNX 文件,导出时注意opset版本不要太高,ONNX Runtime 对太新的 opset 支持可能滞后。class_names的顺序必须和训练时data.yaml里的names完全一致,顺序错了不会报错,但框上的标签会张冠李戴。conf_thres对自定义小目标可以适当降低到 0.3 左右,但要做好误检增多的准备。
导出 ONNX 的常见命令是yolo export model=best.pt format=onnx opset=12,如果你用的是 YOLOv5,则是python export.py --weights best.pt --include onnx。导出后可以用onnxruntime跑一张测试图,确认输出维度是[1, 25200, 85]或[1, 84, 8400]这类格式,不同版本的 YOLO 输出布局不一样,后处理代码要对应调整。
3.3 接摄像头与视频文件:VideoCapture 的参数怎么设
OpenCV 的VideoCapture既能读摄像头也能读视频文件,但两者的参数设置差别很大。摄像头一般用索引0打开,视频文件用路径。分辨率、帧率这些参数不是所有设备都支持,设了不生效也不会报错,只会默默用默认值。
cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) cap.set(cv2.CAP_PROP_FPS, 30) if not cap.isOpened(): raise RuntimeError("摄像头打开失败,检查索引或被占用") while True: ret, frame = cap.read() if not ret: break # 送检测、画框、转 QImage 显示逻辑说明:cap.set的返回值表示是否设置成功,很多 USB 摄像头只支持特定分辨率组合,设 1280x720 失败时会回退到 640x480。cap.read()返回False通常意味着视频结束或摄像头断开,循环里要处理,否则会死循环。如果同时开了多个程序占用摄像头,第二个程序会打开失败,这是常见现象,不是代码问题。
视频文件的话,建议先确认编码格式,OpenCV 对 H.265 的支持取决于编译时的 FFmpeg 版本,遇到打不开的视频可以先用 ffmpeg 转成 H.264 再试。帧率控制不要用sleep,而是根据视频本身的 FPS 做时间戳对齐,否则播放速度会不对。
4. 避坑与排查:Qt 插件、OpenCV 路径、YOLO 输出这三处最容易翻车
4.1 现象:程序启动报 could not find the qt platform plugin
原因:Qt 运行时找不到平台插件目录,常见于手动拷贝 exe 到别的机器,或者环境变量QT_QPA_PLATFORM_PLUGIN_PATH没设对。Windows 下缺qwindows.dll,Linux 下缺libqxcb.so。
解决:把 Qt 安装目录下的plugins/platforms整个文件夹拷到可执行文件同级,或者在代码里用QCoreApplication::addLibraryPath指定路径。Linux 下还要确认libxcb相关依赖已安装,可以用ldd检查。
4.2 现象:OpenCV 读图返回空 Mat,但路径明明存在
原因:路径含中文或空格,OpenCV 的imread在 Windows 下用的是 ANSI 编码,遇到中文会失败。另一个可能是图片格式不被支持,比如某些 CMYK 的 JPEG。
解决:改用cv::imdecode配合QFile读字节流,或者先把路径转成std::wstring再用imread的重载版本。最省事的做法是限制用户只能选英文路径,但这不是长久之计。
4.3 现象:检测框位置整体偏移或框比目标大一圈
原因:预处理用了直接 resize 而不是 letterbox,导致坐标映射回原图时比例算错。另一个可能是模型输入尺寸和代码里写的不一致。
解决:检查preprocess里的 scale 计算,确保后处理时用同一个 scale 把框映射回去。如果模型是 640 输入,代码里写的是 416,框会明显偏移。建议把输入尺寸做成配置项,不要硬编码。
4.4 现象:界面刷新时花屏或程序崩溃
原因:Mat转QImage时没有深拷贝,Mat被析构后QImage还在引用那块内存。多线程下同时读写同一块图像数据也会崩。
解决:转换时用QImage(...).copy()做深拷贝,或者用cvtColor生成新的Mat再转。跨线程传递图像用信号槽的队列连接,不要直接共享指针。
4.5 现象:YOLO 推理速度慢,GPU 利用率低
原因:ONNX Runtime 默认用 CPU,或者 CUDA 版本和 onnxruntime-gpu 不匹配。也可能是每帧都在重新加载模型。
解决:确认providers里用的是CUDAExecutionProvider,并且onnxruntime-gpu版本和 CUDA/cuDNN 对应。模型加载只做一次,放在类初始化里,不要放在循环里。输入尺寸从 640 降到 416 能明显提速,但小目标精度会掉,要权衡。
5. 进阶技巧:把检测结果导出成结构化数据并做批量验证
跑通界面只是第一步,真正在项目里用起来,往往需要把检测结果落成结构化数据,方便后续统计或对接 MES。我一般会在检测循环里加一个结果收集器,每帧的框信息存成列表,支持导出 CSV 或 JSON。下面这段代码演示了怎么把 NMS 后的框转成可导出的格式。
import csv import json def boxes_to_records(boxes, class_names, frame_id): records = [] for box in boxes: x1, y1, x2, y2, conf, cls_id = box records.append({ "frame": frame_id, "class": class_names[int(cls_id)], "confidence": round(float(conf), 4), "x1": int(x1), "y1": int(y1), "x2": int(x2), "y2": int(y2) }) return records def export_csv(records, path): with open(path, "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=records[0].keys()) writer.writeheader() writer.writerows(records)逻辑说明:boxes_to_records把张量格式的框转成字典,方便后续序列化。frame_id用来标记来自哪一帧,视频抽检时很有用。export_csv用DictWriter自动对齐表头,注意encoding="utf-8",否则中文类别名会乱码。如果框数量很大,建议分批写入而不是一次性攒在内存里。
批量验证是另一个实用技巧。把测试集图片放进一个文件夹,写个脚本循环推理,统计每类的检出数量和平均置信度,和标注文件对比就能算出召回率。这一步不需要界面,纯命令行跑,适合在调阈值的时候快速迭代。我自己的习惯是每次改完conf_thres或iou_thres,都强制跑一遍批量验证,把结果记在表格里,避免凭感觉调参。从那以后我每次换模型或者改预处理,都强制走一遍「单图测试 → 批量验证 → 界面确认」这三步,少一步都可能在上线时翻车。希望帮到你。
本文还有配套的精品资源,点击获取