☰
C++ opencv之视频读写(VideoCapture,VideoWriter)的实现
2026/10/8 2:21:36 网站建设 项目流程

前言

视频读写看起来是 OpenCV 里最简单的几个 API:cv::VideoCapture打开,循环read(),cv::VideoWriter写出去。但落到工程里翻车点极多:播放器能播的 MP4,isOpened()却返回false;写出来 0 字节;抽帧帧数和实际对不上;红蓝通道互换;跑几小时内存一直涨。

根源大多不在 API,而在于视频不是"一种格式",而是"容器 + 编码器 + 参数"的三元组。本文先讲清楚视频在 OpenCV 眼里是什么,再讲两个类的参数含义,给出可编译运行的完整示例,最后逐条摊开常见的坑。

一、视频的本质:容器、编解码器与帧

1.1 容器不等于编码

.mp4、.avi、.mkv、.mov是容器格式(container format),定义"如何把若干路数据流打包进一个文件":视频/音频/字幕流的组织方式、时间戳(PTS/DTS)与索引表、以及宽高、帧率、旋转角等元数据。

H.264、H.265、MPEG-4 Part 2、MJPEG 是编码器(codec),定义"一帧图像怎么压缩成字节"。同一个.mp4里可能是 H.264 也可能是 H.265;同一份 H.264 码流既能塞进.mp4也能塞进.mkv。

OpenCV 自己不实现编解码,它只是胶水层,把底层后端(backend)的能力包装成统一 API。所以绝大多数"打不开"本质是后端不支持这个容器/编码器。

1.2 videoio 后端

后端宏底层实现平台特点
cv::CAP_FFMPEGFFmpeg全平台格式最全,文件读写首选
cv::CAP_MSMFMedia FoundationWindows系统自带,无需额外 DLL
cv::CAP_DSHOWDirectShowWindows摄像头兼容性最好
cv::CAP_GSTREAMERGStreamerLinux管道灵活,适合 RTSP 流
cv::CAP_V4L2Video4Linux2LinuxLinux 摄像头标准接口
cv::CAP_OPENCV_MJPEG内置 MJPEG全平台零依赖,但只支持 MJPEG

不指定apiPreference时,OpenCV 按编译期决定的优先级依次尝试。关键点:能打开文件不代表能解码。FFmpeg 打开成功但解码器缺失时,read()返回false,而isOpened()那一刻仍是true。所以判断"能不能读"要看第一次read(),不能只看isOpened()。

1.3 fourcc

fourcc(four character code)用 4 个字符标识编码器,如'MJPG'、'XVID'、'H264'、'mp4v'。

static int cv::VideoWriter::fourcc(char c1, char c2, char c3, char c4);

要点:fourcc 描述编码器,不是容器。写.mp4常用mp4v或avc1,写.avi常用MJPG或XVID。组合错了 FFmpeg 会打印Could not find encoder for codec_id=...,然后isOpened()返回false。

二、VideoCapture:打开与读取

2.1 构造与打开

cv::VideoCapture cap; // 默认构造,未打开 bool ok = cap.open("in.mp4", cv::CAP_FFMPEG); // 显式指定后端 cv::VideoCapture cap2("in.mp4", cv::CAP_FFMPEG); // 一步到位 cv::VideoCapture cam(0, cv::CAP_DSHOW); // 摄像头:索引 0

open()的主要重载是open(const String& filename, int apiPreference = CAP_ANY)与open(int index, int apiPreference = CAP_ANY)。OpenCV 4.5 起还有带params向量的重载,可传硬解等选项,具体支持的键随版本变化,以官方文档为准。

2.2 常用属性

属性通过get(propId)/set(propId, value)访问,返回值与设置值都是double,整数属性要做类型转换。

属性含义备注
CAP_PROP_FRAME_WIDTH/_HEIGHT帧宽 / 帧高摄像头可用set请求分辨率
CAP_PROP_FPS名义帧率很多文件返回 0 或 1000,不可靠
CAP_PROP_FRAME_COUNT总帧数裸流等容器拿不到
CAP_PROP_POS_FRAMES当前帧索引可 seek,精度受关键帧限制
CAP_PROP_POS_MSEC当前时间戳(毫秒)做时间轴对齐更可靠
CAP_PROP_FOURCC当前解码器 fourcc一般只能读,写通常无效

CAP_PROP_FPS为什么不可靠?它来自容器元数据,很多容器(尤其流式生成的文件)只写平均帧率,甚至写 1000 这种占位值。要精确时间轴就用CAP_PROP_POS_MSEC或自己数帧。

2.3 读取

cv::Mat frame; cap >> frame; // 运算符重载,等价 read(),不抛异常 bool ok = cap.read(frame); // 推荐:显式拿到成功与否 cap.grab(); // 只推进不解码,跳帧时可省 CPU cap.retrieve(frame); // 只取已解码帧,不推进

标准循环应把read()放进条件里,因为最后一帧之后read()返回 false 并把frame置空:

cv::Mat frame; while (cap.read(frame)) { // 此处保证 frame 非空 }

三、VideoWriter:写入

cv::VideoWriter writer( "out.mp4", cv::VideoWriter::fourcc('m','p','4','v'), // 编码器 25.0, // 帧率 cv::Size(1280, 720), // 帧尺寸 (宽, 高) true); // isColor if (!writer.isOpened()) { /* 处理失败 */ }

帧尺寸最容易出错。VideoWriter在构造时就把编码器初始化成固定分辨率,之后write()的Mat必须严格匹配;不匹配时 OpenCV 通常不报错,而是静默丢弃或写坏帧——这就是"输出 0 字节"的头号原因。

两个高频错误:


  • cv::Size参数顺序是(width, height),而Mat::rows是高、Mat::cols是宽。写成cv::Size(frame.rows, frame.cols)在非正方形视频上立刻炸。

  • isColor = true时期望BGR三通道;先cvtColor成灰度又用isColor = true去写,会白白大出三倍体积。


资源释放:VideoWriter析构时 flush 并写文件尾(MP4 的moovbox)。若程序异常退出、writer 还没析构,文件头缺失,文件有大小但播不了。稳妥做法是让 writer 待在作用域内,或显式writer.release()。

四、代码实战:视频转码 / 抽帧工具

// main.cpp #include <opencv2/opencv.hpp> #include <filesystem> #include <iostream> namespace fs = std::filesystem; struct Options { std::string input, output; double scale = 1.0; // 缩放比例 double fps = 0.0; // 0 表示沿用源帧率 int everyN = 1; // 每 N 帧处理一帧 }; // 源帧率不可信时回退到 25 static double safeFps(double fps) { return (fps < 1.0 || fps > 1000.0) ? 25.0 : fps; } static int runConvert(const Options& opt) { cv::VideoCapture cap(opt.input, cv::CAP_FFMPEG); if (!cap.isOpened()) { std::cerr << "无法打开: " << opt.input << "\n"; return 1; } // 先读一帧:确认真能解码,同时拿到真实尺寸 cv::Mat first; if (!cap.read(first) || first.empty()) { std::cerr << "打开成功但无法解码首帧(编码器缺失或文件损坏)\n"; return 1; } const cv::Size outSize(static_cast<int>(first.cols * opt.scale), static_cast<int>(first.rows * opt.scale)); const double fps = opt.fps > 0.0 ? opt.fps : safeFps(cap.get(cv::CAP_PROP_FPS)); // 注意 Size 是 (宽, 高) cv::VideoWriter writer(opt.output, cv::VideoWriter::fourcc('m','p','4','v'), fps, outSize, /*isColor=*/true); if (!writer.isOpened()) { std::cerr << "创建输出失败,检查 fourcc 与扩展名是否匹配\n"; return 1; } cv::Mat frame = first; long long readCount = 0, writeCount = 0; do { if (readCount % opt.everyN == 0) { cv::Mat out; if (frame.size() == outSize) out = frame; else cv::resize(frame, out, outSize, 0, 0, cv::INTER_AREA); if (out.size() != outSize) // 兜底,避免静默丢帧 cv::resize(out, out, outSize, 0, 0, cv::INTER_AREA); writer.write(out); ++writeCount; } ++readCount; } while (cap.read(frame)); writer.release(); // 显式 flush,写出 moov box cap.release(); std::cout << "读 " << readCount << " 帧, 写 " << writeCount << " 帧, " << outSize.width << "x" << outSize.height << " @ " << fps << " fps\n"; return 0; } static int runExtract(const Options& opt) { cv::VideoCapture cap(opt.input, cv::CAP_FFMPEG); if (!cap.isOpened()) { std::cerr << "无法打开输入视频\n"; return 1; } fs::create_directories(opt.output); cv::Mat frame; long long idx = 0, saved = 0; while (cap.read(frame)) { if (idx % opt.everyN == 0) { char name[64]; std::snprintf(name, sizeof(name), "frame_%06lld.jpg", idx); fs::path p = fs::path(opt.output) / name; cv::imwrite(p.string(), frame, {cv::IMWRITE_JPEG_QUALITY, 95}); ++saved; } ++idx; } std::cout << "解码 " << idx << " 帧, 保存 " << saved << " 张\n"; return 0; } int main(int argc, char** argv) { if (argc < 4) { std::cerr << "用法:\n" << " " << argv[0] << " convert <in> <out.mp4> [scale] [fps]\n" << " " << argv[0] << " frames <in> <outDir> [everyN]\n"; return 1; } Options opt; opt.input = argv[2]; opt.output = argv[3]; const std::string mode = argv[1]; if (mode == "convert") { if (argc > 4) opt.scale = std::stod(argv[4]); if (argc > 5) opt.fps = std::stod(argv[5]); return runConvert(opt); } if (mode == "frames") { if (argc > 4) opt.everyN = std::stoi(argv[4]); return runExtract(opt); } std::cerr << "未知模式: " << mode << "\n"; return 1; }
cmake_minimum_required(VERSION 3.16) project(video_io_demo LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(OpenCV REQUIRED COMPONENTS core imgproc imgcodecs videoio) add_executable(video_io_demo main.cpp) target_link_libraries(video_io_demo PRIVATE ${OpenCV_LIBS}) target_include_directories(video_io_demo PRIVATE ${OpenCV_INCLUDE_DIRS})
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release cmake --build build --config Release -j ./build/video_io_demo convert input.mp4 output.mp4 0.5 30 ./build/video_io_demo frames input.mp4 ./frames 10

常见坑点

坑 1:摄像头打不开,文件却正常

Windows 默认后端是 MSMF,对部分 USB 摄像头、虚拟摄像头、老采集卡兼容性差,表现为open(0)卡几秒后返回false。

❌ 靠默认值碰运气:

cv::VideoCapture cam(0);

✅ 显式指定并回退:

cv::VideoCapture cam; if (!cam.open(0, cv::CAP_DSHOW)) cam.open(0, cv::CAP_MSMF); if (!cam.isOpened()) std::cerr << "摄像头不可用\n";

另外open(0)的0是设备索引,不要写成open("0")——那会被当成文件名。

坑 2:输出文件 0 字节或无法播放

按概率排序的三个原因:帧尺寸与构造时不一致;fourcc 与容器不匹配但没检查isOpened();writer 没正常析构导致moovbox 缺失。

❌ 只构造不检查:

cv::VideoWriter w("out.mp4", cv::VideoWriter::fourcc('H','2','6','4'), 30, cv::Size(640, 480)); w.write(frame); // 失败也是静默的

✅ 检查 + 断言 + 显式释放:

cv::VideoWriter w("out.mp4", cv::VideoWriter::fourcc('m','p','4','v'), 30, cv::Size(640, 480), true); if (!w.isOpened()) throw std::runtime_error("VideoWriter 初始化失败"); CV_Assert(frame.size() == cv::Size(640, 480)); // 尺寸错了当场暴露 w.write(frame); w.release(); // 必须,保证落盘

坑 3:cv::Size宽高写反

❌cv::Size s(frame.rows, frame.cols);

✅cv::Size s(frame.cols, frame.rows);或直接cv::Size s = frame.size();

正方形视频时这个 bug 不暴露,换 16:9 输入立刻炸。

坑 4:颜色通道错乱

imread与VideoCapture::read给出的都是BGR。当成 RGB 用(喂给只吃 RGB 的推理模型等),红蓝互换。

❌cv::Mat rgb = frame;—— 变量名叫 rgb,其实还是 BGR。

✅cv::Mat rgb; cv::cvtColor(frame, rgb, cv::COLOR_BGR2RGB);

反过来,imwrite和VideoWriter期望的也是 BGR,写文件前不要做 BGR2RGB。

坑 5:CAP_PROP_FPS返回 0 或 1000

❌double fps = cap.get(cv::CAP_PROP_FPS);拿 0 去构造 writer,得到速度飞快的文件。

✅ 范围校验后回退:

double fps = cap.get(cv::CAP_PROP_FPS); if (fps < 1.0 || fps > 1000.0) fps = 25.0;

坑 6:seek 精度不够

set(CAP_PROP_POS_FRAMES, n)不是精确定位。H.264 有 B 帧,解码器只能从最近关键帧(I 帧)开始解码再丢弃前面的帧,因此落点可能偏;频繁 seek 还会反复重解码,性能很差。要逐帧精确处理就顺序解码自己计数,只在粗粒度抽帧时才 seek。

坑 7:FFmpeg 的 DLL 运行时找不到

编译链接都过,一运行报找不到 opencv_videoio_ffmpeg4xx_64.dll。原因是 OpenCV 把 FFmpeg 后端做成运行时动态加载的独立 DLL。

✅ 把该 DLL 复制到可执行文件同目录,或把 OpenCV 的bin加入PATH。另外先确认你的构建确实带 FFmpeg:

std::cout << cv::getBuildInformation() << std::endl; // 在输出里搜 "FFMPEG:",是 NO 的话放再多 DLL 也没用

坑 8:逐帧clone()导致内存暴涨

❌all.push_back(f.clone());—— 1080p 一帧约 6MB,一万帧就是 60GB。

✅cap.read(f)在很多后端上复用同一块缓冲:跨迭代要保留才clone(),当帧处理完即弃则绝不 clone。

cv::Mat f; while (cap.read(f)) process(f);

坑 9:多线程共享一个VideoCapture

它不是线程安全的。正确姿势是一个线程解码、把帧投递给工作线程,或每个线程各自持有VideoCapture。共享一个cap同时read()会导致帧错乱甚至崩溃。

总结

关注点正确做法常见错误
打开指定CAP_FFMPEG/CAP_DSHOW,检查首次read()只看isOpened()
尺寸cv::Size(cols, rows),写前保证一致宽高写反;尺寸不符静默丢帧
帧率校验CAP_PROP_FPS,异常回退直接使用 0 或 1000
颜色读入即 BGR,写盘保持 BGR中途误当 RGB
资源release()或依赖析构异常退出导致文件损坏
内存用完即弃,需保留才clone()逐帧 clone 全量缓存


把视频理解成"容器 + 编码器 + 参数"之后,这些坑基本都能自己推导:尺寸、帧率、颜色、fourcc 都是编码器初始化时就要定死的参数,中途不能变;而"打开成功≠能解码"来自 OpenCV 胶水层的设计。最实用的两招是:写视频代码前先打印cv::getBuildInformation()确认后端,以及在任何write()之前加一条尺寸断言。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询