1. 基于光流算法的对象跟踪
通俗说,对于一个图片序列,把每张图像每个像素在连续帧之间的运动速度和方向(某像素点在连续两帧上的位移矢量)找出来就是光流场。在视频移动对象跟踪中,稀疏光流跟踪是一种经典的对象跟踪算法,可以绘制运动对象的跟踪轨迹与运行方向,是一种简单、实时高效的跟踪算法。
1. 1 算法介绍
例如在下面相邻的两帧 I 和 J 中,存在像素点的运动,也即是上一帧的像素点在下一帧中,其位置会有轻微的变动,这个变动就是位移向量,也就是像素点的光流,如下图,d 为像素点的光流。
要存在光流,即该光流能被计算出来,也就是KLT算法工作的三个假设前提条件:
- 相邻帧之间的亮度恒定。
- 短距离移动。
- 空间一致性,即同一子图像的像素点具有相同的运动。
为什么要有这几个假设?如果判断一个视频的相邻两帧 I、J 在某局部窗口 w 上是一样的,则在窗口w 内有:I(x, y, t) = J(x', y', t+τ),亮度恒定的假设即为了保证其等号成立不受亮度的影响,假设2是为了保证KLT能够找到点,假设3则为以下原因假设(即对于同一个窗口中,所有点的偏移量都相等):
在窗口 w 上,所有(x, y)都往一个方向移动了(dx, dy),从而得到(x', y'),即 t 时刻的(x, y)点在 t+τ 时刻为(x+dx, y+dy),所以寻求匹配的问题可化为对以下的式子寻求最小值,或叫做最小化以下式子:
找角点的的方法一般用** Harris 或 Shi-Tomasi **角点检测。
1. 2 图像的金字塔表示
特征跟踪器的两个关键的特性是准确性和鲁棒性,准确性是指跟踪结果的局部子像素的精度。为了不丢失图像中的细节,小窗口是更好的选择。尤其当图像中两个封闭区域有不同的运动适量时,这种选择就更有必要了。跟踪器的鲁棒性指跟踪性能对光照变化,运动大小等因素的敏感程度。比如,为了处理大运动的情况下,显然选择大的窗口是更合适的,这就要求在选择窗口大小的时候要权衡考虑局部准确性和鲁棒性,为了解决这个问题,提出了一种基于金字塔光流的改进跟踪算法。下图是金字塔光流法的原理示意图。先在图像金字塔的最顶层计算光流,用上层估计到的运动结果作为下一层金字塔的起始点,重复这样的估计直到金字塔的最底层。这样就将不满足运动假设(运动是小而连贯的)的情况转化为满足运动假设的情况来处理,实现对快而且长的运动进行跟踪。
光流跟踪的过程:(1)对一个连续的视频图像帧序列进行处理。(2)针对每一帧图像检测前景目标。(3)如果某一帧出现了前景目标,找到其具有代表性的特征点(一般利用角点来做特征点)。(4)对于之后的两个相邻视频帧,寻找上一帧中出现的特征点在当前帧中的最佳位置,从而得到前景目标在当前帧中的位置信息。(5)如此迭代进行,便可实现目标的跟踪。
例子代码:
#include<opencv2/opencv.hpp>
#include<iostream>
using namespace cv;
using namespace std;
Mat frame, grayImg; // 当前帧
Mat prev_frame,prev_grayImg; //前一帧
vector<Point2f>features; //Shi-Tomasi 焦点检测-特征数据
vector<Point2f>fpts[2]; //保证当前帧和前一帧的特征点位置
vector<Point2f>iniPoints; //初始化特征数据
vector<uchar>status; //特征点跟踪成功标志位
vector<float>errors; //跟踪时候区域误差和
//Shi-Tomasi 焦点检测
void detectFeatures(Mat &inFrame, Mat &inGray){
double maxCorners = 500;
double qualitylevel = 0.01;
double minDistance = 10;
double blockSize = 3;
double k = 0.04;
// 算法很快,满足实时性要求
goodFeaturesToTrack(inGray, features, maxCorners,
qualitylevel, minDistance, Mat(), blockSize, false, k);
cout << "detect features:" << features.size() << endl;
}
//绘制特征点
void drawFeature(Mat &inFrame){
for (int i = 0; i < fpts[0].size(); i++)
{
circle(inFrame, fpts[0][i], 2, Scalar(0, 0, 255), 2);
}
}
//在跟踪到的且移动了的特征点(光流)的开始跟踪的位置到当前跟踪到的位置之间绘制线段
void drawTrackLines(){
for (int i = 0; i < fpts[1].size(); i++)
{
line(frame, iniPoints[i], fpts[1][i], Scalar(0, 255, 0), 2, 8, 0); //绘制线段
circle(frame, fpts[1][i], 2, Scalar(0, 0, 255), 2, 8, 0);
}
}
// 稀疏光流法跟踪 KTL
void KLTrackFeature(){
calcOpticalFlowPyrLK(prev_grayImg, grayImg, fpts[0], fpts[1], status, errors);
int k = 0; //保证跟踪到的特征点数,最后将特征点的尺寸重新设置为 k
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]) //跟踪到的特征点,且距离移动了 2 以上的
{
//将跟踪到的移动了的特征点在 vector 中连续起来,剔除掉损失的和静止不动的特征点(这些跟踪点在前面帧中)
iniPoints[k] = iniPoints[i];
fpts[1][k++] = fpts[1][i]; //同上,只是这些跟踪点在当前帧中
}
}
//保存特征点并绘制跟踪轨迹
iniPoints.resize(k);
fpts[1].resize(k);
drawTrackLines();
swap(fpts[1], fpts[0]); //交换,将此帧跟踪到特征点作为下一帧的待跟踪点
}
void test(){
VideoCapture capture("Video.wmv");
//VideoCapture capture(0); //打开摄像头
if (!capture.isOpened())
{
cout << "could not load video file...\n" << endl;
}
namedWindow("Result", CV_WINDOW_AUTOSIZE);
while (capture.read(frame))
{
cvtColor(frame, grayImg, COLOR_BGR2GRAY);
//跟踪40个特征点,如果跟踪的时候损失了一些特征点,重新检测,追加
if (fpts[0].size() < 40)
{
detectFeatures(frame, grayImg);
fpts[0].insert(fpts[0].end(), features.begin(), features.end()); //追加带跟踪的特征点
iniPoints.insert(iniPoints.end(), features.begin(), features.end());
}
else
{
cout << "tracjing...\n" << endl;
}
if (prev_grayImg.empty())
{
grayImg.copyTo(prev_grayImg);
}
KLTrackFeature(); //稀疏光流跟踪 KLT
drawFeature(frame); //绘制特征点
//更新前一帧的数据
grayImg.copyTo(prev_grayImg);
frame.copyTo(prev_frame);
imshow("Result", frame);
char c = waitKey(50);
if (c == 27)
{
break;
}
}
capture.release();
}
int main(){
test();
waitKey(0);
return 0;
}
效果: