光流Optical Flow介紹與OpenCV實現

 光流(optic flow)是什麼呢?名字很專業,感覺很陌生,但本質上,我們是最熟悉不過的了。因爲這種視覺現象我們每天都在經歷。從本質上說,光流就是你在這個運動着的世界裏感覺到的明顯的視覺運動(呵呵,相對論,沒有絕對的靜止,也沒有絕對的運動)。例如,當你坐在火車上,然後往窗外看。你可以看到樹、地面、建築等等,他們都在往後退。這個運動就是光流。而且,我們都會發現,他們的運動速度居然不一樣?這就給我們提供了一個挺有意思的信息:通過不同目標的運動速度判斷它們與我們的距離。一些比較遠的目標,例如雲、山,它們移動很慢,感覺就像靜止一樣。但一些離得比較近的物體,例如建築和樹,就比較快的往後退,然後離我們的距離越近,它們往後退的速度越快。一些非常近的物體,例如路面的標記啊,草地啊等等,快到好像在我們耳旁發出嗖嗖的聲音。

       光流除了提供遠近外,還可以提供角度信息。與咱們的眼睛正對着的方向成90度方向運動的物體速度要比其他角度的快,當小到0度的時候,也就是物體朝着我們的方向直接撞過來,我們就是感受不到它的運動(光流)了,看起來好像是靜止的。當它離我們越近,就越來越大(當然了,我們平時看到感覺還是有速度的,因爲物體較大,它的邊緣還是和我們人眼具有大於0的角度的)。

       呵呵,說了那麼多,好像還沒進入比較官方的,研究性的定義。那就貼上一個吧。

       光流的概念是Gibson在1950年首先提出來的。它是空間運動物體在觀察成像平面上的像素運動的瞬時速度,是利用圖像序列中像素在時間域上的變化以及相鄰幀之間的相關性來找到上一幀跟當前幀之間存在的對應關係,從而計算出相鄰幀之間物體的運動信息的一種方法。一般而言,光流是由於場景中前景目標本身的移動、相機的運動,或者兩者的共同運動所產生的。

       當人的眼睛觀察運動物體時,物體的景象在人眼的視網膜上形成一系列連續變化的圖像,這一系列連續變化的信息不斷“流過”視網膜(即圖像平面),好像一種光的“流”,故稱之爲光流(optical flow)。光流表達了圖像的變化,由於它包含了目標運動的信息,因此可被觀察者用來確定目標的運動情況。

       研究光流場的目的就是爲了從圖片序列中近似得到不能直接得到的運動場。運動場,其實就是物體在三維真實世界中的運動;光流場,是運動場在二維圖像平面上(人的眼睛或者攝像頭)的投影。

       那通俗的講就是通過一個圖片序列,把每張圖像中每個像素的運動速度和運動方向找出來就是光流場。那怎麼找呢?咱們直觀理解肯定是:第t幀的時候A點的位置是(x1, y1),那麼我們在第t+1幀的時候再找到A點,假如它的位置是(x2,y2),那麼我們就可以確定A點的運動了:(ux, vy) = (x2, y2) - (x1,y1)。

        那怎麼知道第t+1幀的時候A點的位置呢? 這就存在很多的光流計算方法了。

       1981年,Horn和Schunck創造性地將二維速度場與灰度相聯繫,引入光流約束方程,得到光流計算的基本算法。人們基於不同的理論基礎提出各種光流計算方法,算法性能各有不同。Barron等人對多種光流計算技術進行了總結,按照理論基礎與數學方法的區別把它們分成四種:基於梯度的方法、基於匹配的方法、基於能量的方法、基於相位的方法。近年來神經動力學方法也頗受學者重視。

 

       其他的咱們先不說了,迴歸應用吧(呵呵,太高深了,自己說不下去了)。OpenCV中實現了不少的光流算法。

1)calcOpticalFlowPyrLK

通過金字塔Lucas-Kanade 光流方法計算某些點集的光流(稀疏光流)。理解的話,可以參考這篇論文:”Pyramidal Implementation of the Lucas Kanade Feature TrackerDescription of the algorithm”

2)calcOpticalFlowFarneback

用Gunnar Farneback 的算法計算稠密光流(即圖像上所有像素點的光流都計算出來)。它的相關論文是:"Two-Frame Motion Estimation Based on PolynomialExpansion"

3)CalcOpticalFlowBM

通過塊匹配的方法來計算光流。

4)CalcOpticalFlowHS

用Horn-Schunck 的算法計算稠密光流。相關論文好像是這篇:”Determining Optical Flow”

5)calcOpticalFlowSF

這一個是2012年歐洲視覺會議的一篇文章的實現:"SimpleFlow: A Non-iterative, Sublinear Optical FlowAlgorithm",工程網站是:http://graphics.berkeley.edu/papers/Tao-SAN-2012-05/  在OpenCV新版本中有引入。

       稠密光流需要使用某種插值方法在比較容易跟蹤的像素之間進行插值以解決那些運動不明確的像素,所以它的計算開銷是相當大的。而對於稀疏光流來說,在他計算時需要在被跟蹤之前指定一組點(容易跟蹤的點,例如角點),因此在使用LK方法之前我們需要配合使用cvGoodFeatureToTrack()來尋找角點,然後利用金字塔LK光流算法,對運動進行跟蹤。但個人感覺,對於少紋理的目標,例如人手,LK稀疏光流就比較容易跟丟。

       至於他們的API的使用說明,我們直接參考OpenCV的官方手冊就行:

http://www.opencv.org.cn/opencvdoc/2.3.2/html/modules/video/doc/motion_analysis_and_object_tracking.html#calcopticalflowfarneback

 

       IJCV2011有一篇文章,《A Database and Evaluation Methodology for Optical Flow》裏面對主流的光流算法做了簡要的介紹和對不同算法進行了評估。網址是:

http://vision.middlebury.edu/flow/

       感覺這個文章在光流算法的解說上非常好,條例很清晰。想了解光流的,推薦看這篇文章。另外,需要提到的一個問題是,光流場是圖片中每個像素都有一個x方向和y方向的位移,所以在上面那些光流計算結束後得到的光流flow是個和原來圖像大小相等的雙通道圖像。那怎麼可視化呢?這篇文章用的是Munsell顏色系統來顯示。

       關於孟塞爾顏色系統(MunsellColor System),可以看wikibaike。它是美國藝術家阿爾伯特孟塞爾(Albert H. Munsell,1858-1918)在1898年創制的顏色描述系統。


孟塞爾顏色系統的空間大致成一個圓柱形:

南北軸=明度(value,從全黑(1)到全白(10)。

經度=色相(hue)。把一週均分成五種主色調和五種中間色:紅(R)、紅黃(YR)、黃(Y)、黃綠(GY)、綠(G)、綠藍(BG)、藍(B)、藍紫(PB)、紫(P)、紫紅(RP)。相鄰的兩個位置之間再均分10份,共100份。

距軸的距離=色度(chroma),表示色調的純度。其數值從中間(0)向外隨着色調的純度增加,沒有理論上的上限(普通的顏色實際上限爲10左右,反光、熒光等材料可高達30)。由於人眼對各種顏色的的敏感度不同,色度不一定與每個色調和明度組合相匹配。

具體顏色的標識形式爲:色相+明度+色度

       在上面的那個評估的網站有這個從flow到color顯示的Matlab和C++代碼。但是感覺C++代碼分幾個文件,有點亂,然後我自己整理成兩個函數了,並配合OpenCV的Mat格式。

       下面的代碼是用calcOpticalFlowFarneback來計算稠密光流並且用這個顏色系統來顯示的。這個計算稠密光流的方法與其他幾個相比還是比較快的,640x480的視頻我的是200ms左右一幀,但其他的一般都需要一兩秒以上。結果圖中,不同顏色表示不同的運動方向,深淺就表示運動的快慢了。

void calcOpticalFlowFarneback(InputArray prevImg, InputArray nextImg,InputOutputArray flow, double pyrScale, int levels, int winsize, intiterations, int polyN, double polySigma, int flags)

大部分參數在論文中都有一套比較好的值的,直接採用他們的就好了。

[cpp] view plain copy
  1. // Farneback dense optical flow calculate and show in Munsell system of colors  
  2. // Author : Zouxy  
  3. // Date   : 2013-3-15  
  4. // HomePage : http://blog.csdn.net/zouxy09  
  5. // Email  : [email protected]  
  6.   
  7. // API calcOpticalFlowFarneback() comes from OpenCV, and this  
  8. // 2D dense optical flow algorithm from the following paper:  
  9. // Gunnar Farneback. "Two-Frame Motion Estimation Based on Polynomial Expansion".  
  10. // And the OpenCV source code locate in ..\opencv2.4.3\modules\video\src\optflowgf.cpp  
  11.   
  12. #include <iostream>  
  13. #include "opencv2/opencv.hpp"  
  14.   
  15. using namespace cv;  
  16. using namespace std;  
  17.   
  18. #define UNKNOWN_FLOW_THRESH 1e9  
  19.   
  20. // Color encoding of flow vectors from:  
  21. // http://members.shaw.ca/quadibloc/other/colint.htm  
  22. // This code is modified from:  
  23. // http://vision.middlebury.edu/flow/data/  
  24. void makecolorwheel(vector<Scalar> &colorwheel)  
  25. {  
  26.     int RY = 15;  
  27.     int YG = 6;  
  28.     int GC = 4;  
  29.     int CB = 11;  
  30.     int BM = 13;  
  31.     int MR = 6;  
  32.   
  33.     int i;  
  34.   
  35.     for (i = 0; i < RY; i++) colorwheel.push_back(Scalar(255,       255*i/RY,     0));  
  36.     for (i = 0; i < YG; i++) colorwheel.push_back(Scalar(255-255*i/YG, 255,       0));  
  37.     for (i = 0; i < GC; i++) colorwheel.push_back(Scalar(0,         255,      255*i/GC));  
  38.     for (i = 0; i < CB; i++) colorwheel.push_back(Scalar(0,         255-255*i/CB, 255));  
  39.     for (i = 0; i < BM; i++) colorwheel.push_back(Scalar(255*i/BM,      0,        255));  
  40.     for (i = 0; i < MR; i++) colorwheel.push_back(Scalar(255,       0,        255-255*i/MR));  
  41. }  
  42.   
  43. void motionToColor(Mat flow, Mat &color)  
  44. {  
  45.     if (color.empty())  
  46.         color.create(flow.rows, flow.cols, CV_8UC3);  
  47.   
  48.     static vector<Scalar> colorwheel; //Scalar r,g,b  
  49.     if (colorwheel.empty())  
  50.         makecolorwheel(colorwheel);  
  51.   
  52.     // determine motion range:  
  53.     float maxrad = -1;  
  54.   
  55.     // Find max flow to normalize fx and fy  
  56.     for (int i= 0; i < flow.rows; ++i)   
  57.     {  
  58.         for (int j = 0; j < flow.cols; ++j)   
  59.         {  
  60.             Vec2f flow_at_point = flow.at<Vec2f>(i, j);  
  61.             float fx = flow_at_point[0];  
  62.             float fy = flow_at_point[1];  
  63.             if ((fabs(fx) >  UNKNOWN_FLOW_THRESH) || (fabs(fy) >  UNKNOWN_FLOW_THRESH))  
  64.                 continue;  
  65.             float rad = sqrt(fx * fx + fy * fy);  
  66.             maxrad = maxrad > rad ? maxrad : rad;  
  67.         }  
  68.     }  
  69.   
  70.     for (int i= 0; i < flow.rows; ++i)   
  71.     {  
  72.         for (int j = 0; j < flow.cols; ++j)   
  73.         {  
  74.             uchar *data = color.data + color.step[0] * i + color.step[1] * j;  
  75.             Vec2f flow_at_point = flow.at<Vec2f>(i, j);  
  76.   
  77.             float fx = flow_at_point[0] / maxrad;  
  78.             float fy = flow_at_point[1] / maxrad;  
  79.             if ((fabs(fx) >  UNKNOWN_FLOW_THRESH) || (fabs(fy) >  UNKNOWN_FLOW_THRESH))  
  80.             {  
  81.                 data[0] = data[1] = data[2] = 0;  
  82.                 continue;  
  83.             }  
  84.             float rad = sqrt(fx * fx + fy * fy);  
  85.   
  86.             float angle = atan2(-fy, -fx) / CV_PI;  
  87.             float fk = (angle + 1.0) / 2.0 * (colorwheel.size()-1);  
  88.             int k0 = (int)fk;  
  89.             int k1 = (k0 + 1) % colorwheel.size();  
  90.             float f = fk - k0;  
  91.             //f = 0; // uncomment to see original color wheel  
  92.   
  93.             for (int b = 0; b < 3; b++)   
  94.             {  
  95.                 float col0 = colorwheel[k0][b] / 255.0;  
  96.                 float col1 = colorwheel[k1][b] / 255.0;  
  97.                 float col = (1 - f) * col0 + f * col1;  
  98.                 if (rad <= 1)  
  99.                     col = 1 - rad * (1 - col); // increase saturation with radius  
  100.                 else  
  101.                     col *= .75; // out of range  
  102.                 data[2 - b] = (int)(255.0 * col);  
  103.             }  
  104.         }  
  105.     }  
  106. }  
  107.   
  108. int main(intchar**)  
  109. {  
  110.     VideoCapture cap;  
  111.     cap.open(0);  
  112.     //cap.open("test_02.wmv");  
  113.   
  114.     if( !cap.isOpened() )  
  115.         return -1;  
  116.   
  117.     Mat prevgray, gray, flow, cflow, frame;  
  118.     namedWindow("flow", 1);  
  119.   
  120.     Mat motion2color;  
  121.   
  122.     for(;;)  
  123.     {  
  124.         double t = (double)cvGetTickCount();  
  125.   
  126.         cap >> frame;  
  127.         cvtColor(frame, gray, CV_BGR2GRAY);  
  128.         imshow("original", frame);  
  129.   
  130.         if( prevgray.data )  
  131.         {  
  132.             calcOpticalFlowFarneback(prevgray, gray, flow, 0.5, 3, 15, 3, 5, 1.2, 0);  
  133.             motionToColor(flow, motion2color);  
  134.             imshow("flow", motion2color);  
  135.         }  
  136.         if(waitKey(10)>=0)  
  137.             break;  
  138.         std::swap(prevgray, gray);  
  139.   
  140.         t = (double)cvGetTickCount() - t;  
  141.         cout << "cost time: " << t / ((double)cvGetTickFrequency()*1000.) << endl;  
  142.     }  
  143.     return 0;  
  144. }  

這個是效果:

一個揮動的手:

      雖然也有背景在動,但是因爲他們的運動方向不一樣,所以還是可以辨認出來前面那個是手,在前景和背景運動不統一的時候,還是可以辨認出來的。

發佈了220 篇原創文章 · 獲贊 222 · 訪問量 166萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章