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()之前加一条尺寸断言。

相关新闻

Flutter鸿蒙离线同步实战:sql_crdt的CRDT适配与落地

Flutter鸿蒙离线同步实战:sql_crdt的CRDT适配与落地

直接说结论:如果你们团队正在做 Flutter 跨端应用,又被离线同步、多端冲突合并折磨得头疼,那sql_crdt这个库值得花点时间认真研究。它把 CRDT 那一套理论上很复杂的一致性模型,直接封装成了 SQLite 里能跑、Dart 里能调的东西。而…

2026/10/13 2:34:37 阅读更多 →
pcapsipdump 0.2实战:SIP/RTP会话级抓包与排障指南

pcapsipdump 0.2实战:SIP/RTP会话级抓包与排障指南

简介:pcapsipdump 是一款基于 libpcap 开发的开源 SIP 嗅探工具,主要面向 VoIP 运维、SIP 协议研发以及网络抓包分析人员,用于解决 SIP/RTP 会话抓包与后续分类存储的问题。它能够监听指定网络接口,实时捕获流量并自动识别 SIP/RT…

2026/10/12 15:36:21 阅读更多 →
SIP抓包利器pcapsipdump:从噪声中按会话切分pcap的实战指南

SIP抓包利器pcapsipdump:从噪声中按会话切分pcap的实战指南

简介:面向VoIP运维、SIP协议开发者与网络排障人员,这一基于libpcap的开源SIP嗅探工具会监听指定网络接口,自动区分通话会话,将SIP与RTP流量分别写入命名清晰的独立pcap文件,便于使用tcpdump、Wireshark等工具逐会话复盘…

2026/10/12 9:42:30 阅读更多 →

最新新闻

SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0学生信息管理系统实战

SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0学生信息管理系统实战

接手过一个学生信息管理系统的小项目,技术栈正好是SpringBoot2 Vue3 MyBatis-Plus MySQL8.0,标题里这套组合一眼看去很常见,但真正从零落地的时候,细节远比想象中多。当初团队里有人建议用老掉牙的JSP那套,也有人建…

2026/10/13 2:34:32 阅读更多 →
SpringBoot+Vue私房菜定制上门服务系统开发实战解析

SpringBoot+Vue私房菜定制上门服务系统开发实战解析

一个 SpringBoot Vue 的私房菜定制上门服务系统,光看标题可能觉得就是一个普通的管理系统,但真正动手做过的人会知道,这类“定制 上门 多角色”的业务模型,远比表面看起来复杂。它不是简单的 CRUD 堆砌,订单状态的流…

2026/10/13 2:34:32 阅读更多 →
ModernZ键盘快捷键绑定教程:input.conf中script-binding与script-message完全指南

ModernZ键盘快捷键绑定教程:input.conf中script-binding与script-message完全指南

音视频UI组件 【免费下载链接】ModernZ A sleek and modern OSC for mpv. This project is a fork of ModernX that enhances functionality by adding more features while preserving the core standards of mpvs OSC. 项目地址: https://gitcode.com/gh_mirrors/…

2026/10/13 2:34:32 阅读更多 →
数据结构第一章重难点全梳理:逻辑结构、存储结构与复杂度分析

数据结构第一章重难点全梳理:逻辑结构、存储结构与复杂度分析

很多人在学一门新课程时,最容易忽略的偏偏是第一张。第一张通常看起来“全是概念、没有代码”,感觉像是空话,于是翻两页就过去了,等后面真正写程序、做算法题时才发现什么都对不上。这次我把自己重新整理的《数据结构》第一章笔记…

2026/10/13 2:34:32 阅读更多 →
修仙AI小镇xianxia-AI-town:大模型驱动的修仙模拟器,NPC与玩家为何能平权?完整解读

修仙AI小镇xianxia-AI-town:大模型驱动的修仙模拟器,NPC与玩家为何能平权?完整解读

【免费下载链接】xianxia-AI-town-AI- 大模型驱动的npc决策,npc和玩家平权 项目地址: https://gitcode.com/gh_mirrors/xi/xianxia-AI-town-AI- 点击查看 免费下载 修仙AI小镇(xianxia-AI-town)是一款大模型驱动的修仙模拟游戏&a…

2026/10/13 2:34:32 阅读更多 →
SpringBoot+Vue学生成绩分析与可视化平台全解析

SpringBoot+Vue学生成绩分析与可视化平台全解析

每年毕业季我都能在技术社区里看到大量和“成绩管理系统”相关的提问,这个题目属于那种“看起来老套、实则非常能打”的典型选题。特别是把Java、Vue、SpringBoot这三个词绑在一起的时候,它考查的已经不只是“会写增删改查”,而是完整的前后端…

2026/10/13 2:33:32 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →