非剛性人臉跟蹤 —— 實用工具


面向對象設計

  與人臉檢測和人臉識別一樣,人臉跟蹤也由兩部分組成:數據和算法。算法通過預先儲存(即離線)的數據來訓練模型,然后對新來的(即在線)數據執行某類操作。因此,采用面向對象設計是不錯的選擇。

  在 opencv 2.x 版本中,可方便引入 XML/YAML 文件存儲類型,對算法來講,會大大簡化組織離線數據任務。下面通過一個假象類來展示這個功能

  

  • 自定義類 foo
     1 // foo.h
     2 /*
     3     在下面的代碼中,定義了一個序列化函數,可對 I/O 函數 read 和 write 實現序列化。
     4     FileStorage 類支持兩種能被序列化的數據結構類型。
     5     為了簡單起見,本章所有類將采用映射,其中每個用於存儲的變量都會創建一個 FileNode::MAP 類型的 FileNode 對象。
     6     這需要分配給變量中的每個元素唯一鍵。為了保持一致性,將變量名作為標簽
     7 */
     8 
     9 #include <opencv2/opencv.hpp>
    10 #include <iostream>
    11 using namespace cv;
    12 using namespace std;
    13 
    14 class foo {
    15 public:
    16     int a, b;        
    17     void write(FileStorage &fs) const {            // 序列化存儲自定義數據類型
    18         assert(fs.isOpened());
    19         fs << "{" << "a" << a << "b" << b << "}";        // 創建 FileNode::MAP 類型的對象
    20     }
    21     void read(const FileNode& node) {            // 讀取數據
    22         assert(node.type() == FileNode::MAP);
    23         node["a"] >> a;    node["b"] >> b;
    24     }
    25 };
  • 為了使 FileStorage 類的序列化能正常工作,還需要定義write, read函數
     1 template<class T>
     2 void 
     3 write(FileStorage& fs, 
     4       const string&, 
     5       const T& x)
     6 {
     7   x.write(fs);
     8 }
     9 //==============================================================================
    10 template<class T>
    11 void 
    12 read(const FileNode& node, 
    13      T& x,
    14      const T& d)
    15 {
    16   if(node.empty())x = d; else x.read(node);
    17 }

     

 

  • 為了讓保存和加載采用了序列化的用戶自定義類變得容易,采用模塊化函數定義了load_ft,save_ft函數
     1 template <class T> 
     2 T load_ft(const char* fname){
     3   T x; FileStorage f(fname,FileStorage::READ);
     4   f["ft object"] >> x; f.release(); return x;    // 定義與對象關聯的標簽都為 ft object
     5 }
     6 //==============================================================================
     7 template<class T>
     8 void save_ft(const char* fname,const T& x){
     9   FileStorage f(fname,FileStorage::WRITE);
    10   f << "ft object" << x; f.release();
    11 }
  • 將以上定義在 ft.hpp 中
     1 /*
     2     ft.hpp
     3     用於加載、保存對象數據
     4 */
     5 
     6 #ifndef _FT_FT_HPP_
     7 #define _FT_FT_HPP_
     8 #include <opencv2/opencv.hpp> 
     9 //==============================================================================
    10 // 為了讓保存和加載采用了序列化的用戶自定義類變得容易,采用模塊化函數定義了load_ft,save_ft函數
    11 template <class T> 
    12 T load_ft(const char* fname){
    13   T x; FileStorage f(fname,FileStorage::READ);
    14   f["ft object"] >> x; f.release(); return x;    // 定義與對象關聯的標簽都為 ft object
    15 }
    16 //==============================================================================
    17 template<class T>
    18 void save_ft(const char* fname,const T& x){
    19   FileStorage f(fname,FileStorage::WRITE);
    20   f << "ft object" << x; f.release();
    21 }
    22 //==============================================================================
    23 // 為了使 FileStorage 類的序列化能正常工作,還需要定義write, read函數
    24 template<class T>
    25 void 
    26 write(FileStorage& fs, 
    27       const string&, 
    28       const T& x)
    29 {
    30   x.write(fs);
    31 }
    32 //==============================================================================
    33 template<class T>
    34 void 
    35 read(const FileNode& node, 
    36      T& x,
    37      const T& d)
    38 {
    39   if(node.empty())x = d; else x.read(node);
    40 }
    41 //==============================================================================
    42 #endif
    ft.hpp
  • 主函數,有一個問題,儲存到 xml 文件總是報錯,而 yaml 文件可以正常存取
     1 /*
     2     main.cpp
     3     測試 opencv 文件儲存
     4 */
     5 
     6 #include "opencv_hotshots/ft/ft.hpp"
     7 #include "foo.h"
     8 
     9 int main() {
    10     foo A;                // 初始化自定義對象 A
    11     A.a = 1; A.b = 2;
    12     save_ft<foo>("foo.yaml", A);    // 將自定義對象存到 foo.yaml
    13     foo B = load_ft<foo>("foo.yaml");    // 讀取對象
    14     cout << B.a << "," << B.b << endl;
    15 
    16     system("pause");
    17     return 0;
    18 }
  • 程序運行結果

                                  

 

 

 

數據收集:圖像和視頻標注

  現代人臉跟蹤技術幾乎完全是數據驅動,即用來檢測圖像中面部特征位置的算法依靠面部特征的外觀模型和幾何依賴性,該依賴性來自樣本集中人臉間的相對位置。樣本集越大,算法就更具有魯棒性,因為人臉所表現出的變化范圍就更清楚。因此,構建人臉跟蹤算法的第一步是創建用於進行圖像/視頻的標注工具,用戶可用此工具來指定在每個樣本圖中想要的面部特征位置。

  1. 訓練數據類型

  訓練人臉跟蹤算法的數據一般由以下四部分構成:

    • 圖像:這部分是包含整個人臉圖像(圖像或視頻幀)的集合
    • 標注:這部分采用手工方法標注每幅圖像中被跟蹤的面部特征的相對位置
    • 對稱性索引:這部分對定義了雙邊對稱特征的面部特征點都保留了一個編號,以便用來鏡像訓練圖像,可有效地讓訓練集大小增加一倍
    • 連通性索引:這部分是一組標注的索引對,它們定義了面部特征的語義解釋。連通性對可視化跟蹤結果很有用

  這四個組件的可視化情形顯示在下圖中,從左到右依次是原始圖像、臉部特征標注、顏色編碼的雙邊對稱點、鏡像圖像與相應標注、面部特征的連通性。

      

 

  為了方便管理這種數據,需實現具有讀寫功能的類。本章將使用在 ft_data.hpp 頭文件中定義的 ft_data 類,它是按人臉跟蹤數據的特性專門設計的。所有元素都定義成類的公有成員變量,如下所示

1 class ft_data{                             //人臉跟蹤數據
2 public:
3   vector<int> symmetry;                    // 人臉特征點的索引,維數與用戶定義的特征點數一樣
4   vector<Vec2i> connections;               // 定義一對連通的面部特征
5   vector<string> imnames;                  // 存儲每個圖像文件名
6   vector<vector<Point2f> > points;         // 存儲特征點的位置
7   ...
8 }

 

 

  ft_data 類實現了許多訪問數據的有用方法。為了訪問數據集的圖像,可用 get_image 函數加載圖像。使用該函數需指定加載圖像的索引 idx ,以及是否將圖像以 y 軸做鏡像。該函數實現如下:

 1 Mat
 2 ft_data::
 3 get_image(const int idx,    // 圖像索引
 4       const int flag)        // 0=gray,1=gray+flip,2=rgb,3=rgb+flip
 5 {
 6   if((idx < 0) || (idx >= (int)imnames.size()))return Mat();
 7   Mat img,im;
 8   if(flag < 2)img = imread(imnames[idx],0);        // gray
 9   else img = imread(imnames[idx],1);            // rgb
10   if(flag % 2 != 0)flip(img,im,1);                // 以 y 軸做鏡像
11   else im = img;
12   return im;
13 }

 

 

  為了通過指定的索引來得到相應圖像的一個點集,可使用 get_points 函數通過鏡像索引來得到一個基於浮點的坐標向量

 1 vector<Point2f>
 2 ft_data::
 3 get_points(const int idx,        // 相應圖像的索引
 4        const bool flipped)        // 是否以 y 軸做鏡像
 5 {
 6   if((idx < 0) || (idx >= (int)imnames.size()))return vector<Point2f>();
 7   vector<Point2f> p = points[idx];
 8   if(flipped){        // 以 y 軸做鏡像
 9     Mat im = this->get_image(idx,0);    // im 用來獲取圖像的寬度
10     int n = p.size(); vector<Point2f> q(n);
11     for(int i = 0; i < n; i++){            // 沿豎直方向翻轉    
12       q[i].x = im.cols-1-p[symmetry[i]].x;
13       q[i].y = p[symmetry[i]].y;
14     }return q;
15   }else return p;
16 }

 

 

  ft_data 類還實現了一個函數 rm_incomplete_samples,該函數刪除集合中沒有進行相應標注的樣本,具體實現如下:

 1 void
 2 ft_data::
 3 rm_incomplete_samples()        // 刪除集合中沒有進行相應標注的樣本
 4 {
 5   int n = points[0].size(),N = points.size();
 6   // 找出標注數最多的樣本,作為標准樣本
 7   for(int i = 1; i < N; i++)n = max(n,int(points[i].size()));    
 8   for(int i = 0; i < int(points.size()); i++){
 9     if(int(points[i].size()) != n){        // 樣本標注點的數量小於標准樣本標注點數,從樣本中刪除
10       points.erase(points.begin()+i); imnames.erase(imnames.begin()+i); i--;
11     }else{
12       int j = 0;
13       for(; j < n; j++){
14         // 若點的(x,y)存在小於0,則可認為它在相應的圖像中不存在
15         if((points[i][j].x <= 0) || (points[i][j].y <= 0))break;
16       }
17       if(j < n){    // 從樣本中刪除
18     points.erase(points.begin()+i); imnames.erase(imnames.begin()+i); i--;
19       }
20     }
21   }
22 }

 

 

  ft_data 類還實現了函數 read 和 write 的序列化,這樣就可以方便地存儲和加載該類。

 1 void 
 2 ft_data::
 3 write(FileStorage &fs) const
 4 {
 5   assert(fs.isOpened()); 
 6   fs << "{";
 7   fs << "n_connections" << (int)connections.size();        // 面部特征的語義解釋
 8   for(int i = 0; i < int(connections.size()); i++){
 9     char str[256]; const char* ss;
10     sprintf(str,"connections %d 0",i); ss = str; fs << ss << connections[i][0];
11     sprintf(str,"connections %d 1",i); ss = str; fs << ss << connections[i][1];
12   }
13   fs << "n_symmetry" << (int)symmetry.size();            // 特征點的索引
14   for(int i = 0; i < int(symmetry.size()); i++){
15     char str[256]; const char* ss;
16     sprintf(str,"symmetry %d",i); ss = str; fs << ss << symmetry[i];
17   }
18   fs << "n_images" << (int)imnames.size();                // 圖像絕對路徑
19   for(int i = 0; i < int(imnames.size()); i++){
20     char str[256]; const char* ss;
21     sprintf(str,"image %d",i); ss = str; fs << ss << imnames[i];
22   }
23   int n = points[0].size(),N = points.size();            // 描述人臉特征點的結構
24   Mat X(2*n,N,CV_32F); X = -1;
25   for(int i = 0; i < N; i++){
26     if(int(points[i].size()) == n){
27       for(int j = 0; j < n; j++){
28     X.at<float>(2*j  ,i) = points[i][j].x;
29     X.at<float>(2*j+1,i) = points[i][j].y;
30       }
31     }
32   }
33   fs << "shapes" << X << "}";
34 }
35 //==============================================================================
36 void
37 ft_data::
38 read(const FileNode& node)
39 {
40   assert(node.type() == FileNode::MAP);
41   int n; node["n_connections"] >> n; connections.resize(n);
42   for(int i = 0; i < n; i++){
43     char str[256]; const char* ss;
44     sprintf(str,"connections %d 0",i); ss = str; node[ss] >> connections[i][0];
45     sprintf(str,"connections %d 1",i); ss = str; node[ss] >> connections[i][1];
46   }
47   node["n_symmetry"] >> n; symmetry.resize(n);
48   for(int i = 0; i < n; i++){
49     char str[256]; const char* ss;
50     sprintf(str,"symmetry %d",i); ss = str; node[ss] >> symmetry[i];
51   }
52   node["n_images"] >> n; imnames.resize(n);
53   for(int i = 0; i < n; i++){
54     char str[256]; const char* ss;
55     sprintf(str,"image %d",i); ss = str; node[ss] >> imnames[i];
56   }
57   Mat X; node["shapes"] >> X; int N = X.cols; n = X.rows/2; 
58   points.resize(N);
59   for(int i = 0; i < N; i++){
60     points[i].clear();
61     for(int j = 0; j < n; j++){
62       Point2f p(X.at<float>(2*j,i),X.at<float>(2*j+1,i));
63       if((p.x >= 0) && (p.y >= 0))points[i].push_back(p);
64     }
65   }
66 }
read write

 

 

 

  為對數據集進行可視化操作, ft_data 實現了許多用於繪圖的函數。

  1 void
  2 ft_data::
  3 draw_points(Mat &im,
  4         const int idx,
  5         const bool flipped,
  6         const Scalar color,
  7         const vector<int> &pts)
  8 {
  9   if((idx < 0) || (idx >= (int)imnames.size()))return;
 10   int n = points[idx].size();
 11   if(pts.size() == 0){
 12     for(int i = 0; i < n; i++){
 13       if(!flipped)circle(im,points[idx][i],1,color,2,CV_AA);
 14       else{
 15     Point2f p(im.cols - 1 - points[idx][symmetry[i]].x,
 16           points[idx][symmetry[i]].y);
 17     circle(im,p,1,color,2,CV_AA);
 18       }
 19     }
 20   }else{
 21     int m = pts.size();
 22     for(int j = 0; j < m; j++){
 23       int i = pts[j]; if((i < 0) || (i >= n))continue;
 24       if(!flipped)circle(im,points[idx][i],1,color,2,CV_AA);
 25       else{
 26     Point2f p(im.cols - 1 - points[idx][symmetry[i]].x,
 27           points[idx][symmetry[i]].y);
 28     circle(im,p,1,color,2,CV_AA);
 29       }
 30     }
 31   }
 32 }
 33 //==============================================================================
 34 void
 35 ft_data::
 36 draw_sym(Mat &im,
 37      const int idx,
 38      const bool flipped,
 39      const vector<int> &pts)
 40 {
 41   if((idx < 0) || (idx >= (int)imnames.size()))return;
 42   int n = points[idx].size();
 43   RNG rn; vector<Scalar> colors(n); 
 44   for(int i = 0; i < n; i++)colors[i] = Scalar::all(0.0);
 45   for(int i = 0; i < n; i++){
 46     if(colors[i] == Scalar::all(0.0)){
 47       colors[i] = Scalar(rn.uniform(0,255),rn.uniform(0,255),rn.uniform(0,255));
 48       colors[symmetry[i]] = colors[i];
 49     }
 50   }
 51   vector<Point2f> p = this->get_points(idx,flipped); 
 52   if(pts.size() == 0){
 53     for(int i = 0; i < n; i++){circle(im,p[i],1,colors[i],2,CV_AA);}
 54   }else{
 55     int m = pts.size();
 56     for(int j = 0; j < m; j++){
 57       int i = pts[j]; if((i < 0) || (i >= n))continue;
 58       circle(im,p[i],1,colors[i],2,CV_AA);
 59     }
 60   }
 61 }
 62 //==============================================================================
 63 void
 64 ft_data::
 65 draw_connect(Mat &im,
 66          const int idx,
 67          const bool flipped,
 68          const Scalar color,
 69          const vector<int> &con)
 70 {
 71   if((idx < 0) || (idx >= (int)imnames.size()))return;
 72   int n = connections.size();
 73   if(con.size() == 0){    
 74     for(int i = 0; i < n; i++){
 75       int j = connections[i][0],k = connections[i][1];
 76       if(!flipped)line(im,points[idx][j],points[idx][k],color,1);
 77       else{
 78     Point2f p(im.cols - 1 - points[idx][symmetry[j]].x,
 79           points[idx][symmetry[j]].y);
 80     Point2f q(im.cols - 1 - points[idx][symmetry[k]].x,
 81           points[idx][symmetry[k]].y);
 82     line(im,p,q,color,1);
 83       }
 84     }
 85   }else{
 86     int m = con.size();
 87     for(int j = 0; j < m; j++){
 88       int i = con[j]; if((i < 0) || (i >= n))continue;
 89       int k = connections[i][0],l = connections[i][1];
 90       if(!flipped)line(im,points[idx][k],points[idx][l],color,1);
 91       else{
 92     Point2f p(im.cols - 1 - points[idx][symmetry[k]].x,
 93           points[idx][symmetry[k]].y);
 94     Point2f q(im.cols - 1 - points[idx][symmetry[l]].x,
 95           points[idx][symmetry[l]].y);
 96     line(im,p,q,color,1);
 97       }
 98     }
 99   }
100 }
繪圖函數

 

 

 

  2. 標注工具

   為了使生成的標注能被本章中的代碼使用,可在 annotate.cpp 文件中找到一個基本的標注工具。該工具將一個視屏流作為輸入,這個視頻流可以來自文件或相機、使用該工具的過程有如下五個步驟:

  • 捕獲圖像:第一步是將圖像流顯示在屏幕上,用戶按下 S 鍵就可選擇圖像進行標注。
    • 主要代碼如下:
       1 //選擇圖像進行標注
       2 annotation.set_capture_instructions();        // 顯示幫助信息
       3 while (cam.get(CV_CAP_PROP_POS_AVI_RATIO) < 0.999999){    // 循環遍歷每一幀
       4     Mat im, img; cam >> im; 
       5     annotation.image = im.clone();
       6     annotation.draw_instructions();
       7     imshow(annotation.wname, annotation.image);        // 顯示當前幀
       8     int c = waitKey(0);        // 等待按鍵,q 退出,s 選擇圖像進行標注,其它任意鍵 下一幀
       9     if (c == 'q')break;
      10     else if (c == 's'){
      11         int idx = annotation.data.imnames.size(); char str[1024];
      12         if (idx < 10)sprintf(str, "00%d.png", idx);
      13         else if (idx < 100)sprintf(str, "0%d.png",idx);
      14         else               sprintf(str, "%d.png", idx);        // 文件名格式 三位整數.png
      15         imwrite(str, im);        // 保存該幀圖像
      16         annotation.data.imnames.push_back(str);
      17         cam >> im;                // 顯示下一幀
      18         imshow(annotation.wname, im);
      19     }
      20 }
      21 if (annotation.data.imnames.size() == 0)return 0;
      22 annotation.data.points.resize(annotation.data.imnames.size());

       

    • 運行效果:                                                                                                                                          
  • 標注第一幅圖:第二步首先將上一步中第一幅圖呈現給用戶,然后用戶會在這幅圖中選擇需要跟蹤的面部特征位置。
    • 主要代碼如下:
       1 // 標注第一幅圖像
       2 setMouseCallback(annotation.wname, pp_MouseCallback, 0);
       3 annotation.set_pick_points_instructions();    // 顯示幫助信息
       4 annotation.set_current_image(0);        // 選擇第一幅圖像
       5 annotation.draw_instructions();
       6 annotation.idx = 0;
       7 while (1){            // 在鍵入 q 之前,鼠標單擊標注特征點
       8     annotation.draw_points();
       9     imshow(annotation.wname, annotation.image); 
      10     if (waitKey(0) == 'q')break;
      11 }
      12 if (annotation.data.points[0].size() == 0)return 0;
      13 annotation.replicate_annotations(0);    // 保存特征點位置信息
    • 運行效果(為檢驗代碼,只選取三個特征點):

               

 

  • 標注連通性:在這一步中,用戶需選擇將兩組點連接起來,以建立人臉模型的連通性結構
    • 主要代碼如下:
       1 //標注連通性
       2 setMouseCallback(annotation.wname, pc_MouseCallback, 0);
       3 annotation.set_connectivity_instructions();    // 幫助信息
       4 annotation.set_current_image(0);
       5 annotation.draw_instructions();
       6 annotation.idx = 0;
       7 while (1){            // 在鍵入 q 之前,鼠標單擊一組點建立連接
       8     annotation.draw_connections();
       9     imshow(annotation.wname, annotation.image); if (waitKey(0) == 'q')break;
      10 }
      11 save_ft(fname.c_str(), annotation.data);

       

    • 運行效果如下:

            

  •  標注對稱性:這一步仍然使用上一步的圖像,用戶需選出左右對稱的點。
    • 主要代碼如下:
       1 //標注對稱性
       2 setMouseCallback(annotation.wname, ps_MouseCallback, 0);
       3 annotation.initialise_symmetry(0);
       4 annotation.set_symmetry_instructions();
       5 annotation.set_current_image(0);
       6 annotation.draw_instructions();
       7 annotation.idx = 0; annotation.pidx = -1;
       8 while (1){            // 在鍵入 q 之前,鼠標單擊特征點標注對稱性
       9     annotation.draw_symmetry();
      10     imshow(annotation.wname, annotation.image); if (waitKey(0) == 'q')break;
      11 }
      12 save_ft(fname.c_str(), annotation.data);

       

    •  運行效果如下:

             

  •  標注剩下的圖像:重復第 2 步至第 4 步,移動特征點使特征點對應特征位置
    • 主要代碼如下:
       1 //標注剩下的圖像
       2 if (type != 2){
       3     setMouseCallback(annotation.wname, mv_MouseCallback, 0);
       4     annotation.set_move_points_instructions();        // 幫助信息
       5     annotation.idx = 1; annotation.pidx = -1;
       6     while (1){
       7         annotation.set_current_image(annotation.idx);
       8         annotation.draw_instructions();
       9         annotation.set_clean_image();        // 背景圖
      10         annotation.draw_connections();        // 連線
      11         imshow(annotation.wname, annotation.image);
      12         int c = waitKey(0);        // q 退出,p 下一幅圖像,o 上一幅圖像
      13         if (c == 'q')break;
      14         else if (c == 'p'){ annotation.idx++; annotation.pidx = -1; }
      15         else if (c == 'o'){ annotation.idx--; annotation.pidx = -1; }
      16         if (annotation.idx < 0)annotation.idx = 0;
      17         if (annotation.idx >= int(annotation.data.imnames.size()))
      18             annotation.idx = annotation.data.imnames.size() - 1;
      19     }
      20 }
      21 save_ft(fname.c_str(), annotation.data);

       

    • 運行效果如下:

             

 

  該工具將標注數據儲存到 ann.yaml 中,如下:

                        

   3. 准備標注數據( MUCT 數據集)

  為了讓本章的標注工作變得輕松一些,可利用公開的 MUCT 數據集。這個數據集由 3755 張人臉圖像構成,每張人臉有76個點作為標記。數據集的圖像是在不同光照條件和頭部姿勢下拍攝的人,他們來自不同年齡和種族。

  該數據集只包含了標注點,需要自定義連通性和對稱性。標注連通性和對稱性之后效果如下圖左,標注數據儲存在 annotations.yaml 中,如下圖右:

           

  

  visualize_annotations.cpp 實現對數據集可視化操作,關鍵代碼如下:

 1 cout << "n images: " << data.imnames.size() << endl
 2     << "n points: " << data.symmetry.size() << endl
 3     << "n connections: " << data.connections.size() << endl;
 4 // 可視化標注數據
 5 namedWindow("Annotations");
 6 int index = 0; bool flipped = false;
 7 while(1){
 8 Mat image;
 9 if(flipped)image = data.get_image(index,3);
10 else image = data.get_image(index,2);            // 背景圖片
11 data.draw_connect(image,index,flipped);            // 連通
12 data.draw_sym(image,index,flipped);                // 對稱
13 imshow("Annotations",image);
14 int c = waitKey(0);            // q 退出,p 下一張,o 上一張,f 翻轉
15 if(c == 'q')break;
16 else if(c == 'p')index++;
17 else if(c == 'o')index--;
18 else if(c == 'f')flipped = !flipped;
19 if(index < 0)index = 0;
20 else if(index >= int(data.imnames.size()))index = data.imnames.size()-1;
21 }
可視化數據

  運行效果如下:

                

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM