LK光流跟蹤

 個人博客:http://www.chenjianqu.com/

原文鏈接:http://www.chenjianqu.com/show-89.html

光流(Optical Flow)是一種描述像素隨時間在圖像之間運動的方法。隨着時間的流逝,同一個像素會在圖像中運動,我們希望追蹤它的運動過程。其中,計算部分像素運動的稱爲稀疏光流,計算所有像素的稱爲稠密光流。稀疏光流以Lucas Kanade光流爲代表,並可以在SLAM中用於跟蹤特徵點位置。

 

Lucas-Kanade光流

    Lucas–Kanade光流算法是一種兩幀差分的光流估計算法。在LK光流中,認爲來自相機的圖像是隨時間變化的。圖像可以看作時間的函數: I(t),在t時刻,位於(x,y)處的像素的灰度可以寫成 I(x,y,t) 。這種方式把圖像看成了關於位置與時間的函數,它的值域就是圖像中像素的灰度。現在考慮某個固定的空間點,它在t時刻的像索座標爲x,y。由於相機的運動,它的圖像座標將發生變化。我們希望估計這個空間點在其他時刻圖像中位置。LK光流法有三個基本假設:

    1. 灰度不變:一個像素點隨着時間的變化,其亮度值(像素灰度值)是恆定不變的。這是光流法的基本設定。所有光流法都必須滿足。灰度不變假設是很強的假設, 實際當中很可能不成立。事實上,由於物體的材質不同,像素會出現高光和陰影部分;有時,相機會自動調整曝光參數,使得圖像整體變亮或變暗。這此時候灰度不變假設都是不成立的,因此光流的結果也不一定可靠。

    2. 小運動: 時間的變化不會引起位置的劇烈變化。這樣才能利用相鄰幀之間的位置變化引起的灰度值變化,去求取灰度對位置的偏導數。所有光流法必須滿足。

    3. 空間一致:假設某一個窗口內的像素具有相同的運動。這是LK光流法獨有的假定。因爲爲了求取x,y方向的速度,需要建立多個方程聯立求解。而空間一致假設就可以利用鄰域n個像素點來建立n個方程。

1.jpg

    對於t時刻位於(x,y)處的像素,我們設t+dt時刻它運動到(x+dx,y+dy)處。由於灰度不變,有:

I (x+dx, y+dy, t+dt) = I (x,y,t). 

    對上式的左邊進行泰勒展開,保留一階項,得:

I (x+dx, y+dy, t+dt) ≈ I (x,y,t) + (∂I/∂x)dx + (∂I/∂y)dy + (∂I/∂t)dt

    由灰度不變,即下一個時刻的灰度等於之前的灰度,從而:

 (∂I/∂x)dx + (∂I/∂y)dy + (∂I/∂t)dt = 0

    兩邊除以dt,得:

(∂I/∂x)(dx/dt) + (∂I/∂y)(dy/dt) = - ∂I/∂t

    dx/dt是像素在x軸上的運動速度,而dy/dt爲y軸上的速度,記爲u,v。這也是我們想知道的變量。

    ∂I/∂x 是圖像在該點處的x方向的梯度,∂I/∂y是y方向的梯度,記爲Ix,Iy圖像梯度的計算。

    ∂I/∂t 是圖像灰度對時間的變化量,記爲I,即兩幀之間的該點的灰度變化量。

    原式寫成矩陣形式爲:

2.jpg

    我們想要計算的是像素的運動u,v,上式有兩個變量但只有一個方程,因此引入空間一致假設。考慮一個大小爲w*w的窗口,該窗口內的像素具有同樣的運動,因此我們共有w*w個方程:

3.jpg

    於是整個方程爲:

4.jpg

    這是一個關於u,v的超定線性方程,可用最小二乘法解得:

5.jpg

    這樣就得到了像素在圖像間的運動速度u,v。t取離散時刻,可以估計某塊像素在若干圖像中出現的位置。

 

改進的LK光流

    原始的LK光流假設:灰度不變、小運動、空間一致都是較強的假設,並不容易得到滿足。考慮物體的運動速度較大時,算法會出現較大的誤差,那麼我們希望能減少圖像中物體的運動速度。假設當圖像爲400×400時,物體速度爲[16 16],那麼圖像縮小爲200×200時,速度變爲[8,8]。縮小爲100*100時,速度減少到[4,4]。在源圖像縮放後,原算法又變得適用了。所以光流可以通過生成原圖像的金字塔圖像,逐層求解,不斷精確來求得。簡單來說上層金字塔(低分辨率)中的一個像素可以代表下層的兩個。每一層的求解結果乘以2後加到下一層。主要的步驟有三步:建立金字塔基於金字塔跟蹤迭代過程

6.jpg

OpenCV代碼

    OpenCV中使用CalcOpticalFlowPyrLK()這個函數實現LK光流,參數如下:

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位輸入圖像或者通過 buildOpticalFlowPyramid()建立的金字塔
nextImg – 第二個輸入圖像或者和prevImg相同尺寸和類型的金字塔
prevPts – 二維點向量存儲找到的光流;點座標必須是單精度浮點數
nextPts – 輸出二維點向量(用單精度浮點座標)包括第二幅圖像中計算的輸入特徵的新點位置;當OPTFLOW_USE_INITIAL_FLOW 標誌通過,向量必須有和輸入一樣的尺寸。
status – 輸出狀態向量(無符號char);如果相應的流特徵被發現,向量的每個元素被設置爲1,否則,被置爲0.
err – 輸出錯誤向量;向量的每個元素被設爲相應特徵的一個錯誤,誤差測量的類型可以在flags參數中設置;如果流不被發現然後錯誤未被定義(使用status(狀態)參數找到此情形)。
winSize – 在每個金字塔水平搜尋窗口的尺寸。
maxLevel – 基於最大金字塔層次數。如果設置爲0,則不使用金字塔(單級);如果設置爲1,則使用兩個級別,等等。如果金字塔被傳遞到input,那麼算法使用的級別與金字塔同級別但不大於MaxLevel。
criteria – 指定迭代搜索算法的終止準則(在指定的最大迭代次數標準值(criteria.maxCount)之後,或者當搜索窗口移動小於criteria.epsilon。)
flags – 操作標誌,可選參數:
OPTFLOW_USE_INITIAL_FLOW – 使用初始估計,存儲在nextPts中;如果未設置標誌,則將prevPts複製到nextPts並被視爲初始估計。
OPTFLOW_LK_GET_MIN_EIGENVALS – 使用最小本徵值作爲誤差度量(見minEigThreshold描述);如果未設置標誌,則將原始周圍的一小部分和移動的點之間的 L1 距離除以窗口中的像素數,作爲誤差度量。
minEigThreshold – 算法所計算的光流方程的2x2標準矩陣的最小本徵值(該矩陣稱爲[Bouguet00]中的空間梯度矩陣)÷ 窗口中的像素數。如果該值小於MinEigThreshold,則過濾掉相應的特徵,相應的流也不進行處理。因此可以移除不好的點並提升性能。

 

    C++代碼如下:

CMakeLists.txt

cmake_minimum_required(VERSION 2.6)
project(lk_flow)

set( CMAKE_BUILD_TYPE Release )
set( CMAKE_CXX_FLAGS "-std=c++11 -O3" )

find_package( OpenCV )
include_directories( ${OpenCV_INCLUDE_DIRS} )

add_executable(lk_flow main.cpp)
target_link_libraries( lk_flow ${OpenCV_LIBS} )

install(TARGETS lk_flow RUNTIME DESTINATION bin)

 

main.cpp

#include <iostream>
#include <fstream>
#include <list>
#include <vector>
using namespace std; 
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <opencv2/video/tracking.hpp>

using namespace cv;

int main( int argc, char** argv )
{
    //特徵點 因爲要刪除跟蹤失敗的點,使用list
    list<Point2f> keypoints;      
    Mat color, last_color;
    color = imread("1.png");
    Mat showFlowImg = imread("9.png");
    
    // 對第一幀提取FAST特徵點
    vector<KeyPoint> kps;
    Ptr<FastFeatureDetector> detector = FastFeatureDetector::create();
    detector->detect(color, kps);
    for(auto kp:kps)
        keypoints.push_back( kp.pt );
    last_color = color;
    
    //進行光流跟蹤
    for ( int index=2; index<10; index++ ){
        //讀取圖片文件
        color = imread(to_string(index)+".png");
        
        vector<Point2f> next_keypoints; 
        vector<Point2f> prev_keypoints;
        for(auto kp:keypoints)
            prev_keypoints.push_back(kp);
        //用LK跟蹤特徵點
        vector<unsigned char> status;
        vector<float> error; 
        calcOpticalFlowPyrLK(last_color, color, prev_keypoints, next_keypoints, status, error );
        
        // 把跟丟的點刪掉,把新的位置賦給keypoints
        int i=0; 
        for(auto iter=keypoints.begin(); iter!=keypoints.end(); i++){
            if(status[i] == 0 ){
                iter = keypoints.erase(iter);
                continue;
            }
            
            //畫跟蹤的線條
            Point2f beforePoint=*iter;
            Point2f afterPoint=next_keypoints[i];
            line(showFlowImg, beforePoint,afterPoint, Scalar(0,240,0), 1);
    
    
            *iter = next_keypoints[i];
            iter++;
        }
        
        cout<<"tracked keypoints: "<<keypoints.size()<<endl;
        //如果全部跟丟了
        if (keypoints.size() == 0){
            cout<<"all keypoints are lost."<<endl;
            break; 
        }
        
        // 畫出 keypoints
        Mat img_show = color.clone();
        for (auto kp:keypoints)
            circle(img_show, kp, 5, Scalar(0, 240, 0), 1);
        imshow("corners", img_show);
        waitKey(0);
        last_color = color;
    }
    
    imshow("showFlowImg", showFlowImg);
    waitKey(0);
    
    return 0;
}

    特徵點:

7.jpg

    特徵點的軌跡:

8.jpg

    程序運行結果顯示,圖像中大部分特徵點能夠順利跟蹤到,但某些特徵點會丟失。丟失的特徵點或是被移出視野外,或是被其它物體擋住,如果不提取新的特徵點,那麼光流的跟蹤會越來越少。

    位於物體角點處的特徵更加穩定,邊緣處的特徵會沿着邊緣滑動,因爲沿着邊緣移動時特徵塊的內容基本不變,因此被程序認爲是同一個地方。而其它地方的特徵點則會頻繁跳動。因此最好提取的是角點,其次是邊緣點。

    LK光流點不需要計算和匹配描述子,但是本身也需要一定的計算量。另外LK光流跟蹤能直接得到特徵點的對應關係,不太會誤匹配,但是光流必需要求相機的運動是微小的。

 

 

參考文獻

[0]高翔.視覺SLAM14講

[1]一度逍遙.光流法詳解之一(LK光流).https://www.cnblogs.com/riddick/p/10586662.html  .2019-03-25

[2]菜鳥知識搬運工.OpenCV3學習(11.2)LK光流法原理及opencv實現. https://blog.csdn.net/qq_30815237/article/details/87208319  .2019-02-13

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章