【目标跟踪+光流算法】运动跟踪合集
b站視頻:https://www.bilibili.com/video/BV1DE411a7uH?from=search&seid=6718989481068612540
光流(一)--綜述概覽
Eason.wxd2015-09-14 10:49:5410626收藏12
分類專欄:運動跟蹤
原文:
http://blog.csdn.net/zouxy09/article/details/8683859
光流(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等人對多種光流計算技術進行了總結,按照理論基礎與數學方法的區別把它們分成四種:基于梯度的方法、基于匹配的方法、基于能量的方法、基于相位的方法。近年來神經動力學方法也頗受學者重視。
1 基于梯度的方法
基于梯度的方法又稱為微分法,它是利用時變圖像灰度(或其濾波形式)的時空微分(即時空梯度函數)來計算像素的速度矢量。由于計算簡單和較好的結果,該方法得到了廣泛應用和研究。典型的代表是Horn-Schunck的光流計算方法,該方法在光流基本約束方程的基礎上附加了全局平滑假設,從而計算出光流場。基于此思想,大量的改進算法不斷提出。Nagel采用有條件的平滑約束,即通過加權矩陣的控制對梯度進行不同平滑處理;Black和Anandan針對多運動的估計問題,提出了分段平滑的方法。雖然很多基于梯度的光流估計方法取得了較好的光流估計,但由于在計算光流時涉及到可調參數的人工選取、可靠性評價因子的選擇困難,以及預處理對光流計算結果的影響,在應用光流對目標進行實時檢測與自動跟蹤時仍存在很多問題。
2 基于匹配的方法
基于匹配的光流計算方法包括基于特征和區域的兩種。基于特征的方法不斷地對目標主要特征進行定位和跟蹤,對目標大的運動和亮度變化具有魯棒性(robustness)。存在的問題是光流通常很稀疏,而且特征提取和精確匹配也十分困難。基于區域的方法先對類似的區域進行定位,然后通過相似區域的位移計算光流。這種方法在視頻編碼中得到了廣泛的應用。然而,它計算的光流仍不稠密。另外,這兩種方法估計亞像素精度的光流也有困難,計算量很大。在考慮光流精度和稠密性時,基于匹配的方法適用。
3 基于能量的方法
基于能量的方法首先要對輸入圖像序列進行時空濾波處理,這是一種時間和空間整合。對于均勻的流場,要獲得正確的速度估計,這種時空整合是非常必要的。然而,這樣做會降低光流估計的空間和時間分辨率。尤其是當時空整合區域包含幾個運動成分(如運動邊緣)時,估計精度將會惡化。此外,基于能量的光流技術還存在高計算負荷的問題。此方法涉及大量的濾波器,目前這些濾波器是主要的計算消費。然而,可以預期,隨著相應硬件的發展,在不久的將來,濾波將不再是一個嚴重的限制因素,所有這些技術都可以在幀速下加以實現。
4 基于相位的方法
Fleet和Jepson首次從概念上提出了相位信息用于光流計算的問題。因為速度是根據帶通濾波器輸出的相位特性確定的,所以稱為相位方法。他們根據與帶通速度調諧濾波器輸出中的等相位輪廓相垂直的瞬時運動來定義分速度。帶通濾波器按照尺度、速度和定向來分離輸入信號。
基于相位的光流技術的綜合性能是比較好的:速度估計比較精確且具有較高的空間分辨率,對圖像序列的適用范圍也比較寬。同時,這里仍有幾個問題值得討論:
(1)與基于能量的光流技術一樣,基于相位的模型既有一定的生物合理性,又有較高的計算復雜性;
(2)盡管相位技術用兩幀圖像就可計算光流,但要獲得足夠的估計精度,就必須有一定的整合時間,這個延遲將會降低邊緣處運動估計的時間分辨率;
(3)Fleet和Jespon的方法對輸入圖像序列中的時間混疊比較敏感。
5 神經動力學方法
計算機視覺研究的初衷就是為了模仿人類視覺系統的功能。然而人類理解與識別圖像的能力與計算機形成了巨大的反差。視覺科學家們迫切期望借鑒人類處理圖像的方法,以擺脫困境。對于光流計算來講,如果說前面的基于能量或相位的模型有一定的生物合理性的話,那么近幾年出現的利用神經網絡建立的視覺運動感知的神經動力學模型則是對生物視覺系統功能與結構的更為直接的模擬。
Grossberg等人的視覺運動感知神經動力學模型描述了運動感知中視皮層簡單細胞、復雜細胞、超復雜細胞以及視網膜雙極細胞之間的相互作用,揭示了運動分割與組合、競爭與合作的神經整合機制。這個稱為運動邊界輪廓系統的神經網絡解釋了復雜運動圖形上的局部模糊運動如何被積極地組織成一個整體一致的運動信號,給出了整體小孔問題的一個解。這個模型對于整體運動方向的判別非常有效,然而它卻不能給出運動速度的大小。
Fay和Waxman模仿視網膜中的時空處理和大腦的視覺運動通路,基于并聯動力學提出了一個多層神經網絡,它涉及光適應、邊緣增強和邊緣速度提取等幾個處理階段。網絡中,每個節點的動力學特性類似于具有可變電導的細胞膜,光適應利用神經元間的抑制作用來獲取,空間對比度增強借助于一個修正的on-中心/off-周邊反饋網絡來實現,最后的速度估計由一個稱為對傳活化法的動力學方程來提取。這個神經網絡在一并行機上實現了30幀/秒的幀速下的速度提取。遺憾的是它僅能提供運動邊緣的法向速度估計,為了恢復整個模式的光流場,還必須用速度泛函方法將估計的法向流整合成一個致密的光流場。盡管用這些神經動力學模型來測量光流還很不成熟,然而這些方法及其結論為進一步研究打下了良好的基礎,是將神經機制引入運動計算方面所做的極有意義的嘗試。一并行機上實現了30幀/秒的幀速下的速度提取。遺憾的是它僅能提供運動邊緣的法向速度估計,為了恢復整個模式的光流場,還必須用速度泛函方法將估計的法向流整合成一個致密的光流場。
目前,對光流的研究方興未艾,新的計算方法還在不斷涌現。這里對光流技術的發展趨勢與方向提出以下幾點看法:
(1)現有技術各有自己的優點與缺陷,方法之間相互結合,優勢互補,建立光流計算的多階段或分層模型,是光流技術發展的一個趨勢;
(2)通過深入的研究發現,現有光流方法之間有許多共通之處。如微分法和匹配法的前提假設極為相似;某些基于能量的方法等效于區域匹配技術;而相位方法則將相位梯度用于法向速度的計算。這些現象并不是偶然的。Singh指出,現有各種光流估計方法基本上可以統一在一個框架之中,這個框架將光流信息分成兩類:保持信息和鄰域信息,光流場的恢復通過兩種信息的提取和融合來實現。光流計算的統一框架的研究是這個領域的又一趨勢;
(3)盡管光流計算的神經動力學方法還很不成熟,然而對它的研究卻具有極其深遠的意義。隨著生物視覺研究的不斷深入,神經方法無疑會不斷完善,也許光流計算乃至計算機視覺的根本出路就在于神經機制的引入。神經網絡方法是光流技術的一個發展方向。
其他的咱們先不說了,回歸應用吧(呵呵,太高深了,自己說不下去了)。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
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)
大部分參數在論文中都有一套比較好的值的,直接采用他們的就好了
// 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 : zouxy09@qq.com
// 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.3modulesvideosrcoptflowgf.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;
}
// 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 : zouxy09@qq.com
// 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.3modulesvideosrcoptflowgf.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;
}
這個是效果:
一個揮動的手:
雖然也有背景在動,但是因為他們的運動方向不一樣,所以還是可以辨認出來前面那個是手,在前景和背景運動不統一的時候,還是可以辨認出來的。
總結
以上是生活随笔為你收集整理的【目标跟踪+光流算法】运动跟踪合集的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 吴佩琪 网站
- 下一篇: RTX 2070 super与GTX 1