TLD 詳細解析之整體框架

下面是自己在看論文和這些大牛的分析過程中,對代碼進行了一些理解,但是由於自己接觸圖像處理和機器視覺沒多久,另外由於自己編程能力比較弱,所以分析過程可能會有不少的錯誤,希望各位不吝指正。而且,因爲編程很多地方不懂,所以註釋得非常亂,還海涵。

 

從main()函數切入,分析整個TLD運行過程如下:

(這裏只是分析工作過程,全部註釋的代碼見博客的更新)

1、分析程序運行的命令行參數;

./run_tld -p ../parameters.yml -s ../datasets/06_car/car.mpg -b ../datasets/06_car/init.txt –r

 

2、讀入初始化參數(程序中變量)的文件parameters.yml;

 

3、通過文件或者用戶鼠標框選的方式指定要跟蹤的目標的Bounding Box;

 

4、用上面得到的包含要跟蹤目標的Bounding  Box和第一幀圖像去初始化TLD系統,

   tld.init(last_gray, box, bb_file); 初始化包含的工作如下:

 

4.1、buildGrid(frame1, box);

檢測器採用掃描窗口的策略:掃描窗口步長爲寬高的 10%,尺度縮放係數爲1.2;此函數構建全部的掃描窗口grid,並計算每一個掃描窗口與輸入的目標box的重疊度;重疊度定義爲兩個box的交集與它們的並集的比;

 

4.2、爲各種變量或者容器分配內存空間;

 

4.3、getOverlappingBoxes(box, num_closest_init);

此函數根據傳入的box(目標邊界框),在整幀圖像中的全部掃描窗口中(由上面4.1得到)尋找與該box距離最小(即最相似,重疊度最大)的num_closest_init(10)個窗口,然後把這些窗口歸入good_boxes容器。同時,把重疊度小於0.2的,歸入bad_boxes容器;相當於對全部的掃描窗口進行篩選。並通過BBhull函數得到這些掃描窗口的最大邊界。

   

4.5、classifier.prepare(scales);

準備分類器,scales容器裏是所有掃描窗口的尺度,由上面的buildGrid()函數初始化;

TLD的分類器有三部分:方差分類器模塊、集合分類器模塊和最近鄰分類器模塊;這三個分類器是級聯的,每一個掃描窗口依次全部通過上面三個分類器,才被認爲含有前景目標。這裏prepare這個函數主要是初始化集合分類器模塊;

集合分類器(隨機森林)基於n個基本分類器(共10棵樹),每個分類器(樹)都是基於一個pixel comparisons(共13個像素比較集)的,也就是說每棵樹有13個判斷節點(組成一個pixel comparisons),輸入的圖像片與每一個判斷節點(相應像素點)進行比較,產生0或者1,然後將這13個0或者1連成一個13位的二進制碼x(有2^13種可能),每一個x對應一個後驗概率P(y|x)= #p/(#p+#n) (也有2^13種可能),#p和#n分別是正和負圖像片的數目。那麼整一個集合分類器(共10個基本分類器)就有10個後驗概率了,將10個後驗概率進行平均,如果大於閾值(一開始設經驗值0.65,後面再訓練優化)的話,就認爲該圖像片含有前景目標;

後驗概率P(y|x)= #p/(#p+#n)的產生方法:初始化時,每個後驗概率都得初始化爲0;運行時候以下面方式更新:將已知類別標籤的樣本(訓練樣本)通過n個分類器進行分類,如果分類結果錯誤,那麼相應的#p和#n就會更新,這樣P(y|x)也相應更新了。

pixel comparisons的產生方法:先用一個歸一化的patch去離散化像素空間,產生所有可能的垂直和水平的pixel comparisons,然後我們把這些pixel comparisons隨機分配給n個分類器,每個分類器得到完全不同的pixel comparisons(特徵集合),這樣,所有分類器的特徵組統一起來就可以覆蓋整個patch了。

特徵是相對於一種尺度的矩形框而言的,TLD中第s種尺度的第i個特徵features[s][i] = Feature(x1, y1, x2, y2);是兩個隨機分配的像素點座標(就是由這兩個像素點比較得到0或者1的)。每一種尺度的掃描窗口都含有totalFeatures = nstructs * structSize個特徵;nstructs爲樹木(由一個特徵組構建,每組特徵代表圖像塊的不同視圖表示)的個數;structSize爲每棵樹的特徵個數,也即每棵樹的判斷節點個數;樹上每一個特徵都作爲一個決策節點;

prepare函數的工作就是先給每一個掃描窗口初始化了對應的pixel comparisons(兩個隨機分配的像素點座標);然後初始化後驗概率爲0;

 

4.6、generatePositiveData(frame1, num_warps_init);

此函數通過對第一幀圖像的目標框box(用戶指定的要跟蹤的目標)進行仿射變換來合成訓練初始分類器的正樣本集。具體方法如下:先在距離初始的目標框最近的掃描窗口內選擇10個bounding box(已經由上面的getOverlappingBoxes函數得到,存於good_boxes裏面了,還記得不?),然後在每個bounding box的內部,進行±1%範圍的偏移,±1%範圍的尺度變化,±10%範圍的平面內旋轉,並且在每個像素上增加方差爲5的高斯噪聲(確切的大小是在指定的範圍內隨機選擇的),那麼每個box都進行20次這種幾何變換,那麼10個box將產生200個仿射變換的bounding box,作爲正樣本。具體實現如下:

getPattern(frame(best_box), pEx, mean, stdev);此函數將frame圖像best_box區域的圖像片歸一化爲均值爲0的15*15大小的patch,存於pEx(用於最近鄰分類器的正樣本)正樣本中(最近鄰的box的Pattern),該正樣本只有一個。

generator(frame, pt, warped, bbhull.size(), rng);此函數屬於PatchGenerator類的構造函數,用來對圖像區域進行仿射變換,先RNG一個隨機因子,再調用()運算符產生一個變換後的正樣本。

classifier.getFeatures(patch, grid[idx].sidx, fern);函數得到輸入的patch的特徵fern(13位的二進制代碼);

pX.push_back(make_pair(fern, 1));   //positive ferns <features, labels=1>然後標記爲正樣本,存入pX(用於集合分類器的正樣本)正樣本庫;

以上的操作會循環 num_warps * good_boxes.size()即20 * 10 次,這樣,pEx就有了一個正樣本,而pX有了200個正樣本了;

 

4.7、meanStdDev(frame1(best_box), mean, stdev);

統計best_box的均值和標準差,var = pow(stdev.val[0],2) * 0.5;作爲方差分類器的閾值。

 

4.8、generateNegativeData(frame1);

     由於TLD僅跟蹤一個目標,所以我們確定了目標框了,故除目標框外的其他圖像都是負樣本,無需仿射變換;具體實現如下:

     由於之前重疊度小於0.2的,都歸入 bad_boxes了,所以數量挺多,把方差大於var*0.5f的bad_boxes都加入負樣本,同上面一樣,需要classifier.getFeatures(patch, grid[idx].sidx, fern);和nX.push_back(make_pair(fern, 0));得到對應的fern特徵和標籤的nX負樣本(用於集合分類器的負樣本);

    然後隨機在上面的bad_boxes中取bad_patches(100個)個box,然後用 getPattern函數將frame圖像bad_box區域的圖像片歸一化到15*15大小的patch,存在nEx(用於最近鄰分類器的負樣本)負樣本中。

這樣nEx和nX都有負樣本了;(box的方差通過積分圖像計算)

 

4.9、然後將nEx的一半作爲訓練集nEx,另一半作爲測試集nExT;同樣,nX也拆分爲訓練集nX和測試集nXT;

 

4.10、將負樣本nX和正樣本pX合併到ferns_data[]中,用於集合分類器的訓練;

 

4.11、將上面得到的一個正樣本pEx和nEx合併到nn_data[]中,用於最近鄰分類器的訓練;

 

4.12、用上面的樣本訓練集訓練 集合分類器(森林) 和 最近鄰分類器:

  classifier.trainF(ferns_data, 2); //bootstrap = 2

對每一個樣本ferns_data[i] ,如果樣本是正樣本標籤,先用measure_forest函數返回該樣本所有樹的所有特徵值對應的後驗概率累加值,該累加值如果小於正樣本閾值(0.6* nstructs,這就表示平均值需要大於0.6(0.6* nstructs / nstructs),0.6是程序初始化時定的集合分類器的閾值,爲經驗值,後面會用測試集來評估修改,找到最優),也就是輸入的是正樣本,卻被分類成負樣本了,出現了分類錯誤,所以就把該樣本添加到正樣本庫,同時用update函數更新後驗概率。對於負樣本,同樣,如果出現負樣本分類錯誤,就添加到負樣本庫。

  classifier.trainNN(nn_data);

     對每一個樣本nn_data,如果標籤是正樣本,通過NNConf(nn_examples[i], isin, conf, dummy);計算輸入圖像片與在線模型之間的相關相似度conf,如果相關相似度小於0.65 ,則認爲其不含有前景目標,也就是分類錯誤了;這時候就把它加到正樣本庫。然後就通過pEx.push_back(nn_examples[i]);將該樣本添加到pEx正樣本庫中;同樣,如果出現負樣本分類錯誤,就添加到負樣本庫。

 

4.13、用測試集在上面得到的 集合分類器(森林) 和 最近鄰分類器中分類,評價並修改得到最好的分類器閾值。

  classifier.evaluateTh(nXT, nExT);

   對集合分類器,對每一個測試集nXT,所有基本分類器的後驗概率的平均值如果大於thr_fern(0.6),則認爲含有前景目標,然後取最大的平均值(大於thr_fern)作爲該集合分類器的新的閾值。

   對最近鄰分類器,對每一個測試集nExT,最大相關相似度如果大於nn_fern(0.65),則認爲含有前景目標,然後取最大的最大相關相似度(大於nn_fern)作爲該最近鄰分類器的新的閾值。

 

5、進入一個循環:讀入新的一幀,然後轉換爲灰度圖像,然後再處理每一幀processFrame;

 

6、processFrame(last_gray, current_gray, pts1, pts2, pbox, status, tl, bb_file);逐幀讀入圖片序列,進行算法處理。processFrame共包含四個模塊(依次處理):跟蹤模塊、檢測模塊、綜合模塊和學習模塊;

 

6.1、跟蹤模塊:track(img1, img2, points1, points2);

track函數完成前一幀img1的特徵點points1到當前幀img2的特徵點points2的跟蹤預測;

 

6.1.1、具體實現過程如下:

(1)先在lastbox中均勻採樣10*10=100個特徵點(網格均勻撒點),存於points1:

bbPoints(points1, lastbox);

(2)利用金字塔LK光流法跟蹤這些特徵點,並預測當前幀的特徵點(見下面的解釋)、計算FB error和匹配相似度sim,然後篩選出 FB_error[i] <= median(FB_error) 和 sim_error[i] > median(sim_error) 的特徵點(捨棄跟蹤結果不好的特徵點),剩下的是不到50%的特徵點

tracker.trackf2f(img1, img2, points, points2);

(3)利用剩下的這不到一半的跟蹤點輸入來預測bounding box在當前幀的位置和大小 tbb:

bbPredict(points, points2, lastbox, tbb);

(4)跟蹤失敗檢測:如果FB error的中值大於10個像素(經驗值),或者預測到的當前box的位置移出圖像,則認爲跟蹤錯誤,此時不返回bounding box:

if (tracker.getFB()>10 || tbb.x>img2.cols ||  tbb.y>img2.rows || tbb.br().x < 1 || tbb.br().y <1)

(5)歸一化img2(bb)對應的patch的size(放縮至patch_size = 15*15),存入pattern:

getPattern(img2(bb),pattern,mean,stdev);

(6)計算圖像片pattern到在線模型M的保守相似度:

classifier.NNConf(pattern,isin,dummy,tconf);

(7)如果保守相似度大於閾值,則評估本次跟蹤有效,否則跟蹤無效:

if (tconf>classifier.thr_nn_valid) tvalid =true;

 

6.1.2、TLD跟蹤模塊的實現原理和trackf2f函數的實現:

   TLD跟蹤模塊的實現是利用了Media Flow 中值光流跟蹤和跟蹤錯誤檢測算法的結合。中值流跟蹤方法是基於Forward-Backward Error和NNC的。原理很簡單:從t時刻的圖像的A點,跟蹤到t+1時刻的圖像B點;然後倒回來,從t+1時刻的圖像的B點往回跟蹤,假如跟蹤到t時刻的圖像的C點,這樣就產生了前向和後向兩個軌跡,比較t時刻中 A點和C點的距離,如果距離小於一個閾值,那麼就認爲前向跟蹤是正確的;這個距離就是FB_error;

bool LKTracker::trackf2f(const Mat& img1, const Mat& img2, vector<Point2f> &points1, vector<cv::Point2f> &points2)

函數實現過程如下:

(1)先利用金字塔LK光流法跟蹤預測前向軌跡:

  calcOpticalFlowPyrLK( img1,img2, points1, points2, status, similarity, window_size, level, term_criteria, lambda, 0);

(2)再往回跟蹤,產生後向軌跡:

  calcOpticalFlowPyrLK( img2,img1, points2, pointsFB, FB_status,FB_error, window_size, level, term_criteria, lambda, 0);

(3)然後計算 FB-error:前向與 後向 軌跡的誤差:

  for( int i= 0; i<points1.size(); ++i )

        FB_error[i] = norm(pointsFB[i]-points1[i]);     

(4)再從前一幀和當前幀圖像中(以每個特徵點爲中心)使用亞象素精度提取10x10象素矩形(使用函數getRectSubPix得到),匹配前一幀和當前幀中提取的10x10象素矩形,得到匹配後的映射圖像(調用matchTemplate),得到每一個點的NCC相關係數(也就是相似度大小)。

normCrossCorrelation(img1, img2, points1, points2);

(5)然後篩選出 FB_error[i] <= median(FB_error) 和 sim_error[i] > median(sim_error) 的特徵點(捨棄跟蹤結果不好的特徵點),剩下的是不到50%的特徵點;

filterPts(points1, points2);

 

6.2、檢測模塊:detect(img2);

TLD的檢測分類器有三部分:方差分類器模塊、集合分類器模塊和最近鄰分類器模塊;這三個分類器是級聯的。當前幀img2的每一個掃描窗口依次通過上面三個分類器,全部通過才被認爲含有前景目標。具體實現過程如下:

先計算img2的積分圖,爲了更快的計算方差:

integral(frame,iisum,iisqsum);

然後用高斯模糊,去噪:

  GaussianBlur(frame,img,Size(9,9),1.5); 

下一步就進入了方差檢測模塊:

 

6.2.1、方差分類器模塊:getVar(grid[i],iisum,iisqsum) >= var

利用積分圖計算每個待檢測窗口的方差,方差大於var閾值(目標patch方差的50%)的,則認爲其含有前景目標,通過該模塊的進入集合分類器模塊:

 

6.2.2、集合分類器模塊:

集合分類器(隨機森林)共有10顆樹(基本分類器),每棵樹13個判斷節點,每個判斷節點經比較得到一個二進制位0或者1,這樣每棵樹就對應得到一個13位的二進制碼x(葉子),這個二進制碼x對應於一個後驗概率P(y|x)。那麼整一個集合分類器(共10個基本分類器)就有10個後驗概率了,將10個後驗概率進行平均,如果大於閾值(一開始設經驗值0.65,後面再訓練優化)的話,就認爲該圖像片含有前景目標;具體過程如下:

(1)先得到該patch的特徵值(13位的二進制代碼):

classifier.getFeatures(patch,grid[i].sidx,ferns);

(2)再計算該特徵值對應的後驗概率累加值:

conf = classifier.measure_forest(ferns);           

(3)若集合分類器的後驗概率的平均值大於閾值fern_th(由訓練得到),就認爲含有前景目標:

if (conf > numtrees * fern_th)  dt.bb.push_back(i); 

(4)將通過以上兩個檢測模塊的掃描窗口記錄在detect structure中;

(5)如果順利通過以上兩個檢測模塊的掃描窗口數大於100個,則只取後驗概率大的前100個;

nth_element(dt.bb.begin(), dt.bb.begin()+100, dt.bb.end(),

CComparator(tmp.conf));

進入最近鄰分類器:

 

6.2.3、最近鄰分類器模塊

(1)先歸一化patch的size(放縮至patch_size = 15*15),存入dt.patch[i];

getPattern(patch,dt.patch[i],mean,stdev); 

(2)計算圖像片pattern到在線模型M的相關相似度和保守相似度:

classifier.NNConf(dt.patch[i],dt.isin[i],dt.conf1[i],dt.conf2[i]);

(3)相關相似度大於閾值,則認爲含有前景目標:

if (dt.conf1[i]>nn_th)  dbb.push_back(grid[idx]);

到目前爲止,檢測器檢測完成,全部通過三個檢測模塊的掃描窗口存在dbb中;

 

6.3、綜合模塊:

TLD只跟蹤單目標,所以綜合模塊綜合跟蹤器跟蹤到的單個目標和檢測器可能檢測到的多個目標,然後只輸出保守相似度最大的一個目標。具體實現過程如下:

(1)先通過 重疊度 對檢測器檢測到的目標bounding box進行聚類,每個類的重疊度小於0.5:

clusterConf(dbb, dconf, cbb, cconf);

(2)再找到與跟蹤器跟蹤到的box距離比較遠的類(檢測器檢測到的box),而且它的相關相似度比跟蹤器的要大:記錄滿足上述條件,也就是可信度比較高的目標box的個數:

if (bbOverlap(tbb, cbb[i])<0.5 && cconf[i]>tconf) confident_detections++;

(3)判斷如果只有一個滿足上述條件的box,那麼就用這個目標box來重新初始化跟蹤器(也就是用檢測器的結果去糾正跟蹤器):

if (confident_detections==1)  bbnext=cbb[didx];

(4)如果滿足上述條件的box不只一個,那麼就找到檢測器檢測到的box與跟蹤器預測到的box距離很近(重疊度大於0.7)的所以box,對其座標和大小進行累加:

if(bbOverlap(tbb,dbb[i])>0.7)  cx += dbb[i].x;……

(5)對與跟蹤器預測到的box距離很近的box 和 跟蹤器本身預測到的box 進行座標與大小的平均作爲最終的目標bounding box,但是跟蹤器的權值較大:

bbnext.x = cvRound((float)(10*tbb.x+cx)/(float)(10+close_detections));……

(6)另外,如果跟蹤器沒有跟蹤到目標,但是檢測器檢測到了一些可能的目標box,那麼同樣對其進行聚類,但只是簡單的將聚類的cbb[0]作爲新的跟蹤目標box(不比較相似度了??還是裏面已經排好序了??),重新初始化跟蹤器:

bbnext=cbb[0];

至此,綜合模塊結束。

 

6.4、學習模塊:learn(img2);

    學習模塊也分爲如下四部分:

6.4.1、檢查一致性:

(1)歸一化img(bb)對應的patch的size(放縮至patch_size = 15*15),存入pattern:

  getPattern(img(bb), pattern, mean, stdev);

(2)計算輸入圖像片(跟蹤器的目標box)與在線模型之間的相關相似度conf:

  classifier.NNConf(pattern,isin,conf,dummy);

(3)如果相似度太小了或者如果方差太小了或者如果被被識別爲負樣本,那麼就不訓練了;

if (conf<0.5)……或if (pow(stdev.val[0], 2)< var)……或if(isin[2]==1)……

 

6.4.2、生成樣本:

先是集合分類器的樣本:fern_examples:

(1)先計算所有的掃描窗口與目前的目標box的重疊度:

grid[i].overlap = bbOverlap(lastbox, grid[i]);

(2)再根據傳入的lastbox,在整幀圖像中的全部窗口中尋找與該lastbox距離最小(即最相似,重疊度最大)的num_closest_update個窗口,然後把這些窗口歸入good_boxes容器(只是把網格數組的索引存入)同時,把重疊度小於0.2的,歸入 bad_boxes 容器:

  getOverlappingBoxes(lastbox, num_closest_update);

(3)然後用仿射模型產生正樣本(類似於第一幀的方法,但只產生10*10=100個):

generatePositiveData(img, num_warps_update); 

(4)加入負樣本,相似度大於1??相似度不是出於0和1之間嗎?

idx=bad_boxes[i];

if (tmp.conf[idx]>=1) fern_examples.push_back(make_pair(tmp.patt[idx],0));

然後是最近鄰分類器的樣本:nn_examples:

if (bbOverlap(lastbox,grid[idx]) < bad_overlap)

        nn_examples.push_back(dt.patch[i]);

 

6.4.3、分類器訓練:

classifier.trainF(fern_examples,2);

classifier.trainNN(nn_examples);

 

6.4.4、把正樣本庫(在線模型)包含的所有正樣本顯示在窗口上

classifier.show();

至此,tld.processFrame函數結束。

 

7、如果跟蹤成功,則把相應的點和box畫出來:

    if (status){

      drawPoints(frame,pts1);

      drawPoints(frame,pts2,Scalar(0,255,0));  //當前的特徵點用藍色點表示

      drawBox(frame,pbox);

      detections++;

}

 

8、然後顯示窗口和交換圖像幀,進入下一幀的處理:

    imshow("TLD", frame);

swap(last_gray, current_gray);

至此,main()函數結束(只分析了框架)。

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