
简介本资源是一个基于OpenCV、Qt与YOLO的轻量级实时人体检测系统实现面向计算机视觉初学者及嵌入式/桌面端应用开发者解决视频流或静态图像中快速定位并框选人体的目标检测需求适用于安防监控、人流统计等入门级项目实践。压缩包共26个文件含5个核心CPP源码、4个H头文件、1个UI界面设计文件、6个PNG/JPG测试图及2个PDF说明文档辅以CMakeLists.txt构建配置与resources资源管理整体大小为4.96MB结构清晰、模块分离明确。已有84人学习下载配套README.pdf与介绍.pdf详细说明环境配置、编译步骤与YOLO模型部署要点代码注释充分支持直接编译运行并可视化检测结果便于理解OpenCV图像采集、Qt界面交互与YOLO推理集成的关键流程。 前两天翻到之前打包的一个项目文件名字就叫“基于opencv qt yolo 实现的简单检测系统.zip”功能说白了就是把视频流里出现的人用框圈出来。OpenCV负责画面读取和绘制Qt负责把界面撑起来YOLO负责判断哪里有人。这个组合在入门目标检测的人里特别流行因为三个库互相补位折腾一遍几乎能把图像处理、GUI、深度学习推理的几个关键环节全过一遍。网上这类压缩包很多但里面环境、依赖、代码版本经常对不上照着跑容易卡在半路。所以我把自己整理过的、能真正跑通的思路完整写出来从选型到踩坑一次说清楚。1. 为什么这个组合能火OpenCV、Qt、YOLO的分工逻辑1.1 各干各的谁都不越界很多人第一次看到“OpenCV Qt YOLO”这套组合第一反应是“叠buff”。其实它们在这个项目里分工非常清晰简单说就是OpenCV管图像Qt管界面YOLO管识别。OpenCV干的事从摄像头或视频文件里读帧把帧从BGR色彩空间转成RGB给界面用在检测结果上画矩形框和标签甚至可以顺手做缩放、翻转、亮度调整。它更像是一个图像处理工具箱不负责“判断画面里是不是人”。Qt干的事搭建一个窗体和用户交互的壳子。打开按钮、关闭按钮、状态栏文本、显示图像的区域这些都属于GUI范畴。没有它你就只能在终端里看到一串坐标数字没法直观看到“人到底在画面哪个位置”。YOLO干的事目标检测。给它一张图它告诉你图里有几个目标、每个目标的类别和位置框。近几年用的版本基本都是YOLOv5或者YOLOv8项目小的话直接装ultralytics库一行代码加载模型很方便。1.2 为什么不用纯OpenCV或者纯Qt纯OpenCV也能做人检测官方的HOG SVM行人检测大家应该都听过但效果放到今天来看比较拉胯稍微拥挤一点、遮挡一点就漏检。而且HOG特征对姿态变化很敏感稍微换个角度就失效。YOLO这类深度学习模型在这方面的鲁棒性明显好得多。纯Qt更不用说了没有图像处理能力你总不能真拿Qt去逐像素分析。Qt能读图片但不擅长做实时视频处理和检测。所以把这个三个东西拼起来属于典型的“用合适的工具干合适的事”。1.3 我用的是哪一套我用的是Python 3.9 PyQt5 OpenCV 4.8 YOLOv8n。模型用最小的nano版本因为核显CPU也能跑不需要GPU。检测类别只保留person这一类也就是把COCO数据集的80类过滤成1类这样能减少误检、提升一点速度。如果你一定要用C思路完全一样Qt 5.15 OpenCV 4.8 ONNXRuntime或者LibTorch加载YOLO模型。只是代码量会多不少编译坑也多。我个人建议先进门用Python复现业务逻辑等真正要部署到嵌入式设备了再换C。2. 压缩包里的目录结构一个检测系统是怎么组织的2.1 没有把代码全塞进一个main.py网上很多“简单检测系统.zip”解压后通常是一个main.py顶一切几百行代码糊在一起能跑但是没法改。我自己的习惯是拆文件哪怕项目再小也拆。这样换模型、换界面、换摄像头来源都只要动对应模块就行。一个比较清爽的结构差不多是这样simple_detection/ ├── main.py # 程序入口 ├── detector.py # YOLO推理封装 ├── camera_thread.py # 摄像头采集线程 ├── main_window.py # Qt主窗口 ├── models/ │ └── yolov8n.pt # 模型文件 └── requirements.txtmain.py只做一件事启动QApplication创建主窗口。detector.py封装模型加载和推理camera_thread.py负责从摄像头读帧并通过信号发出去main_window.py放按钮、标签、布局。2.2 每个文件到底承担什么detector.py对外暴露一个detect(frame)方法输入BGR图像返回检测框列表。调用方不需要关心YOLO内部是预处理还是后处理只需要知道“传进去一帧出来的就是一组框”。这是很基础的分层思想也是很多人容易忽略的。camera_thread.py继承QThread在run()里循环读取摄像头帧然后发出frame_ready pyqtSignal(object)信号。注意这里发的是object就是把整帧numpy数组传出去省得转成其他类型再传。信号传到主线程后主界面负责显示。main_window.py里最核心的就是update_frame(frame)方法它会调用detector.detect()然后把检测完的帧转换成Qt能显示的QImage再放到QLabel上。这种结构的好处是以后你想把检测结果存成视频直接订阅frame_ready信号拿到帧后写入VideoWriter就行不用去改界面代码。2.3 入口代码其实很短import sys from PyQt5.QtWidgets import QApplication from main_window import MainWindow if __name__ __main__: app QApplication(sys.argv) win MainWindow() win.show() sys.exit(app.exec_())入口短说明逻辑没有堆在这里。很多人写程序喜欢从入口往下堆堆到最后连自己都看不懂其实把入口做成薄薄一层后面所有精力都能花在业务逻辑上。3. 搭环境最容易翻车的几个点OpenCV和Qt的版本搭配3.1 Python环境下先用pip一把梭Python环境下装依赖是真的省心我把requirements.txt贴出来opencv-python4.8.1.78 PyQt55.15.9 ultralytics8.0.100 numpy1.24.4安装命令就一句pip install -r requirements.txt这里面最容易出问题的就是opencv-python和opencv-contrib-python不能同时装。很多人想用SIFT、ORB这些extra模块就额外装了opencv-contrib-python结果两个包打架出现AttributeError: module cv2 has no attribute SIFT之类的怪问题。建议要么装普通版要么装contrib版别两个都上。3.2 PyQt5和PyQt6不是随便替换的PyQt6现在装的人不少但代码和PyQt5有一些差异比如pyqtSignal在两种版本里都能用但exec_()在PyQt6里改成了exec()枚举名的访问方式也变了。为了少踩点坑项目简单的话直接用PyQt5网上资料最多搜问题也好搜。C那边如果不用pip情况就复杂一点。Qt 5.15.2配合OpenCV 4.8.0如果编译器是MSVC2019那OpenCV也必须下载对应的MSVC2019版本。OpenCV官方提供的是基于MSVC2019构建的你用VS2017去链接大概率会报一堆LNK2019。表格整理一下常见组合Qt版本编译器OpenCV版本是否推荐Qt 5.15.2MSVC2019 64bitOpenCV 4.8.0 官方版推荐Qt 5.15.2MinGW 8.1 64bitOpenCV 自编译MinGW版不推荐编译太累Qt 6.5MSVC2019 64bitOpenCV 4.8.0可行但资料少PyQt5Python 3.9opencv-python省心推荐3.3 验证环境只需要一行代码装完之后先别急着写界面先在命令行验证python -c import cv2; print(cv2.__version__) python -c from PyQt5.QtWidgets import QApplication; print(qt ok) python -c from ultralytics import YOLO; print(yolo ok)三个都打印正常再继续。这一步能帮你把“环境问题”和“代码问题”隔离开。如果你是在C环境里至少先写一个读图片、显示窗口的测试程序确认OpenCV和Qt都能正常工作再往上叠YOLO推理。4. 模型推理链路从图像帧到人形框的完整过程4.1 直接用ultralytics的简单姿势YOLOv8用ultralytics库很简单加载模型和推理都封装好了。from ultralytics import YOLO model YOLO(models/yolov8n.pt) def detect_person(frame): results model.predict( frame, imgsz640, conf0.4, classes[0], # 0对应COCO里的person verboseFalse ) boxes results[0].boxes.data.cpu().numpy() return boxesboxes的每一行是[x1, y1, x2, y2, confidence, class_id]这个坐标已经映射回原图了可以直接画。很多人第一次用会担心“我是不是要自己处理letterbox”实际上ultralytics在predict输出之前已经帮我们把坐标映射回原图分辨率你拿到的框就是原始图像坐标系上的。4.2 想搞明白原理手动做一遍预处理和后处理如果只用ultralytics那确实有些黑盒。想搞清楚YOLO完整链路建议手动写一遍ONNX推理。核心步骤就四段第一步letterbox缩放。YOLO输入一般是640x640而摄像头来的帧可能是1280x720不能直接拉伸否则目标会变形。所以先把图像等比缩放长边放到640短边补灰边到640补成正方形。这一步叫letterbox。缩放比例和padding偏移量要记住后面画框的时候要用。第二步归一化和维度变换。把图像的每一个像素值从0~255归一化到0~1然后把维度从HWC转成CHW再加一个batch维度变成[1, 3, 640, 640]。OpenCV的blobFromImage可以做这个但默认的减均值要小心YOLOv5/YOLOv8一般是直接除以255不设减均值。第三步推理输出。以YOLOv8的80类COCO模型为例输出shape是[1, 84, 8400]。其中8400是三个尺度的anchor点总和84是“4个坐标 80个类别概率”的意思。和YOLOv5不一样的是YOLOv8从输出上已经去掉了objectness直接就是类别概率所以解析起来反而更简单。第四步坐标反算和NMS。把8400个候选框里置信度低于阈值的丢掉然后做非极大值抑制NMS把同一个目标的重复框合并掉。最后按之前记录的letterbox偏移量把坐标缩放回原图。手动后处理代码量不小但一旦写过再看ultralytics就相当于看说明书了。很多人把模型换成ONNX之后觉得效果不对多数时候就是letterbox的偏移量没处理好画出来的框整体偏移。4.3 画框OpenCV的拿手好戏后处理拿到的是[x1, y1, x2, y2, conf, cls]画框就很直接了for det in boxes: x1, y1, x2, y2, conf, cls map(int, det[:4]), det[4], int(det[5]) label fperson {conf:.2f} cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, label, (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)要注意的是cv2.rectangle的坐标全得是整数。如果直接把float传进去OpenCV虽然不报错但画出来的框会有点对不齐。坐标转换是这种小项目里最容易被忽略的细节。5. Qt界面与OpenCV画面的互相转换别被Mat和QImage搞晕5.1 BGR到RGB是第一步不能省OpenCV读出来的帧是BGR顺序而Qt的QImage默认是RGB如果你不转换直接丢进界面画面里人脸会变成蓝色调整体颜色非常诡异。所以每次把帧送进界面之前先做一次rgb_frame cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)这一步看起来简单但很多人就是会忘。C下同理cv::cvtColor是必需的。5.2 QImage的构造参数有一个最容易被忽略的坑转换完BGR到RGB之后构造QImage时需要注意bytesPerLine这个参数。我先给一个正确写法h, w, ch rgb_frame.shape bytes_per_line ch * w image QImage(rgb_frame.data, w, h, bytes_per_line, QImage.Format_RGB888)bytesPerLine必须等于图像一行占用的字节数。正常情况下是3 * w但如果图像在内存里有行对齐直接用rgb_frame.strides[0]更保险。很多人写成w * h或者不写结果图像显示出来就是歪的、带斜线的就是这一步错了。还有一个深坑QImage使用rgb_frame.data构造时是共享内存的并不负责持有数据。如果你在函数里构造完QImage函数返回后原rgb_frame被释放QImage就会变成悬空指针轻则花屏重则崩溃。解决办法是image QImage(rgb_frame.data, w, h, bytes_per_line, QImage.Format_RGB888).copy().copy()会把数据复制一份让QImage拥有自己的内存。或者你也可以在持有frame引用的前提下同时持有QImage但这样做容易越改越乱不如直接拷贝省心。5.3 QLabel上显示检测画面QImage最终要变成QPixmap才能显示pixmap QPixmap.fromImage(image) pixmap pixmap.scaled(self.label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation) self.label.setPixmap(pixmap)scaled这步会把图像缩小到和QLabel一样宽高同时保持宽高比。如果不缩放图像多大就显示多大1280x720的帧在界面上只能看到左上角一块。如果你想再提升一点流畅度可以不用SmoothTransformation。这个模式缩放出来的图质量好但慢在低端CPU上会拖后腿。改成FastTransformation后画面会粗糙一点但帧率能上去一些。实际项目里我经常留一个设置变量让用户自己选画质优先还是流畅优先。6. 实测效果与性能调优多线程、跳帧、还有那些坑6.1 单线程跑YOLO会卡到什么程度我自己的CPU是六核i5用YOLOv8n、输入640x640MPS或者纯CPU推理大概要200到300毫秒一帧也就是每秒只能跑三四帧。摄像头本身是30帧但你每帧都阻塞在检测上界面就会一顿一顿鼠标拖动窗口都发飘。所以“简单检测系统”想真正能用多线程是绕不开的。最常见的方案是一个线程读摄像头一个线程做推理一个主线程显示。class CameraThread(QThread): frame_ready pyqtSignal(object) def run(self): cap cv2.VideoCapture(0) while not self.isInterruptionRequested(): ret, frame cap.read() if ret: self.frame_ready.emit(frame)主界面的槽函数收到frame后再拿去做YOLO推理。这样看起来好像是把推理放在主线程还是卡但至少摄像头采集不丢帧。如果你想连界面也不卡就得把推理也放到另一个线程检测结果通过信号发回来。6.2 三种调优方案亲测下来各有取舍我把试过的方案整理成表方案表现适用场景主线程同步推理简单但界面卡死只处理图片不处理视频摄像头线程 主线程推理视频能打开但显示帧率跟着检测帧率走偶尔用一下的Demo摄像头线程 推理线程 队列界面流畅检测结果滞后几帧正式演示、长时间运行第三种方案里摄像头线程负责读帧并直接显示到界面相当于“预览”。推理线程从队列里取帧检测完把带框的帧发回界面界面只在收到检测结果时更新一次画框画面。这样预览一直流畅检测结果稍微慢一点但人的视觉感知不会觉得别扭。6.3 跳帧也是有效手段如果不想上多线程也不想卡可以跳帧。比如每3帧才推理一次中间两帧直接显示原始画面。这样检测频率是10FPS但显示依然是30FPS的流畅度。缺点是检测框会有那种“一卡一卡”的跟踪感跳帧越多越明显但小项目完全够用。frame_count 0 det_box_cache [] while True: ret, frame cap.read() frame_count 1 if frame_count % 3 0: det_box_cache detect_person(frame) for box in det_box_cache: draw_box(frame, box)这样检测结果不是实时刷新的但CPU占用直接降到原来的三分之一很多老电脑也能跑起来。6.4 内存增长、摄像头打不开这些老问题实际跑起来之后最常见的三个问题第一内存越用越大。ultralytics在循环里反复调用predict有些内部缓存会积累尤其opencv窗口也开着时更明显。我后来把predict(frame)换成model(frame)并在detect方法里用with torch.no_grad():包住推理内存稳定不少。第二摄像头打不开。cv2.VideoCapture(0)返回False的原因很多常见是权限没给或者笔记本的摄像头索引不是0而是1。建议在界面上留一个输入框让用户自己填摄像头索引而不是写死。第三检测框在画面缩放后对不准。如果你把帧缩放后再推理同时也把帧缩放后显示那么检测框坐标要按显示比例重新换算。很多人在这个点算反了导致框偏到右上角。正确的换算很简单scale_x label_width / original_width scale_y label_height / original_height然后画框时把x1 * scale_x、y1 * scale_y。6.5 换模型、换类别就两行代码的事这套系统最让我满意的点是扩展性。想检测所有COCO类别就把classes[0]去掉想换成大模型提高准确率把yolov8n.pt换成yolov8s.pt或者yolov8m.pt想检测车牌就找一个车牌检测的模型权重或者重新训练。系统本身不用动这就是一开始拆分模块的好处。我后来还加了一个截图功能检测到人的时候按一下快捷键把当前带框画面存成PNG。这个功能实际演示时特别加分因为观众能看到“从视频流里识别出人”的闭环。存储只加了一个按钮和一个槽函数代码量很小但效果很直观。如果你也想做类似的一个小系统我建议别急着去找现成的zip包先按这个思路自己搭一遍。搭的过程里踩的坑每一个都是实打实的调试经验。等你自己跑通之后再回头看那些压缩包里的代码就会发现它们很多其实写得并不好只是“看起来全”。自己动手过一遍比下载一百个demo都有用。本文还有配套的精品资源点击获取