原文:http://blog.csdn.net/crzy_sparrow/article/details/7407604
本文目錄:
一.基於特征點的目標跟蹤的一般方法
二.光流法
三.opencv中的光流法函數
四.用類封裝基於光流法的目標跟蹤方法
五.完整代碼
六.參考文獻
一.基於特征點的目標跟蹤的一般方法
基於特征點的跟蹤算法大致可以分為兩個步驟:
1)探測當前幀的特征點;
2)通過當前幀和下一幀灰度比較,估計當前幀特征點在下一幀的位置;
3)過濾位置不變的特征點,余下的點就是目標了。
很顯然,基於特征點的目標跟蹤算法和1),2)兩個步驟有關。特征點可以是Harris角點(見我的另外一篇博文),也可以是邊緣點等等,而估計下一幀位置的方法也有不少,比如這里要講的光流法,也可以是卡爾曼濾波法(咱是控制系的,上課經常遇到這個,所以看光流法看着看着就想到這個了)。
本文中,用改進的Harris角點提取特征點(見我另一篇博文:http://blog.csdn.net/crzy_sparrow/article/details/7391511),用Lucas-Kanade光流法實現目標跟蹤。
二.光流法
這一部分《learing opencv》一書的第10章Lucas-Kanade光流部分寫得非常詳細,推薦大家看書。我這里也粘帖一些選自書中的內容。
另外我對這一部分附上一些個人的看法(謬誤之處還望不吝指正):
1.首先是假設條件:
(1)亮度恆定,就是同一點隨着時間的變化,其亮度不會發生改變。這是基本光流法的假定(所有光流法變種都必須滿足),用於得到光流法基本方程;
(2)小運動,這個也必須滿足,就是時間的變化不會引起位置的劇烈變化,這樣灰度才能對位置求偏導(換句話說,小運動情況下我們才能用前后幀之間單位位置變化引起的灰度變化去近似灰度對位置的偏導數),這也是光流法不可或缺的假定;
(3)空間一致,一個場景上鄰近的點投影到圖像上也是鄰近點,且鄰近點速度一致。這是Lucas-Kanade光流法特有的假定,因為光流法基本方程約束只有一個,而要求x,y方向的速度,有兩個未知變量。我們假定特征點鄰域內做相似運動,就可以連立n多個方程求取x,y方向的速度(n為特征點鄰域總點數,包括該特征點)。
2.方程求解
多個方程求兩個未知變量,又是線性方程,很容易就想到用最小二乘法,事實上opencv也是這么做的。其中,最小誤差平方和為最優化指標。
3.好吧,前面說到了小運動這個假定,聰明的你肯定很不爽了,目標速度很快那這貨不是二掉了。幸運的是多尺度能解決這個問題。首先,對每一幀建立一個高斯金字塔,最大尺度圖片在最頂層,原始圖片在底層。然后,從頂層開始估計下一幀所在位置,作為下一層的初始位置,沿着金字塔向下搜索,重復估計動作,直到到達金字塔的底層。聰明的你肯定發現了:這樣搜索不僅可以解決大運動目標跟蹤,也可以一定程度上解決孔徑問題(相同大小的窗口能覆蓋大尺度圖片上盡量多的角點,而這些角點無法在原始圖片上被覆蓋)。

#include "opencv2/opencv.hpp" #include <sstream> #include <iomanip>
using namespace std; using namespace cv; class FrameProcessor; //幀處理基類
class FrameProcessor{ public: virtual void process(Mat &input,Mat &ouput)=0; }; //特征跟蹤類,繼承自幀處理基類
class FeatureTracker : public FrameProcessor{ Mat gray; //當前灰度圖
Mat gray_prev; //之前的灰度圖
vector<Point2f> points[2];//前后兩幀的特征點
vector<Point2f> initial;//初始特征點
vector<Point2f> features;//檢測到的特征
int max_count; //要跟蹤特征的最大數目
double qlevel; //特征檢測的指標
double minDist;//特征點之間最小容忍距離
vector<uchar> status; //特征跟蹤狀態
vector<float> err; //跟蹤時的錯誤
public: FeatureTracker():max_count(500),qlevel(0.01),minDist(10.){} void process(Mat &frame,Mat &output){ //得到灰度圖
cvtColor (frame,gray,CV_BGR2GRAY); frame.copyTo (output); //特征點太少了,重新檢測特征點
if(addNewPoint()){ detectFeaturePoint (); //插入檢測到的特征點
points[0].insert (points[0].end (),features.begin (),features.end ()); initial.insert (initial.end (),features.begin (),features.end ()); } //第一幀
if(gray_prev.empty ()){ gray.copyTo (gray_prev); } //根據前后兩幀灰度圖估計前一幀特征點在當前幀的位置 //默認窗口是15*15
calcOpticalFlowPyrLK ( gray_prev,//前一幀灰度圖
gray,//當前幀灰度圖
points[0],//前一幀特征點位置
points[1],//當前幀特征點位置
status,//特征點被成功跟蹤的標志
err);//前一幀特征點點小區域和當前特征點小區域間的差,根據差的大小可刪除那些運動變化劇烈的點
int k = 0; //去除那些未移動的特征點
for(int i=0;i<points[1].size ();i++){ if(acceptTrackedPoint (i)){ initial[k]=initial[i]; points[1][k++] = points[1][i]; } } points[1].resize (k); initial.resize (k); //標記被跟蹤的特征點
handleTrackedPoint (frame,output); //為下一幀跟蹤初始化特征點集和灰度圖像
std::swap(points[1],points[0]); cv::swap(gray_prev,gray); } void detectFeaturePoint(){ goodFeaturesToTrack (gray,//圖片
features,//輸出特征點
max_count,//特征點最大數目
qlevel,//質量指標
minDist);//最小容忍距離
} bool addNewPoint(){ //若特征點數目少於10,則決定添加特征點
return points[0].size ()<=10; } //若特征點在前后兩幀移動了,則認為該點是目標點,且可被跟蹤
bool acceptTrackedPoint(int i){ return status[i]&& (abs(points[0][i].x-points[1][i].x)+ abs(points[0][i].y-points[1][i].y) >2); } //畫特征點
void handleTrackedPoint(Mat &frame,Mat &output){ for(int i=0;i<points[i].size ();i++){ //當前特征點到初始位置用直線表示
line(output,initial[i],points[1][i],Scalar::all (0)); //當前位置用圈標出
circle(output,points[1][i],3,Scalar::all(0),(-1)); } } }; class VideoProcessor{ private: VideoCapture caputure; //寫視頻流對象
VideoWriter writer; //輸出文件名
string Outputfile; int currentIndex; int digits; string extension; FrameProcessor *frameprocessor; //圖像處理函數指針
void (*process)(Mat &,Mat &); bool callIt; string WindowNameInput; string WindowNameOutput; //延時
int delay; long fnumber; //第frameToStop停止
long frameToStop; //暫停標志
bool stop; //圖像序列作為輸入視頻流
vector<string> images; //迭代器
public: VideoProcessor() : callIt(true),delay(0),fnumber(0),stop(false),digits(0),frameToStop(-1){} //設置圖像處理函數
void setFrameProcessor(void (*process)(Mat &,Mat &)){ frameprocessor = 0; this->process = process; CallProcess (); } //打開視頻
bool setInput(string filename){ fnumber = 0; //若已打開,釋放重新打開
caputure.release (); return caputure.open (filename); } //設置輸入視頻播放窗口
void displayInput(string wn){ WindowNameInput = wn; namedWindow (WindowNameInput); } //設置輸出視頻播放窗口
void displayOutput(string wn){ WindowNameOutput = wn; namedWindow (WindowNameOutput); } //銷毀窗口
void dontDisplay(){ destroyWindow (WindowNameInput); destroyWindow (WindowNameOutput); WindowNameInput.clear (); WindowNameOutput.clear (); } //啟動
void run(){ Mat frame; Mat output; if(!isOpened()) return; stop = false; while(!isStopped()){ //讀取下一幀
if(!readNextFrame(frame)) break; if(WindowNameInput.length ()!=0) imshow (WindowNameInput,frame); //處理該幀
if(callIt){ if(process) process(frame,output); else if(frameprocessor) frameprocessor->process (frame,output); } else{ output = frame; } if(Outputfile.length ()){ cvtColor (output,output,CV_GRAY2BGR); writeNextFrame (output); } if(WindowNameOutput.length ()!=0) imshow (WindowNameOutput,output); //按鍵暫停,繼續按鍵繼續
if(delay>=0&&waitKey (delay)>=0) waitKey(0); //到達指定暫停鍵,退出
if(frameToStop>=0&&getFrameNumber()==frameToStop) stopIt(); } } //暫停鍵置位
void stopIt(){ stop = true; } //查詢暫停標志位
bool isStopped(){ return stop; } //返回視頻打開標志
bool isOpened(){ return caputure.isOpened ()||!images.empty (); } //設置延時
void setDelay(int d){ delay = d; } //讀取下一幀
bool readNextFrame(Mat &frame){ if(images.size ()==0) return caputure.read (frame); else{ if(itImg!=images.end()){ frame = imread (*itImg); itImg++; return frame.data?1:0; } else
return false; } } void CallProcess(){ callIt = true; } void dontCallProcess(){ callIt = false; } //設置停止幀
void stopAtFrameNo(long frame){ frameToStop = frame; } // 獲得當前幀的位置
long getFrameNumber(){ long fnumber = static_cast<long>(caputure.get ((CV_CAP_PROP_POS_FRAMES))); return fnumber; } //獲得幀大小
Size getFrameSize() { if (images.size()==0) { // 從視頻流獲得幀大小
int w= static_cast<int>(caputure.get(CV_CAP_PROP_FRAME_WIDTH)); int h= static_cast<int>(caputure.get(CV_CAP_PROP_FRAME_HEIGHT)); return Size(w,h); } else { //從圖像獲得幀大小
cv::Mat tmp= cv::imread(images[0]); return (tmp.data)?(tmp.size()):(Size(0,0)); } } //獲取幀率
double getFrameRate(){ return caputure.get(CV_CAP_PROP_FPS); } vector<string>::const_iterator itImg; bool setInput (const vector<string> &imgs){ fnumber = 0; caputure.release (); images = imgs; itImg = images.begin (); return true; } void setFrameProcessor(FrameProcessor *frameprocessor){ process = 0; this->frameprocessor = frameprocessor; CallProcess (); } //獲得編碼類型
int getCodec(char codec[4]) { if (images.size()!=0) return -1; union { // 數據結構4-char
int value; char code[4]; } returned; //獲得編碼值
returned.value= static_cast<int>( caputure.get(CV_CAP_PROP_FOURCC)); // get the 4 characters
codec[0]= returned.code[0]; codec[1]= returned.code[1]; codec[2]= returned.code[2]; codec[3]= returned.code[3]; return returned.value; } bool setOutput(const string &filename,int codec = 0,double framerate = 0.0,bool isColor = true){ //設置文件名
Outputfile = filename; //清空擴展名
extension.clear (); //設置幀率
if(framerate ==0.0){ framerate = getFrameRate (); } //獲取輸入原視頻的編碼方式
char c[4]; if(codec==0){ codec = getCodec(c); } return writer.open(Outputfile, codec, framerate, getFrameSize(), isColor); } //輸出視頻幀到圖片fileme+currentIndex.ext,如filename001.jpg
bool setOutput (const string &filename,//路徑
const string &ext,//擴展名
int numberOfDigits=3,//數字位數
int startIndex=0 ){//起始索引
if(numberOfDigits<0) return false; Outputfile = filename; extension = ext; digits = numberOfDigits; currentIndex = startIndex; return true; } //寫下一幀
void writeNextFrame(Mat &frame){ //如果擴展名不為空,寫到圖片文件中
if(extension.length ()){ stringstream ss; ss<<Outputfile<<setfill('0')<<setw(digits)<<currentIndex++<<extension; imwrite (ss.str (),frame); } //反之,寫到視頻文件中
else{ writer.write (frame); } } }; //幀處理函數:canny邊緣檢測
void canny(cv::Mat& img, cv::Mat& out) { //灰度變換
if (img.channels()==3) cvtColor(img,out,CV_BGR2GRAY); // canny算子求邊緣
Canny(out,out,100,200); //顏色反轉,看起來更舒服些
threshold(out,out,128,255,cv::THRESH_BINARY_INV); } int main(int argc, char *argv[]) { VideoProcessor processor; FeatureTracker tracker; //打開輸入視頻
processor.setInput ("bike.avi"); processor.displayInput ("Current Frame"); processor.displayOutput ("Output Frame"); //設置每一幀的延時
processor.setDelay (1000./processor.getFrameRate ()); //設置幀處理函數,可以任意
processor.setFrameProcessor (&tracker); // processor.setOutput ("./bikeout.avi"); // processor.setOutput ("bikeout",".jpg");
processor.run (); return 0; }
運行結果:
六.參考文獻
【1】The classic article by B. Lucas and T. Kanade, An iterative image registration technique with an application to stereo vision in Int. Joint Conference in Artificial Intelligence, pp. 674-679,1981, that describes the original feature point tracking algorithm.
【2】The article by J. Shi and C. Tomasi, Good Features to Track in IEEE Conference on Computer Vision and Pattern Recognition, pp. 593-600, 1994, that describes an improved version of the original feature point tracking algorithm.