12. OpenCV视频处理:视频读取与写入、帧处理、运动检测与背景减除
视频处理,说白了就是跟一串连续的图片打交道。每一帧就是一张图,但连起来就有了时间维度。我刚开始接触OpenCV时,觉得视频读写不就是打开文件、循环读帧嘛,结果踩了不少坑——编码格式不对、写入卡顿、运动检测全是噪点……今天咱们就把这些硬骨头啃下来。
12.1 视频读取与写入:基础操作
OpenCV用VideoCapture读视频,用VideoWriter写视频。这两个类封装了底层编解码,但有几个细节不注意,代码就跑不通。
12.1.1 读取视频文件或摄像头
读取本地文件,传文件路径就行。读取摄像头,传设备ID(通常是0)。我个人习惯先检查是否成功打开,不然后面读帧会崩。
cv::VideoCapture cap("test.mp4");
if (!cap.isOpened()) {
std::cerr << "打开视频失败" << std::endl;
return -1;
}
cv::Mat frame;
while (cap.read(frame)) {
// 处理每一帧
cv::imshow("Frame", frame);
if (cv::waitKey(30) == 27) break; // ESC退出
}
cap.release();
cap.set(cv::CAP_PROP_FRAME_WIDTH, 640)设置分辨率。但注意,不是所有摄像头都支持你设置的值,最好先读一下实际值。
12.1.2 写入视频文件
写入视频需要指定编码器、帧率、尺寸。编码器用cv::VideoWriter::fourcc()生成四个字符的编码标识。我常用'X','V','I','D'或'M','J','P','G',前者压缩比高,后者兼容性好。
cv::VideoWriter writer("output.avi",
cv::VideoWriter::fourcc('X','V','I','D'),
30.0,
cv::Size(640, 480));
if (!writer.isOpened()) {
std::cerr << "写入视频失败" << std::endl;
return -1;
}
cv::Mat frame;
while (cap.read(frame)) {
writer.write(frame); // 写入帧
}
writer.release();
VideoWriter初始化时一致,否则会写入失败或生成损坏文件。我曾经因为resize忘记改尺寸,折腾了半天才发现。
12.2 帧处理:逐帧操作的艺术
视频处理的核心就是帧处理。每一帧都是一张cv::Mat,你可以做任何图像操作:缩放、滤波、颜色转换、边缘检测……但要注意性能,别让处理速度跟不上帧率。
12.2.1 帧预处理流水线
我通常把帧处理分成三步:降噪 → 颜色转换 → 尺寸归一化。降噪用高斯模糊,颜色转换转成灰度(很多算法只需要亮度信息),尺寸归一化到固定大小。
cv::Mat processFrame(const cv::Mat& src) {
cv::Mat gray, blurred, resized;
cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);
cv::GaussianBlur(gray, blurred, cv::Size(5,5), 1.5);
cv::resize(blurred, resized, cv::Size(320, 240));
return resized;
}
你想想看,如果每帧都做全尺寸处理,720p的视频每秒30帧,CPU很快就扛不住了。缩小到320x240,处理量直接降到原来的1/6,效果差别不大。
12.2.2 帧率控制与跳帧
有时候我们不需要处理每一帧,比如做运动检测时,每秒处理10帧就够了。可以用cap.set(cv::CAP_PROP_POS_FRAMES, frameIndex)跳帧,或者简单计数。
int frameCount = 0;
while (cap.read(frame)) {
if (frameCount % 3 == 0) { // 每3帧处理一次
processFrame(frame);
}
frameCount++;
}
waitKey()的延时。如果跳帧后帧率变低,延时可以适当减小,否则视频播放会变慢。
12.3 运动检测:让程序“看见”变化
运动检测,说白了就是找两帧之间的差异。最简单的办法是帧差法,但噪声多。我更喜欢用背景减除,OpenCV提供了专门的类。
12.3.1 帧差法
帧差法就是相邻帧相减,取绝对值,再阈值化。优点是速度快,缺点是对光照变化敏感,且运动物体内部容易出现空洞。
cv::Mat prevFrame, currFrame, diff, mask;
cap.read(prevFrame);
cv::cvtColor(prevFrame, prevFrame, cv::COLOR_BGR2GRAY);
while (cap.read(currFrame)) {
cv::cvtColor(currFrame, currFrame, cv::COLOR_BGR2GRAY);
cv::absdiff(prevFrame, currFrame, diff);
cv::threshold(diff, mask, 30, 255, cv::THRESH_BINARY);
cv::imshow("Motion", mask);
prevFrame = currFrame.clone();
if (cv::waitKey(30) == 27) break;
}
clone()复制前一帧,不能直接赋值。直接赋值是浅拷贝,修改当前帧会影响前一帧。我曾经在这里栽过跟头,排查了半天。
12.3.2 背景减除:MOG2与KNN
OpenCV提供了cv::BackgroundSubtractor接口,实现类有MOG2和KNN。MOG2基于高斯混合模型,能适应光照渐变;KNN基于最近邻,对动态背景(如树叶晃动)更鲁棒。
cv::Ptr<cv::BackgroundSubtractor> bgSubtractor =
cv::createBackgroundSubtractorMOG2(500, 16, true);
// 或者 cv::createBackgroundSubtractorKNN(500, 400.0, false);
cv::Mat frame, fgMask;
while (cap.read(frame)) {
bgSubtractor->apply(frame, fgMask);
cv::imshow("FG Mask", fgMask);
if (cv::waitKey(30) == 27) break;
}
MOG2的构造函数参数:history(历史帧数,默认500)、varThreshold(方差阈值,越大越不敏感)、detectShadows(是否检测阴影)。我一般把varThreshold设到16~32之间,太低会引入太多噪点。
12.4 运动检测与背景减除的实战流程
下面这张图展示了完整的运动检测流程,从读取视频到输出结果。我习惯把每一步都封装成函数,方便调试和复用。
12.4.1 后处理:形态学操作与轮廓提取
背景减除得到的前景掩码通常有噪点,需要用开运算(先腐蚀后膨胀)去除小斑点,再用闭运算填充空洞。然后找轮廓,用外接矩形框出运动区域。
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(5,5));
cv::morphologyEx(fgMask, fgMask, cv::MORPH_OPEN, kernel);
cv::morphologyEx(fgMask, fgMask, cv::MORPH_CLOSE, kernel);
std::vector<std::vector<cv::Point>> contours;
cv::findContours(fgMask, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
for (const auto& contour : contours) {
double area = cv::contourArea(contour);
if (area < 500) continue; // 过滤小区域
cv::Rect rect = cv::boundingRect(contour);
cv::rectangle(frame, rect, cv::Scalar(0, 255, 0), 2);
}
frame.total() * 0.001作为动态阈值。
12.4.2 阴影检测与去除
MOG2自带阴影检测,掩码中阴影部分像素值为127(前景为255,背景为0)。如果你不需要阴影,可以把它置为0。
cv::threshold(fgMask, fgMask, 128, 255, cv::THRESH_BINARY);
但注意,这样会把真正的暗色运动物体也误删。更好的做法是用cv::compare(fgMask, 127, shadowMask, cv::CMP_EQ)单独处理阴影区域。
12.5 性能优化与实战建议
视频处理最怕卡顿。我总结了几条优化原则:
- 降低分辨率: 处理前先缩小到320x240或更小,运动检测精度影响不大。
- 使用ROI: 如果运动只发生在画面局部,只处理那一块区域。
- 多线程: 读取帧和处理帧分开线程,用队列缓冲。OpenCV的
VideoCapture读取本身是阻塞的,但可以配合std::thread。 - 选择合适算法: 静态背景用帧差法,动态背景用MOG2或KNN。KNN对树叶晃动、水面波纹更鲁棒,但计算量稍大。
12.6 常见问题与避坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 写入视频打不开 | 编码器不支持或尺寸不匹配 | 换用'M','J','P','G'编码,检查帧尺寸 |
| 运动检测全是噪点 | 阈值太低或未做形态学处理 | 提高varThreshold,增加开运算 |
| 检测到阴影 | MOG2阴影检测默认开启 | 设置detectShadows=false或后处理去除 |
| 处理速度跟不上帧率 | 每帧处理太耗时 | 缩小尺寸、跳帧、用多线程 |
嗯,视频处理这块内容不少,但核心就是帧操作+背景建模。你只要把帧差法和MOG2跑通,再配合形态学去噪,大部分场景都能应付。我在做智能安防项目时,就是用这套流程检测入侵物体,效果挺稳的。