一、前言
本项目完整实现工业可用的视频 AI 播放器:FFmpeg 原生解码视频流,独立解码线程、独立 AI 检测线程,队列满丢弃旧帧,断流指数退避重连,本地视频 / 图片也支持检测,QPainter 绘制检测框,可直接编译运行,适合二次开发。
二、展示
本地视频
在线视频
三、项目简介
RTMPFaceYOLO:Qt + FFmpeg + OpenCV DNN,拉取 RTMP/RTSP/HTTP 直播流、本地视频、图片,运行 YOLOv5 ONNX 目标检测,画面叠加检测框、类别、置信度,左上角悬浮展示解码 FPS、检测 FPS、单帧推理耗时。
3.1 核心功能
- 输入源:RTMP / RTSP / HTTP 网络流、本地视频文件、单张图片
- AI 推理:OpenCV DNN 加载 YOLOv5s‑ONNX 模型,COCO80 类目标检测
- 多线程隔离:UI 渲染线程、FFmpeg 解码线程、YOLO 检测线程完全分离
- 流稳定性:RTSP over‑tcp、低延迟参数配置;网络断开指数退避自动重连
- 背压处理:检测队列有界,队列满丢弃最旧帧,永远处理最新画面,延迟不会累积
- 可观测性:实时统计解码 FPS、检测 FPS、推理耗时,区分 “无检出” 和 “未执行推理”
- 本地文件播放:按原视频帧率做时间对齐播放,不会倍速飞速播放
- 资源安全:FFmpeg 资源统一释放,跨线程自定义结构体元类型注册,信号槽安全传递检测结果
3.2 技术栈
- C++ / Qt Widgets
- FFmpeg:视频流解码、格式转换 sws_scale
- OpenCV DNN:YOLOv5 ONNX 推理,letterbox 预处理、NMS 非极大抑制
- 构建:CMake/qmake,Windows/Linux 均可编译
四、目录结构
RTMPFaceYOLO/ ├── src/ # 源码 │ ├── main.cpp # 程序入口:命令行解析、元类型注册、默认拉流地址 │ ├── videoplayer.h/.cpp # VideoPlayer(UI+渲染)+ DecodeThread(FFmpeg 解码线程) │ ├── detectthread.h/.cpp # DetectThread(有界队列 + 抽帧检测线程) │ └── detector.h/.cpp # Detector(OpenCV DNN 封装:加载/预处理/推理/后处理) ├── models/ │ ├── yolov5s.onnx # fp16转fp32实际加载的模型(fp32, COCO 80 类, 输入 640×640) │ └── yolov5s_orig_fp16_opset17.onnx # 原始备用 ├── bin/ # 运行时输出(exe + Qt/FFmpeg/OpenCV dll) ├── build/ # 构建中间产物五、类职责说明
| 类 | 文件 | 职责 |
|---|---|---|
VideoPlayer | videoplayer.cpp | UI 主窗口;管理解码、检测线程生命周期;模型路径查找;接收帧与检测结果;QPainter 绘制框标签;FPS 悬浮统计 |
DecodeThread | videoplayer.cpp | QThread 子类;FFmpeg 拉流解码;sws 转 RGB24;发送sigImage(QImage);断流指数退避重连;本地视频帧率同步 |
DetectThread | detectthread.cpp | QThread 子类;内置 Detector;有界队列,满丢旧帧;按时间间隔抽帧推理;信号回传检测结果 |
Detector | detector.cpp | 非线程安全;ONNX 模型加载、letterbox 预处理、推理、置信度过滤、NMS、坐标反映射;仅在检测线程串行调用 |
Detection结构体 | detector.h | 单目标检测结果:原图 QRect 框、类别 ID、置信度;需要Q_DECLARE_METATYPE支持跨线程信号槽 |
六、整体数据流
┌─────────────┐ sigImage(QImage RGB888) ┌──────────────┐ │ DecodeThread│ ─────────────────────────► │ VideoPlayer │ │ (FFmpeg解码) │ │ slotShowImage └─────────────┘ └──────┬───────┘ │ submit(img) ▼ ┌──────────────┐ │ DetectThread │ │ 有界队列+抽帧 │ └──────┬───────┘ │ Detector::detect() │ ├─ preprocess: letterbox→640×640→blob │ ├─ forward(): ONNX 推理(计时) │ └─ postprocess: 阈值过滤→NMS→坐标反映射 ▼ sigResult(QVector<Detection>) ┌──────────────┐ │ VideoPlayer │ │ slotDetection│ │ → render() │ QPainter 画框+标签 └──────────────┘main.cpp解析命令行流地址,调用VideoPlayer::open(url);- 如果是图片执行单次检测;视频 / 网络流启动
DecodeThread,开启检测则同时启动DetectThread; - 解码线程输出 RGB888 格式
QImage,信号发送给 UI 线程;UI 保存最新帧,把图像提交给检测线程队列; - 检测线程做时间限流(距离上次推理不足 100ms 直接跳过),调用 Detector 推理;
- 推理完成通过信号把检测结果回传给 UI;UI 保存上一次检测结果,使用
QPainter绘制矩形框、类别标签底色,刷新界面显示。
关键点:检测是旁路尽力而为任务,画面流畅性完全由解码线程保障,检测慢不会拖慢播放。
七、核心设计思路:视频流如何做到不卡顿
现实场景解码 30FPS,YOLO CPU 推理只有 3‑6FPS,如果每帧都推理或者无限队列,延迟会越来越大。本项目采用三流水线并行 + 丢帧背压策略。
表格
| 环节 | 实现机制 |
|---|---|
| 解码不阻塞 UI | FFmpeg 解码放在独立QThread,仅通过信号发送 QImage,UI 线程只负责渲染 |
| 解码不阻塞检测 | 检测独立线程,有界队列做解耦 |
| 检测慢不拖累播放 | 队列设置上限,队列满丢弃最旧帧,永远处理最新画面,播放依旧保持高 FPS |
| 推理不抢占全部 CPU | FFmpeg 解码器设置线程数,预留 CPU 资源给 AI 推理 |
| 避免画面闪烁消失 | 保留上一次检测结果,直到新推理结果到达,不会因为推理间隔导致检测框瞬间消失 |
| 本地文件无时间漂移 | 使用QElapsedTimer累计时间,解码速度快于文件帧率时主动 sleep 补齐时间 |
| 渲染轻量化 | 没有新检测结果直接显示原图,只有检测更新时才复制图像绘制框 |
核心思想:播放和检测解耦,画面优先,检测尽力而为。
八、项目亮点 & 踩坑记录
8.1 亮点
- QImage 与 cv::Mat 零拷贝包装:直接复用 QImage 内存构造 cv::Mat,仅 resize 推理输入时才发生内存拷贝,降低 CPU 内存开销。
- 有界队列丢弃旧帧:队列满丢掉最老帧,只保留最新画面,杜绝实时流延迟持续上涨,是视频 AI 工程非常关键的设计。
- 断流指数退避重连:重试间隔 1s→2s→4s,最大 30s 封顶;sleep 支持中断,关闭播放器时可以立刻退出线程,不会卡死等待 sleep 结束。
- FFmpeg 低延迟拉流参数:
nobuffer、low_delay、rtsp‑transport=tcp,实现网络流秒开,降低缓冲延迟。 - 本地视频帧率对齐:不倍速飞速播放,按照源视频时间轴播放。
- 完整可观测指标:同时展示解码 FPS、检测 FPS、推理耗时,方便定位性能瓶颈;区分 “没有目标” 和 “还没有执行推理” 两种状态。
- 线程安全处理:自定义结构体
Detection做元类型注册;Detector 明确非线程安全,只在检测线程串行调用;FFmpeg 资源统一释放出口。
8.2 踩坑记录
- YOLO ONNX 输出维度顺序坑YOLOv5 导出 ONNX 存在两种 shape:
[1,25200,85]与[1,85,25200]。如果转置逻辑写反,现象很隐蔽:推理耗时正常,但是完全输出不到检测框。需要代码做 shape 自动判断与归一化。 - 检测框闪烁消失问题解码 30FPS,推理只有 4FPS,如果推理结果一完成就清空旧结果,检测框只会闪现几十毫秒肉眼几乎看不见。解决方案:保留上一轮检测结果,直到新结果到来才更新。
- BGR/RGB 通道顺序问题FFmpeg sws 输出 RGB24,YOLOv5 训练输入是 RGB,调用
blobFromImage时swapRB=false;如果换成 OpenCV VideoCapture 读取,输出 BGR,此时必须开启 swapRB,否则推理效果完全报废。 - 跨线程传递自定义结构体
QVector<Detection>跨线程信号槽,头文件Q_DECLARE_METATYPE(Detection),main 函数执行qRegisterMetaType<QVector<Detection>>(),否则 Qt 队列信号槽直接失效。 - 模型修复下载的yolov5s.onnx 是 opset 17 + FP16 导出的,OpenCV 4.5.5 DNN 不兼容。
写转换脚本 fix_all.py做了三类图变换:FP16→FP32(权重 + Constant 节点)、Split→Slice(OpenCV 只支持等分切分)、axes 输入转 attribute。
九、后续升级扩展方向
9.1 性能优化
- GPU 推理:OpenCV DNN 切换
DNN_BACKEND_CUDA,推理耗时可以从 160ms 下降至 10‑20ms;需要自行编译带 CUDA 支持的 OpenCV。 - 更换推理引擎:ONNX Runtime / TensorRT,加载 fp16 模型进一步提速。
- 调整推理输入尺寸:640→416/320,牺牲小目标检测换取速度;或者更换轻量模型 YOLOv5n / YOLOv8n / YOLO11n。
- 硬件解码:FFmpeg 开启 cuvid/qsv/d3d11va 硬件解码,降低 CPU 占用。
9.2 新增功能
- 目标追踪:接入 ByteTrack/SORT,给目标分配稳定 ID,消除检测框跳动,解决推理间隔带来的画面错位。
- 周界告警:越线检测、区域入侵检测,弹窗 / 声音报警。
- 截图与录像:检测触发自动保存图片 / 片段。
- 结果上报:检测结果输出 JSON/CSV 日志,WebSocket 推送给后端服务。
- 多路视频:多解码线程 + 检测线程池,网格布局多路摄像头同时检测。
9.3 UI 交互
- 参数配置面板:置信度阈值、NMS 阈值、检测抽帧间隔、类别过滤。
- 检测框插值平滑,推理间隔内做位置线性插值,消除画面卡顿感。
十、小结
本项目完整落地了一个Qt 视频 AI 播放器,把 FFmpeg 解码、多线程架构、背压丢帧、异常重连、OpenCV‑DNN 推理全部整合。
🔗Gitee 源码地址:先空下(...)
标签:QtFFmpegOpenCVYOLOv5ONNXRTSPRTMP多线程DNN视频目标检测