LK光流算法:提高计算精度和增加搜索范围
关于LK算法的基本理论,见:http://www.cnblogs.com/dzyBK/p/4960630.html
这里主要阐述如何提高LK算法的计算精度和在高斯金字塔上应用LK算法。
1.提高LK算法的精度
其实这也并不是什么高大尚的东西。通俗地讲,就是反复调用LK算法来提高精度。这种反复调用算法本身来提高算法精度的方法,不仅对LK算法可以使用,对其它光流算法也可以使用。的确也有很多光流算法是这么做的。除了光流算法,其它领域的很算法也都可以这么做。其实,就是一个迭代的思想。既然是一种思想,应用当然就很广啦。
在阐述如何提高LK算法精度之前,先来作一个通俗形象的比喻。假设你在A点,你想跳到离你不远处的B点。也许你想一下就跳到B点,但是你并不能保证你跳一下(步)就能准确地到达B点。也许更好的方法是允许你跳很多步,这样你就可以精确地(至少比跳一步精确)到达B点。具体跳多少步,还可以这样来决定,当你的位置与B的位置小于指定值时,就认为你已经到过了B点,此时你就可以停止跳跃,好好休息一下啦。这个过程就包含迭代的思想。其实,你到B点不一要选择跳啊,你还可以选择爬啊(比喻有点不恰当哈,但能明白什么意思就行),具体爬多少步了,也是可以人为定的。
这里的跳或者爬就是你要选择的方法。对应到我们这篇文章的目的上来,你选择计算光流,也可以有很多种方法。只是我们这里以LK为例来讲解而已。
以下对如何提高LK算法的计算精度作正式阐述。
设前一帧I上的某个像素点的位置是P(a,b),这个像素点在当前帧J的位置是Q(c,d),于是两帧之间的偏移量(偏移量对时间的导数就光流,所以得到偏移量也就间接地得到了光流啦)(u,v)=(c,d)-(a,b)。LK算法可表示为:
这里的F就表法LK算法,算法的输入是前后帧图像I及J和要计算其偏移量的点P,输出是点P的偏移量。对于多个点的情况,则每个点都这样表示即可。对于稠密光流的情况(即图像上每个点的光流都计算),则可表示为:
这里的E表示光流图,尺寸与I或J一样,但E是双通道图像。一个通道表示x方向的偏移量,一个通道表示y方向的偏移量。
前面已经说到,提高LK算法的计算精度就是一个迭代的过程。那到底是怎么一回事勒?其实就是当我们得到一个光流(u,v)以后,就将当前帧J平移(u,v)(也可以将I平移(-u,-v))。平移之后,I和J就更接近重合了,P和Q也更接近了。然后我们再对I和平移后的J使用LK算法,又得到一个光流,然后又平移J。如此反复,P和Q越来越接近,我们得到光流也会越来越小。于是我们设置一个阈值,当得到光流小于此阈值时,我们认为P和Q已经完全重合。此时,我们把所有得到光流相加,就是最终的结果,即P和Q之间的偏移量。这个过程,我们可以表示为如下代码:
以上每个步骤中,我们还需判断Ui的模长Di,当Di小于指定的值d则终步迭代,于是更好表示代码如下:
2.在高斯金字塔应用LK算法
应用高斯金字塔的目的是为了增加LK算法的搜索范围,使得算法对于长距离移动的像素点也有效。怎么理解这个长距离了?同前面一样,我们设前一帧上有个像素点P,相对于当前帧它移动了U,它在当前帧的位置为Q,则P+U=Q。当U小于一定的值时,LK算法有效,当U大于一定值时,LK算法就一不定有效了。这时,就发挥高斯金字塔的作用啦。高斯金字塔可以得到一系列缩小的图像,在缩小的图像上的移动量U也会相应地缩小。此时,再应用LK算法不就可以有效啦。当然,也不是说有了高斯金字塔,移动量多大,光流算法都有效,还得看算法本身的性能。
现在我们来详细阐述下如何在高斯金字塔上应用LK算法。
首先,要为前后帧建立高斯金字塔(能看到这里不会不知道如何建高斯金字吧)。设前后帧分别为I和J,我们用Ii-1表示金字塔的第i层,用I0表示金字塔的最底层,I0=I。对于J,表示法也一样。设金字塔共有N层。在金字塔上应用LK算法的代码如下:
其中PN-1等于P坐标除以2N。从代码中可以看出,LK算法从金字塔顶层到底层逐层应用,上一层的偏移量乘以2后(乘以2的前提是金字塔是按原图缩小一半来建立的)作为下一层的初始偏移量,下一层的J先按这个偏移量平移之后再作为LK算法的输入,金字塔减小LK算法搜索范围的关键就在这里,因为平移之后,这层的I和J就更接近重合啦,所以搜索范围也就自然地减小啦。
值得注意的是,初始的Un设置为(0,0)是因为不知道初始的大约偏移量,若已知初始的大约偏移量,则可以在此指定,但记得要先除以2N。
3.综合代码
以下是在OpenCV下的代码,但并没有实现具体的细节,只是给出了算法的原理及算法的接口,只要实现给出的接口就可以实现LK算法啦。
#include <opencv2/opencv.hpp> using namespace cv; using namespace std; Mat SobelX(Mat &src){ return src; } Mat SobelY(Mat &src){ return src; } vector<Mat> buildPyramid(Mat &sr){ vector<Mat> vecMat(); return vecMat; } Mat Shift(Mat &src, Point2f d) { return src; } Point2f MatToPoint(InputArray &src) { Point2f x; return x; } void LK_Pyramid() { Mat I, J;//给定前一帧和当前帧(输入) Point2f P;//给定前一帧上的某个位置(输入) int winSize;//给定的位置领域内多少个像素的光流都相等(输入) float acy;//程序跳出循环的阈值精度(输入) Point2f Q;//给定位置在当前帧对应的位置(返回 或 返回+输入) vector<Mat> pyramidI, pyramidJ;//给定前一帧和当前帧的金字塔 Mat Ixi, Iyi, P_Ixi_ROI, P_Iyi_ROI, Gi;//变量中的i表示金字塔的第i层 Mat Itij, bij;//变量中的i表示金字塔的第i层,j表示在金字塔的第i层的第j次执行LK算法 Point2f Pi;//给定的位置在金字塔的第i层的值 Point2f Ui, Di;// Point2f Uij, Dij;// I = imread("./../TestData/I.png"); J = imread("./../TestData/J.png"); P = Point2f(, ); winSize = ; acy = 0.001f; pyramidI = buildPyramid(I); pyramidJ = buildPyramid(J); ; i >= ; i--) {//每层循环改变Di从而改变下一层的Ui,即将每层循环的微小变化(即Di)累加到Ui上从而得到精确的Ui(最初的Ui为0或给定的值) Pi = P / pow(.f, i);//给定的位置在金字塔的第i层的值 Ixi = SobelX(pyramidI[i]);//金字塔的第i层的x微分 Iyi = SobelY(pyramidI[i]);//金字塔的第i层的y微分 Gi.create(, , CV_32F);//金字塔第i层的空间梯度矩阵(原理见LK算法) P_Ixi_ROI = Ixi(Rect(Pi.x - winSize / , Pi.y - winSize / , winSize, winSize));//P领域的x微分 P_Iyi_ROI = Iyi(Rect(Pi.x - winSize / , Pi.y - winSize / , winSize, winSize));//P领域的y微分 Gi = (Mat_<, ) << sum(P_Ixi_ROI*P_Ixi_ROI)[], sum(P_Ixi_ROI*P_Iyi_ROI)[], sum(P_Ixi_ROI*P_Iyi_ROI)[], sum(P_Iyi_ROI*P_Iyi_ROI)[]); ) Ui = Point2f(, );//当前帧是金字塔的最顶层的初始偏移量(即偏移量) 或 等于Q-P(Q存储有给定的大约位置,此时Q即是返回变量也是输入变量) * (Ui + Di);//当前帧的金字塔的第i层的初始偏移量(即大约偏移量) = 2 * (前一层的Ui + 前一层的Di) ; sqrt(Dij.x*Dij.x + Dij.y*Dij.y)< acy; j++) {//每层循环改变Dij从而改变下一层的Uij,即将每层循环的微小变化(即Dij)累加到Uij上从而得到精确的Uij(最初的Uij为0) ) Uij = Point2f(, );//首次循环的偏移量(即大约偏移量) else Uij = Uij + Dij;//第j次循环的初始偏移量(即大约偏移量)= 前一次循环的Uij + 前一次循环的Dij Itij = pyramidI[i] - (Shift(pyramidJ[i], Ui + Uij));//前一帧的金字塔的第i层的第j次循环的t微分 bij = (Mat_<, ) << sum(Itij*Ixi)[], sum(Itij*Iyi)[]); Dij = MatToPoint(Gi.inv()*bij);//本层循环的Dij被改变,但Uij不变 } Di = Uij + Dij;//最后一次循环的Uij还没有叠加到Dij上,所以再加一次 } Q = P + Ui + Di; //最后一次循环的dL还没有叠加到gL上,所以再加一次。 }