博客原文在这里,此为摘抄笔记
一、算法介绍
什么光流?
光流是由于目标物体或相机的运动,而引起的目标物体对应像素点在两个连续帧之间的位移,从而形成的矢量,这就是目标所产生的光流
光流算法有哪些?
分为稠密光流跟踪算法与稀疏光流跟踪算法两大类。
稀疏光流跟踪算法是对每一帧图像的稀疏特征点集进行光流跟踪,而稠密光流跟踪算法是对每一帧图像的所有点进行光流跟踪
这里学习稀疏光流跟踪算法常用的一种--KLT稀疏光流算法
基本思想:
首先通过角点检测获取前一帧图像的稀疏特征点集,再利用前一帧图像及前一帧图像的稀疏特征点集和后一帧图像,来获取在后一帧图像中具有同样特征的点集,进一步判断、并获得两帧图像的稀疏点集之间存在的光流
缺点:
由于KLT稀疏光流算法只需要每个特征角点的邻域空间窗口内的局部信息,所以当目标在相邻两帧图像之间运动过大时会导致该目标的特征点移出该领域空间窗口,导致算法无法寻找到该特征点。
算法应用前提条件:
(1)目标物体的像素值在连续帧之间保持恒定不变(亮度不变性);
(2)目标物体在每相邻两帧图像之间只进行短距离移动;
(3)对于某像素点而言,其周围的邻近像素点也具有同样的移动距离,也就是具有空间一致性。
二、OpenCV实现
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 ,最小特征值响应,低于最小值不做处理
三、代码
int main()
{
VideoCapture capture;
capture.open(0);
if(!capture.isOpened()) return 1;
// 读取第一副图像,进行初始化
Mat pre_image;
capture.read(pre_image);
cvtColor(pre_image, pre_image, COLOR_BGR2GRAY);
//检测关键点
/* 光流检测必须位浮点型坐标点*/
vector<Point2f> prePts; // 定义上一帧图像的稀疏特征点集
vector<Point2f> initPoint; // 定义上一帧图像中保留的稀疏特征点集,用于绘制轨迹
vector<Point2f> features; // 用于存放从图像中获得的特征角点
goodFeaturesToTrack(pre_image,features, 100, 0.3, 10, Mat(), 3, false);
// inset(插入位置,插入对象的首地址, 插入对象的尾地址)
initPoint.insert(initPoint.end(), features.begin(), features.end()); // 初始化当前帧的特征点集
prePts.insert(prePts.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,prePts,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(prePts[i].x) - double(nextPts[i].x) ) +
abs( double(prePts[i].y) - double(nextPts[i].y));
if (status[i] && dist > 2)
{
// 将存在光流的非静止特征点保留起来
prePts[k] = prePts[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(r, b, g), -1, 8, 0);
}
}
// 将稀疏特征点集更新为现有的容量,也就是保存下来的特征点数
prePts.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); // initpoint 连接最初与下一帧
// }
// 为移动目标添加矩形框 https://blog.csdn.net/qq_18343569/article/details/48000035
if (nextPts.size() > 3)
{
Rect tmpRect = boundingRect(Mat(nextPts));
rectangle(frame, tmpRect.tl(), tmpRect.br(), 1, 8);
}
imshow("frame", frame);
// swap() 交换两个变量的数据
swap(nextPts, prePts); // 将下一帧图像的稀疏点集,变为上一帧
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());
prePts.insert(prePts.end(), features.begin(), features.end());
}
char ch = waitKey(20);
if (ch == 'q')
{
break;
}
}
capture.release();
}