读写操作
cv::VideoCapture capture("bike.avi"); // 生成 cv::VideoCapture 类的对象,并初始化
//如果在定义上述对象的时候没有初始化,也可以用对象的 open() 函数打开文件
// capture.open("bike.avi");
// 检查是否成功打开
if(!capture.isOpened()){ ... }
// 获取帧速率 FPS
double rate = capture.get(cv::CAP_PROP_FPS);
// 循环读取视频的每一帧
bool stop(false);
int delay = 1000/rate;
while (!stop){
if (!capture.read(frame)) // 读完了所有的帧
break;
if (cv::waitKey(delay)>=0)
stop = true;
}
// 关闭视频,不是必须的, cv::VideoCapture 类的解构函数中会自动调用它
capture.release();
上述 .get()
函数还可以获得很多信息:
-
cv::CAP_PROP_FRAME_COUNT
:视频的总帧数 -
cv::CAP_PROP_POS_FRAMES
:得到下一帧的序号 -
cv::CAP_PROP_FOURCC
:编码器的四字符代码 -
cv::CAP_PROP_FRAME_WIDTH
:图像的宽 -
cv::CAP_PROP_FRAME_HEIGH
:图像的高
与之对应的,还有 .set()
函数,对视频进行某些设置。例如令视频跳转到某个位置:
double position = 50.0;
capture.set(cv::CAP_PROP_POS_FRAMES, position); // 跳转到第 50 帧
capture.set(cv::CAP_PROP_POS_MSEC, time_place); // 以毫秒为单位的时间
capture.set(cv::CAP_PROP_AVI_RATIO, ratio_place); // 按比例指定,0.0 表示开头, 1.0 表示结束
需要注意的是,.get()
和 .set()
都是通用函数,为了涵盖尽可能多的参数获取和设置场景,它们将参数的数据类型统一设定为 double 。因此,有时需要进行数据类型转换,例如要获得 int 类型的 frame 序号,就需要将 .get()
的返回结果转成 int 型。
当将视频读取为一帧一帧的图片时,后续的处理操作与普通的静态图片完全相同。
处理之后,一帧一帧的图片如何保存成视频文件呢?
在读视频的时候用的是 cv::VideoCapture
类,在写的时候用 cv::VideoWriter
类。
cv::VideoWriter writer;
writer.open(const String & filename, // 写入的文件名
int fourcc, // 视频编码,可以送入格式类似于 CV_FOURCC('X', 'V', 'I', 'D'),会自动转化为整型格式
double fps, // 帧率
cv::Size frameSize, // 画面大小
bool isColor = true // 是否为彩色
);
// 设置好了写入格式之后,就可以将处理之后的 OutFrame 写入文件
writer.write(OutFrame);
案例1:提取视频中的前景物体
要区分前景与背景,基本思想是将那些改变很少的区域作为背景。
如果我们事先有了静态的背景图,则只需要跟当前图像对比,像素值不同的那些部分就是前景。
但这里有些问题:
- 静态背景图不可知
- 即使给定了背景图,由于光照等变化,像素值也会发生变化
一个解决方案是用滑动平均的方法动态的构建背景图。
其中 为 时刻估算的背景图, 为 时刻的新图, 为权重因子, 表示背景图保持不变,始终为初始背景图; 表示将当前图像作为背景图。在实际应用中一般采用比较小的值,例如 ,维持背景的相对稳定性。
下边是提取前景物体的核心代码:
cv::VideoCapture capture("bike.avi"); // 生成视频读取对象,并初始化
if (!capture.isOpened()) // 确认成功生成了对象
return 1;
double rate = capture.get(cv::CAP_PROP_FPS);
int delay = 1000/rate; // 这两句计算恰当的帧延迟,保证观看效果
cv::Mat frame, gray, background, backImage, foreground, output;
while (capture.read(frame)){
cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY); // 转化成灰度图,容易对比
if (background.empty())
gray.convertTo(background, CV_32F); // 初始时将第一帧画面作为背景图
background.convertTo(backImage, CV_8U); // 为方便与新的 frame 对比,转化成统一的 CV_8U
cv::absdiff(backImage, gray, foreground); // 计算灰度差别
cv::threshold(foreground, output, 25, 255, cv::THRESH_BINARY_INV); // 前景的灰度设置为 0
cv::accumulateWeighted(gray, background, 0.01, output); // 用滑动平均的方式更新背景,前景部分不参与更新
cv::imshow("foreground", output);
cv::waitKey(delay);
}
本文使用的视频例子 ”bike.avi“ 来自 https://github.com/laganiere/OpenCV3Cookbook/blob/master/src/images/bike.avi
前景物体提取效果如下:
案例2:跟踪目标
核心程序如下:
cv::Mat frame; // 存储当前读取的 frame
cv::Mat output; // frame 的 copy, 画图用
cv::Mat gray, gray_prev; // 当前 frame 和前一 frame 的灰度图
std::vector points[2]; // 两个关键点坐标向量,分别存储前一时刻和当前时刻关键点的位置
std::vector initial; // 初始关键点位置,画图用
std::vector features; // 初始化关键点,以及增补关键点
std::vector status; // 是否成功跟踪了关键点
std::vector err; // 跟踪的误差量
while (capture.read(frame)){
cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY);
frame.copyTo(output);
// 初始时刻添加关键点,如果在整个跟踪过程中,由于关键点跟踪丢失导致数目太少,也会增补
// 这里用了 goodFeaturesToTrack 算法,这里返回的是关键点位置(Point2f),前边在特征点检测部分我们用过了它的相关算法来检测关键点
if (points[0].size() <= 10){
cv::goodFeaturesToTrack(gray, features, 500, 0.01, 10);
points[0].insert(points[0].end(), features.begin(), features.end());
initial.insert(initial.end(), features.begin(), features.end());
}
// 初始化前一个 frame
if (gray_prev.empty()){
gray.copyTo(gray_prev);
}
// 用 Lukas-Kanade 算法跟踪特征
cv::calcOpticalFlowPyrLK(gray_prev, gray, points[0], points[1], status, err);
// 只保留成功跟踪的、动态的特征
int k = 0;
for (int i=0; i2)){
initial[k] = initial[i];
points[1][k++] = points[1][i];
}
}
points[1].resize(k);
initial.resize(k);
// 画图,直观显示跟踪的过程
for (int i=0; i< points[1].size(); i++){
cv::line(output, initial[i], points[1][i], cv::Scalar(255, 255, 255));
cv::circle(output, points[1][i], 3, cv::Scalar(255, 255, 255), -1);
}
std::swap(points[1], points[0]);
cv::swap(gray_prev, gray);
cv::imshow("result", output);
cv::waitKey(delay);
}
这里在检测关键点时采用了 cv::goodFeatureToTrack
方法。前边在特征检测时已经使用了与之相关的检测特征点的函数 cv::GFTTDetector
。
这里用的跟踪算法是 Lukas-Kanade 算法,它的基本思想如下:
首先假设前后帧中特征点的强度/灰度值没有变化,因此有
其中 为特征点的位移,也就是待求量。
将上式右边的像素值是位置和时间的函数,本质上等价于 这种函数形式,因此在 处使用泰勒展开得:
由于强度值没有变化,所以最后关系式为:
这就是光流约束方程(optical flow constraint eqaution)。
这个方程中只有 是未知数。
我们进一步假定关键点邻域中的像素具有同样的位移量,这样就可以得到若干方程,可以计算出均方误差意义下的最优解。
实例:继续使用上边的 "bike.avi" 例子。物体跟踪效果如下:
案例3:绘制光流场
所谓光流就是图片中光亮模式的变化。
通过光流场可以粗略的判断运动场,即 3D 运动在 2D 平面上的投影。但也有例外,比如:
- 在白墙前边移动相机,光流场近似为 0, 但运动场不为 0
- 静止物体在不同光照下可能会产生非 0 的光流场,但其运动场为 0.
计算光流场主要基于两个假设:
- 光流约束方程,反映了同一幅图片中像素变化规律
- 光流向量的拉普拉斯算子,反映了相邻两帧图片中光流场变化的平滑性
OpenCV 提供了几种估算光流场的算法,这里采用 TVL-1
算法:
cv::Ptr tvl1 = cv::createOptFlow_DualTVL1();
cv::Mat oflow;
tvl1 -> calc(frame1, frame2, oflow);
计算得到的 oflow
是 cv::Mat
类型的数据,其中每个元素又是一个二维向量,反映了两帧在 方向上的变化。
为了直观显示光流场,可以编写画图函数如下:
void drawOpticalFlow(const cv::Mat& oflow, cv::Mat& flowImage, int stride, float scale){
if (flowImage.size() != oflow.size()){
flowImage.create(oflow.size(), CV_8UC3);
flowImage = cv::Vec3i(255, 255, 255);
}
for (int y=0; y(y,x);
cv::line(flowImage, cv::Point(x,y), cv::Point(static_cast(x+scale*vector.x+0.5), static_cast(y+scale*vector.y+0.5)), cv::Scalar(0,0,0));
cv::circle(flowImage, cv::Point(static_cast(x+scale*vector.x+0.5), static_cast(y+scale*vector.y+0.5)), 1, cv::Scalar(0,0, 0),-1);
}
}
}
为了清晰显示,每隔 8 个像素画一次光流线条,长度缩放因子为 2。
我们采用如下的两帧图片:
用上述程序计算出光流场如下: