OpenCV 视频写入实战:cv::VideoWriter、FOURCC 与视频文件容器解析
本篇基于 OpenCV 官方教程 video_write.markdown 展开,讲解如何用 cv::VideoWriter 写出视频文件、FOURCC 编码的原理与转换方法,以及如何从输入视频中提取单个 BGR 颜色通道并写成新的 AVI 文件。读完后你将能够独立编写“读入视频→逐帧处理→写出视频”的完整管线,并理解视频容器、编码器的关系及 OpenCV 写入端的边界。
1. 目标与演示程序
当你处理视频流时,常常需要把图像处理结果保存为一个新的视频文件。对于简单的视频输出,OpenCV 内置了专为该场景设计的 cv::VideoWriter 类。本教程围绕三个核心问题展开:
- 如何用 OpenCV 创建视频文件;
- OpenCV 能创建哪些类型的视频文件;
- 如何从一个视频中提取指定颜色通道。
作为演示,官方示例提取输入视频 BGR 三通道中的某一个,写入新的视频文件。应用程序的行为由命令行参数控制:
| 参数 | 含义 |
|---|---|
| 第 1 个 | 要处理的视频文件路径 |
| 第 2 个 | R、G、B 之一,指定要提取的通道 |
| 第 3 个 | Y 或 N。若为 N,输出视频沿用输入视频的编码器;若为 Y,运行时弹出窗口让你从系统已安装的编码器中自行选择 |
一个合法的命令行示例:
video-write.exe video/Megamind.avi R Y
2. 视频文件的内部结构
在写视频之前,需要先建立对视频文件结构的认识:
- 每个视频文件本身是一个容器(container)。容器类型由文件扩展名表达,例如 avi、mov、mkv;
- 容器内部包含多个轨道(track):视频流、音频流、字幕等其他数据;
- 各轨道的具体存储方式由各自的编解码器(codec)决定。音频轨道常见 mp3、aac;视频轨道的常见编码器包括 XVID、DIVX、H264,以及无损编码器 LAGS(Lagarith Lossless Codec)。系统上可用的编码器完整列表取决于你安装了哪些编解码器。
OpenCV 的定位是计算机视觉库,而非视频流/编解码器库,因此开发者刻意将写入部分保持得尽可能简单。按教程所述,OpenCV 对视频容器仅支持 avi 扩展名及其第一版本格式。由此带来两个直接限制:
- 不能保存大于 2 GB 的视频文件(32 位 AVI 索引的固有上限);
- 容器内只能创建和扩展单一视频轨道,不支持音频或其他轨道的编辑。
尽管如此,系统中存在的任何视频编码器在 OpenCV 写入端都可能可用。遇到上述限制时,教程建议的路线是:使用更专门的视频写入库(如 FFmpeg)或无损编码器(HuffYUV、CorePNG、LCL);或者先用 OpenCV 创建视频轨道,再用 VirtualDub、AviSynth 等工具加音轨或转封装。
补充说明:从当前仓库头文件 videoio.hpp 的文档注释看,现代 OpenCV 的写入后端已经扩展——Linux 默认使用 FFMPEG 写视频,Windows 可选 FFMPEG/MSWF/DSHOW,macOS 使用 AVFoundation;并且通过
img_%02d.jpg这类文件名配合fourcc=0或fps=0还可以写出图像序列。这与教程成文时期的“仅 AVI”限制属于同一主题的演进,实际能力应以你所用版本的构建配置为准。
3. cv::VideoWriter 类:五个关键参数
创建视频文件只需实例化一个 cv::VideoWriter。其属性既可以在构造函数中给出,也可以稍后通过 cv::VideoWriter::open() 给出,两者参数完全相同。当前实现见 VideoWriter 类定义,核心签名与参数如下:
VideoWriter(const String& filename, int fourcc, double fps,
Size frameSize, bool isColor = true);
bool open(const String& filename, int fourcc, double fps,
Size frameSize, bool isColor = true);
3.1 参数 1:输出文件名(容器类型)
文件名包含容器扩展名,示例中只支持 avi。示例代码从输入文件名派生出输出文件名:取输入文件去掉扩展名的部分,追加通道名(R/G/B),再拼上 .avi:
const string source = argv[1]; // 源文件名
string::size_type pAt = source.find_last_of('.'); // 找到扩展名位置
const string NAME = source.substr(0, pAt) + argv[2][0] + ".avi"; // 构造带容器扩展名的新文件名
3.2 参数 2:编码器 FOURCC
每个视频编码器都有一个不超过 4 个字符的唯一短名,如 XVID、DIVX、H264,这种形式称为 four character code(FOURCC)。它可以直接从输入视频读取,使用 VideoCapture::get(CAP_PROP_FOURCC)(该属性枚举值见 videoio.hpp:CAP_PROP_FOURCC = 6)。由于 get 是通用接口,返回值恒为 double:
- 双精度浮点数占 64 位,而四个字符是 4 字节、32 位;
- 这 4 个字符编码在该 double 值的低 32 位中;
- 最简单的“丢弃高 32 位”方式就是直接转换成
int:
VideoCapture inputVideo(source); // 打开输入
int ex = static_cast<int>(inputVideo.get(CAP_PROP_FOURCC)); // 取编码器类型(整型形式)
OpenCV 内部使用这种整型表示,并期望 fourcc 参数以此形式传入。从整型转回字符串有两种方法:
位运算法(按字节“与”+移位,末尾补 0 结束字符串):
char EXT[] = {(char)(ex & 0XFF), (char)((ex & 0XFF00) >> 8),
(char)((ex & 0XFF0000) >> 16), (char)((ex & 0XFF000000) >> 24), 0};
union 法(赋值后自动完成转换,且编码器变化时无需重复运算):
union { int v; char c[5]; } uEx;
uEx.v = ex; // 通过 union 从 int 到 char
uEx.c[4] = '\0';
如果事先知道编码器的四个字符,则用 CV_FOURCC 宏由字符构造整型,例如 MPEG-1 编码器:
CV_FOURCC('P','I','M','1') // 由字符到整型,MPEG1 编码器
该宏在当前仓库中的实现见 cap_ffmpeg_impl.hpp:
#define CV_FOURCC(c1, c2, c3, c4) (((c1) & 255) + (((c2) & 255) << 8) + (((c3) & 255) << 16) + (((c4) & 255) << 24))
可见它正是“每字符占 1 字节,依次左移 0/8/16/24 位”的组合方式,与 3.2 节读取方向的位运算严格互逆。当前头文件还提供等价的静态成员函数 VideoWriter::fourcc(char c1, char c2, char c3, char c4)(定义位置)。
特殊值:若该参数传 -1,运行时将弹出一个包含系统全部已安装编码器的窗口,供你交互式选择:
3.3 参数 3 与 4:帧率与帧尺寸
- fps:输出视频的每秒帧数。示例沿用输入视频的帧率,通过
get(CAP_PROP_FPS)获取; - frameSize:输出视频的帧尺寸。示例同样沿用输入视频,通过
get(CAP_PROP_FRAME_WIDTH)与get(CAP_PROP_FRAME_HEIGHT)获取。
3.4 参数 5:isColor
最后一个参数是可选的,默认为 true,表示输出为彩色视频(写入时送入三通道图像);传入 false 则输出灰度视频。头文件注释还提醒:颜色图像一般期望为 BGR 顺序。
示例中完整的调用方式:
VideoWriter outputVideo;
Size S = Size((int) inputVideo.get(CAP_PROP_FRAME_WIDTH), // 获取输入尺寸
(int) inputVideo.get(CAP_PROP_FRAME_HEIGHT));
outputVideo.open(NAME, ex, inputVideo.get(CAP_PROP_FPS), S, true);
3.5 打开检查与逐帧写入
- 用
cv::VideoWriter::isOpened()判断open是否成功; - 视频文件会在
VideoWriter对象析构时自动关闭(析构函数内部先调用release(),见 头文件注释); - 打开成功后即可用
write()按顺序写入帧,或使用等价的流运算符operator <<:
outputVideo.write(res); // 或
outputVideo << res;
注意帧的尺寸必须与 open 时指定的尺寸一致,否则底层后端会拒绝写入。从当前实现注释看(write 接口),write() 返回 true 表示底层后端成功写入该帧,false 表示编码器失败等错误,实际写循环中建议检查该返回值。
4. 从 BGR 图像中提取单个颜色通道
“提取一个颜色通道”的语义是:保留目标通道,把其余两个 BGR 通道全部置零。可以用逐像素扫描实现,也可以用 split + merge 操作完成:先把通道拆分为独立图像,把非目标通道置为同尺寸同类型的零矩阵,再合并回去:
split(src, spl); // 处理 - 只提取正确的通道
for (int i = 0; i < 3; ++i)
if (i != channel)
spl[i] = Mat::zeros(S, spl[0].type());
merge(spl, res);
其中通道号与字符的映射关系为:'B' → 0、'G' → 1、'R' → 2(OpenCV 的 BGR 通道顺序中 B 为第 0 通道)。
5. 完整示例源码走读
完整示例位于 video-write.cpp,与上文各片段一一对应:
- 参数校验(L26-L30):要求恰好 3 个参数,否则打印帮助并退出;
- 打开输入视频(L35-L40):
VideoCapture inputVideo(source),并检查isOpened(); - 构造输出文件名与读取 FOURCC(L42-L47):如 3.1、3.2 节所述,位运算法转字符数组
EXT用于打印输入编码器名称; - 打开输出视频(L52-L62):当第 3 个参数为
Y时以fourcc = -1打开(弹出编码器选择框),否则复用输入视频的编码器;打开后再次检查isOpened(); - 打印输入信息(L64-L66):分辨率、帧数(
CAP_PROP_FRAME_COUNT)、输入编码器类型; - 主写入循环(L78-L91):
inputVideo >> src逐帧读取,src.empty()时结束;每帧执行split→ 置零非目标通道 →merge→outputVideo << res; - 收尾:循环结束后打印 "Finished writing",
VideoWriter对象在函数返回析构时自动关闭文件。
6. 实践要点与常见坑
结合教程与当前仓库头文件,整理以下可直接落地的注意事项:
| 事项 | 说明 |
|---|---|
| 大小上限 | 教程指出 AVI 容器下不能保存超过 2 GB 的视频;需要更大文件时应转向 FFmpeg 等方案 |
| 轨道限制 | 容器内仅支持单一视频轨道,无音频;需要音轨时后续用专门工具合成 |
| fourcc = -1 | 对部分后端会弹出系统编码器选择对话框(头文件提示) |
| 图像序列输出 | 使用 img_%02d.jpg 这类文件名并配合 fourcc=0 或 fps=0 可写出图像序列;用未压缩格式(如 BMP)可保存原始帧 |
| 无损输出 | 多数编码器有损;需要无损时使用无损编码器(如 FFV1、Huffman HFYU、Lagarith LAGS 等) |
| 奇数尺寸 | 启用 FFMPEG 时允许奇数宽或高,但会截断最右列/最底行(部分底层 swscale 函数要求偶数宽高) |
| 后端选择 | 头文件提供了带 apiPreference 参数的重载(如 cv::CAP_FFMPEG、cv::CAP_GSTREAMER),可在多个后端可用时强制指定实现 |
| 灰度输出 | isColor=false 时编码器按灰度帧工作 |
7. 小结
本教程以“提取输入视频的单个 BGR 通道并写成新视频”为主线,串起了 OpenCV 视频写入的完整知识链:视频容器与编码器的关系、cv::VideoWriter 五个参数的确切含义、FOURCC 在 double/int/char 三种表示间的互转,以及 split/merge 完成通道提取的标准做法。配套的完整源码可直接参考 samples/cpp/tutorial_code/videoio/video-write/video-write.cpp,API 细节以 modules/videoio/include/opencv2/videoio.hpp 中的 VideoWriter 类文档为准。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00


