先说一个很多刚接触视觉算法的人都会问的问题:我用Python调个OpenCV,cv2.VideoCapture("test.mp4")直接读,不是也能一帧一帧地处理吗?怎么就说“AI检测程序不能直接处理MP4”?
这里有个关键区别:OpenCV帮你把解码和帧提取的脏活累活全干了,你拿到手的已经是Mat对象(也就是一张张图像),而不是MP4文件本身。AI检测程序真正“吃”进去的,从来不是视频文件,而是图像帧。MP4只是运输图像的容器,视觉算法要处理的是“卸货”后的原始像素数据。把这条链路拆开看,你会发现中间藏着不少坑,尤其是当你自己动手写高性能检测管线、处理监控视频、或者用深度学习框架做视频推理的时候。
这篇文章我尽量用大白话把整条链路讲清楚:MP4文件结构、解码原理、为什么要转成帧序列、转帧时的参数怎么选、踩过的坑怎么避,末尾附一份常见的视频打不开/解码失败排查表。适合刚入门视觉算法、或者工作中需要让检测程序吃视频的开发者参考。
1. 视频文件的真实结构:MP4只是个“集装箱”
要理解为什么AI程序不能直接处理MP4,第一步是要搞清楚MP4到底是个什么东西。
MP4的学名叫ISO/IEC 14496-12(MPEG-4 Part 12),它本质上是一个容器格式(Container Format)。所谓容器,你可以把它想象成一个国际物流集装箱:里面装什么货、怎么摆放、货单怎么填,都由集装箱标准来规定,但“集装箱”本身不等于“货物”。
MP4这个集装箱里,通常装的是两类东西:
- 视频流:一段经过编码压缩的视频数据,常见编码是H.264(AVC)、H.265(HEVC),老一点的可能有MPEG-4 Part 2、VP9、AV1等。
- 音频流:常见编码是AAC、MP3、AC-3等。
- 元数据:包括时长、分辨率、帧率、码率、创建时间、画面旋转信息等。
所以当你看到一个文件叫test.mp4时,你其实看到的是一个“箱子”,箱子里面放着一卷用H.264编码器压缩过的“胶卷”(视频流)和一段AAC压缩的“音轨”(音频流),外加一张写着货物说明的“运单”(元数据)。
这里就引出一个核心概念:MP4里的视频数据不是图像,是编码后的二进制流。这些二进制数据长什么样?举一个H.264的例子:H.264编码后的数据由一系列NAL Unit组成,每个NAL Unit的前几个字节是起始码(00 00 00 01或00 00 01),后面跟着NAL头,再后面是编码数据(Slice、SPS、PPS等)。你把这些字节直接丢给AI模型,模型什么都学不到,因为这不是图像。
我见过不少新手把MP4文件直接读成二进制,然后想喂给卷积神经网络,结果报错或者推理结果完全随机,这就是没搞懂容器和编码的关系。
1.1 MP4的“箱内结构”:moov、mdat和编解码器
MP4内部采用Box(也叫Atom)结构,真正重要的几个Box有:
ftyp:文件类型,声明这是MP4文件。moov:元数据区,相当于“运单”。包含每个轨道(Track)的信息、编解码器配置、时间戳、关键帧索引等。mdat:媒体数据区,就是真正的视频+音频压缩数据。
这里有个非常实际的坑:有些MP4文件的moovBox放在文件末尾(比如录制中断、直播录制、某些相机拍出来的视频),如果moov不在文件头部,播放器或者解码器就不知道如何定位视频数据。这就是为什么很多视频“下载下来打不开”“数据恢复后不能播放”——要么moov丢了,要么moov和mdat的索引对不上。
从AI检测程序的角度看,我们真正关心的是解码器(Decoder)。无论MP4容器里装的是H.264还是H.265,都需要对应的解码器把它解成YUV原始帧,再转换成RGB图像才能作为视觉算法的输入。这也是为什么“AI不能直接处理MP4”的第一层原因:MP4里的数据是编码压缩状态,不是可直接用于视觉推理的像素矩阵。
1.2 为什么编码压缩对AI是“污染”
你可能会想:数据压缩只是体积变小了,里面的图像信息不是还在吗?确实在,但编码过程不是简单的“缩小图片”,而是做了一系列有损变换。
以H.264为例,编码过程大概经历了这些步骤:
- 把当前帧划分成16x16的宏块(Macroblock)。
- 利用时域参考帧做运动估计和运动补偿,算出运动矢量和残差。
- 对残差做DCT变换(离散余弦变换)和量化。
- 对量化后的系数做熵编码(CABAC或CAVLC)。
整个流程下来,解码端拿到的YUV帧,和原始拍摄帧相比已经有了一定失真(有损编码)。但这个失真对肉眼来说可能不明显,对AI来说是额外的“噪声”。更关键的是:
- 编码器输出的I帧(关键帧)、P帧(预测帧)、B帧(双向预测帧)里,只有I帧是完整的图像,P帧和B帧存储的是“参考帧+运动补偿残差”的编码数据。
- 解码器必须按顺序解码,并且依赖参考帧,不能任意跳帧。
- 从编码流里直接提取P帧或B帧的压缩数据给AI,AI拿到的是残差和运动矢量,不是图像。这就是“压缩域处理”的研究方向,虽然学术界有人做,但工程上极少直接采用,因为通用性太差。
所以,AI检测程序要处理的,必须是解码后的、完整的RGB或YUV帧。MP4容器本身、H.264的宏块结构、运动矢量,对目前主流的卷积神经网络或Transformer视觉模型来说,统统是无效信息。
2. 完整链路拆解:从MP4文件到AI模型输入
搞清楚了MP4的本质,接下来我把“AI检测程序处理视频”的完整链路分成四段,每一段都有明确的输入和输出。
2.1 第一阶段:解封装(Demux)
输入:test.mp4输出:H.264/H.265编码的视频流 + AAC编码的音频流
解封装就是拆箱子。FFmpeg里的avformat_open_input和avformat_find_stream_info干的就是这个活,它读取ftyp、moov、mdat等Box结构,找到视频流(Video Stream)和音频流(Audio Stream),并把压缩数据包(Packet)从文件里提取出来。
注意:这一步只拆箱,不解码。视频流仍然是H.264编码的二进制数据。
2.2 第二阶段:解码(Decode)
输入:H.264/H.265编码的视频流 输出:YUV原始帧(通常是YUV420P或NV12)
这一步是解码器的工作。FFmpeg里的avcodec_send_packet和avcodec_receive_frame就是干这个的。解码器把H.264的NAL Unit解码成一张张完整的YUV图像,这个过程需要硬件或CPU算力。
解码后的帧是什么格式?常见的是YUV420P:
- Y平面:亮度信息,每像素1字节。
- U平面:色度信息,每像素0.25字节。
- V平面:色度信息,每像素0.25字节。
- 所以一个1080p的YUV420P帧,大小 = 1920×1080×1.5 ≈ 3MB。
但YUV不是视觉算法最喜欢的输入格式,绝大多数深度学习框架(PyTorch、TensorFlow、OpenCV)默认使用RGB或BGR顺序。
2.3 第三阶段:像素格式转换与尺寸调整
输入:YUV420P帧 输出:RGB/BGR图像,可能还会Resize到模型的输入尺寸(如640×640、416×416)
这一步包括:
- YUV转RGB(OpenCV的
cvtColor,FFmpeg的sws_scale)。 - 缩放(Resize)到模型输入尺寸,注意保持宽高比或者直接拉伸,两种方式各有取舍。
- 有可能还要做归一化:把像素值从0-255映射到0-1,或者按模型的均值和标准差做标准化。
拿YOLO系列检测器举例,YOLOv8的输入尺寸通常是640×640,训练时会把图片Resize到640后再做Letterbox(保持原始宽高比,填充灰色边条),这个细节后面细说。
2.4 第四阶段:张量化与归一化
输入:RGB图像(numpy数组,H×W×C) 输出:模型的输入张量(NCHW或NHWC,float32,归一化后)
深度学习框架要求输入是张量,且通常要求第0维是batch size。所以一帧1920×1080的图像,经过预处理后变成(1, 3, 640, 640)的float32张量,值范围是0到1(或按均值方差标准化)。
至此,MP4文件才算真正变成视觉算法能“消化”的东西。
我经常用一个类比帮助新手理解:MP4是快递包裹,解码是拆包裹,YUV是没整理的零件,RGB是拼好的成品零件,模型输入是标准化包装后的最终产品。AI程序要的是最终产品,不是快递包裹。
3. 实操环节:用FFmpeg把MP4变成AI能吃的帧序列
理论讲完,是时候动手了。最常见的实操需求有两个:一是把视频抽成帧序列保存成jpg,二是实时解码喂给检测程序。我这里先把抽帧讲透,实时推理放到后面结合OpenCV和PyTorch讲。
3.1 前置工具:FFmpeg安装与验证
FFmpeg是这个链路里最核心的工具,没有之一。它同时搞定解封装、解码、像素转换、缩放,几乎全能。
安装方式(Linux Ubuntu):
sudo apt update sudo apt install ffmpegmacOS:
brew install ffmpegWindows可以直接去FFmpeg官网下载编译好的exe,或者用winget install ffmpeg。
安装完成后验证:
ffmpeg -version ffprobe -versionffprobe是FFmpeg家族的探测工具,用来查看视频信息,一定要装。
3.2 关键参数怎么选:抽帧的四种姿势
抽帧看起来简单,但参数选择会直接影响AI检测的效果和速度。我按需求分成四种情况:
情况一:按固定间隔抽帧,适合做数据集
ffmpeg -i input.mp4 -vf "fps=1" frame_%04d.jpgfps=1表示每秒抽1帧。如果你想每2秒抽1帧,就写fps=1/2。
情况二:抽关键帧(I帧),适合快速预览
ffmpeg -i input.mp4 -vf "select='eq(pict_type,I)'" -vsync vfr keyframe_%04d.jpg这个命令只提取GOP里的I帧。好处是抽出来的都是完整清晰的画面,坏处是I帧间隔不固定,可能两帧之间隔好几秒。
情况三:从指定时间开始连续抽N帧,适合取视频中某一段
ffmpeg -ss 00:01:30 -i input.mp4 -t 10 -vf "fps=10" segment_%04d.jpg-ss 00:01:30表示从第1分30秒开始,-t 10表示持续10秒,fps=10表示每秒抽10帧。这条命令常用于从视频中截取事件片段做分析。
情况四:解码全部帧,不丢帧,适合做逐帧检测
ffmpeg -i input.mp4 -vsync 0 frame_%05d.jpg注意-vsync 0这个参数,它告诉FFmpeg不要丢帧。默认情况下,如果视频帧率和输出帧率不匹配,FFmpeg会丢帧或重复帧,这在逐帧检测时是致命的(比如你要统计检测到的目标数量)。
3.3 实时检测的Python示例
保存帧到磁盘适合做离线分析,但AI检测程序更多时候是实时处理的。下面给一个用OpenCV + PyTorch风格的伪代码框架,展示从视频到模型输入的完整链路:
import cv2 import numpy as np # 假设你有一个训练好的检测模型,输入是640x640的RGB图像 def preprocess_frame(frame_rgb, input_size=640): # 1. 保持宽高比缩放 + Letterbox填充 h, w = frame_rgb.shape[:2] scale = min(input_size / w, input_size / h) new_w, new_h = int(w * scale), int(h * scale) resized = cv2.resize(frame_rgb, (new_w, new_h)) # 2. 创建画布并填充 canvas = np.full((input_size, input_size, 3), 114, dtype=np.uint8) x_offset = (input_size - new_w) // 2 y_offset = (input_size - new_h) // 2 canvas[y_offset:y_offset + new_h, x_offset:x_offset + new_w] = resized # 3. BGR转RGB + HWC转CHW + 归一化 rgb = cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB) chw = np.transpose(rgb, (2, 0, 1)).astype(np.float32) / 255.0 # 4. 增加batch维度 [1, 3, 640, 640] return np.expand_dims(chw, axis=0) cap = cv2.VideoCapture("test.mp4") fps = cap.get(cv2.CAP_PROP_FPS) total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) print(f"视频信息: {fps:.2f} FPS, 总帧数 {total_frames}") frame_count = 0 while True: ret, frame_bgr = cap.read() if not ret: break # 每隔N帧处理一次(根据检测速度动态调整) if frame_count % 2 == 0: tensor = preprocess_frame(frame_bgr) # 这里调用你的模型: outputs = model(tensor) # 后处理:坐标变换时记得把Letterbox的偏移和缩放还原回去 frame_count += 1 cap.release()这里有个细节非常容易踩坑:模型输出的坐标是640×640坐标系下的,要映射回原始视频分辨率,必须先减去偏移量再除以缩放比例。如果你直接拿模型输出坐标画框,框的位置会整体偏移。
还有个容易被忽略的点:OpenCV读取视频时默认是BGR顺序,而模型训练时通常用RGB。顺序搞反了,检测结果会明显变差,尤其是颜色敏感的任务(比如红绿灯检测、颜色标签识别)。
3.4 为什么不用循环直接拿OpenCV逐帧处理就好?
一个很自然的疑问是:既然OpenCV能直接读视频,为什么还要单独学FFmpeg?
我的回答是:OpenCV底层就是调FFmpeg。但直接用OpenCV做视频处理时,你会失去很多控制力:
- OpenCV屏蔽了解封装格式细节,遇到损坏文件、异常编码参数时,报错信息极其有限。
- OpenCV不支持所有编码格式。比如某些H.265的视频,OpenCV自带的VideoCapture可能打不开,但FFmpeg的命令行早就解出来了。
- 高性能场景下,你会希望用GPU硬解(NVDEC、VAAPI),OpenCV的VideoCapture在这方面配置麻烦,FFmpeg命令行反而更直接。
- 生产环境中,视频可能来自RTSP流、海康/大华私有协议、或者其他非MP4封装,OpenCV的兼容性不够。
所以我的建议是:调试和快速验证用OpenCV,生产级管线用FFmpeg或封装FFmpeg的Python库(比如PyAV、imageio-ffmpeg)。
PyAV是一个很好的中间层,它在Python里暴露了FFmpeg的完整能力,又比直接调命令灵活得多:
import av container = av.open("test.mp4") stream = container.streams.video[0] for frame in container.decode(stream): # frame.to_image() 得到PIL图像 img = frame.to_image() # 转成numpy再给模型 # ...PyAV踩坑提醒:解码出来的帧可能是YUV格式,frame.to_image()内部会转成RGB的PIL图像,但这个过程耗时。在高帧率场景下,直接用frame.to_ndarray(format='bgr24')更高效。
4. 避坑手册:视频打不开、解码失败、检测结果不对怎么办
这条链路我踩过的坑至少能写一百条,这里挑最典型、最影响效率的几类,列一个速查表。
4.1 视频打不开/解码失败的排查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| FFmpeg报“moov atom not found” | MP4文件未正常写入,moov索引缺失或损坏 | 用ffmpeg -i broken.mp4 -c copy repaired.mp4尝试修复;或用untrunc等工具配合参考视频修复 |
| 海康/大华监控导出的MP4播放不了 | 部分监控厂商的MP4封装不规范,或使用了私有编码参数 | 先用ffprobe -show_streams xxx.mp4确认编码格式;再用ffmpeg -i 源文件 -c:v libx264 -c:a aac 输出.mp4重新转码 |
| OpenCV打不开但播放器能播 | OpenCV编译时没带对应解码器(如H.265) | 升级OpenCV,或改用PyAV;或者先用FFmpeg把视频转成H.264编码 |
| 抽帧出来全是绿屏/花屏 | 解码器选了硬件解码但驱动不匹配 | 强制软件解码:ffmpeg -hwaccel none -i ...;或检查是否因为丢帧导致P帧参考不完整 |
| 数据恢复后的视频无法播放 | 文件头损坏、moov丢失、mdat数据不连续 | 尝试recover_mp4工具,输入一个同设备拍的正常参考视频来重建moov |
| 重装系统后视频没权限访问 | NTFS权限设置问题 | 右键文件→属性→安全→编辑权限→添加当前用户→完全控制 |
| 抽帧数量不对 | -vsync参数设置不当导致丢帧/重复帧 | 加-vsync 0禁止自动帧同步 |
| 视频帧率显示为0 | 容器元数据损坏 | 用ffprobe探测实际帧率,解码时手动指定-r |
4.2 解码性能优化的几个真实经验
视频逐帧推理的性能瓶颈通常不在模型,而在解码环节。我实测过一个1080p视频:
- 纯CPU软解H.264:单线程解码耗时约8-15ms/帧,四线程约4-6ms/帧。
- 模型推理(YOLOv8s,640×640):大约10-20ms/帧(取决于GPU)。
- 解码+预处理+推理+后处理全链路:通常会到20-40ms/帧。
如果想让检测程序跑满实时(25FPS以上),几个关键经验:
- 用硬件解码。FFmpeg支持NVDEC(NVIDIA显卡)、VAAPI(Intel/AMD核显)、QSV(Intel)。加了硬解,解码耗时能降到1-2ms/帧。FFmpeg命令示例:
ffmpeg -hwaccel cuda -i input.mp4 -vf "fps=30" -f null -注意硬解对输入格式有要求(比如H.264/H.265),不是所有编码都能硬解。
别用OpenCV逐帧办法加模型推理。它的VideoCapture在解码稳定性上不如PyAV,内部拷贝也更多。实测同样一段视频,PyAV读帧比OpenCV快20%-40%。
预处理放到模型之前的GPU上做。如果不方便用GPU预处理,至少用多线程把解码和推理流水线化——解码线程只管出帧,推理线程只管算模型,中间用队列缓冲。
4.3 一个典型的坐标映射错误示例
假设你的模型输入是640×640,原视频是1920×1080,Letterbox后原图被缩放并居中。模型输出的目标框坐标是(x_center, y_center, w, h),纵坐标的偏移和缩放怎么还原?
具体步骤:
# 模型输出 x_center, y_center, w, h = outputs[0] # 640x640坐标 # 反向计算原图坐标 input_size = 640 orig_w, orig_h = 1920, 1080 scale = min(input_size / orig_w, input_size / orig_h) new_w, new_h = int(orig_w * scale), int(orig_h * scale) x_offset = (input_size - new_w) // 2 y_offset = (input_size - new_h) // 2 # 减去偏移,除以缩放 orig_x_center = (x_center - x_offset) / scale orig_y_center = (y_center - y_offset) / scale orig_w = w / scale orig_h = h / scale如果视频是竖屏的(比如手机拍的1080×1920),Letterbox时缩放比例和偏移方向跟横屏不同。很多模型在横屏视频上没问题、竖屏视频上检测框全部偏移,就是这里写错了。
4.4 mpkg/m3u8/m4s等非MP4格式的处理
关于热词里出现的mpkg转mp4、wallpaper pkg转mp4、m3u8转mp4,这里顺带说几句:
- mpkg/ pkg:通常是某些软件私有封装(比如Wallpaper Engine的主题包),本质上里面可能是视频+JSON配置。转mp4前,先把它解包再说,而不是直接用FFmpeg转。解包后拿到真实的视频文件,再考虑转码。
- m3u8:是HLS直播/点播的索引文件,里面是一串.ts/.m4s分片地址。转MP4的正确姿势:
ffmpeg -i "https://example.com/playlist.m3u8" -c copy output.mp4如果分片文件是加密的(有#EXT-X-KEY),需要先拿到密钥,再在命令里拼接解密参数,这个场景比较复杂。
- mp4转m4s:m4s是B站等平台的媒体分片格式,通常是fMP4的流切片。反过来用FFmpeg也能处理。
关于5.1 surround sound test files various formats aac ac3 mp4 dts这类测试文件需求,其实是想验证播放器/解码器对各种音频编码的兼容性。这和AI检测链路的关系不大,但从解码器调试的角度看,多备几个不同编码的测试文件确实很有用。
5. 检测程序内部的图像预处理:为什么模型不能直接吃视频帧
前面讲了从MP4到RGB帧的过程,但RGB帧也不能直接被模型吃。模型训练的时候,输入图像的尺寸、通道顺序、值域范围都是有讲究的。这里展开讲几个容易忽略的预处理环节。
5.1 通道顺序:BGR和RGB的“错位陷阱”
OpenCV读图是BGR顺序,而PyTorch模型训练时大多用RGB顺序,这个事大家基本都知道。但坑在于:如果你用的是Caffe训练的老模型,或者某些用OpenCV做数据增强的工具,通道顺序可能是BGR,而你没注意。
一个判断方法:把图像转成numpy数组,打印img[0, 0, :]的三个值,看蓝色通道和红色通道哪个值大。如果是一张蓝天白天的照片,蓝色通道值应该明显大于红色,如果是BGR顺序,img[..., 0]查到的就是蓝色值。
在写正式检测管线时,我建议统一写一个预处理函数,严格注释清楚输入和输出的通道顺序,这个坑能帮你省掉很多排查时间。
5.2 归一化的三种“流派”
不同模型对像素值的要求不一样,大概有三种:
- 0-1归一化:直接
pixel / 255.0,PyTorch官方权重很多用这个。 - 均值-标准差标准化:
(pixel - mean) / std,ImageNet训练常用,比如mean=[0.485, 0.456, 0.406],std=[0.229, 0.224, 0.225]。 - 无归一化:像素保持0-255,有些边缘检测或传统视觉算法不需要归一化。
归一化做错了,模型照样能跑,但精度会掉。你要是发现“同一个模型,别人跑mAP很高,我跑出来怎么这么差”,先检查预处理是不是和训练时一致。
5.3 视频抽帧的“帧率陷阱”和“时间基准”
视频文件里每帧都有一个PTS(Presentation Time Stamp),表示这一帧应该在哪一刻显示。用OpenCV或FFmpeg抽帧时,默认是按解码顺序输出,但B帧的存在会让解码顺序和显示顺序不一致。
具体表现为:抽出来的帧序列播放时画面跳动,或者相邻两帧的时间间隔不均匀。要按时间顺序输出,需要正确处理帧的PTS。FFmpeg命令行里一般自动处理了,但如果你自己用libavcodec编程,一定要检查pts和dts。
另外,视频的帧率可能不是整数。比如某监控视频帧率是29.97fps,如果你按fps=30抽帧,实际抽出来的帧数和预期会偏差。做数据集时,最好用ffprobe先确认真正的帧率。
5.4 检测结果不稳定的“隐性问题”:GOP结构和丢帧
在线性流水线里,如果解码不够快,系统会丢帧。但丢帧会导致检测的目标在时间上不连贯,比如车辆跟踪时ID跳变。这个问题不一定来自AI模型,而是来自上游丢帧。
解决办法是解码器线程和推理线程解耦:解码线程不管推理有多慢,先把帧都解出来放队列;推理线程按自己的节奏从队列取帧。队列的深度要按实际场景调,太浅会丢帧,太深会引入延迟。
6. 完整实战:构建一个监控视频的异常事件检测流程
把前面所有内容串起来,我用一个真实项目来演示:有一段海康摄像头导出的监控MP4,需要对画面中的“人”做检测,并统计每个时间段的人数变化。
6.1 第一步:探查视频信息
拿到文件先别急着跑代码,先探测:
ffprobe -show_streams -show_format camera_001.mp4重点看:
codec_name:是h264还是hevc,决定解码方案。width、height:分辨率。avg_frame_rate:平均帧率。nb_frames:总帧数(如果moov里有)。pix_fmt:像素格式,常见yuv420p。
如果codec_name=hevc,我的建议是先用FFmpeg转成H.264:
ffmpeg -i camera_001.mp4 -c:v libx264 -preset fast -crf 20 -c:a copy camera_001_h264.mp4为什么转?因为H.265解码对CPU要求高,很多老旧设备不支持硬解,转成H.264后兼容性和解码速度都更好。保存到Python端后用OpenCV/PyAV直接读H.264版本,省心很多。
6.2 第二步:按时间段抽帧做检测
一天24小时的监控,不可能逐帧全跑模型。合理方案是按事件触发抽帧,比如定时抽1fps,或者检测到画面变化才触发。我这里的示例是简单定时抽帧:
import av import cv2 import numpy as np container = av.open("camera_001_h264.mp4") stream = container.streams.video[0] stream.thread_type = "AUTO" # 多线程解码 frames_to_process = [] sample_interval = 30 # 每隔30帧处理一次 for idx, frame in enumerate(container.decode(stream)): if idx % sample_interval != 0: continue img = frame.to_ndarray(format="bgr24") frames_to_process.append((idx, img))这里用PyAV而不是OpenCV,主要原因是stream.thread_type = "AUTO"能启用多线程解码,在大分辨率视频上明显更快。
6.3 第三步:模型推理 + 坐标还原
模型推理部分就不展开写完整YOLO代码了,但坐标还原的细节值得再强调一次。
def restore_coords(box, orig_shape, input_size=640): # box: [x_center, y_center, w, h] in input_size坐标系 orig_h, orig_w = orig_shape[:2] scale = min(input_size / orig_w, input_size / orig_h) pad_x = (input_size - orig_w * scale) / 2 pad_y = (input_size - orig_h * scale) / 2 x_center_orig = (box[0] - pad_x) / scale y_center_orig = (box[1] - pad_y) / scale w_orig = box[2] / scale h_orig = box[3] / scale x1 = int(x_center_orig - w_orig / 2) y1 = int(y_center_orig - h_orig / 2) x2 = int(x_center_orig + w_orig / 2) y2 = int(y_center_orig + h_orig / 2) return x1, y1, x2, y2我踩过的坑是:frame.to_ndarray()返回的数组是高×宽×通道,但有些人拿到的图像方向可能是翻转的,尤其是一些监控摄像头在元数据里写了旋转信息。检测前先cv2.imshow看一眼,确认方向正确再批量跑。
6.4 第四步:结果落盘和异常统计
检测结果建议直接写成结构化数据,比如CSV或JSON,不要只存标注后的视频。
{ "frame_id": 12345, "timestamp_sec": 411.5, "detections": [ {"class": "person", "confidence": 0.87, "bbox": [320, 150, 410, 680]}, {"class": "person", "confidence": 0.62, "bbox": [900, 200, 980, 710]} ] }时间戳的换算很重要:timestamp_sec = frame_id / fps(前提是定帧率视频)。如果视频是VFR(可变帧率),需要用帧的PTS换算,不能简单除帧率。
7. 几个更进一步的思考
7.1 视频理解模型可以直接吃视频吗
前面说的是“AI检测程序”,主要指目标检测、分类、分割这些处理单张图像的模型。确实也有视频理解的模型,比如用3D卷积(C3D、SlowFast)、Video Transformer(TimeSformer、VideoMAE),它们能同时输入多帧。
但这些模型依然不能直接吃MP4。它们的输入是形状为(T, C, H, W)的张量,T是时间维。你需要先把MP4解码成帧序列,再按固定间隔采样T帧(比如均匀抽8帧或16帧),然后做和图像模型一样的预处理。
所以结论没有变:无论模型多高级,MP4必须先解码成帧。视频理解的进步是模型层面的,不是输入层面的。
7.2 压缩域推理为什么难落地
学术界一直有人研究“直接在压缩域上做检测/识别”,即不完整解码,只解析运动矢量、残差、I帧的DCT系数等。好处是省去解码开销,理论上能大幅提升速度。
但工程上极少用这种方案的现实原因是:
- 不同编码器(H.264/H.265/VP9/AV1)的压缩域特征格式完全不同,模型无法跨编码器通用。
- 编码质量影响压缩域特征的语义稳定性,比如码率低时残差噪声大。
- 运动矢量只描述块级别运动,不是像素级光流,精度受限。
- 工业界有现成的GPU硬解,解码开销已经很小,压缩域那点性能优势不够吸引人。
所以现阶段,老老实实解码成帧,再送给模型,是最可靠、最通用的路径。
7.3 边缘设备上的视频处理
最后说一句边缘设备(Jetson、RK3588、树莓派)。在边缘设备上做视频AI检测,内存和CPU都紧张,需要注意:
- 优先用硬解。Jetson上有NVDEC,RK3588有MPP硬解,用起来确实比CPU软解快好几倍。
- 避免把每一帧都存成RGB大图再喂模型,最好解码后直接在YUV上做缩放(在解码器层面做scale),再转格式。
- 队列缓冲区要设上限,否则连续跑几小时后内存会涨到崩。
我在RK3588上做过一个8路视频并发检测,最开始的方案是8个OpenCV VideoCapture各开一个线程,结果内存直接爆了。后来换成统一的FFmpeg解码池,每一路解码出来只保留最近一帧RGB,模型按需取帧,内存占用降了大约70%。
所以在设计视频AI系统时,解码环节和模型推理一样需要认真规划,链路里的每一环都可能成为瓶颈。
这条从MP4到视觉算法的完整链路,说难不算难,说简单也藏着不少细节。核心记住一句话:AI程序吃的是图像,不是视频文件。把这句话刻在脑子里,再遇到解码失败、画面花屏、检测框偏移这类问题,你大概就能猜到问题出在链路的哪个环节了。