在 OpenCV 的视频处理流程里,cv2.VideoWriter_fourcc 是我见过最像“咒语”的一行代码。它读起来差不多是乱码,但 VideoWriter 能不能把视频正常写进文件,绝大多数情况下都由这串四个字符决定。我早期刚接触 OpenCV 的时候踩过好几次坑:摄像头画面明明能正常弹窗,writer.write() 也一路执行完不报错,最后生成的视频却只有几百字节,甚至根本没有文件生成。折腾一圈后才发现,问题全出在我对 fourcc 的理解太浅——只知道“抄别人代码里的字符串”,不知道它背后的编码机制、容器匹配和安装包差异。
这篇文章我打算用自己的项目经历,把 cv2.VideoWriter_fourcc 这个函数彻底聊透:它到底是什么、不同编码格式怎么选、常见失败场景怎么排查,以及我在实际录制摄像头、批量合成图片视频时总结的稳定方案。无论你是刚开始学 OpenCV,还是已经在做视觉项目但被视频输出折磨过,这篇文章应该都能给你省下不少时间。
1. 拆开四个字符:fourcc 到底在告诉 OpenCV 什么
1.1 Four Character Code 的由来和本质
fourcc 的全称是 Four Character Code,直译就是“四字符代码”。这个设计最早可以追溯到老一代多媒体系统,后来在视频容器和编码领域被广泛沿用。它的本质非常简单:用四个可打印的 ASCII 字符,比如 M、J、P、G,拼成一个唯一的标识,用来告诉媒体库“我想用哪一种编码器”。
在 OpenCV 里,cv2.VideoWriter_fourcc 的返回值并不是字符串,而是一个 32 位整数。VideoWriter 构造函数拿到这个整数后,会把它翻译成底层视频后端能识别的编码器编号。所以你可以打印一下看看:
python复制import cv2
code = cv2.VideoWriter_fourcc(*"MJPG")
print(type(code))
print(code)
输出会是一个 int 类型,而不是 "MJPG" 字符串。理解了这一点,很多初学者的疑惑就解开了:为什么明明传了一个看起来正常的四个字母,视频却写不出来?因为你的 OpenCV 环境里,
根本没注册这个四个字符对应的编码器,或者这个编码器根本不能被写进你指定的容器文件。
1.2 Python 里那个容易被忽略的星号
Python 接口使用 cv2.VideoWriter_fourcc(*"MJPG") 这种写法,其中 *"MJPG" 是把字符串拆成四个单字符参数,等价于:
python复制cv2.VideoWriter_fourcc('M', 'J', 'P', 'G')
这是很多从 C++ 转 Python 的开发者最容易忽视的细节。C++ 里的写法本来就是四个参数:
cpp复制int fourcc = VideoWriter::fourcc('M', 'J', 'P', 'G');
Python 里如果漏掉星号,写成 cv2.VideoWriter_fourcc("MJPG"),解释器会直接抛出类型错误,因为它期望 4 个参数,而你只给了 1 个。这个报错还算友好,怕的是你把一个字符串变量传进去,报错的时机又很靠后,容易被误判成编码器问题。
我见过不少人在循环里反复创建 VideoWriter,每次都用字符串拼接来生成不同文件名。这里有个大坑:如果文件名后缀、容器格式和 fourcc 编码器不匹配,VideoWriter 构造函数不会立刻抛异常,而是默默创建一个写不进去的“僵尸对象”,直到你调用 isOpened() 才发现返回 False。所以代码里每创建一次 writer,都应该立刻检查一次打开结果,否则后面排查成本会成倍增加。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 编码格式选型:为什么同样叫 mp4,输出文件差别这么大
2.1 常用编码器对照表
我在不同项目里轮流用过 MJPG、XVID、mp4v、avc1 这些 fourcc,它们的生产环境表现差异很大。下面这张表是我根据自己的使用经验整理的,不代表所有环境,但作为选型参考已经够用:
| FourCC | 实际编码 | 常见容器 | 压缩效率 | 兼容性特征 |
|---|---|---|---|---|
| MJPG | Motion JPEG | AVI | 很低 | 几乎万能播放,文件体积非常大 |
| XVID | MPEG-4 Part 2 | AVI | 中等 | 老式播放器和系统兼容性较好 |
| DIVX | DivX MPEG-4 | AVI | 中等 | 与 XVID 类似,不同设备解码器需求不同 |
| mp4v | MPEG-4 Part 2 | MP4 | 中等 | MP4 基础兼容,部分新播放器支持一般 |
| avc1 / H264 | H.264/AVC | MP4 | 很高 | 当前平台兼容性最好,但编码器不一定内置 |
| hvc1 / HEVC | H.265/HEVC | MP4 | 非常高 | 高压缩率,但老旧设备和部分播放器打不开 |
| VP90 | VP9 | WebM | 很高 | 浏览器生态好,OpenCV 默认支持不稳定 |
先从我自己最常用的几个说。MJPG 是将每一帧图像单独压缩成 JPEG 再封装进视频流,帧内编码,没有利用时间上的冗余,所以同样一段画面,它的文件体积可能比 mp4v 大出好几倍,画面内容一旦复杂,差距还会继续拉大。优点是什么设备几乎都能解码,编码速度也快,非常适合做视觉算法的中间过程采集,例如保存检测结果视频用于后续调试。
mp4v 是我早期做成品项目时的默认选项,因为它写进 .mp4 后缀文件时,很多普通播放器都能打开。但后来我遇到过会议演示现场打不开的情况,换了播放器又是正常的。这是因为 mp4v 对应的实际编码是 MPEG-4 Part 2,不是大家默认以为的 H.264,部分软件对它的支持并不彻底。所以如果视频要给客户交付,我建议优先考虑 avc1 或 H264。
2.2 容器、后缀和编码器,三者要匹配
很多人把文件后缀 .mp4、.avi 当作“视频格式”,这其实是把容器和编码混为一谈了。视频文件的外壳叫容器,它负责把视频流、音频流、字幕等打包在一起;编码器才负责真正压缩画面。.mp4 是容器格式,可以装 H.264、MPEG-4、HEVC 等多种视频流;.avi 也是容器,常见搭配是 MJPG、XVID 等。
你给 VideoWriter 传入的文件后缀决定了 OpenCV 底层会用哪种封装器,而 fourcc 决定封装器里要放哪一种编码流。如果两者不匹配,表面看文件可能能生成,但播放器打开往往报错或者黑屏。我遇到过一个很典型的反例:有人把 XVID 编码的视频存成 output.mp4,用系统自带播放器打开秒失败,换 VLC 又能放。原因就是容器和编码的搭配超出了部分播放器的解析能力。
在 OpenCV 官方文档示例里,常见组合是:.mp4 后缀就配 mp4v、avc1;.avi 后缀就配 MJPG、XVID、DIVX;.mov 后缀可以尝试 mp4v。这不是绝对规则,但至少能绕开大部分稀奇古怪的问题。我给你的建议是,不要把四个字符和后缀当成可以自由排列组合的积木使用,尽量遵守约定俗成的搭配。
2.3 有些 fourcc 能不能用,和 OpenCV 编译环境强相关
同样是 avc1,在一台机器上一切正常,换到另一台机器上就 isOpened() 返回 False。这不是代码写错了,而是 OpenCV 在安装时是否携带了对应的编码后端决定的。现在大家普遍直接 pip install opencv-python,不同版本、不同平台的预编译包,内置的 FFmpeg 能力并不完全一致,尤其 H.264、H.265、VP9 这种编码器,是否默认开启要看构建配置。
最简单的验证方法是把编码器写进 VideoWriter 后立刻检测:
python复制writer = cv2.VideoWriter(
"test.mp4",
cv2.VideoWriter_fourcc(*"avc1"),
25.0,
(1280, 720),
)
print(writer.isOpened())
如果输出 False,就说明当前环境不支持这个 fourcc。这时候你不需要怀疑人生,更不用急着重装整个 OpenCV。先把 fourcc 换成 mp4v 看看能不能写;如果能写,说明只是缺 H.264 编码器,对大多数普通应用影响不大。如果连 mp4v 都写不了,那才需要去检查 OpenCV 的安装包和底层依赖。
3. 打不开、写不进、文件秒变 0 字节:三个最常踩的坑
3.1 症结一:writer.isOpened() 返回 False,文件根本没被创建
关于这个问题,我想先还原一个常见场景。很多人写的代码长这样:
python复制import cv2
cap = cv2.VideoCapture(0)
fourcc = cv2.VideoWriter_fourcc(*"MJPG")
writer = cv2.VideoWriter("result.avi", fourcc, 20.0, (640, 480))
while True:
ret, frame = cap.read()
if not ret:
break
writer.write(frame)
cv2.imshow("frame", frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
cap.release()
writer.release()
cv2.destroyAllWindows()
看起来流程没问题,但录像文件就是 0 字节或者压根不存在。这时候第一步不是去检查写入循环,而是应该确认 writer 对象到底有没有成功打开。把创建后的 writer.isOpened() 打印出来是最直接的。如果返回 False,常见原因有四个:
第一,输出目录不存在。如果你写的是 video/result.avi,但当前路径下根本没有 video 这个文件夹,OpenCV 不会帮你自动创建,writer 就会打开失败。第二,目录没有写入权限,比如写到了程序安装目录或者系统保护目录。第三,文件名后缀和 fourcc 搭配不被底层后端识别,比如用了不常见的 .mkv 又配了 MJPG,OpenCV 不一定能写入。第四,当前 OpenCV 构建不支持该编码器。
有一次我排查了很久,才发现问题出在第二轮循环时,明明上一个 writer 对象还没释放,我又创建了另一个指向相同文件名的 writer。在 Windows 下,文件被占用会导致新的 writer 打不开。所以循环里反复创建 writer 时,要先释放上一个对象,或者确认 isOpened() 为 True 再继续。
3.2 症结二:帧尺寸不一致,写入大概率直接失败
VideoWriter 对分辨率非常死板。构造时你给了一个宽高,那么每次 write() 传入的帧必须严格等于这个尺寸,OpenCV 不会自动缩放。很多人的摄像头实际输出是 1280x720,但构造 writer 时想当然地写成了 (640, 480)。这种情况下,writer.write() 并不会每次都抛异常,但最终文件可能只写进去几帧,或者干脆是个坏文件。
正确的做法是从 VideoCapture 里读取真实采样尺寸,再传给 writer:
python复制width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS)
if fps <= 0 or fps != fps: # 第二个判断其实在检查 NaN
fps = 25.0
writer = cv2.VideoWriter(
"capture.mp4",
cv2.VideoWriter_fourcc(*"mp4v"),
fps,
(width, height),
)
这里的 fps != fps 在 Python 里可以用来判断 NaN,因为 NaN 不等于任何值,包括它自己。很多摄像头驱动获取不到帧率时返回 0,所以设置一个兜底帧率非常重要。
如果确实想用固定尺寸输出,比如统一保存成 640x480,那就不应该让摄像头原尺寸直接进入 writer,而是先用 cv2.resize 缩放后再写入,不要指望底层自动适配。
3.3 症结三:颜色通道顺序和 isColor 参数错位
OpenCV 一贯使用 BGR 通道顺序,VideoWriter 默认也认为你传进来的每一帧是 BGR 三通道图像。如果你从 PIL、matplotlib 或者其他图像库拿到的数据是 RGB,直接塞给 writer.write(),保存出来的视频整体会偏蓝偏红互换,颜色完全不对劲。正确的做法是用 cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) 先做一次转换。
还有一个容易踩的是灰度视频。如果你想把灰度图直接写入视频,需要在构造 writer 时明确设置 isColor=False:
python复制writer = cv2.VideoWriter(
"gray.avi",
cv2.VideoWriter_fourcc(*"MJPG"),
25.0,
(width, height),
isColor=False,
)
如果不设置,writer 默认按三通道处理,而你传入的是单通道灰度图,写入过程会报错或者产出不可用文件。这个参数的位置在构造参数最后,很多人抄代码时直接漏掉,等到写灰度视频时就撞上了。
4. 顺着一次真实排障,看 writer 失效的完整排查链路
4.1 当时的故障现象
去年有一个项目需要把工业相机拍到的画面实时录制成 MP4,作为检测系统的过程回溯文件。代码写完第一次联调,录制功能就是不出东西。现象非常诡异:相机画面正常,程序不崩溃,writer.write() 没有异常,但录制结束后的 MP4 文件永远只有 3KB 左右。3KB 大概只够放一个文件头,说明视频流根本没有实际写进去。
我一开始怀疑是磁盘满了,查了剩余空间完全充足。又怀疑是相机分辨率太高导致编码性能不够,但降低到 640x480 依然没用。后来把焦点放到 writer.isOpened() 上,才发现它从头到尾一直是 False。
4.2 逐层缩小的排查顺序
那次之后,我再遇到视频写不出来的问题,就固定按下面这个顺序查,基本能快速定位。
第一,先确认 writer 是否打开成功。代码里加上 assert writer.isOpened(),或者直接打印出来。如果 False,直接进下一步。
第二,换一个“最廉价但一定可靠”的组合,用来排除编码器问题。我把目标文件从 .mp4 换成 test.avi,fourcc 换成 MJPG。这一步非常关键,因为它能快速把问题拆成两类:如果 AVI+MJPG 能写,说明你的 OpenCV 本身具备视频写入能力,问题出在特定的 mp4 编码器上;如果 AVI+MJPG 也写不了,那就要从安装和路径端去找原因。
第三,检查当前 OpenCV 的编译信息。在 Python 里运行:
bash复制python -c "import cv2; print(cv2.getBuildInformation())"
输出信息很长,重点搜索 FFMPEG 和 Video I/O 这两个段落,看是不是标记为 YES。如果 FFmpeg 没有启用,很多视频格式都不能写,你很快就会在 MJPG 测试阶段就发现问题。
第四,如果 MP4 相关编码器不行,Avi 能行,就逐个测试 mp4v、avc1,看看哪些能打开:
python复制import cv2
for tag in ["mp4v", "avc1", "XVID", "MJPG"]:
writer = cv2.VideoWriter(
f"test_{tag}.mp4",
cv2.VideoWriter_fourcc(*tag),
25.0,
(640, 480),
)
print(tag, writer.isOpened())
writer.release()
注意,这段测试为了简洁混用了 .mp4 后缀和 XVID,如果发现某些组合不行,不要马上认为是编码器坏了,先换成对应容器再判断。
4.3 根因和后续规避方案
那次问题的根因其实很朴素:我用的预编译 OpenCV 包没有带可用的 H.264 编码器,指定 avc1 后底层无法创建编码器,writer 自然打不开。后来我改用 mp4v 输出 MP4,问题立刻消失,视频也能正常播放。
这件事给我留下的教训是:不要在项目一开始就默认“mp4 等于 H.264”。OpenCV 的 VideoWriter 对编码器的支持范围,远不如 FFmpeg 命令行工具那么完整。如果你一定要 H.264,最简单的做法是先用 OpenCV 写出无损或低压缩的中间视频,再用 FFmpeg 之类工具做二次转码;如果环境允许,也可以找一个把 H.264 编码器完整编译进去的 OpenCV 版本。
另外,写完视频后马上检查文件大小,是一个成本极低但效果极好的习惯。我一般会在录制代码末尾加一句:
python复制if os.path.exists("output.mp4"):
size = os.path.getsize("output.mp4")
if size < 1024:
print("视频文件偏小,可能写入失败")
虽然文件大小受画面复杂度影响,但一段正常录制几秒钟的视频如果连 1KB 都不到,基本可以断定写入过程出了问题。
5. 把视频稳定写出来的完整样板:摄像头录制与图像序列合成
5.1 从摄像头采集并保存 MP4 的稳定写法
先说我从摄像头实时录制时最常用的一套模板。这个模板经历过多个项目检验,虽然不花哨,但够稳:
python复制import time
import cv2
cap = cv2.VideoCapture(0)
if not cap.isOpened():
raise RuntimeError("无法打开摄像头")
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS)
if fps <= 0 or fps != fps:
fps = 25.0
writer = cv2.VideoWriter(
"record.mp4",
cv2.VideoWriter_fourcc(*"mp4v"),
fps,
(width, height),
)
if not writer.isOpened():
raise RuntimeError("VideoWriter 未能打开")
frame_interval = 1.0 / fps
last_write_time = time.time()
while True:
ret, frame = cap.read()
if not ret:
break
now = time.time()
if now - last_write_time >= frame_interval:
writer.write(frame)
last_write_time = now
cv2.imshow("preview", frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
cap.release()
writer.release()
cv2.destroyAllWindows()
这段代码里有一个细节很多人不在意:循环读取摄像头的速度并不等于 fps。如果摄像头实际帧率是 30,而循环本身能跑到 60 帧,那每秒写入的视频帧数就不是 30,最终播放时画面会比实际速度快。所以我通过 frame_interval 做限流,保证写入节奏尽量贴近设定的 fps。
如果你是在做离线视频处理,不是实时采集,不需要限流也没关系,因为处理速度稳定时,总帧数除以 fps 就是正确时长。
5.2 把一堆图片合成视频,图片尺寸必须先统一
做计算机视觉项目时,经常要把算法输出的中间帧拼成视频用于展示。这里最核心的问题是图片尺寸必须完全统一。假设你有一个目录里的图片有 1920x1080,也有 1280x720,直接混着写进同一个 writer,能写成功才怪。
我建议在循环里先显式统一尺寸:
python复制import glob
import cv2
image_paths = sorted(glob.glob("frames/*.jpg"))
if not image_paths:
raise RuntimeError("没有找到图片")
first = cv2.imread(image_paths[0])
height, width = first.shape[:2]
writer = cv2.VideoWriter(
"timelapse.mp4",
cv2.VideoWriter_fourcc(*"mp4v"),
25.0,
(width, height),
)
for path in image_paths:
frame = cv2.imread(path)
if frame is None:
continue
if frame.shape[1] != width or frame.shape[0] != height:
frame = cv2.resize(frame, (width, height))
writer.write(frame)
writer.release()
很多人在这一步踩坑是因为读图片时用了 cv2.imread,如果路径里包含中文,OpenCV 在某些版本会直接返回 None,并不会报错。如果只拿 first 读出来就取尺寸,结果可能顺利,但循环里后续图片遇到 None 时,需要跳过处理,不要让它中断整个合成过程。
5.3 关于帧率和总时长的关系,再补一句
曾经有个同事问我,为什么他用 30 张图片、fps 写成 30,生成的视频时长不是 1 秒,而是更短。原因在于 VideoWriter 保存视频时,时长不是通过“真实等待时间”计算的,而是通过“总帧数除以播放帧率”计算出来的。也就是说,30 帧写入 30fps 的视频,播放时长就是 1 秒左右,即使循环里每帧之间没有任何 sleep,也没关系。
这跟实时录制不同。实时录制时如果你不能按设定的 fps 均匀写入,最后视频播放节奏就会失真。所以做图片序列合成时,只要保证写入帧数正确,fps 填多少,播放时长就是多少,不需要在代码里额外 sleep;实时录制时,则要额外控制写入节拍。
6. 后续工程实践里不断补齐的几则细节经验
6.1 路径里的中文字符和分隔符,能避就避
cv2.VideoWriter 对路径的容错能力不如现代文件 API 那么强。在 Windows 上使用中文路径或者含特殊字符的路径,有时候能写,有时候不能写,表现很迷。我后来养成的习惯是输出路径一律用纯英文目录,比如 D:/workspace/project/output/record.mp4。
分隔符方面,OpenCV 的接口通常能同时接受正斜杠和反斜杠,但在 Python 字符串里写反斜杠要小心转义问题。我见过太多因为 "\t"、"\n" 这种转义字符导致路径错乱的例子。最简单的方式是全部使用正斜杠,或者在路径字符串前加 r 变成原始字符串。如果你的项目必须要用中文路径,最稳妥的方案是先 os.chdir() 切换到目标目录,再用相对文件名创建 writer。
6.2 release() 必须调用,它不是可选项
很多人写 OpenCV 脚本时没有调用 writer.release() 的习惯,因为程序结束后操作系统会自动回收资源。但视频文件和普通内存不一样,封装器需要在结尾写入索引信息,比如 MP4 文件里的 moov box 位置。如果不调用 release(),进程被强杀或者异常退出,文件尾部索引缺失,很多播放器就认为文件损坏,表现为打不开、进度条拖不动、只能播放前几帧。
有一次我循环录制多个视频片段,每个片段之间创建新的 writer,但忘记释放上一个。Windows 下新的 writer 持续打不开,折腾了挺久才定位到是文件句柄没释放。后来我的代码里都有一个不成文的规范:writer 创建后,不管逻辑走哪个分支,最终都要保证执行 release()。如果脚本复杂,可以用 try/finally 包一层,或者至少在所有退出路径都手动释放。
6.3 VideoWriter 不处理音频,音轨不要指望它
OpenCV 的 VideoWriter 只负责写视频流,不会帮你处理音频。很多人拿 OpenCV 录了摄像头画面后,想直接把麦克风声音也存进同一个 MP4,但做出来的文件只有画面没有声音,这是因为从架构上它就只管视频这一路。
项目里需要音视频同步时,我一般用 OpenCV 保存无声视频,再用音频处理工具把声音单独封装进去,或者干脆用支持多路采集的方案。这里提醒大家不要花太多时间试图让 VideoWriter 带上音频,方向不对,纯属浪费精力。
6.4 没有线程安全可以依赖,多线程写入要自己做锁
OpenCV 的 VideoWriter 本质上不是线程安全的。我在做多路摄像头采集时,一开始觉得每路摄像头一个线程,分别写各自的 writer,应该没有冲突。实际结果显示,当多个 writer 同时往磁盘写数据,底层编码器对 buffered 资源的管理会出现问题,轻则丢帧,重则文件损坏。
如果确实需要多线程写入,有两种比较稳的做法:一是每一路采集线程维护独立的 writer,并且把磁盘写入路径分开;二是把编码写入任务统一放到同一个线程里处理,用队列把帧从其他线程收集过来,再由该线程按顺序写文件。第二种方法能避免底层资源竞争,写出来的文件也更稳定。当然,后面这种做法对帧率控制的要求更高,需要根据实际采集卡或者摄像头的输出节奏做适配。
6.5 别把中间产物和交付物混用同一种编码
录制原始检测画面、保存算法调试视频、给客户提交最终演示视频,这三类场景对编码格式的要求完全不同。我现在的默认策略是:
- 调试阶段:用
MJPG + AVI,兼容性最好,即使写一半程序崩了,前面的文件也大概率能播放; - 算法中间产物:用
mp4v + MP4,体积适中,便于跨平台查看; - 交付演示:用 H.264 编码的 MP4,如果 OpenCV 环境不支持,就先用上面的组合生成中间文件,再做一次转码。
这个看起来“多此一举”的分类,帮我避开了很多最终交付前才发现视频打不开的尴尬。毕竟你最不希望发生的事情,就是在客户面前试播视频时,发现文件损坏或者播放器不兼容。
最后再分享一个实用技巧。每当你换了一台新电脑或者新环境,先别直接跑整个项目,花十秒钟写下面这个 mini 测试脚本,把目标编码全部探测一遍:
python复制import cv2
combos = [
("MJPG", "avi"),
("XVID", "avi"),
("mp4v", "mp4"),
("avc1", "mp4"),
("hvc1", "mp4"),
]
for tag, ext in combos:
path = f"probe_{tag}.{ext}"
writer = cv2.VideoWriter(
path,
cv2.VideoWriter_fourcc(*tag),
20.0,
(320, 240),
)
ok = writer.isOpened()
if ok:
fake_frame = __import__("numpy").zeros((240, 320, 3), dtype="uint8")
writer.write(fake_frame)
writer.release()
print(tag, ext, "OK" if ok else "FAILED")
这个脚本会把当前环境所有可用编码一次性探明,后面再写 VideoWriter 的时候,你心里就有底了。视频写入看起来只是 OpenCV 里的小环节,但编码格式、容器、环境依赖、资源释放这些点全部纠缠在一起时,确实能把人折腾得够呛。把 cv2.VideoWriter_fourcc 的原理和这些实战细节吃透,录制视频这件事就能从“靠运气”变成“稳稳可控”。
