ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

C++ opencv之视频读写(VideoCapture,VideoWriter)的实现

2026/10/8 2:22:02 拓冰建站 浏览量
C++ opencv之视频读写(VideoCapture,VideoWriter)的实现 前言视频读写看起来是 OpenCV 里最简单的几个 APIcv::VideoCapture打开循环read()cv::VideoWriter写出去。但落到工程里翻车点极多播放器能播的 MP4isOpened()却返回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系统自带无需额外 DLLcv::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 fourccfourccfour 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); // 摄像头索引 0open()的主要重载是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_castint(first.cols * opt.scale), static_castint(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_TYPERelease 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(); // 必须保证落盘坑 3cv::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。坑 5CAP_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;坑 6seek 精度不够set(CAP_PROP_POS_FRAMES, n)不是精确定位。H.264 有 B 帧解码器只能从最近关键帧I 帧开始解码再丢弃前面的帧因此落点可能偏频繁 seek 还会反复重解码性能很差。要逐帧精确处理就顺序解码自己计数只在粗粒度抽帧时才 seek。坑 7FFmpeg 的 DLL 运行时找不到编译链接都过一运行报找不到 opencv_videoio_ffmpeg4xx_64.dll。原因是 OpenCV 把 FFmpeg 后端做成运行时动态加载的独立 DLL。✅ 把该 DLL 复制到可执行文件同目录或把 OpenCV 的bin加入PATH。另外先确认你的构建确实带 FFmpegstd::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()之前加一条尺寸断言。