機器學習實踐系列之5
提到 目標跟蹤(Object Tracking),很多專業人士都不陌生,它是計算機視覺裡面 用於視訊分析的一個很大的分類,就像目標檢測一樣,是視訊分析演算法的底層支撐。
目標跟蹤的演算法有很多,像 Mean-Shift、光流法、粒子濾波、卡爾曼濾波等 傳統方法,也有 TLD、CT、Struct、KCF 等摻雜了某些 “外力”,不那麼純粹的方法。但不管怎樣,Tracking這項工作有著很大的研究群體,也不乏有人為之奮鬥終生!
• CamShift
CamShift 是 連續自適應的Mean-Shift演算法(Continuously Adaptive Mean-SHIFT),作為入門級的目標跟蹤,包括兩部分:
1. 對視訊中的每一幀做 Mean-Shift;
2. 將上一幀的 Mean-Shift 結果 作為下一幀的輸入,反覆迭代;
演算法步驟非常簡單,問題可以演化為: Mean-Shift 是什麼?其輸入輸出又是什麼?
Mean-Shift 就是大名鼎鼎的 均值漂移,這裡面有兩層含義:
1)均值
空間R中有N個樣本點,任選一點x0,假定有k個點落在x的鄰域範圍(半徑h)內,那麼MeanShift向量可以定義為:
其中Sk是一個半徑為h的高維球區域,滿足公式:
說的再通俗一點,分兩步:
A) 任選空間內的一點x0,以該點為圓心做一個半徑h的球(可能擴充套件到高維),統計球內的所有點,為x0的鄰域點;
B) 圓心到鄰域點連線作為向量,所有向量和求均值,即得到 Mean-Shift 向量。
通過均值計算,我們得到一個(x0, x'0)的向量,如圖橘色箭頭。
2)漂移
將圓心 x0 移動到 Mean-Shift 向量 的終點
x'0 ,就是 漂移(比平移好聽)。以x'0 為新的中心,重複上面的過程,迭代計算,直到收斂。
Mean-Shift 這種特徵使得其在 目標跟蹤、聚類、影象平滑等問題上都有應用。
核心思想是 利用概率密度的梯度爬升 來尋找區域性最優。輸入一個影象的區域範圍,逐步迭代,對應區域朝 質心(重心)漂移。
概率密度函式 與 反向投影
事實上,我們需要將 待跟蹤的目標 ROI進行提取 直方圖,計算輸入影象對應直方圖的 反向投影,得到輸入影象在已知目標顏色直方圖的條件下的顏色概率密度分佈圖,包含了目標在當前幀中的相干資訊。
對於目標區域內的畫素,可得到該畫素屬於目標畫素的概率,而對於非目標區域內的畫素,該概率為0。
參考下面程式碼進行理解:
/* linolzhang 2013.11
CamShift跟蹤
*/
#include "opencv/cv.h"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/video/tracking.hpp"
#include "opencv2/highgui/highgui.hpp"
#pragma comment(lib,"opencv_core2410.lib")
#pragma comment(lib,"opencv_imgproc2410.lib")
#pragma comment(lib,"opencv_video2410.lib")
#pragma comment(lib,"opencv_highgui2410.lib")
#define SAT_MIN 65 // 定義最小飽和度,低於該飽和度的色調不穩定
#define V_MIN 10
using namespace cv;
IplImage* getHSV(const IplImage *img,IplImage **img_h,IplImage **img_s,IplImage **img_v)
{
IplImage *img_hsv = cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 3);
cvCvtColor(img,img_hsv,CV_BGR2HSV);
cvSplit(img_hsv, *img_h, img_s==NULL?NULL:*img_s,img_v==NULL?NULL:*img_v, NULL);
// 定義掩碼,只處理畫素值為H:0~180,S:SAT_MIN~255的部分
IplImage *img_msk = cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 1);
cvInRangeS(img_hsv, cvScalar(0,SAT_MIN,V_MIN,0),cvScalar(180,255,255,0), img_msk);
cvReleaseImage(&img_hsv);
return img_msk;
}
int main(int argc, char** argv)
{
IplImage *src = cvLoadImage("1.jpg", -1); // 載入源影象 - 包含待跟蹤目標
CvRect rcROI = cvRect(132,296,132,176); // 待跟蹤目標位置
// 1.將目標轉換到HSV空間,提取Hue分量
IplImage *src_h = cvCreateImage(cvGetSize(src), IPL_DEPTH_8U, 1);
IplImage *src_s = cvCreateImage(cvGetSize(src), IPL_DEPTH_8U, 1);
IplImage *hsv_mask = getHSV(src,&src_h,&src_s,NULL);
// 2.計算分量直方圖 - 1維
int hist_size = 256; // 64 | 128 | 256
float range[] = {0,180};
float *ranges[] ={ range };
CvHistogram *hist = cvCreateHist(1, &hist_size, CV_HIST_ARRAY, ranges);
cvSetImageROI(src,rcROI); // 設定源影象ROI
cvCalcHist(&src_h, hist,0,hsv_mask);
cvReleaseImage(&hsv_mask);
cvResetImageROI(src); // 清空ROI
// 3.計算反向投影,得到概率密度圖 img_prob
IplImage *dest = cvLoadImage("2.jpg", -1); // 從這幅圖搜尋
IplImage *dest_h = cvCreateImage(cvGetSize(dest), IPL_DEPTH_8U, 1);
IplImage *dest_s = cvCreateImage(cvGetSize(dest), IPL_DEPTH_8U, 1);
hsv_mask = getHSV(dest,&dest_h,&dest_s,NULL);
IplImage *img_prob = cvCreateImage(cvGetSize(dest),IPL_DEPTH_8U,1);
cvCalcBackProject(&dest_h,img_prob,hist);
cvAnd(img_prob, hsv_mask, img_prob, 0);
cvReleaseImage(&hsv_mask);
// 4.呼叫MeanShift函式計算
// int cvMeanShift(IplImage* imgprob,CvRect windowIn,CvTermCriteria criteria,CvConnectedComp* out);
// 引數說明:imgprob: 2D概率密度圖 windowIn:初始視窗 criteria:迭代終止條件 out:輸出結果
CvConnectedComp conn_comp;
cvMeanShift(img_prob,rcROI, cvTermCriteria(CV_TERMCRIT_ITER,10,0.1), &conn_comp);
//CvBox2D track_box;
//cvCamShift(img_prob,rcROI,cvTermCriteria(CV_TERMCRIT_ITER, 100, 0.01 ),&conn_comp, &track_box );
// 5.繪製結果
cvRectangle( src,cvPoint(rcROI.x,rcROI.y),cvPoint(rcROI.x+rcROI.width,rcROI.y+rcROI.height),cvScalar(0,255,0) );
cvShowImage("源影象",src);
cvShowImage("概率密度圖",img_prob);
cvRectangle( dest,cvPoint(rcROI.x,rcROI.y),cvPoint(rcROI.x+rcROI.width,rcROI.y+rcROI.height),cvScalar(0,255,0) );
CvRect& rc = conn_comp.rect;
cvRectangle( dest,cvPoint(rc.x,rc.y),cvPoint(rc.x+rc.width, rc.y+rc.height),cvScalar(255,255,0) );
cvShowImage("Track結果",dest);
cvWaitKey(0);
// 釋放資源
cvReleaseImage(&src);
cvReleaseImage(&dest);
cvReleaseImage(&img_prob);
return 0;
}
CamShift 演算法在 Mean-Shift 演算法基礎上進行了改進:
連續自適應:利用前一幀的目標尺寸調節搜尋視窗大小,對有尺寸變化的目標可準確定位;
不過,CamShift 演算法在計算目標模板直方圖分佈時,沒有使用核函式進行加權處理,也就是說目標區域內的每個畫素點在目標模型中有著相同的權重,故演算法的抗噪能力低於Mean-Shift 演算法。
另外,CamShift 演算法仍然採用目標的色彩資訊來進行跟蹤(Hue in HSV),很難處理目標與背景顏色(或其他物件)相近的情況,跟蹤結果的魯棒性仍然較差。
Cam Shift 通常用於單目標跟蹤,雖然也有人進行過多目標跟蹤擴充套件,不過效果並不好。
• TLD
純粹的跟蹤已經out了,Tracking by Detecting 才是主流。
TLD(Tracking-Learning-Detection)是一種新的單目標長時間跟蹤演算法。該演算法的貢獻在於 將傳統的跟蹤演算法 和檢測演算法 相結合解決 目標在跟蹤過程中發生的形變、部分遮擋等問題。
Ubuntu下編譯過程,進入OpenTLD-master目錄:
1)$mkdir build
2)$cd build
3)$cmake ../src
4)make
可能會提示錯誤 PatchGenerator不是cv的一個成員,這是OpenCV版本過高導致的相容問題(原版本是2.3),可以在TLD.h檔案標頭檔案新增:
#include <opencv2/legacy/legacy.hpp>
除錯執行:
5)cd .. # 進入程式根目錄
6)sudo bin/run_tld -p parameters.yml -s datasets/06_car/car.mpg
能夠看到,TLD的跟蹤效果還是很不錯的。
TLD演算法原理:
1)通過 跟蹤器 對目標進行跟蹤,作者採用的是 光流法(Lucas-Kanade),這裡作者引用了一個FB誤差,可以描述為:
a)在 t 時刻,目標框隨機初始化跟蹤點,通過正向追蹤得到 t+1 時刻的目標位置;
b)反向追蹤到 t 時刻,計算誤差,選擇其中誤差最小的一半點作為最佳跟蹤點;
類似 RanSac,找一些對結果貢獻大的點。
示意如下圖所示:
2)通過 檢測器 對目標進行檢測,作者使用了一個 級聯分類器 作為Detector,其中用到了隨機蕨(Random Ferns),這裡不再多說;
3)通過 學習器 線上學習目標特徵,根據上面 跟蹤器和檢測器 獲得的正負樣本進行線上訓練,學習目標特徵,並將訓練特徵更新到檢測器。
話說Online 真是個好思路,都在用,由於光線、遮擋、觀測角度等原因,在運動過程中目標特徵會有所變化。
演算法不算複雜,這裡面有個關鍵,就是作者提出的 P-N學習(P-N Learning)方法。
P-N學習 針對 檢測器 對樣本分類時產生的兩種錯誤提供了兩種“專家”進行糾正:
P專家(P-expert):檢出漏檢(false negative,正樣本誤分為負樣本)的正樣本;
N專家(N-expert):改正誤檢(false positive,負樣本誤分為正樣本)的正樣本。
說的通俗點就是,將檢測結果和跟蹤結果進行整合,哪個好用哪個,P-N學習 是一個裁判員。
TLD 方法思想是非常值得借鑑的,當然這裡面的 檢測、跟蹤、特徵學習 環節都可以基於你的需要進行修改和替換,畢竟TLD也已經不新了,你可以用深度學習的方法,也可以用效率更高的方法,Whatever。
• CT
壓縮跟蹤(Compressive Tracking) 是一種基於壓縮感知的跟蹤演算法,來看作者(Kaihua Zhang,香港理工大學) 對該演算法的簡介:
首先利用符合壓縮感知 RIP條件的隨機感知矩對多尺度影象特徵進行降維,然後在降維後的特徵上採用簡單的樸素貝葉斯分類器進行分類。
核心點:
可以看到,壓縮跟蹤 方法是一種 Tracking by Detecting 的思路,實際就是在 目標鄰域進行檢測。 壓縮跟蹤 的關鍵在於壓縮,也就是降維,壓縮感知有很多 Blog進行了介紹,可以自己學習一下(給個參考):1. 在 t 時刻,進行影象取樣(Patch),得到若干 正樣本(目標)和 負樣本(背景),通過金字塔變換得到多尺度特徵;
2. 通過 稀疏測量矩陣M 對多尺度影象特徵降維,然後利用降維後的特徵 訓練 分類器C(作者用了樸素貝葉斯);
3. 在 t+1 時刻,在目標位置周圍取樣N個Candidate(鄰域原則),同樣通過 稀疏測量矩陣M 對其降維,提取特徵;
4. 用 t 時刻用 分類器C 進行分類,Score最大的視窗就是目標視窗。
• Struct
來自一篇2011年的ICCV:
Struck:Structured Output Tracking with Kernels
這裡面用到了 高斯核函式,具體方法作者並沒有詳細研究,請自行腦補吧。
• KCF
KCF是一個非常經典的演算法(kernelized correlation filters),速度快、效果好,來自論文:
High-speed tracking with kernelized correlation filters(ECCV 2012, TPAMI 2015)
KCF演算法的主要貢獻:
1. 使用目標周圍區域的迴圈矩陣採集正負樣本,利用嶺迴歸訓練目標檢測器;
演算法利用 迴圈矩陣在傅立葉空間可對角化的性質將矩陣的運算轉化為向量的Hadamad積,即元素的點乘,大大降低了運算量。
2. 將線性空間的嶺迴歸通過核函式對映到非線性空間,在非線性空間通過求解一個對偶問題和某些常見的約束,同樣的可以使用迴圈矩陣傅立葉空間對角化簡化計算。
3. 給出了一種將多通道資料融入該演算法的途徑。
OpenCV3.1.0 在 contrib裡提供了KCF的實現,需要用CMake重新編譯,這裡作者用的是VS2013(對應vc12)。
配置編譯步驟:
1. 下載及專案配置
選擇Source及binaries(生成位置),指定generator,Finish完成配置,如下圖所示:
2. 新增編譯選項
找到OPENCV_EXTRA_MODULES_PATH,加入opencv_contrib目錄。
例如:作者的opencv_contrib 路徑為D:/opencv3.1/opencv_contrib-master/modules
編譯並生成,configure & generate
3. 配置VS2013
配置標頭檔案 和 庫檔案,設定環境dll,除錯執行程式
參考程式碼:
#include <opencv2/core/utility.hpp>
#include <opencv2/tracking.hpp>
#include <opencv2/videoio.hpp>
#include <opencv2/highgui.hpp>
#include <iostream>
using namespace std;
using namespace cv;
int main()
{
Rect2d roi;
Mat frame;
// create a tracker object
Ptr<Tracker> tracker = Tracker::create("KCF");
VideoCapture cap("1.avi");
cap >> frame;
// [selectroi]選擇目標roi以GUI的形式
roi = selectROI("tracker", frame);
if (roi.width == 0 || roi.height == 0) // Invalid ROI
return 0;
// init
tracker->init(frame, roi);
// perform the tracking process
printf("Start the tracking process\n");
while(true)
{
cap >> frame;
if (frame.rows == 0 || frame.cols == 0)
break;
tracker->update(frame, roi);
rectangle(frame, roi, Scalar(255, 0, 0), 2, 1); // draw roi
imshow("tracker", frame); // show image
if(waitKey(1) == 27)
break;
}
return 0;
}