金字塔LK光流法的三个假设
- 亮度恒定,即图像场景中目标的像素在帧间运动时外观上保持不变;
- 时间连续或者运动是”小运动“,即图像的运动随时间的变化比较缓慢;
- 空间一致,即一个场景中同一表面上邻近的点具有相似的运动。
光流法的原理
它是空间运动物体在观察成像平面上的像素运动的瞬时速度,是利用图像序列中像素在时间域上的变化以及相邻帧之间的相关性来找到上一帧跟当前帧之间存在的对应关系,从而计算出相邻帧之间物体的运动信息的一种方法。
当人的眼睛观察运动物体时,物体的景象在人眼的视网膜上形成一系列连续变化的图像,这一系列连续变化的信息不断“流过”视网膜(即图像平面),好像一种光的“流”,故称之为光流(optical flow)。光流表达了图像的变化,由于它包含了目标运动的信息,因此可被观察者用来确定目标的运动情况。从图片序列中近似得到不能直接得到的运动场。运动场,其实就是物体在三维真实世界中的运动;光流场,是运动场在二维图像平面上(人的眼睛或者摄像头)的投影。
每张图像中每个像素的运动速度和运动方向找出来就是光流场。第t帧的时候A点的位置是(x1, y1),那么我们在第t+1帧的时候再找到A点,假如它的位置是(x2,y2),那么我们就可以确定A点的运动了:(ux, vy) = (x2, y2) - (x1,y1)。
流程图
如何获取起始的points[0]
光流法预测当前帧的点
源代码
/************************************************************************ * @ Creator:OYXL * @ Project Creation time:2018/5/31 * @ Function:LK光流法跟踪点 * @ Attention:NO ************************************************************************/ #include "opencv2/video/tracking.hpp" #include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include <iostream> #include <ctype.h> using namespace cv; using namespace std; static void help() { // print a welcome message, and the OpenCV version cout << "\nThis is a demo of Lukas-Kanade optical flow lkdemo(),\n" "Using OpenCV version " << CV_VERSION << endl; cout << "\nIt uses camera by default, but you can provide a path to video as an argument.\n"; cout << "\nHot keys: \n" "\tESC - quit the program\n" "\tr - auto-initialize tracking\n" "\tc - delete all the points\n" "\tn - switch the \"night\" mode on/off\n" "To add/remove a feature point click it\n" << endl; } Point2f point; bool addRemovePt = false; static void onMouse( int event, int x, int y, int /*flags*/, void* /*param*/ ) { if( event == CV_EVENT_LBUTTONDOWN ) { point = Point2f((float)x, (float)y); addRemovePt = true; } } int main() { help(); VideoCapture cap(0); TermCriteria termcrit(CV_TERMCRIT_ITER|CV_TERMCRIT_EPS, 20, 0.03); Size subPixWinSize(10,10), winSize(31,31); const int MAX_COUNT = 500; bool needToInit = false; bool nightMode = false; if( !cap.isOpened() ) { cout << "Could not initialize capturing...\n"; return 0; } namedWindow( "LK Demo", 1 ); setMouseCallback( "LK Demo", onMouse, 0 ); Mat gray, prevGray, image, frame; vector<Point2f> points[2]; while (true) { cap >> frame; if( frame.empty() ) break; frame.copyTo(image); cvtColor(image, gray, COLOR_BGR2GRAY); if( nightMode ) image = Scalar::all(0); //<按下"r"按键,自动初始化 if(needToInit) { // automatic initialization goodFeaturesToTrack(gray, points[1], MAX_COUNT, 0.01, 10, Mat(), 3, 0, 0.04); cornerSubPix(gray, points[1], subPixWinSize, Size(-1,-1), termcrit); addRemovePt = false; } else if(!points[0].empty())//<前一帧的点不能为空 { vector<uchar> status; vector<float> err; if(prevGray.empty()) { gray.copyTo(prevGray); } calcOpticalFlowPyrLK(prevGray, gray, points[0], points[1], status, err, winSize,3, termcrit, 0, 0.001); int i, k; for( i = k = 0; i < points[1].size(); i++ ) { if( addRemovePt )//<鼠标添加点 { if( norm(point - points[1][i]) <= 5)//<point到points[1][i]的距离<5之间不能描点 { addRemovePt = false; continue; } } //<如果相应特征的流发现则status置为1,否则,为0。 if( !status[i] ) continue; points[1][k++] = points[1][i]; circle( image, points[1][i], 3, Scalar(0,255,0), -1, 8); } points[1].resize(k); } //<添加点 if( addRemovePt && points[1].size() < (int)MAX_COUNT ) { vector<Point2f> tmp;//<初始化 tmp.push_back(point); cornerSubPix( gray, tmp, winSize, cvSize(-1,-1), termcrit); points[1].push_back(tmp[0]); addRemovePt = false; } needToInit = false; imshow("LK Demo", image); char c = (char)waitKey(10); if( c == 27 ) break; switch( c ) { case 'r': needToInit = true; break; case 'c': points[0].clear(); points[1].clear(); break; case 'n': nightMode = !nightMode; break; } std::swap(points[1], points[0]); cv::swap(prevGray, gray); } return 0; }
分析
按下“r”键,自动检测角点,将检测到角点存入points[1]中
// automatic initialization goodFeaturesToTrack(gray, points[1], MAX_COUNT, 0.01, 10, Mat(), 3, 0, 0.04); cornerSubPix(gray, points[1], subPixWinSize, Size(-1,-1), termcrit);
鼠标描点,存入points[1]中
static void onMouse( int event, int x, int y, int /*flags*/, void* /*param*/ ) { if( event == CV_EVENT_LBUTTONDOWN ) { point = Point2f((float)x, (float)y); addRemovePt = true; } }
LK光流法预测当前帧的点
calcOpticalFlowPyrLK(prevGray, gray, points[0], points[1], status, err, winSize,3, termcrit, 0, 0.001);
函数原型:
void calcOpticalFlowPyrLK( InputArray prevImg, InputArray nextImg, InputArray prevPts, CV_OUT 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:深度为8位的前一帧图像或金字塔图像。
nextImg:和prevImg有相同的大小和类型,后一帧图像或金字塔。
prevPts:计算光流所需要的输入2D点矢量,点坐标必须是单精度浮点数(前一帧的点集)。
nextPts:输出2D点矢量(也是单精度浮点数坐标),点矢量中包含的是在后一帧图像上计算得到的输入特征新位置(预测当前帧的点)。
status:输出状态矢量(元素是无符号char类型,uchar),如果相应特征的流发现则矢量元素置为1,否则,为0。
err:输出误差矢量。
winSize:每个金字塔层搜索窗大小。
maxLevel:金字塔层的最大数目;如果置0,金字塔不使用(单层);如果置1,金字塔2层,等等以此类推。
criteria:指定搜索算法收敛迭代的类型,默认criteria=TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 30, 0.01)。
flags:默认为0
minEigThreshold:算法计算的光流等式的2x2常规矩阵的最小特征值。
注意
if( norm(point - points[1][i]) <= 5)//<point到points[1][i]的距离<5之间不能描点
这句话表达的意思是,鼠标新画的点与点points[1][i]之间的距离不能小于5个像素,否则画的点将不会显示在当前帧上,有兴趣的话可以将这个值改大一点,再试试。
norm(point - points[1][i])这句话可以翻译成sqrt((point.x-points[1][i].x)^2+(point.y-points[1][i].y)^2),sqrt是开根函数。
效果图
如果将前一帧的也画出来,然后将前后两帧的点用线段连接起来,看起来效果会更明显一点。
如果想要了解算法公式,推荐博客:
https://blog.csdn.net/zouxy09/article/details/8683859