OpenCV学习之路(八)——光流追踪

一、基于特征点的目标跟踪的一般方法

基于特征点的跟踪算法大致可以分为以下步骤:

  1)探测当前帧的特征点;

  2)通过当前帧和下一帧灰度比较,估计当前帧特征点在下一帧的位置;

  3)过滤位置不变的特征点,余下的点就是目标了。

很显然,基于特征点的目标跟踪算法和1),2)两个步骤有关。特征点可以是Harris角点,也可以是边缘点等等,而估计下一帧位置的方法也有不少,比如这里要讲的光流法,也可以是卡尔曼滤波法。
本文中,用改进的Harris角点提取特征点,用Lucas-Kanade光流法实现目标跟踪。

二、光流法

1.首先是假设条件:

   (1)亮度恒定,就是同一点随着时间的变化,其亮度不会发生改变。这是基本光流法的假定(所有光流法变种都必须满足),用于得到光流法基本方程;

   (2)小运动,这个也必须满足,就是时间的变化不会引起位置的剧烈变化,这样灰度才能对位置求偏导(换句话说,小运动情况下我们才能用前后帧之间单位位置变化引起的灰度变化去近似灰度对位置的偏导数),这也是光流法不可或缺的假定;

   (3)空间一致,一个场景上邻近的点投影到图像上也是邻近点,且邻近点速度一致。这是Lucas-Kanade光流法特有的假定,因为光流法基本方程约束只有一个,而要求x,y方向的速度,有两个未知变量。我们假定特征点邻域内做相似运动,就可以连立n多个方程求取x,y方向的速度(n为特征点邻域总点数,包括该特征点)。

  2.方程求解

  多个方程求两个未知变量,又是线性方程,很容易就想到用最小二乘法,事实上opencv也是这么做的。其中,最小误差平方和为最优化指标。

  3.好吧,前面说到了小运动这个假定,聪明的你肯定很不爽了,目标速度很快那这货不是二掉了。幸运的是多尺度能解决这个问题。首先,对每一帧建立一个高斯金字塔,最大尺度图片在最顶层,原始图片在底层。然后,从顶层开始估计下一帧所在位置,作为下一层的初始位置,沿着金字塔向下搜索,重复估计动作,直到到达金字塔的底层。聪明的你肯定发现了:这样搜索不仅可以解决大运动目标跟踪,也可以一定程度上解决孔径问题(相同大小的窗口能覆盖大尺度图片上尽量多的角点,而这些角点无法在原始图片上被覆盖)。

LK算法只需要每个感兴趣点周围小窗口的局部信息,但是较大的运动会将点移除这个小窗口,从而造成算法无法再找到这些点。金字塔的LK算法可以解决这个问题,即从金字塔的最高层(细节最少)开始向金字塔的最低层(丰富的细节)进行跟踪。跟踪图像金字塔允许小窗口部或较大的运动。

在开始跟踪前,首先要在初始帧中检测特征点,之后在下一帧中尝试跟踪这些点。你必须找到新的图像帧中这些点的位置,因此,你必须在特征点的先前位置附近进行搜索,以找到下一帧中它的新位置。这正是cv::calcOpticalFlowPyrLK函数所实现的工作。你输入两个连续的图像帧以及第一幅图像中检测到的特征点数组,该函数将返回一组新的特征点为位置。为了跟踪完整的序列,你需要在帧与帧之间重复这个过程,不可避免地你也会丢失其中一些点,于是被跟踪的特征点数目会减少。为了解决这个问题,我们可以不时地检测新的特征值。

三、OpenCV光流实现

 calcOpticalFlowPyrLK( InputArray prevImg, InputArray nextImg,
                                        InputArray prevPts, InputOutputArray nextPts,
                                        OutputArray status, OutputArray err,
                                        Size winSize = Size(21,21), int maxLevel = 3,
                                        TermCriteria criteria = TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 30, 0.01),
                                        int flags = 0, double minEigThreshold = 1e-4 );
prevImg:第一帧(跟踪图像的前一帧,一般是定位特征点) 
nextImg: 第二帧/当前帧 
prev_Pts:第一帧特征点集
next_Pts:计算输出的第二帧光流特征点集
status :状态标志位,如果对应特征的光流被发现,数组中的每一个元素都被设置为 1, 否则设置为 0。 
err:双精度数组,包含原始图像碎片与移动点之间的误差。

四、完整代码

#include<opencv2\opencv.hpp>
using namespace cv;
using namespace std;

void detectFeatures(Mat &gray);
void drawFeatures(Mat &frame);
void KLTtracker();
void drawTrackLine();
vector<Point2f>features;//shi-Tomasi角点检测特征数据
vector<Point2f>iniPoints;//初始化特征数据
vector<Point2f>fpts[2];//保存当前帧和前一帧的特征点位置
vector<uchar>status;//特征点跟踪成功标志位
vector<float>errors;//跟踪误差
Mat frame, gray, pregray;

int main(int arc, char** argv) {
	VideoCapture capture("003.avi");
	namedWindow("output", CV_WINDOW_AUTOSIZE);
	capture.read(frame);//因为摄像头打开会有延迟,所以提前打开一帧
	while (capture.read(frame)) {
		imshow("frame", frame);
		cvtColor(frame, gray, CV_BGR2GRAY);
		if (fpts[0].size() < 10) {
			detectFeatures(gray);
			fpts[0] = features;
			iniPoints = features;
		}
		if (pregray.empty()) {
			gray.copyTo(pregray);
		}
		KLTtracker();
		drawFeatures(frame);
		//光流跟踪后把当前帧当作前一帧,再与下一帧进行匹配跟踪
		gray.copyTo(pregray);
		imshow("output", frame);
		char c = waitKey(50);
		if (c == 27) {
			break;
		}
	}
	capture.release();
	waitKey(0);
	return 0;
}
void detectFeatures(Mat &gray) {
	goodFeaturesToTrack(gray, features, 5000, 0.01, 10, Mat(), 3, false);
}
void drawFeatures(Mat &frame) {
	for (int i = 0; i < fpts[0].size(); i++) {
		circle(frame, fpts[0][i], 2, Scalar(0, 0, 255), 2);
	}
}
void KLTtracker() {
	calcOpticalFlowPyrLK(pregray, gray, fpts[0], fpts[1], status, errors);
	int k = 0;
	for (int i = 0; i < fpts[1].size(); i++) {
		double dist = abs(fpts[0][i].x - fpts[1][i].x) + abs(fpts[0][i].y - fpts[1][i].y);
		if (dist > 2 && status[i]) {
			iniPoints[k] = iniPoints[i];
			fpts[1][k++] = fpts[1][i];
		}
	}
	//保存特征点并绘制跟踪轨迹
	iniPoints.resize(k);
	fpts[1].resize(k);
	drawTrackLine();
	//fpts[0] = fpts[1];
	swap(fpts[1], fpts[0]);
}
void drawTrackLine() {
	for (int i = 0; i < fpts[1].size(); i++) {
		line(frame, iniPoints[i], fpts[1][i], Scalar(0, 255, 0), 2);
		circle(frame, features[i], 2, Scalar(0, 0, 255), 2);
	}
}

效果图
OpenCV学习之路(八)——光流追踪