Optical flow(通俗講解)
阿新 • • 發佈:2019-01-03
現在四軸飛行器越來越火,如何在室內進行定位呢?不同於傳統四軸的姿態控制,電機驅動,室外定位,都有了一套完整的方案,室內定位還是沒有完全成熟。,目前大四軸可以利用的GPS定高,小四軸比較成熟的也就是光流方案了。
先看一下光流的效果
這是一個揮動的手
雖然也有背景在動,但是因為他們的運動方向不一樣,所以還是可以辨認出來前面那個是手,在前景和背景運動不統一的時候,還是可以辨認出來的。
那麼光流(optic flow)是什麼呢?
名字很專業,感覺很陌生。從本質上說,光流就是你在這個運動著的世界裡感覺到的明顯的視覺運動。例如,當你坐在火車上,然後往窗外看。你可以看到樹、地面、建築等等,他們都在往後退。這個運動就是光流。而且,我們都會發現,他們的運動速度居然不一樣?這就給我們提供了一個挺有意思的資訊:通過不同目標的運動速度判斷它們與我們的距離 。一些比較遠的目標,例如雲、山,它們移動很慢,感覺就像靜止一樣。但一些離得比較近的物體,例如建築和樹,就比較快的往後退,然後離我們的距離越近,它們往後退的速度越快。一些非常近的物體,例如路面的標記啊,草地啊等等,快到好像在我們耳旁發出嗖嗖的聲音。
光流除了提供遠近外,還可以提供角度資訊。與咱們的眼睛正對著的方向成90度方向運動的物體速度要比其他角度的快,當小到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新版本中有引入。
還有他們的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顏色系統來顯示。
孟塞爾顏色系統的空間大致成一個圓柱形:
南北軸=明度(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)
大部分引數在論文中都有一套比較好的值的,直接採用他們的就好了。
先看一下光流的效果
這是一個揮動的手
雖然也有背景在動,但是因為他們的運動方向不一樣,所以還是可以辨認出來前面那個是手,在前景和背景運動不統一的時候,還是可以辨認出來的。
那麼光流(optic flow)是什麼呢?
名字很專業,感覺很陌生。從本質上說,光流就是你在這個運動著的世界裡感覺到的明顯的視覺運動。例如,當你坐在火車上,然後往窗外看。你可以看到樹、地面、建築等等,他們都在往後退。這個運動就是光流。而且,我們都會發現,他們的運動速度居然不一樣?這就給我們提供了一個挺有意思的資訊:通過不同目標的運動速度判斷它們與我們的距離
光流除了提供遠近外,還可以提供角度資訊。與咱們的眼睛正對著的方向成90度方向運動的物體速度要比其他角度的快,當小到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新版本中有引入。
還有他們的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顏色系統來顯示。
孟塞爾顏色系統的空間大致成一個圓柱形:
南北軸=明度(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)
大部分引數在論文中都有一套比較好的值的,直接採用他們的就好了。
- // Farneback dense optical flow calculate and show in Munsell system of colors
- // Author : Zouxy
- // Date : 2013-3-15
- // HomePage : http://blog.csdn.net/zouxy09
- // Email : [email protected]
- // API calcOpticalFlowFarneback() comes from OpenCV, and this
- // 2D dense optical flow algorithm from the following paper:
- // Gunnar Farneback. "Two-Frame Motion Estimation Based on Polynomial Expansion".
- // And the OpenCV source code locate in ..\opencv2.4.3\modules\video\src\optflowgf.cpp
- #include <iostream>
- #include "opencv2/opencv.hpp"
- using namespace cv;
- using namespace std;
- #define UNKNOWN_FLOW_THRESH 1e9
- // Color encoding of flow vectors from:
- // http://members.shaw.ca/quadibloc/other/colint.htm
- // This code is modified from:
- // http://vision.middlebury.edu/flow/data/
- void makecolorwheel(vector<Scalar> &colorwheel)
- {
- int RY = 15;
- int YG = 6;
- int GC = 4;
- int CB = 11;
- int BM = 13;
- int MR = 6;
- int i;
- for (i = 0; i < RY; i++) colorwheel.push_back(Scalar(255, 255*i/RY, 0));
- for (i = 0; i < YG; i++) colorwheel.push_back(Scalar(255-255*i/YG, 255, 0));
- for (i = 0; i < GC; i++) colorwheel.push_back(Scalar(0, 255, 255*i/GC));
- for (i = 0; i < CB; i++) colorwheel.push_back(Scalar(0, 255-255*i/CB, 255));
- for (i = 0; i < BM; i++) colorwheel.push_back(Scalar(255*i/BM, 0, 255));
- for (i = 0; i < MR; i++) colorwheel.push_back(Scalar(255, 0, 255-255*i/MR));
- }
- void motionToColor(Mat flow, Mat &color)
- {
- if (color.empty())
- color.create(flow.rows, flow.cols, CV_8UC3);
- static vector<Scalar> colorwheel; //Scalar r,g,b
- if (colorwheel.empty())
- makecolorwheel(colorwheel);
- // determine motion range:
- float maxrad = -1;
- // Find max flow to normalize fx and fy
- for (int i= 0; i < flow.rows; ++i)
- {
- for (int j = 0; j < flow.cols; ++j)
- {
- Vec2f flow_at_point = flow.at<Vec2f>(i, j);
- float fx = flow_at_point[0];
- float fy = flow_at_point[1];
- if ((fabs(fx) > UNKNOWN_FLOW_THRESH) || (fabs(fy) > UNKNOWN_FLOW_THRESH))
- continue;
- float rad = sqrt(fx * fx + fy * fy);
- maxrad = maxrad > rad ? maxrad : rad;
- }
- }
- for (int i= 0; i < flow.rows; ++i)
- {
- for (int j = 0; j < flow.cols; ++j)
- {
- uchar *data = color.data + color.step[0] * i + color.step[1] * j;
- Vec2f flow_at_point = flow.at<Vec2f>(i, j);
- float fx = flow_at_point[0] / maxrad;
- float fy = flow_at_point[1] / maxrad;
- if ((fabs(fx) > UNKNOWN_FLOW_THRESH) || (fabs(fy) > UNKNOWN_FLOW_THRESH))
- {
- data[0] = data[1] = data[2] = 0;
- continue;
- }
- float rad = sqrt(fx * fx + fy * fy);
- float angle = atan2(-fy, -fx) / CV_PI;
- float fk = (angle + 1.0) / 2.0 * (colorwheel.size()-1);
- int k0 = (int)fk;
- int k1 = (k0 + 1) % colorwheel.size();
- float f = fk - k0;
- //f = 0; // uncomment to see original color wheel
- for (int b = 0; b < 3; b++)
- {
- float col0 = colorwheel[k0][b] / 255.0;
- float col1 = colorwheel[k1][b] / 255.0;
- float col = (1 - f) * col0 + f * col1;
- if (rad <= 1)
- col = 1 - rad * (1 - col); // increase saturation with radius
- else
- col *= .75; // out of range
- data[2 - b] = (int)(255.0 * col);
- }
- }
- }
- }
- int main(int, char**)
- {
- VideoCapture cap;
- cap.open(0);
- //cap.open("test_02.wmv");
- if( !cap.isOpened() )
- return -1;
- Mat prevgray, gray, flow, cflow, frame;
- namedWindow("flow", 1);
- Mat motion2color;
- for(;;)
- {
- double t = (double)cvGetTickCount();
- cap >> frame;
- cvtColor(frame, gray, CV_BGR2GRAY);
- imshow("original", frame);
- if( prevgray.data )
- {
- calcOpticalFlowFarneback(prevgray, gray, flow, 0.5, 3, 15, 3, 5, 1.2, 0);
- motionToColor(flow, motion2color);
- imshow("flow", motion2color);
- }
- if(waitKey(10)>=0)
- break;
- std::swap(prevgray, gray);
- t = (double)cvGetTickCount() - t;
- cout << "cost time: " << t / ((double)cvGetTickFrequency()*1000.) << endl;
- }
- return 0;
- }