MMDetection视频检测demo实现:从image_demo到video_demo的完整改造与避坑记录

发布时间:2026/10/17 11:55:20

MMDetection视频检测demo实现:从image_demo到video_demo的完整改造与避坑记录 如果你也在折腾 MMDetection大概率会遇到这样一个需求训练好的模型不光要处理单张图片还要拿它去检测一段视频并且把检测结果保存成新的视频文件。官方 demo 目录里通常只有处理单张图片的脚本想把视频检测跑起来要么自己敲代码要么去找别人封装好的轮子。我最初的想法很简单——在图片 demo 外面套一层视频循环不就完事了吗真正动手之后才发现视频检测并保存这个需求比想象中多踩了三个坑视频编码器配置、颜色空间转换、输出尺寸对齐。这篇文章就是我给 demo 目录添加 video_demo.py 的完整记录从设计思路到逐段代码再到参数调优和问题排查一次性说清楚。1. 为什么官方 demo 缺一个 video_demo.py从 image_demo 的技能边界说起1.1 image_demo.py 到底干了什么先回头看仓库里自带的图片 demo。它做的事情非常集中读入一张图片加载模型调用推理接口拿到结果然后把检测框画在原图上最终保存成一张新图片。整个过程是单帧、单张、单次推理不存在视频流的概念。这个脚本最核心的流程可以概括为模型初始化一次图片读取一次推理一次绘图一次写文件一次。所有环节都是一次性操作所以它可以写得很简洁也不需要关心资源的持续占用问题。视频检测恰恰是在这个基础上增加了“重复”和“持续”两个维度持续读取、持续推理、持续写入。1.2 直接在 image_demo 上改会踩哪些坑很多人包括当时的我会觉得把图片循环换成视频循环代码量应该很小。但实际上有几个坑是隐藏的第一图片 demo 最终调用模型自带的绘制方法把结果写到指定路径。这个绘制方法内部会做窗口显示、结果归一化等操作在视频场景里逐帧调用会非常慢而且一旦在无显示环境比如服务器运行OpenCV 的窗口函数可能会直接卡住或报错。第二图片 demo 的输入输出都是静态图没有编码器、帧率、分辨率的概念。视频保存必须用 VideoWriter 指定编码格式、帧率和帧尺寸。这三项任何一个不对输出文件要么直接打不开要么时长不对要么内容偏色。第三图片 demo 做一次推理就退出了不需要关心显存和内存的释放。但视频可能是几千帧如果每一帧的结果都保存在内存里不去清理长视频跑到一半就可能撑爆内存。1.3 视频检测的整体需求拆分动手写脚本之前我习惯先把需求拆开。视频检测并保存本质上需要以下五个模块模型初始化、视频读取、逐帧推理、结果绘制、视频写出。模型初始化是复用 init_detector不需要重写。视频读取用 OpenCV 的 VideoCapture 就可以。逐帧推理调用的是和图片 demo 相同的 inference_detector 接口。结果绘制这一块要绕开模型自带的 show_result改为自己用 cv2 画框。视频写出则要关注编码器、帧率、尺寸三件事。想清楚这五个模块之后脚本的结构其实就清晰了。难点不在于“推理”而在于“视频读写”这两个平时容易被忽略的环节。所以这篇文章的标题虽然是 video_demo.py但有一半篇幅其实是在讲视频保存的工程细节。2. video_demo.py 完整实现从初始化到逐帧写出的关键代码2.1 流程设计与模块划分我最终写的脚本整体流程是解析参数加载模型打开视频文件读取帧的宽高和帧率初始化 VideoWriter进入循环读帧每读一帧就推理一次把结果画上去写进输出文件直到视频结束最后释放所有资源。参数部分我用了 argparse保留 --config、--checkpoint、--input、--output、--score-thr、--device 这几个常用参数。这样无论你是用自己的训练权重还是测试模型的权重都能通过命令行传参不用改代码。这里有一个设计上的取舍为什么不把绘制检测框的代码继续交给模型的 show_result原因我在刚才提过视频场景下逐帧走它内部那一套东西既慢又容易出问题。自己用 cv2 画框虽然代码多写几行但控制力强得多阈值过滤、颜色选择、是否显示类别文字都变得可控。2.2 核心代码分段拆解模型初始化部分和图片 demo 没有任何区别from mmdet.apis import init_detector, inference_detector model init_detector(config_path, checkpoint_path, devicecuda:0)这里需要提醒一点config 文件和 checkpoint 文件必须配套。如果你用的是别人训练好的权重最好用权重对应的 config 文件去初始化否则即使不报错检测出来的类别和框也可能完全不对。视频读取和参数获取cap cv2.VideoCapture(video_path) fps cap.get(cv2.CAP_PROP_FPS) width int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))fps 和 width、height 这三个值必须从源视频读取而不是自己硬编码。原因后面会详细说这里先记住一个原则写入参数要和输入源对齐。VideoWriter 初始化fourcc cv2.VideoWriter_fourcc(*mp4v) writer cv2.VideoWriter(output_path, fourcc, fps, (width, height))拿到这些参数之后进入循环frame_idx 0 while True: ret, frame cap.read() if not ret: break result inference_detector(model, frame) # 手动绘制检测框 for class_id, bboxes in enumerate(result): for bbox in bboxes: if bbox[4] args.score_thr: continue x1, y1, x2, y2 [int(v) for v in bbox[:4]] cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) label f{class_id} {bbox[4]:.2f} cv2.putText(frame, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) writer.write(frame) frame_idx 12.3 完整脚本参考把上面的代码拼起来加上参数解析和资源释放就是一个能跑的 video_demo.pyimport argparse import cv2 from mmdet.apis import inference_detector, init_detector def parse_args(): parser argparse.ArgumentParser(descriptionvideo demo) parser.add_argument(--config, typestr, requiredTrue) parser.add_argument(--checkpoint, typestr, requiredTrue) parser.add_argument(--input, typestr, requiredTrue) parser.add_argument(--output, typestr, defaultoutput.mp4) parser.add_argument(--score-thr, typefloat, default0.3) parser.add_argument(--device, typestr, defaultcuda:0) return parser.parse_args() def main(): args parse_args() model init_detector(args.config, args.checkpoint, deviceargs.device) cap cv2.VideoCapture(args.input) if not cap.isOpened(): raise IOError(无法打开视频文件检查路径和视频格式) fps cap.get(cv2.CAP_PROP_FPS) width int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) fourcc cv2.VideoWriter_fourcc(*mp4v) writer cv2.VideoWriter(args.output, fourcc, fps, (width, height)) if not writer.isOpened(): raise IOError(VideoWriter 初始化失败检查编码器和输出路径) frame_idx 0 while True: ret, frame cap.read() if not ret: break result inference_detector(model, frame) for class_id, bboxes in enumerate(result): for bbox in bboxes: if bbox[4] args.score_thr: continue x1, y1, x2, y2 [int(v) for v in bbox[:4]] cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) label f{class_id} {bbox[4]:.2f} cv2.putText(frame, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) writer.write(frame) frame_idx 1 cap.release() writer.release() print(f处理完成共 {frame_idx} 帧输出文件{args.output}) if __name__ __main__: main()一个值得注意的细节是推理结果的遍历方式。在 MMDetection 常用版本中inference_detector 的返回值是“按类别拆开的检测框集合”每个类别对应一个数组数组的每一行是[x1, y1, x2, y2, score]。所以用enumerate(result)拿到类别编号再遍历该类别下的所有框逐框过滤分数。这个写法在不同版本之间相对稳定这也是我推荐手动遍历而不是直接取result[0]的原因。3. 视频保存最容易翻车的三个位置编码器、颜色空间、输出尺寸3.1 编码器 fourcc 选择不对文件直接写不出来第一个让我折腾了大半个小时的坑就是 fourcc。OpenCV 的 VideoWriter 必须指定一个四字符编码码比如 mp4v、avc1、XVID不同的扩展名要和不同的编码器搭配。实测下来比较稳的组合是这样输出格式fourcc备注.mp4mp4v兼容性最好绝大多数环境能写出 mp4.mp4avc1部分环境不支持会提示编码器缺失.aviXVID老牌组合文件体积偏大.movmp4v也可以但部分播放器表现一般如果你发现输出文件是 0 字节或者程序跑完没有任何报错但文件打不开优先怀疑 fourcc 和环境解码器的兼容问题。我通常的做法是先用短视频测试分别换 mp4v 和 XVID 各试一次看哪个能稳定写出来再用它跑长视频。很多人在这一步走了弯路因为 OpenCV 的 VideoWriter 初始化时不一定报错甚至返回的 writer.isOpened() 也可能是 True但实际写出来的文件根本没法播放。这也是为什么我在脚本里加了if not writer.isOpened()这个检查至少能拦截一部分初始化失败的情况。3.2 颜色空间搞反检测框和画面整体偏色第二个坑是颜色空间。OpenCV 的 VideoCapture 读出来的 frame 是 BGR 顺序而 MMDetection 模型内部期望的输入是 RGB 顺序同时在预处理阶段会做归一化。也就是说推理接口本身会处理这个转换我们不需要在外部手动改。但问题是很多人画框时想先展示一下于是不自觉地把 frame 转成了 RGB 再操作。一旦你把 frame 从 BGR 转成 RGB再画框、写文件输出视频里所有物体的颜色都会变蓝偏色看起来非常奇怪像加了一层色调滤镜。正确做法是读出来的 frame 保持 BGR 不动直接传给 inference_detector返回的检测框坐标也是对应原始尺寸的。画框时也用 BGR 顺序画比如绿色框就是(0, 255, 0)写进 VideoWriter 时仍然保持 BGR。整个链路不要做任何额外的颜色转换。如果你确实要在画框时调用 cv2.cvtColor 做颜色转换那写入前一定要再转回 BGR。这个转换看似无关紧要但一旦忘记转回来输出视频的颜色就全不对了而且视频不像图片那样能一眼看出问题经常是跑完整个视频才发现。3.3 帧率和输出尺寸必须和源视频对齐第三个坑是尺寸。VideoWriter 初始化时需要传入 frameSize也就是 (width, height)。这个尺寸必须和你实际写入的每一帧形状完全一致。如果视频源的宽高是 1920×1080但你在初始化时传了 1280×720OpenCV 在写入时会出错或者文件写出来只有前几帧能看。有一个更隐蔽的情况某些视频的帧率通过cap.get(cv2.CAP_PROP_FPS)拿到的值可能是 0.0。这通常发生在一些编码不标准的视频上。如果 fps 是 0VideoWriter 初始化时不会明显报错但输出视频的时长会异常。遇到这种情况可以手动兜底一个默认帧率比如 25.0 或 30.0。另外宽高在读取后要转成 int。因为cap.get返回的是 float而 VideoWriter 的 frameSize 需要整数元组。如果不转OpenCV 在某些版本中也能跑但在另一些版本里会直接抛异常。这种低级错误排查起来反而最浪费时间所以我干脆在脚本里先转好。4. 从能跑通到跑得好显存、漏检和结果导出的调优记录4.1 长时间运行要注意显存与内存占用视频检测和图片检测一个很大的区别在于运行时间。一个 10 分钟的视频可能有上万帧模型要重复推理上万次。虽然推理引擎本身不参与梯度计算不会积累梯度图但如果你在一帧推理完成后把大尺寸的 tensor 一直保留在变量里随着循环推进显存占用会缓慢上升最终可能导致“CUDA out of memory”。我的建议是循环体内尽量复用变量名推理结果用完就丢掉不要把所有帧的检测结果都存到 list 里。如果你需要保存分析数据单独用一个轻量结构记录坐标和分数就好不要保留原始的特征图或者整帧数据。还有一个常见的工程问题跑长视频时如果中途显存崩溃前面所有处理都白费了。稳妥的做法是先跑一小段视频验证流程比如截取 30 秒的片段确定模型、编码器、绘制逻辑都没问题再跑完整视频。这个习惯能帮你省下很多无意义的等待时间。4.2 输出视频检测不到目标按这个顺序排查如果你发现输出视频完全没有检测框或者框的数量明显少于预期先别急着怀疑代码。我遇到过的原因一般有三种第一种是分数阈值设得太高。默认的 score_thr 我习惯设为 0.3如果你的模型本身置信度偏低可以降到 0.1 或者 0.2 试试很多人为了“看起来干净”把阈值调到 0.5 以上结果小目标或者遮挡目标全被过滤掉。第二种是模型和数据集不匹配。比如 config 里注册的是 80 类的模型但 checkpoint 是在自定义数据集上训练的这时推理结果里的类别编号和画框对应的类别名就对不上。排查方法是用同一张图片分别跑图片 demo 和视频脚本对比两类输出结果。如果图片 demo 能出框、视频代码不出框问题一定在视频链路如果图片 demo 都不出框就要回头检查权重和 config 的匹配。第三种是读取帧本身出了问题。比如视频编码问题导致某些帧解码后是空的cap.read()返回了 False 直接跳出循环。可以在循环里加一个简单的统计打印读取到的帧数和检测到目标的帧数就能快速定位是解码问题还是模型推理问题。4.3 同时导出结构化检测结果而不只是输出视频很多时候你检测视频不只是为了得到一段画了框的视频还需要分析每帧的目标位置、类别和置信度。这时可以在画框的同时把检测结果写进一个 JSON 或 CSV 文件。最简单的结构就是按帧号记录records [] records.append({ frame: frame_idx, detections: [ {class_id: int(class_id), score: float(bbox[4]), bbox: [int(v) for v in bbox[:4]]} for bbox in bboxes if bbox[4] args.score_thr ] })全程逻辑和画框保持一致只是把画框的代码换成了记录数据的代码。如果视频很长不要每帧都往 JSON 里写最好每隔一定帧数批量写一次或者干脆用 pandas 逐行追加到文件避免内存里积累太多数据。把所有检测结果导出来之后后续做统计、画轨迹、算目标出现频次都方便很多。这是我在实际项目里反复用到的一个扩展点也是视频 demo 从“能看”变成“能用”的关键一步。最后分享一个我踩过多次的经验视频检测脚本里真正影响项目进度的往往不是模型的推理精度而是视频读写的那些边角细节。fourcc 选错、颜色空间没转对、尺寸没对齐这三个问题任意一个出现都能让你花掉比模型调优更多的时间。我的习惯是先把一小段视频完整跑通从读取到保存都验证一遍再上长视频和复杂场景。
延伸阅读

更多相关文章

2026/10/17 11:55:20

AI智能体“大龙虾”爆火:从原理到自建AI打工仔实战指南

2026年开工第一周,技术群里的画风突然变得不太一样:有人转发一只红色大龙虾的图片,有人把它当头像,有人已经拿着它在项目里跑了两天。问一圈之后你会发现,大家聊的并不是海鲜,而是开年被疯抢的新一代AI智能…

2026/10/17 11:50:20

MiniCPM+ghfind实现GitHub开发者风格量化分析

1. 项目概述:一个轻量级代码作者风格分析工具的诞生逻辑 “ghfind用MiniCPM模型,锐评了上千开发者”——这个标题乍看像段程序员圈内的玩笑话,实则藏着一套非常务实的技术路径:它不是在搞AI人格扮演,也不是做情绪化网…

2026/10/17 11:50:20

Unity塔防游戏开发:从场景搭建到性能优化的完整实践

1. 为什么塔防游戏是Unity新手的“黄金练兵场”塔防类游戏(Tower Defense)在Unity3D学习路径中,从来不是“随便选个类型来玩玩”的过渡项目——它是一套被反复验证过的、结构清晰、边界明确、反馈即时的完整开发闭环。我带过不少刚接触Unity的…

2026/10/17 13:00:25

基于机器学习的中文书目自动分类:从TF-IDF到系统实现

做计算机毕业设计,最怕的不是不会写代码,而是选题本身没搞清楚要做什么。我当年选的是“基于机器学习的中文书目自动分类的设计与实现”,听起来像是一个老掉牙的NLP课题,但真正动手之后才发现,这里面从数据、特征到模型…

2026/10/17 13:00:25

VLAN分类详解:从端口到MAC、协议与子网的划分原理与实验验证

1. VLAN到底是什么,为什么要谈分类做网络这一行,VLAN算是最绕不开的基础话题之一了。很多刚入行或者正在备考网络工程师认证的朋友,可能一开始接触VLAN的时候都会有这种感觉:概念听懂了,配置命令也能敲出来&#xff0c…

2026/10/17 12:55:25

人机协同的陷阱与解法:从效率困局到协作机制设计

这个话题我一直想认真聊聊。周围团队用AI工具的比例已经非常高了,写文案的、画图的、写代码的、做数据分析的,几乎每个岗位都能找到对应的AI工具。但说句实话,我观察到的现象是:工具用起来了,协作效率却没有想象中提升…

2026/10/16 5:33:43

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/16 4:03:48

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/15 13:42:16

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/17 0:04:32

PlanarSLAM:面向曼哈顿世界的点线面联合SLAM系统

简介:本资源是面向机器人视觉与SLAM研究者的C开源RGB-D SLAM系统实现,专为结构化室内场景(如办公室、走廊)设计,通过融合点、线、平面三类几何特征并引入“曼哈顿世界”假设,显著提升位姿跟踪鲁棒性与稠密地…

2026/10/17 0:04:32

TDH Workflow调度实战:从DAG依赖驱动到任务编排与故障排查

1. 从一个调度脚本说起:TDH Workflow到底在解决什么问题第一次接触TDH的Workflow,是在一个数据清洗项目里。当时团队每天要跑几十个ETL任务,任务之间有严格的先后依赖——A表清洗完才能跑B表的关联,B表跑完才能触发C表的聚合。最开…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑