今天要整理的笔记内容是基于上一篇笔记《OpenCV4学习笔记(32)——角点检测》的一个拓展应用,那就是基于shi-tomas角点检测的光流检测算法。
首先我们需要知道,什么是光流? 这里引用百度百科上的一段定义:
这种图像亮度模式的表观运动(apparent motion)就是光流。光流表达了图像的变化,由于它包含了目标运动的信息,因此可被观察者用来确定目标的运动情况。 由光流的定义可以引申出光流场,它是指图像中所有像素点构成的一种二维(2D)瞬时速度场,其中的二维速度矢量是景物中可见点的三维速度矢量在成像表面的投影。
简单的来说,光流就是指由于目标物体或相机的运动,而引起的目标物体对应像素点在两个连续帧之间的位移,从而形成的矢量,这就是目标所产生的光流。
光流(Optical Flow)跟踪算法是运动图像分析的一种重要方法,在对运动物体的检测、跟踪等等方面都有所应用。光流跟踪算法分为稠密光流跟踪算法与稀疏光流跟踪算法两大类,其中,稀疏光流跟踪算法是对每一帧图像的稀疏特征点集进行光流跟踪,而稠密光流跟踪算法是对每一帧图像的所有点进行光流跟踪。
今天先整理稀疏光流跟踪算法中比较常见的一种——KLT稀疏光流算法。
KLT稀疏光流算法的基本思想是:首先通过shi-tomas角点检测获取前一帧图像的稀疏特征点集,再利用前一帧图像及前一帧图像的稀疏特征点集和后一帧图像,来获取在后一帧图像中具有同样特征的点集,进一步判断、并获得两帧图像的稀疏特征点集之间存在的光流。
由于KLT稀疏光流算法只需要每个特征角点的邻域空间窗口内的局部信息,所以当目标在相邻两帧图像之间运动过大时会导致该目标的特征点移出该领域空间窗口,导致算法无法寻找到该特征点。
因此KLT具有三个前提条件:
(1)目标物体的像素值在连续帧之间保持恒定不变(亮度不变性);
(2)目标物体在每相邻两帧图像之间只进行短距离移动;
(3)对于某像素点而言,其周围的邻近像素点也具有同样的移动距离,也就是具有空间一致性。
如果我们的目标物体的运动情况符合上述三个前提条件,那么就可以通过KLT稀疏光流算法来对该目标进行光流跟踪了。
OpenCV中提供了KLT光流检测算法的APIcalcOpticalFlowPyrLK()
。这个API是默认使用图像金字塔的KLT光流检测算法,将每帧图像建立图像金字塔,并自顶向底依次将前后两帧图像的金字塔中的同一层图像进行光流检测,直至检测完所有每一层图像,得到最后的输出。由于自顶向底的金字塔图像其分辨率是从低到高,当目标出现较大的运动时,低分辨率图像中的特征点也不会移动出邻近窗口,仍能被算法检测到。所以使用图像金字塔的KLT算法能够允许目标比较大的运动。其参数含义如下:
第一个参数prevImg:输入的上一帧图像;
第二个参数nextImg:输入的下一帧图像;
第三个参数prevPts:输入的上一帧图像的稀疏特征点集;
第四个参数nextPts:输出的下一帧图像对应的稀疏特征点集;
第五个参数status:输出点的状态向量,可用于判断某特征点在两帧图像之间是否存在光流;如果某点在两帧图像之间存在光流,则status向量中对应该点的元素设置为1,否则设置为0;
第六个参数err:输出错误的向量,向量的每个元素都设置为相应特征点的错误;
第七个参数winsize:搜索窗口大小;
第八个参数maxLevel = 3, 金字塔层数,0表示只检测当前图像,不使用图像金字塔的KLT算法;
第九个参数criteria = TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 30, 0.01), 表示窗口搜索停止条件;
第十个参数int flags = 0, 操作标志;
第十一个参数minEigThreshold = 1e-4 ,最小特征值响应,低于最小值不做处理。
下面看下具体代码演示:
VideoCapture capture;
//capture.open("D:\\OpenCV\\opencv\\sources\\samples\\data\\vtest.avi");
capture.open("D:\\opencv_c++\\opencv_tutorial\\data\\images\\video.avi");
if (!capture.isOpened())
{
return 0;
}
//读取第一帧图像,进行初始化;
Mat pre_image;
capture.read(pre_image);
cvtColor(pre_image, pre_image, COLOR_BGR2GRAY);
//光流检测必须为浮点型坐标点
vector<Point2f> prevPts; //定义上一帧图像的稀疏特征点集
vector<Point2f> initpoint; //定义上一帧图像中保留的稀疏特征点集,用于绘制轨迹
vector<Point2f>features; //用于存放从图像中获得的特征角点
goodFeaturesToTrack(pre_image, features, 100, 0.3, 10, Mat(), 3, false); //获取第一帧图像的稀疏特征点集
//insert(插入位置,插入对象的首地址,插入对象的尾地址)
initpoint.insert(initpoint.end(), features.begin(), features.end()); //初始化当前帧的特征点集
prevPts.insert(prevPts.end(), features.begin(), features.end()); //初始化第一帧的特征角点
Mat frame;
while (capture.read(frame))
{
Mat next_image;
flip(frame, frame, 1);
cvtColor(frame, next_image, COLOR_BGR2GRAY);
vector<Point2f> nextPts; //下一帧图像检测到的对应稀疏特征点集
vector<uchar> status; //输出点的状态向量;如果某点在两帧图像之间存在光流,则该向量中对应该点的元素设置为1,否则设置为0。
vector<float>err; //输出错误的向量; 向量的每个元素都设置为相应特征点的错误
calcOpticalFlowPyrLK(pre_image, next_image, prevPts, nextPts, status, err, Size(31,31));
RNG rng;
int k = 0;
for (int i = 0; i < nextPts.size(); i++) //遍历下一帧图像的稀疏特征点集
{
//计算两个对应特征点的(dx+dy)
double dist = abs(double(prevPts[i].x) - double(nextPts[i].x)) + abs(double(prevPts[i].y) - double(nextPts[i].y));
if (status[i] && dist > 2) //如果该点在两帧图像之间存在光流,且两帧图像中对应点的距离大于2,即非静止点
{
//将存在光流的非静止特征点保留起来
prevPts[k] = prevPts[i];
nextPts[k] = nextPts[i];
initpoint[k] = initpoint[i];
k++;
//绘制保留的特征点
int b = rng.uniform(0, 256);
int g = rng.uniform(0, 256);
int r = rng.uniform(0, 256);
circle(frame, nextPts[i], 3, Scalar(b, g, r), -1, 8, 0);
}
}
//将稀疏特征点集更新为现有的容量,也就是保存下来的特征点数
prevPts.resize(k);
nextPts.resize(k);
initpoint.resize(k);
//在每一帧图像中绘制当前特征点走过的整个路径
for (int j = 0; j < initpoint.size(); j++)
{
int b = rng.uniform(0, 256);
int g = rng.uniform(0, 256);
int r = rng.uniform(0, 256);
line(frame, initpoint[j], nextPts[j], Scalar(b, g, r), 1, 8, 0);
}
imshow("frame", frame);
//swap()交换两个变量的数据
swap(nextPts, prevPts); //将下一帧图像的稀疏特征点集,变为上一帧
swap(pre_image, next_image); //将下一帧图像变为上一帧图像
//当特征点的数量被筛选得低于阈值时,重新从下一帧图像中寻找特征角点;注意此时的上下两帧图像已经互换
if (initpoint.size() < 40)
{
goodFeaturesToTrack(pre_image, features, 100, 0.01, 10, Mat(), 3, false);
initpoint.insert(initpoint.end(), features.begin(), features.end());
prevPts.insert(prevPts.end(), features.begin(), features.end());
}
char ch = waitKey(20);
if (ch == 27)
{
break;
}
}
capture.release();
在上述代码中,当我们检测到前后两帧图像都存在相同特征的特征点时,需要判断它们之间是否存在相对移动,所以我们计算两个对应特征点的(dx+dy),如果这两个点的(dx+dy)大于2,且在status向量中对应的状态值为1,证明这两个对应特征点之间是存在光流的,就可以将这两个点保存起来,并进行光流的绘制。
需要注意的是,每次对一帧图像操作完后,需要把当前帧图像置为上一帧图像,也就是需要对上一帧图像和上一帧图像的特征点集进行更新,才能进入下一帧图像的操作,否则会出现各种奇奇怪怪的问题。例如把上述代码中的这一步骤去掉,就会出现光流一直存在、且只在运动物体经过其周围时才短暂跟踪的现象,而这种现象并不是我们想要的!!!
还要注意的一点是,由于每次对图像进行特征点的筛选时,会导致特征点数目逐渐减少,如果一直减少到0就会导致没有特征点可用来进行光流跟踪的错误,表现为视频开始对运动物体的光流跟踪是正常的,但进行到某一时刻就突然失效甚至崩溃了。所以需要对特征点数目设置一个阈值,当特征点数目低于阈值时就对上一帧图像进行shi-tomas角点检测,并更新上一帧图像对应的特征点集。
下面来看一下演示效果截图:
上图中,走动的人会有光流轨迹被绘制出来,而站立不动的人则没有。
上面两张图是通过调用摄像头的光流检测,如果保持画面静止的话就不会有那些光流轨迹,如果像上图一样有运动物体(我的手)就能捕捉到它运动的光流,从而实现对运动物体的光流跟踪。【请忽视那两只不小心入镜的娃娃】
好的,今天关于KLT稀疏光流跟踪算法的笔记整理到此结束,有空再整理一下稠密光流跟踪算法的相关内容。谢谢阅读~
PS:本人的注释比较杂,既有自己的心得体会也有网上查阅资料时摘抄下的知识内容,所以如有雷同,纯属我向前辈学习的致敬,如果有前辈觉得我的笔记内容侵犯了您的知识产权,请和我联系,我会将涉及到的博文内容删除,谢谢!