人臉跟蹤問題可認為是尋找一種高效和魯棒性的方法,它能將各種面部特征的單獨檢測與這些特征的幾何依賴性結合起來,已得到連續幀中每幅圖像面部特征位置的精確估計。基於此,需仔細考慮幾何依賴性的必要性。下圖為用幾何約束和不用幾何約束所檢測出來的面部特征。
該結果清楚地說明利用空間上面部特征的相互依賴性非常有好處。兩種方法的相對性能會因檢測結果噪聲過多而受影響,其原因在於對於每一個每一個面部特征的響應矩陣達到最大值時並不總是在正確的位置。但面部特征檢測器要解決因圖象噪聲、光照變化、表情變化等帶來的問題,僅有的方法是利用特征之間彼此共享的幾何關系。
有一個特別簡單,但非常有效的方法可將面部幾何依賴加到跟蹤過程中,該方法將人臉特征提取的結果投影到形狀模型的線性子空間(shape model),也就是最小化原始點集到其在人臉子空間最接近合理形狀分布的投影點集的距離。(就是說,把通過模版匹配檢測到的原始點集 A 投影到人臉子空間產生新的點集 B,再按照某種約束規則,通過對 A 迭代變化,使得 A’到 B 的距離最小)。
一、人臉跟蹤實現
人臉跟蹤算法的實現可以在 face_tracker 類中找到(見 face_tracker.cpp 和 face_tracker.hpp 文件)。下面這段代碼來自其頭文件,這展示了該類的主要功能:
1 class face_tracker{ //face tracking class 2 public: 3 bool tracking; //are we in tracking mode? 4 fps_timer timer; // 用來保存幀速率的變化 5 vector<Point2f> points; //current tracked points 6 face_detector detector; //detector for initialisation 7 shape_model smodel; //shape model 8 patch_models pmodel; //feature detectors 9 10 face_tracker(){tracking = false;} 11 12 int //0 = failure 13 track(const Mat &im, //image containing face 14 const face_tracker_params &p = //fitting parameters 15 face_tracker_params()); //default tracking parameters 16 17 void 18 reset(){ //reset tracker 19 tracking = false; timer.reset(); 20 } 21 .... 22 protected: 23 vector<Point2f> //points for fitted face in image 24 fit(const Mat &image, //image containing face 25 const vector<Point2f> &init, //initial point estimates 26 const Size ssize = Size(21,21), //search region size 27 const bool robust = false, //use robust fitting? 28 const int itol = 10, //maximum number of iterations to try 29 const float ftol = 1e-3); //convergence tolerance 30 };
face_tracker::track 函數,有兩種功能。當 traking 標志位為 false 時,程序屬於構建模型(detectmode)階段,為第一幀或下一幀圖像初始化的人臉特征,所用技術就是上面上一節所講的;當 tracking 標志位為 true 時,則根據上一幀人臉特征點的位置估計下一幀的人臉特征,這個操作主要由 fit 函數完成。代碼如下:
1 int 2 face_tracker:: 3 track(const Mat &im,const face_tracker_params &p) 4 { 5 //convert image to greyscale 6 Mat gray; if(im.channels()==1)gray = im; else cvtColor(im,gray,CV_RGB2GRAY); 7 8 //initialise,為第一幀或下一幀初始化人臉特征 9 if(!tracking) 10 points = detector.detect(gray,p.scaleFactor,p.minNeighbours,p.minSize); 11 if((int)points.size() != smodel.npts())return 0; 12 13 //fit,通過迭代縮小的搜索范圍,估計當前幀中的人臉特征點 14 for(int level = 0; level < int(p.ssize.size()); level++) 15 points = this->fit(gray,points,p.ssize[level],p.robust,p.itol,p.ftol); 16 17 //set tracking flag and increment timer 18 tracking = true; timer.increment(); return 1; 19 }
face_tracker::fit 函數的主要功能:給定一幀圖像及上一幀人臉特征點集,在當前圖像上搜索該點集附近的人臉特征,並產生新的人臉特征點集。
1 vector<Point2f> 2 face_tracker:: 3 fit(const Mat &image, // 當前幀灰度圖像 4 const vector<Point2f> &init, // 上一幀人臉特征點集(幾何位置) 5 const Size ssize, // 搜索區域大小 6 const bool robust, // 標志位,決定是否采用 robustmodel fitting 流程,應對人臉特征的孤立點 7 const int itol, // robustmodel fitting 迭代上限 8 const float ftol) // 迭代收斂判斷閾值 9 { 10 int n = smodel.npts(); // number of points int the shape model 11 assert((int(init.size())==n) && (pmodel.n_patches()==n)); 12 smodel.calc_params(init); vector<Point2f> pts = smodel.calc_shape(); 13 14 //find facial features in image around current estimates 15 vector<Point2f> peaks = pmodel.calc_peaks(image,pts,ssize); 16 17 //optimise 18 if(!robust){ 19 smodel.calc_params(peaks); //compute shape model parameters 20 pts = smodel.calc_shape(); //update shape 21 }else{ 22 Mat weight(n,1,CV_32F),weight_sort(n,1,CV_32F); 23 vector<Point2f> pts_old = pts; 24 for(int iter = 0; iter < itol; iter++){ 25 //compute robust weight 26 for(int i = 0; i < n; i++)weight.fl(i) = norm(pts[i] - peaks[i]); 27 cv::sort(weight,weight_sort,CV_SORT_EVERY_COLUMN|CV_SORT_ASCENDING); 28 double var = 1.4826*weight_sort.fl(n/2); if(var < 0.1)var = 0.1; 29 pow(weight,2,weight); weight *= -0.5/(var*var); cv::exp(weight,weight); 30 31 //compute shape model parameters 32 smodel.calc_params(peaks,weight); 33 34 //update shape 35 pts = smodel.calc_shape(); 36 37 //check for convergence 38 float v = 0; for(int i = 0; i < n; i++)v += norm(pts[i]-pts_old[i]); 39 if(v < ftol)break; else pts_old = pts; 40 } 41 }return pts; 42 }
上面代碼中,有兩個函數:
-
- shape_model::calc_param,為了得到合理的人臉總空間投影,我們需要求參數向量 p,該函數通過人臉子空間投影坐標集合 pts 與人臉特征空間標准基 V,計算得到參數向量
1 void 2 shape_model:: 3 calc_params(const vector<Point2f> &pts,const Mat weight,const float c_factor) 4 { 5 int n = pts.size(); assert(V.rows == 2*n); 6 Mat s = Mat(pts).reshape(1,2*n); //point set to vector format 7 if(weight.empty())p = V.t()*s; //simple projection 8 else{ //scaled projection 9 if(weight.rows != n){cout << "Invalid weighting matrix" << endl; abort();} 10 int K = V.cols; Mat H = Mat::zeros(K,K,CV_32F),g = Mat::zeros(K,1,CV_32F); 11 for(int i = 0; i < n; i++){ 12 Mat v = V(Rect(0,2*i,K,2)); float w = weight.fl(i); 13 H += w*v.t()*v; g += w*v.t()*Mat(pts[i]); 14 } 15 solve(H,g,p,DECOMP_SVD); 16 }this->clamp(c_factor); //clamp resulting parameters 17 }
- shape_model::calc_peaks,根據人臉子空間點集在當前圖像內搜索人臉特征,並產生新的人臉特征位置估計
1 vector<Point2f> 2 patch_models:: 3 calc_peaks(const Mat &im, // 當前包含人臉的灰度圖像 4 const vector<Point2f> &points, // 前一幀估計的人臉特征點集在人臉子空間投影坐標集合 5 const Size ssize) // 搜索區域大小 6 { 7 int n = points.size(); assert(n == int(patches.size())); 8 Mat pt = Mat(points).reshape(1,2*n); 9 Mat S = this->calc_simil(pt); // 計算當前點集到人臉參考模型的變化矩陣 10 Mat Si = this->inv_simil(S); // 對矩陣 S 求逆 11 // 人臉子空間坐標經過仿射變換轉成圖像空間中的坐標 12 vector<Point2f> pts = this->apply_simil(Si,points); 13 for(int i = 0; i < n; i++){ 14 Size wsize = ssize + patches[i].patch_size(); Mat A(2,3,CV_32F); 15 A.fl(0,0) = S.fl(0,0); A.fl(0,1) = S.fl(0,1); 16 A.fl(1,0) = S.fl(1,0); A.fl(1,1) = S.fl(1,1); 17 A.fl(0,2) = pt.fl(2*i ) - 18 (A.fl(0,0) * (wsize.width-1)/2 + A.fl(0,1)*(wsize.height-1)/2); 19 A.fl(1,2) = pt.fl(2*i+1) - 20 (A.fl(1,0) * (wsize.width-1)/2 + A.fl(1,1)*(wsize.height-1)/2); 21 Mat I; warpAffine(im,I,A,wsize,INTER_LINEAR+WARP_INVERSE_MAP); 22 // 搜索人臉特征的匹配位置 23 Mat R = patches[i].calc_response(I,false); 24 Point maxLoc; minMaxLoc(R,0,0,0,&maxLoc); 25 // 修正人臉特征估計點位置 26 pts[i] = Point2f(pts[i].x + maxLoc.x - 0.5*ssize.width, 27 pts[i].y + maxLoc.y - 0.5*ssize.height); 28 } 29 // 再次將圖像中的坐標投影到人臉特征子空間中,作為下一幀特征估計點位置 30 return this->apply_simil(S,pts); 31 }
- shape_model::calc_param,為了得到合理的人臉總空間投影,我們需要求參數向量 p,該函數通過人臉子空間投影坐標集合 pts 與人臉特征空間標准基 V,計算得到參數向量
在對每幀圖像進行人臉跟蹤時,track 函數都會通過 fit 函數迭代產生多個人臉子空間坐標集合,並且每次迭代的時候,搜索區域都在減小。在迭代過程中,可能會產生很多孤立的特征點(錯誤估計點)。為了得到更精確的人臉跟蹤效果,如果存在孤立點時,仍采用簡單投影,會嚴重影響跟蹤效果。因此,在計算投影參數 calc_param 時引入了權重,搞了一套 robust model fitting 流程,特意去除孤立點。
二、訓練與可視化
訓練一個 face_tracker 對象不會涉及任何的學習過程。在 train_face_tracker.cpp 中簡單實現了該功能,代碼如下:
1 //create face tracker model 2 face_tracker tracker; 3 tracker.smodel = load_ft<shape_model>(argv[1]); // 加載 shape_model 對象 4 tracker.pmodel = load_ft<patch_models>(argv[2]); // 加載 patch_model 對象 5 tracker.detector = load_ft<face_detector>(argv[3]); // 加載 face_detector 對象 6 7 //save face tracker 8 save_ft<face_tracker>(argv[4],tracker); // 保存 face_tracker 對象
將 face_tracker 對象保存到 tracker_model.yaml 中,如下:
在 visualize_face_tracker.cpp 文件中,可視化程序將 cv::VideoCapture 類從攝像機或視頻文件的圖像流作為輸入,該程序有一個簡單的循環,該循環在讀到圖像流最后或用戶按 Q 鍵就會終止,否則就會跟蹤出現的每一幀。用戶隨時可按 D 鍵來重置跟蹤選項。主要代碼如下:
1 while(cam.get(CV_CAP_PROP_POS_AVI_RATIO) < 0.999999){ 2 Mat im; cam >> im; 3 if(tracker.track(im,p))tracker.draw(im); 4 draw_string(im,"d - redetection"); 5 tracker.timer.display_fps(im,Point(1,im.rows-1)); 6 imshow("face tracker",im); 7 int c = waitKey(10); 8 if(c == 'q')break; 9 else if(c == 'd')tracker.reset(); 10 }
運行效果如下:
Ending !!!