简介:基于OpenCV、Qt与YOLO实现的实时人像检测系统完整工程资源,面向计算机视觉入门者及需要快速搭建桌面检测演示的Qt开发者,也适合毕业设计或课程项目参考,解决“从摄像头或静态图片中检测人并框选”的落地问题。压缩包共26个文件,约4.96MB,主体为C++源码(.cpp/.h)、Qt界面与资源配置(.ui/.qrc)、模型配置文件(.json)及说明文档(.pdf),另含样例图片和演示动图(.png/.jpg/.gif),便于直接查看运行效果;工程内置CMakeLists.txt,可在常见环境下编译验证。已有85人学习浏览;通过源码可学习YOLO模型推理与置信度框输出、OpenCV图像预处理与VideoCapture视频捕获、Qt画布叠加边界框等关键模块,并了解主循环控制与性能优化思路。资源体量精简但链路完整,从界面到算法形成闭环,适合作为目标检测工程化的实践样例和二次开发起点。
1. 一个 OpenCV + Qt + YOLO 的人形检测系统,为什么值得自己亲手搭一遍
做机器视觉的人,迟早会碰上这么一件事:算法明明在 Python 脚本里跑得挺好,一但要做成“能给人用的工具”,就卡住了。要窗口、要按钮、要视频流实时显示、要能在画面里把人框出来——这时候你需要的不是一个算法笔记本,而是一个完整的桌面应用。基于 opencv + qt + yolo 实现的简单检测系统,解决的就是这个“从脚本到软件”的鸿沟。它用 OpenCV 做图像采集与预处理,Qt 负责界面和交互,YOLO 负责把画面里的人检测出来并用矩形框标定位置。适合刚接触 Qt 和 YOLO 的开发者,也适合要快速出演示项目的工程师。这套东西的好处是每一层都能单独替换——觉得 YOLOv5 太重换 YOLOv8 也行,觉得 QTwidget 太老换 QML 也成。先把管线跑通,后面才有资格谈优化。
2. 把检测系统拆成三层:界面、视频流、推理各自该干什么
2.1 OpenCV 在系统里的角色:不是跑模型,而是管好每一帧画面
很多人一上来就把 OpenCV 当成“跑 YOLO 的库”,这是理解上的偏差。在这套系统里,OpenCV 的主要职责是采集视频、处理图像格式、画检测框、显示画面。YOLO 模型的推理工作可以交给 OpenCV 的 DNN 模块来做,但也可以换成 PyTorch 或 ONNX Runtime——OpenCV 只负责“喂帧”和“取结果”。
我在做这个系统时的习惯是:用cv::VideoCapture打开摄像头或视频文件,拿到cv::Mat后先做缩放和通道转换(因为 YOLO 输入通常要 640x640,而 OpenCV 默认是 BGR),推理完成后把返回的坐标和置信度画到原图上,最后把这张cv::Mat转成QImage显示在 Qt 的控件里。
// 视频帧处理核心循环:采集 -> 预处理 -> 推理 -> 绘制 -> 转显示 cv::Mat frame; cv::VideoCapture cap(0); // 0 表示默认摄像头,换成文件路径则读视频 if (!cap.isOpened()) { qDebug() << "无法打开视频源"; return; } while (cap.read(frame)) { cv::Mat resized; cv::resize(frame, resized, cv::Size(640, 640)); // 匹配 YOLO 输入尺寸 // 推理函数返回检测到的目标列表 std::vector<Detection> dets = detector.detect(resized); // 在原图画框,注意坐标要映射回原图尺寸 for (const auto& d : dets) { cv::rectangle(frame, d.box, cv::Scalar(0, 255, 0), 2); cv::putText(frame, d.label + " " + QString::number(d.confidence, 'f', 2).toStdString(), cv::Point(d.box.x, d.box.y - 5), cv::FONT_HERSHEY_SIMPLEX, 0.6, cv::Scalar(0, 255, 0), 2); } // 转成 QImage 发给界面显示 cv::Mat rgb; cv::cvtColor(frame, rgb, cv::COLOR_BGR2RGB); QImage qimg(rgb.data, rgb.cols, rgb.rows, rgb.step, QImage::Format_RGB888); emit frameReady(qimg.copy()); // 拷贝一份,避免悬垂指针 }这里有两个容易翻车的点:一是坐标映射。yolo 的检测结果是在 640x640 坐标系下给出的,如果不缩放回原图尺寸,画出来的框会偏。二是QImage构造时传入的是cv::Mat的数据指针,如果cv::Mat在 Qt 事件循环处理完之前被释放,界面就会显示花屏或崩溃。我用qimg.copy()做了一次深拷贝,代价是多一次内存复制,换来的是线程安全。
2.2 Qt 界面层:用 Qt Designer 拖出来还是手写布局
Qt 这边的选择,我一般推荐新手直接用 Qt Designer 拖控件,比手写代码布局快得多,也直观。主界面上只需要三个东西:一个用于显示视频画面的QLabel,一个“开始检测”按钮,一个显示当前帧率或检测人数的QLabel。老手会觉得这界面太简单,但正因为简单,才不容易在 UI 上浪费时间。
# 用 PySide6 搭检测界面的最小骨架 from PySide6.QtWidgets import QMainWindow, QLabel, QPushButton, QVBoxLayout, QWidget from PySide6.QtCore import Qt class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("OpenCV + Qt + YOLO 检测系统") self.video_label = QLabel("等待视频源...") self.video_label.setAlignment(Qt.AlignCenter) self.video_label.setMinimumSize(640, 480) self.btn_start = QPushButton("开始检测") self.btn_start.clicked.connect(self.start_detection) self.status_label = QLabel("人数: 0 | 帧率: 0 FPS") layout = QVBoxLayout() layout.addWidget(self.video_label) layout.addWidget(self.btn_start) layout.addWidget(self.status_label) container = QWidget() container.setLayout(layout) self.setCentralWidget(container)Qt 界面层最容易踩的坑是把推理放在 GUI 线程里跑。YOLO 推理一次少说几十毫秒,放主线程里界面会“卡死”,表现为拖不动窗口、按钮点了没反应。解决方式是开一个QThread专门跑视频循环,通过信号把帧发回主线程更新界面。这也是 Qt 官方推荐的模式——永远不要在 GUI 线程里做耗时操作。
2.3 YOLO 推理封装:模型的加载和预处理必须固定下来
YOLO 部分的核心是一个封装好的检测器类。无论你最终选 YOLOv5、YOLOv8 还是 YOLOv7,对外暴露的接口保持一致——输入一张cv::Mat,输出一组检测框、类别和置信度。这样后面换模型时主程序一分不用改。
import cv2 import numpy as np class YOLODetector: def __init__(self, weights_path, config_path, conf_thresh=0.5, iou_thresh=0.45): # OpenCV DNN 加载 YOLO 模型 self.net = cv2.dnn.readNetFromDarknet(config_path, weights_path) # 优先用 CUDA 加速,没有就退回 CPU self.net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA) self.net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA) self.conf_thresh = conf_thresh self.iou_thresh = iou_thresh self.classes = self._load_classes("coco.names") def detect(self, frame): # 构建 YOLO 输入 blob:缩放到 640x640,归一化到 0-1 blob = cv2.dnn.blobFromImage(frame, 1/255.0, (640, 640), swapRB=True, crop=False) self.net.setInput(blob) # 获取 YOLO 的三个输出层 layer_names = self.net.getLayerNames() output_layers = [layer_names[i - 1] for i in self.net.getUnconnectedOutLayers()] outputs = self.net.forward(output_layers) boxes, confs, class_ids = self._post_process(outputs, frame.shape) return boxes, confs, class_ids def _post_process(self, outputs, frame_shape): h, w = frame_shape[:2] boxes, confs, class_ids = [], [], [] for output in outputs: for detection in output: scores = detection[5:] class_id = np.argmax(scores) confidence = scores[class_id] if confidence > self.conf_thresh: # 将中心点坐标转为左上角坐标 cx, cy, bw, bh = detection[:4] * np.array([w, h, w, h]) x = int(cx - bw / 2) y = int(cy - bh / 2) boxes.append([x, y, int(bw), int(bh)]) confs.append(float(confidence)) class_ids.append(class_id) # NMS 去除重叠框 idxs = cv2.dnn.NMSBoxes(boxes, confs, self.conf_thresh, self.iou_thresh) final_boxes = [boxes[i] for i in idxs.flatten()] final_confs = [confs[i] for i in idxs.flatten()] final_ids = [class_ids[i] for i in idxs.flatten()] return final_boxes, final_confs, final_ids这段代码的blobFromImage参数有个细节:swapRB=True是因为 OpenCV 读图是 BGR,而 YOLO 训练时用的是 RGB,必须交换通道;1/255.0是归一化。如果漏了swapRB,检测效果会严重劣化——不报错,但就是测不准,属于典型的“黑匣子”问题。还有 NMS 的iou_thresh,如果你做的是人形检测,建议 0.45 左右,太小会漏掉重叠人群中的目标,太大则会把一整个人框切成好几个框。
3. 用 OpenCV 的 DNN 模块跑 YOLO:从下载权重到跑通第一帧的最小方案
3.1 模型文件从哪里来:YOLO 预训练模型下载与格式转换
标题里的“简单检测系统”,我理解不需要自己训练模型,直接用预训练权重就行。问题在于 YOLO 官方仓库给的是.pt格式(PyTorch),OpenCV 的 DNN 模块认的是.weights(Darknet)或.onnx。所以拿到手的第一件事是转换格式,或者直接下载 Darknet 版本的权重。
# 方式一:下载 Darknet 官方预训练权重(YOLOv4-tiny 约 23MB,适合 CPU 实时) wget https://github.com/AlexeyAB/darknet/releases/download/yolov4/yolov4-tiny.weights wget https://raw.githubusercontent.com/AlexeyAB/darknet/master/cfg/yolov4-tiny.cfg wget https://raw.githubusercontent.com/pjreddie/darknet/master/data/coco.names # 方式二:把 PyTorch 权重转成 ONNX(需要先装好 torch) python -c " import torch model = torch.hub.load('ultralytics/yolov5', 'yolov5s', pretrained=True) model.eval() dummy = torch.zeros(1, 3, 640, 640) torch.onnx.export(model, dummy, 'yolov5s.onnx', opset_version=11, input_names=['images']) print('转换完成') "我个人的偏好是用 ONNX 格式,因为 OpenCV 对 ONNX 的支持比 Darknet 原生格式更稳定,而且 ONNX 还能被其他推理引擎(如 ONNX Runtime、TensorRT)复用。如果你只是想在本地快速验证,yolov4-tiny.weights + cfg + coco.names三件套就够了,不用装 PyTorch 直接跑。
读取 ONNX 模型的方式比 Darknet 格式简单很多:
import cv2 # ONNX 格式加载只需一个文件 net = cv2.dnn.readNetFromONNX("yolov5s.onnx") # 检查是否支持 CUDA if cv2.cuda.getCudaEnabledDeviceCount() > 0: net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA) net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA) print("使用 CUDA 加速") else: net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV) net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU) print("CUDA 不可用,退回 CPU")注意:YOLOv5 的 ONNX 输出格式和 Darknet YOLO 不一样,v5 的输出是一个1x25200x85的张量,后处理时不需要遍历三个输出层,直接用这个张量做阈值过滤和 NMS 即可。如果你把 YOLOv4 的后处理代码套到 YOLOv5 上,会报维度错误或者检不出任何东西。
3.2 CPU 和 GPU 的选型:为什么我建议先跑通 CPU 再考虑加速
经常有人问:我的机器能用 GPU 跑吗?我的建议是:第一次搭系统,先把 CPU 方案跑通,再考虑 CUDA。原因很现实——OpenCV 的 CUDA 支持依赖编译时的选项,很多预编译的 OpenCV(比如pip install opencv-python)根本没有启用 CUDA,你写了setPreferableBackend(DNN_BACKEND_CUDA)它会静默回退到 CPU,不报错但也不会变快。排查这个比排查模型本身还要费时间。
# 检查当前 OpenCV 是否启用了 CUDA python -c "import cv2; print('CUDA enabled:', cv2.cuda.getCudaEnabledDeviceCount() > 0)"如果返回False,你有两个选择:一是去下载带 CUDA 的预编译 OpenCV(比如 opencv-python 的 cuda 版轮子),二是从源码自己编译。自己编译 OpenCV 是一个大坑,CMake 配置项多、耗时长,新手很容易在这里消耗一整天。所以我建议先确认你的显卡型号,如果是 NVIDIA 的卡,找 opencv-python 的 CUDA 预编译版本直接装,免得在编译上折腾。
# 安装带 CUDA 支持的 OpenCV(示例,具体包名以你本机环境为准) pip install opencv-python-headless # 无 GUI 依赖的版本,服务器上常用 # 如果你要 CUDA 支持,需要找对应的轮子或自行 cmake 编译CPU 跑yolov4-tiny在 640x640 输入下大概 200-400ms 一帧,用来验证流程完全够用。如果你要实时检测,后面再按“ONNX 模型 → TensorRT”的路线走,那是一个独立的优化工程,不在“简单检测系统”的讨论范围内。
3.3 Qt 与 OpenCV 的图像类型转换:BGR、RGB 与 QImage 的一次性整明白
这也是我看到的“血泪经验”最多的地方。OpenCV 的cv::Mat默认是 BGR,Qt 的QImage支持Format_RGB888,但不支持Format_BGR888。直接把 BGR 数据塞进 QImage 会导致红蓝通道互换——画面里的红色物体会显示成蓝色,人脸肤色发青。
// 正确的转换方式:先 cvtColor 再构造 QImage cv::Mat rgb_frame; cv::cvtColor(bgr_frame, rgb_frame, cv::COLOR_BGR2RGB); // 注意这里必须用 rgb_frame.data,不能挪用原 bgr_frame 的指针 QImage img(rgb_frame.data, rgb_frame.cols, rgb_frame.rows, rgb_frame.step, QImage::Format_RGB888);还有一种更高效的做法是直接用QImage::Format_BGR888(Qt 5.14 之后才支持)。如果你的 Qt 版本够新,可以省掉一次cvtColor:
// 需要 Qt 5.14+,且确认 OpenCV Mat 是连续内存 QImage img(frame.data, frame.cols, frame.rows, frame.step, QImage::Format_BGR888);但这个写法的坑在于QImage默认不管理内存——cv::Mat一旦被释放,img就成了野指针。所以我始终建议在跨线程传帧时用copy()或深拷贝,这是最稳妥的后悔药。等你把整个流程跑通了,再回来优化掉这次拷贝也不迟。
4. 把三块拼成一个可运行的系统:工程结构与线程模型
4.1 推荐的项目目录结构
到了这一步,你的代码不再是一个.py文件,而是一个真正的工程。合理的目录结构能让你后面加功能时不用重构。
detection_system/ ├── main.py # 程序入口 ├── detector/ │ ├── __init__.py │ ├── yolo_detector.py # YOLO 推理封装 │ └── config.py # 模型路径、阈值等配置 ├── ui/ │ ├── __init__.py │ ├── main_window.py # 主窗口逻辑 │ └── camera_thread.py # 视频采集与推理线程 ├── models/ │ ├── yolov4-tiny.weights │ ├── yolov4-tiny.cfg │ └── coco.names └── requirements.txt模型文件单独放一个models目录是个好习惯——你会发现每次换模型、调参数,实际上只动了models和config.py,主程序基本不用碰。
4.2 线程分离:采集与界面更新互不阻塞
前面提过推理不能放在 GUI 线程。常规做法是开一个QThread,在里面循环读帧和推理,每处理完一帧就发一个信号回主线程更新界面。这里我给一个标准的QThread实现:
from PySide6.QtCore import QThread, Signal import cv2 import numpy as np class CameraThread(QThread): # 定义两个信号:一个传画面,一个传统计信息 frame_ready = Signal(object) # 传 QImage stats_updated = Signal(int, float) # 人数, FPS def __init__(self, detector, video_source=0, parent=None): super().__init__(parent) self.detector = detector self.video_source = video_source self.running = True self.target_fps = 30 def run(self): cap = cv2.VideoCapture(self.video_source) if not cap.isOpened(): self.stats_updated.emit(-1, 0) # 用 -1 表示出错 return import time frame_count = 0 start_time = time.time() while self.running: ret, frame = cap.read() if not ret: break # 检测 boxes, confs, class_ids = self.detector.detect(frame) # 只统计 person(COCO 中 person 的 id 是 0) person_count = sum(1 for cid in class_ids if cid == 0) # 画框 for box, conf, cid in zip(boxes, confs, class_ids): if cid == 0: # 只画 person x, y, w, h = box cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) # 计算 FPS frame_count += 1 elapsed = time.time() - start_time if elapsed >= 1.0: fps = frame_count / elapsed start_time = time.time() frame_count = 0 self.stats_updated.emit(person_count, fps) # 转 QImage 并发送 rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = rgb.shape qimg = QImage(rgb.data, w, h, rgb.step, QImage.Format_RGB888).copy() self.frame_ready.emit(qimg) cap.release() def stop(self): self.running = False self.wait()线程模型的关键在于信号发出去之后不要再去碰那个对象。frame_ready信号里的QImage是通过.copy()深拷贝出来的,所以主线程拿到手之后可以放心用。很多人会在这里偷懒不调用.copy(),结果界面偶发闪退,查半天才发现是数据被线程释放了。
4.3 主窗口槽函数怎么接信号
主线程这边的槽函数很简单:收到frame_ready就把它贴到QLabel上,收到stats_updated就更新状态栏文字。
from PySide6.QtWidgets import QMainWindow, QLabel, QPushButton, QVBoxLayout, QWidget class MainWindow(QMainWindow): def __init__(self): super().__init__() self.detector = None self.camera_thread = None self._setup_ui() def _setup_ui(self): self.setWindowTitle("检测系统") self.video_label = QLabel() self.video_label.setMinimumSize(640, 480) self.btn_toggle = QPushButton("开始检测") self.btn_toggle.clicked.connect(self.toggle_detection) self.status_label = QLabel("未启动") layout = QVBoxLayout() layout.addWidget(self.video_label) layout.addWidget(self.btn_toggle) layout.addWidget(self.status_label) widget = QWidget() widget.setLayout(layout) self.setCentralWidget(widget) def toggle_detection(self): if self.camera_thread is None: # 初始化检测器和线程 self.detector = YOLODetector( weights_path="models/yolov4-tiny.weights", config_path="models/yolov4-tiny.cfg" ) self.camera_thread = CameraThread(self.detector, 0) self.camera_thread.frame_ready.connect(self.update_frame) self.camera_thread.stats_updated.connect(self.update_stats) self.camera_thread.start() self.btn_toggle.setText("停止检测") else: self.camera_thread.stop() self.camera_thread = None self.btn_toggle.setText("开始检测") def update_frame(self, qimg): self.video_label.setPixmap( QPixmap.fromImage(qimg).scaled( self.video_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation ) ) def update_stats(self, person_count, fps): self.status_label.setText(f"人数: {person_count} | FPS: {fps:.1f}")toggle_detection里的一个坑:每次点击“停止检测”再“开始检测”,如果CameraThread没有完全退出就重新创建,旧的线程还在跑会导致摄像头被占用。这里的stop()方法我写了self.wait(),确保线程完全退出后才置None,这是一个小细节但非常关键。
5. 避坑清单:从环境搭建到运行时常见的 5 个翻车点
5.1 环境安装失败:fatal: cannot mix incompatible Qt library与 Qt 版本冲突
现象:qmake 编译通过后,make时报错fatal: cannot mix incompatible Qt library (version ex50601) with this library。
原因:系统里存在多个 Qt 版本,或者代码编译用的头文件版本和运行时链接的库版本不一致。这个报错最常见于装了 Qt 5.6 的某个组件后又装了 Qt 5.15,或者是系统自带的 OpenCV 依赖了旧的 Qt 库。
解决:先检查qmake -v和运行环境里的 Qt 路径。如果你用的是 Qt Creator,直接在项目设置里指定明确的 Qt 版本路径。如果是命令行编译,检查LD_LIBRARY_PATH是否被污染——把系统的/usr/lib/x86_64-linux-gnu加到最前面,确保链接到系统库而不是某个自编译的 Qt 目录。
5.2 运行时崩溃:qt.qpa.plugin: could not find the qt platform plugin "linuxfb"
现象:在嵌入式设备或无显示环境下运行 Qt 程序,报错找不到 linuxfb 插件。
原因:Qt 安装时没有包含linuxfb平台插件,或者程序在无界面环境下被要求加载显示插件。我在树莓派上交叉编译 Qt 时经常碰到这个问题。
解决:确认你的系统是否需要linuxfb(比如纯 framebuffer 设备),如果需要,在 Qt 安装时勾选相应的平台插件模块;如果不需要,设置环境变量QT_QPA_PLATFORM=offscreen或xcb来绕过:
export QT_QPA_PLATFORM=offscreen # 无界面调试时用 # 或者指定 xcb export QT_QPA_PLATFORM=xcb5.3 检测框颜色错乱:OpenCV 的 BGR 和 Qt 的 RGB 不一致
现象:检测框位置正确,但画面整体颜色偏蓝/偏红,人脸发青。
原因:这是把cv::Mat直接转QImage时最常见的坑。前文已经讲过cv::Mat默认是 BGR,而QImage::Format_RGB888期望 RGB。
解决:统一在 Qt 显示前做一次cv::cvtColor(frame, rgb_frame, cv::COLOR_BGR2RGB)。如果有多个显示路径,建议写一个工具函数来做转换,不要到处复制代码:
QImage cvMatToQImage(const cv::Mat& mat) { cv::Mat rgb; cv::cvtColor(mat, rgb, cv::COLOR_BGR2RGB); return QImage(rgb.data, rgb.cols, rgb.rows, rgb.step, QImage::Format_RGB888).copy(); }5.4 NMS 返回空结果:contourarea() 未定义标识符或输出层解析错误
现象:程序不崩溃、不报错,但检测框始终是空的。打开调试日志后看到idxs.flatten()是空的。
原因:分两种情况——如果报contourarea() 未定义标识符,通常是 OpenCV 版本过旧,cv2.dnn.NMSBoxes在某些旧版本里行为不一致,返回空结果;如果没有报错但检测不到,大概率是 YOLOv5 的输出格式和 Darknet 格式混用了,后处理代码取错了维度。
解决:先确认你的 YOLO 版本。YOLOv4 及之前要用getUnconnectedOutLayers()拿输出层;YOLOv5 及之后的 ONNX 直接一个输出张量搞定。检查方法:
# 打印输出层的 shape 来确认是哪一代 YOLO outputs = net.forward() print(outputs.shape) # (1, 25200, 85) 说明是 YOLOv5 的合并输出 # ([1, 255, 80, 80], [1, 255, 40, 40], [1, 255, 20, 20]) 是 Darknet YOLO另外iou_thresh调太低也会导致 NMS 有意外的空结果。我一般会加上日志打印idxs的长度来确认 NMS 这一步是否出了问题。
5.5 摄像头打不开或指示灯亮但画面黑
现象:cv2.VideoCapture(0)返回True,但read()一直拿到空帧;或者程序能打开摄像头但画面是黑的。
原因:多半是权限问题(Linux 下/dev/video0没有读权限),或者摄像头被其他进程占用。另一个隐藏问题是分辨率——有些摄像头不支持默认的 640x480 以外的分辨率,设置为 1920x1080 后帧率暴跌或直接黑屏。
解决:先用系统自带的相机工具确认摄像头本身可用,再用 OpenCV 尝试设置较低的分辨率和帧率:
cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FPS, 30)如果还是黑屏,加一个解码后端试试:cv2.VideoCapture(0, cv2.CAP_V4L2)。在部分 USB 摄像头上,默认的CAP_ANY会选错后端。
6. 进阶用法:把检测结果保存到文件,以及调优帧率的一个必改参数
系统能跑起来只是第一步。多数人接下来会遇到两个实际需求:一是要把检测结果录下来或者存成日志,二是觉得画面太卡想拉高帧率。这两个问题其实都绕不开同一件事——你要把“检测”变成“记录”。
保存检测结果最简单的方式是在画框之后,同时把原始帧和标注帧都写入VideoWriter:
fourcc = cv2.VideoWriter_fourcc(*'mp4v') writer = cv2.VideoWriter("output.mp4", fourcc, 20.0, (frame_w, frame_h)) # 在 run() 循环里,画完框之后写入 writer.write(frame)注意VideoWriter写入尺寸必须和frame的尺寸一致,否则文件打不开或只有 0KB。还有一个细节:fourcc选mp4v写 MP4 文件,兼容性最好;按实时推理的帧率来定义写入帧率(比如你的 FPS 只有 12,那就写 12,不要写 30),否则视频播放速度是慢动作。
日志保存则简单很多——在stats_updated信号里把时间戳和人数追加到 CSV:
import csv from datetime import datetime def log_detection(person_count): with open("detection_log.csv", "a", newline="") as f: writer = csv.writer(f) writer.writerow([datetime.now().isoformat(), person_count])拉帧率这件事,很多人的第一反应是换 GPU、换模型,但实际最先应该调的是 YOLO 的输入尺寸。blobFromImage里的(640, 640)改成(416, 416),推理时间几乎减半,而检测精度下降并不明显(尤其是人形检测,人形在低分辨率下特征依然很突出)。如果你的场景是室内固定摄像头,人的像素占比足够大,416 完全可用。
我的习惯是把输入尺寸做成配置文件里的一个参数,训练好的模型如果尺度变化不大,推理时做一次自适应——目标帧率达不到 25 就先降到 416,还不够就考虑yolov4-tiny替换yolov5s,而不是一开始就上 TensorRT 那套重型方案。检测系统这种工程,改一个参数能解决的事,不值得引入一个新框架。
最后一句经验之谈:这类的系统,最容易被忽视的不是模型精度,而是视频流的生命周期管理——打开、关闭、重开都要测试,线程的退出路径必须干净,否则界面退出了摄像头灯还在亮。每次写完代码,我都会把“停止检测→再启动→关闭窗口”这个流程反复跑三遍,确认没有资源泄漏才敢交付。希望这份笔记能帮你少走几趟弯路。
本文还有配套的精品资源,点击获取