当相机进行拍摄时,拍摄到的亮度图案会投射到图像传感器上,从而形成图像。在视频序列中,我们通常需要捕捉运动模式,即不同场景元素的 3D
运动在图像平面上的投影,这种投影 3D
运动矢量的图像称为运动场 (motion field
)。但是,我们无法从相机传感器直接测量场景点的 3D
运动,我们所观察到的只是一种逐帧运动的亮度模式,亮度图案的这种运动称为光流 (optical flow
)。运动场并不完全等同于光流,一个简单的例子是拍摄无明显变化的物体,例如,如果摄像机在白墙前移动,则不会产生光流;另一个经典的例子是旋转杆产生的运动错觉:
在上图所示情况下,当垂直圆柱体围绕其主轴旋转时,运动场为水平方向的运动矢量。然而,在视觉上这种运动看起来是红色和蓝色条带向上移动,这也是光流所展现的内容。尽管存在这些差异,但光流通常可以认为是运动场的有效近似。本节将学习如何估计图像序列的光流。
光流估计意味着量化图像序列中亮度模式的运动。因此,考虑在给定时刻的一帧视频,如果查看当前帧上 ( x , y ) (x, y) (x,y) 的一个特定像素,我们想知道该点在后续帧中移动到哪里。该点的坐标随时间移动可以表示为 ( x ( t ) , y ( t ) ) (x(t), y(t)) (x(t),y(t)),我们的目标是估计该点的速度 ( d x d t , d y d t ) (\frac {dx}{dt}, \frac {dy}{dt}) (dtdx,dtdy)。可以通过查看序列的相应帧,即 I ( x ( t ) , y ( t ) , t ) I(x(t), y(t), t) I(x(t),y(t),t) 来获得该特定点在给定时间点 t t t 的亮度。根据图像亮度恒定假设,我们可以假设该点的亮度不随时间变化:
d I ( x ( t ) , y ( t ) , t ) d t = 0 \frac {dI(x(t), y(t), t)} {dt}=0 dtdI(x(t),y(t),t)=0
根据链式法则,可以得到以下等式:
d I d x d x d t + d I d y d y d t + d I d t = 0 \frac {dI} {dx}\frac {dx} {dt}+\frac {dI} {dy}\frac {dy} {dt}+\frac {dI} {dt}=0 dxdIdtdx+dydIdtdy+dtdI=0
该方程称为亮度恒定方程,它将光流分量(即 x x x 和 y y y 相对于时间的导数)与图像导数相关联。这与我们在特征点追踪一节中推导出的方程完全相同,仅仅使用不同的形式来表示。
然而,这个方程(由两个未知数组成)不足以计算像素位置的光流。因此,我们需要添加一个额外的约束。一个常见的做法是假设光流是平滑的,这意味着相邻的光流向量应该相似,此约束基于光流的拉普拉斯算子:
∂ 2 ∂ x 2 d x d t + ∂ 2 ∂ y 2 d y d t \frac {\partial^2}{\partial x^2}\frac {dx} {dt}+\frac {\partial^2}{\partial y^2}\frac {dy} {dt} ∂x2∂2dtdx+∂y2∂2dtdy
因此,目标是找到最小化与亮度恒定方程和流向量拉普拉斯算子的偏差的光流场。
我们可以使用 cv::DualTVL1OpticalFlow
类解决密集光流估计问题,该类构建为通用 cv::Algorithm
基类的子类。
(1) 首先创建 cv::DualTVL1OpticalFlow
类的实例并获取它的指针:
// 创建光流算法
cv::Ptr<cv::optflow::DualTVL1OpticalFlow> tvl1 = cv::optflow::createOptFlow_DualTVL1();
(2) 使用创建的对象调用计算两帧之间光流场的方法:
cv::Mat oflow;
tvl1->calc(frame1, frame2, oflow);
计算结果为 2D
向量 (cv::Point
) 的图像,表示两帧之间每个像素的位移。为了显示结果,我们必须显示这些向量,因此,我们需要创建一个为光流场生成图像映射的函数。
(3) 为了控制向量的可见性,我们使用两个参数。第一个是定义的步幅值,以便仅显示特定数量像素上的向量,步幅值为向量的显示腾出了空间。第二个参数是扩展矢量长度以使其具有更明显的比例因子。绘制的每个光流向量都是一条简单的线,以圆圈结束表示箭头。因此,映射函数如下:
// 在图像上绘制光流矢量
void drawOpticalFlow(const cv::Mat &oflow, // 光流
cv::Mat &flowImage, // 结果图像
int stride, // 矢量步长
float scale, // 向量的乘数
const cv::Scalar &color) { // 颜色
// 为图像分配内存
if (flowImage.size()!=oflow.size()) {
flowImage.create(oflow.size(), CV_8UC3);
flowImage = cv::Vec3i(255, 255, 255);
}
for (int y=0; y<oflow.rows; y+=stride) {
for (int x=0; x<oflow.cols; x+=stride) {
// 获取矢量
cv::Point2f vector = oflow.at<cv::Point2f>(y, x);
// 绘制直线
cv::line(flowImage, cv::Point(x, y),
cv::Point(static_cast<int>(x+scale*vector.x+0.5),
static_cast<int>(y+scale*vector.y+0.5)),
color);
cv::circle(flowImage, cv::Point(static_cast<int>(x+scale*vector.x+0.5),
static_cast<int>(y+scale*vector.y+0.5)),
1, color, -1);
}
}
}
(4) 我们使用以下两帧:
(5) 使用以上帧,通过调用绘图函数来可视化估计的光流场:
// 绘制光流图像
cv::Mat flowImage;
drawOpticalFlow(oflow, flowImage, 8, 2, cv::Scalar(0, 0, 0));
在本节中,我们解释了可以通过最小化结合亮度恒定性约束和平滑度函数的函数来估计光流场。该方法称为 Dual TV L1
方法。它有两个主要组成;第一个是使用平滑约束,旨在最小化光流梯度的绝对值,这种选择减少了平滑项的影响,特别是在不连续区域,例如,移动物体的光流矢量与背景中的光流矢量完全不同;第二个是使用一阶泰勒近似,即亮度恒定约束,这种线性化有助于光流场的迭代估计,但线性近似仅对小位移有效。
在本节中,我们使用了带有默认参数的 Dual TV L1
方法,使用 setter
和 getter
方法可以修改可能对解决方案的质量和计算速度产生影响的参数。例如,可以修改金字塔估计中使用的尺度数或指定严格停止标准;另一个重要参数是与亮度恒定性约束相对于平滑性约束相关联的权重,例如,如果我们需要将亮度恒定性约束的重要性降低两倍,那么可以获得更平滑的光流场:
// 获得更平滑的光流
tvl1->setLambda(0.05);
头文件 (videoprocessor.h
) 完整代码参考视频序列处理一节,主函数文件 (flow.cpp
) 完整代码如下所示:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "videoprocessor.h"
// 在图像上绘制光流矢量
void drawOpticalFlow(const cv::Mat &oflow, // 光流
cv::Mat &flowImage, // 结果图像
int stride, // 矢量步长
float scale, // 向量的乘数
const cv::Scalar &color) { // 颜色
// 为图像分配内存
if (flowImage.size()!=oflow.size()) {
flowImage.create(oflow.size(), CV_8UC3);
flowImage = cv::Vec3i(255, 255, 255);
}
for (int y=0; y<oflow.rows; y+=stride) {
for (int x=0; x<oflow.cols; x+=stride) {
// 获取矢量
cv::Point2f vector = oflow.at<cv::Point2f>(y, x);
// 绘制直线
cv::line(flowImage, cv::Point(x, y),
cv::Point(static_cast<int>(x+scale*vector.x+0.5),
static_cast<int>(y+scale*vector.y+0.5)),
color);
cv::circle(flowImage, cv::Point(static_cast<int>(x+scale*vector.x+0.5),
static_cast<int>(y+scale*vector.y+0.5)),
1, color, -1);
}
}
}
int main() {
cv::Mat frame1 = cv::imread("3.png", 0);
cv::Mat frame2 = cv::imread("4.png", 0);
// 组合显示
cv::Mat combined(frame1.rows, frame1.cols + frame2.cols, CV_8U);
frame1.copyTo(combined.colRange(0, frame1.cols));
frame2.copyTo(combined.colRange(frame1.cols, frame1.cols+frame2.cols));
cv::imshow("Frames", combined);
// 创建光流算法
cv::Ptr<cv::optflow::DualTVL1OpticalFlow> tvl1 = cv::optflow::createOptFlow_DualTVL1();
std::cout << "regularization coeeficient: " << tvl1->getLambda() << std::endl;
std::cout << "Number of scales: " << tvl1->getScalesNumber() << std::endl;
std::cout << "Scale step: " << tvl1->getScaleStep() << std::endl;
std::cout << "Number of warpings: " << tvl1->getWarpingsNumber() << std::endl;
std::cout << "Stopping criteria: " << tvl1->getEpsilon() << " and " << tvl1->getOuterIterations() << std::endl;
cv::Mat oflow;
tvl1->calc(frame1, frame2, oflow);
// 绘制光流图像
cv::Mat flowImage;
drawOpticalFlow(oflow, flowImage, 8, 2, cv::Scalar(0, 0, 0));
cv::imshow("Optical Flow", flowImage);
// 获得更平滑的光流
tvl1->setLambda(0.05);
tvl1->calc(frame1, frame2, oflow);
// 绘制光流图像
cv::Mat flowImage2;
drawOpticalFlow(oflow, flowImage2, 8, 2, cv::Scalar(0, 0, 0));
cv::imshow("Smoother Optical Flow", flowImage2);
cv::waitKey();
}
光流估计 (Optical Flow estimation
) 在视频理解、动作识别、目标跟踪、全景拼接等领域具有重要应用,在各类视频分析任务中它反映了视频内部的运动信息,是一种重要视觉线索。本节中,介绍了光流估计的基本原理,并使用 cv::DualTVL1OpticalFlow
类解决密集光流估计问题。
OpenCV实战(1)——OpenCV与图像处理基础
OpenCV实战(2)——OpenCV核心数据结构
OpenCV实战(3)——图像感兴趣区域
OpenCV实战(4)——像素操作
OpenCV实战(5)——图像运算详解
OpenCV实战(6)——OpenCV策略设计模式
OpenCV实战(7)——OpenCV色彩空间转换
OpenCV实战(8)——直方图详解
OpenCV实战(9)——基于反向投影直方图检测图像内容
OpenCV实战(10)——积分图像详解
OpenCV实战(11)——形态学变换详解
OpenCV实战(12)——图像滤波详解
OpenCV实战(13)——高通滤波器及其应用
OpenCV实战(14)——图像线条提取
OpenCV实战(15)——轮廓检测详解
OpenCV实战(16)——角点检测详解
OpenCV实战(17)——FAST特征点检测
OpenCV实战(18)——特征匹配
OpenCV实战(19)——特征描述符
OpenCV实战(20)——图像投影关系
OpenCV实战(21)——基于随机样本一致匹配图像
OpenCV实战(22)——单应性及其应用
OpenCV实战(23)——相机标定
OpenCV实战(24)——相机姿态估计
OpenCV实战(25)——3D场景重建
OpenCV实战(26)——视频序列处理
OpenCV实战(27)——追踪视频中的特征点