一、如何初始化第一幀及檢測人臉
迄今為止所介紹的人臉跟蹤方法都是假設圖像中所找到的面部特征與當前的估計比較接近。雖然整個跟蹤過程中幀之間的人臉變化相當小,這樣的假設可認為很合理,但必須要面對的問題是,如何用視頻序列的第一幀來初始化模型。解決該問題的簡單方法是使用 OpenCV 內置的級聯檢測器來搜索人臉。按照數據驅動的思路,通過學習訓練使我們的系統能夠學習人臉外界矩形與人臉跟蹤特征之間的幾何關系detector_offset 向量,然后利用該向量對人臉參考形狀矩陣 reference 進行仿射變換,獲得外界矩形區域內的人臉特征點。
face_detector 類完全實現了該解決方案。下面這段代碼描述了該類的主要功能:
1 class face_detector{ //face detector for initialisation 2 public: 3 string detector_fname; // 級聯分類器名稱 4 Vec3f detector_offset; //offset from center of detection,用來將之前訓練好的形狀模型鑲嵌到人臉上 5 Mat reference; // 參考形狀模型 6 CascadeClassifier detector; //face detector 7 8 vector<Point2f> //points for detected face in image 9 detect(const Mat &im, //image containing face 10 const float scaleFactor = 1.1, //scale increment 11 const int minNeighbours = 2, //minimum neighbourhood size 12 const Size minSize = Size(30,30));//minimum detection window size 13 14 void 15 train(ft_data &data, //training data 16 const string fname, //cascade detector 17 const Mat &ref, //reference shape 18 const bool mirror = false, //mirror data? 19 const bool visi = false, //visualise training? 20 const float frac = 0.8, //fraction of points in detected rect 21 const float scaleFactor = 1.1, //scale increment 22 const int minNeighbours = 2, //minimum neighbourhood size 23 const Size minSize = Size(30,30)); //minimum detection window size 24 ... 25 };
face_detector::detect 函數將一幅圖像作為輸入,還有一些 cv::CascadeClassifier 類使用的標准參數,該函數返回對圖像中面部特征位置的粗略估計。detect 函數的具體實現如下:
1 vector<Point2f> 2 face_detector:: 3 detect(const Mat &im, 4 const float scaleFactor, 5 const int minNeighbours, 6 const Size minSize) 7 { 8 //convert image to greyscale 9 Mat gray; if(im.channels()==1)gray = im; else cvtColor(im,gray,CV_RGB2GRAY); 10 11 //detect faces 12 vector<Rect> faces; Mat eqIm; equalizeHist(gray,eqIm); 13 detector.detectMultiScale(eqIm,faces,scaleFactor,minNeighbours,0 14 |CV_HAAR_FIND_BIGGEST_OBJECT // 只跟蹤圖像中最明顯的人臉 15 |CV_HAAR_SCALE_IMAGE,minSize); 16 if(faces.size() < 1){return vector<Point2f>();} 17 18 // 根據人臉檢測方框來將參考模型放置到圖像中 19 Rect R = faces[0]; // 人臉外界矩形 20 Vec3f scale = detector_offset*R.width; 21 int n = reference.rows/2; vector<Point2f> p(n); 22 for(int i = 0; i < n; i++){ 23 p[i].x = scale[2]*reference.fl(2*i ) + R.x + 0.5 * R.width + scale[0]; 24 p[i].y = scale[2]*reference.fl(2*i+1) + R.y + 0.5 * R.height + scale[1]; 25 }return p; 26 }
學習外界矩形與人臉特征點之間的幾何關系 detector_offset 可用 face_detector::train 函數實現,具體過程如下:
1. 加載級聯分類器
2. 對手工標注的每一幅圖片,使用級聯分類器搜索人臉區域
3. 判斷人臉的外接矩陣內是否包含足夠多的標注點(防止錯誤學習)
4. 如果包含足夠的標注點,則計算 detector_offset
代碼如下:
1 void 2 face_detector:: 3 train(ft_data &data, // 包含了手工標注信息 4 const string fname, // 級聯分類器名稱 5 const Mat &ref, // 參考形狀矩陣 6 const bool mirror, // 鏡像樣本圖像標記 7 const bool visi, // 訓練過程可視化標記 8 const float frac, // 有效特征點比率閾值 9 const float scaleFactor, 10 const int minNeighbours, 11 const Size minSize) 12 { 13 // 載入級聯分類器 14 detector.load(fname.c_str()); 15 detector_fname = fname; reference = ref.clone(); 16 vector<float> xoffset(0),yoffset(0),zoffset(0); 17 for(int i = 0; i < data.n_images(); i++){ 18 // 獲取每一張訓練圖片 19 Mat im = data.get_image(i,0); if(im.empty())continue; 20 // 獲取訓練圖片對應的特征點 21 vector<Point2f> p = data.get_points(i,false); int n = p.size(); 22 Mat pt = Mat(p).reshape(1,2*n); 23 vector<Rect> faces; Mat eqIm; 24 // 直方圖均衡化 25 equalizeHist(im,eqIm); 26 // 人臉檢測 27 detector.detectMultiScale(eqIm,faces,scaleFactor,minNeighbours,0 28 |CV_HAAR_FIND_BIGGEST_OBJECT 29 |CV_HAAR_SCALE_IMAGE,minSize); 30 if(faces.size() >= 1){ 31 if(visi){ 32 // 框出人臉區域 33 Mat I; cvtColor(im,I,CV_GRAY2RGB); 34 for(int i = 0; i < n; i++)circle(I,p[i],1,CV_RGB(0,255,0),2,CV_AA); 35 rectangle(I,faces[0].tl(),faces[0].br(),CV_RGB(255,0,0),3); 36 imshow("face detector training",I); waitKey(10); 37 } 38 //check if enough points are in detected rectangle 39 if(this->enough_bounded_points(pt,faces[0],frac)){ 40 Point2f center = this->center_of_mass(pt); float w = faces[0].width; 41 // X,Y 平移 Z 縮放比例 42 xoffset.push_back((center.x - (faces[0].x+0.5*faces[0].width ))/w); 43 yoffset.push_back((center.y - (faces[0].y+0.5*faces[0].height))/w); 44 zoffset.push_back(this->calc_scale(pt)/w); 45 } 46 } 47 if(mirror){ 48 im = data.get_image(i,1); if(im.empty())continue; 49 p = data.get_points(i,true); 50 pt = Mat(p).reshape(1,2*n); 51 equalizeHist(im,eqIm); 52 detector.detectMultiScale(eqIm,faces,scaleFactor,minNeighbours,0 53 |CV_HAAR_FIND_BIGGEST_OBJECT 54 |CV_HAAR_SCALE_IMAGE,minSize); 55 if(faces.size() >= 1){ 56 if(visi){ 57 Mat I; cvtColor(im,I,CV_GRAY2RGB); 58 for(int i = 0; i < n; i++)circle(I,p[i],1,CV_RGB(0,255,0),2,CV_AA); 59 rectangle(I,faces[0].tl(),faces[0].br(),CV_RGB(255,0,0),3); 60 imshow("face detector training",I); waitKey(10); 61 } 62 //check if enough points are in detected rectangle 63 if(this->enough_bounded_points(pt,faces[0],frac)){ 64 Point2f center = this->center_of_mass(pt); float w = faces[0].width; 65 xoffset.push_back((center.x - (faces[0].x+0.5*faces[0].width ))/w); 66 yoffset.push_back((center.y - (faces[0].y+0.5*faces[0].height))/w); 67 zoffset.push_back(this->calc_scale(pt)/w); 68 } 69 } 70 } 71 } 72 //choose median value 73 // 對 X、Y、Z集合分別按升序排序,取各自的中值 74 Mat X = Mat(xoffset),Xsort,Y = Mat(yoffset),Ysort,Z = Mat(zoffset),Zsort; 75 cv::sort(X,Xsort,CV_SORT_EVERY_COLUMN|CV_SORT_ASCENDING); int nx = Xsort.rows; 76 cv::sort(Y,Ysort,CV_SORT_EVERY_COLUMN|CV_SORT_ASCENDING); int ny = Ysort.rows; 77 cv::sort(Z,Zsort,CV_SORT_EVERY_COLUMN|CV_SORT_ASCENDING); int nz = Zsort.rows; 78 detector_offset = Vec3f(Xsort.fl(nx/2),Ysort.fl(ny/2),Zsort.fl(nz/2)); return; 79 }
在文件 train_face_detector.cpp 中有一個簡單的示例程序,該程序展示如何得到 face_detector 對象並保存這些對象以便用於后面的跟蹤系統。主要代碼如下:
1 //load data 2 ft_data data = load_ft<ft_data>(argv[2]); 3 shape_model smodel = load_ft<shape_model>(argv[3]); 4 smodel.set_identity_params(); 5 vector<Point2f> r = smodel.calc_shape(); 6 Mat ref = Mat(r).reshape(1,2*r.size()); 7 8 //train face detector 9 face_detector detector; 10 detector.train(data,argv[1],ref,mirror,true,frac); 11 12 //save detector 13 save_ft<face_detector>(argv[4],detector);
下面是其中兩張樣本圖像以及對應的標注點與人臉位置示意圖:
下面是保存 face_detector 對象的 detector_model.yaml 文件:
為了檢測形狀放置程序的性能,文件 visualize_face_detector.cpp 中的程序會調用 face_detector::detect 函數來處理視頻文件或攝像機輸入的視頻流,並將結果顯示在屏幕上。主要代碼如下:
1 //detect until user quits 2 namedWindow("face detector"); 3 while(cam.get(CV_CAP_PROP_POS_AVI_RATIO) < 0.999999){ 4 Mat im; cam >> im; 5 vector<Point2f> p = detector.detect(im); 6 if(p.size() > 0){ 7 for(int i = 0; i < int(p.size()); i++) 8 circle(im,p[i],1,CV_RGB(0,255,0),2,CV_AA); 9 } 10 imshow("face detector",im); 11 if(waitKey(10) == 'q')break; 12 }
運行效果圖如下: