《Master Opencv...读书笔记》非刚性人脸跟踪 IV (终)

一、我们目前为止拥有什么

为了有一个连续完整的认识,在介绍最后一节前,先梳理下至今我们训练了哪些数据特征,并且训练它们的目的是什么。

 

1.      ft_data:利用手工标注工具,获取最原始的样本训练数据,包括以下内容:

  •  图像名称集合imnames:表明在哪幅图像上标注特征点;
  • 二维坐标集合points:手工标准点,后续更高级别特征均围绕这些特征点展开;
  • 对称坐标索引集合symmetry:标注样本图像的镜像图像上的特征点,扩大样本库;
  •  连接索引集合connections:描述手工标注的人脸特征点之间的几何约束及相对位置;

博文地址:http://blog.csdn.net/jinshengtao/article/details/42614091

涉及主要技术:如何利用Opencv序列化存储“类”的结构数据

 

2.      shape_model:由于人脸的高度结构化特征对局部形变产生了极大的约束,因此我们需要提取一种特征来描述手工标注点集与人脸器官在几何空间上的对应关系。这种关系包括全局形变(人脸平移、缩放、旋转)和局部形变(描述不同人、不同表情之间脸部形状的不同)。这种特征的训练结果包含如下内容:

  • 参数向量p:在手工标注的特征点集被投影到人脸子空间(之前被称作联合分布空间)前,需要设置被投影点集在子空间的缩放比例、旋转角度、还有投影的范围;
  • 子空间标准基V:描述人脸模型的联合投影矩阵,包括k个局部形变参数(表情模型)和4个全局形变参数,用于将图像标注点投影到人脸特征子空间;
  • 参数变化向量e:手工标注样本点投影到人脸特征子空间后得到坐标集合的标准差,由于投影本身会导致人脸特征的失真,所以用该标准差作为阈值,修正失真clamp函数;
  • 连接矩阵C:描述之前标注的连接关系矩阵,沿用ft_data中的连接索引集合,并未做其他操作;

     我们训练该特征,就是为了得到样本图像的标注点投影到人脸特征子空间的投影矩阵。另外,该投影矩阵内的k个局部形变参数代表了k个表情,一次投影将产生k组子空间坐标。

博文地址:http://blog.csdn.net/jinshengtao/article/details/43376049

涉及主要技术:奇异值分解、Procrustes Analysis、求施密特正交矩阵

 

3.      patch_model:团块特征模版,即人脸每个部位的特征图像。团块特征的训练结果包含如下内容:

  • 参考形状矩阵reference:通过人工指定参数向量p,在人脸子空间产生k种投影的坐标集合。由于图像的全局几何约束,为了提取更好的团块模型,我们需要求人工标注点到该矩阵reference的仿射变化矩阵(calc_simil函数完成),从而对样本图像也进行相应的仿射变化
  • 团块矩阵P:它是一种归一化的图像,代表当前特征点附近的图像特征

      在人脸跟踪时,需要对人脸不同部位各自的描述信息,以便于对每个特征点周围的图像进行模版匹配,达到人脸精细化跟踪的目的。

博文地址:http://blog.csdn.net/jinshengtao/article/details/43974311

涉及主要技术:随机梯度下降法、最小二乘法

 

二、打算怎么去跟踪,完整的跟踪方案

1.      手工标注数据,获取原始训练样本(多人,多表情)

2.      训练形状模型(提取这些表情模型,几何依赖关系保证后面的跟踪“像人脸”)

3.      训练团块模型(提取每个表情所包含的团块特征,人脸跟踪全靠这个模版匹配了)

4.      初始化人脸检测器(怎么在第一帧或跟踪失败时,开始/继续人脸检测)

5.      根据上一帧的人脸特征点,结合形状和团块信息,估计当前帧的人脸特征点集(考虑空间高斯噪声,此噪声是跟踪错误导致,不是图像噪声)

 

三、如何初始化第一帧及检测人脸

   由于人脸在相邻帧之间的动作变化较小,所以到目前为止,我们假设每帧图像中人脸的特征都分布在当前估计点周围的合理范围内。但是我们仍面临着一个严重的问题,到底怎样初始化第一帧得到其中的人脸特征模型

   对于第一帧,这里我们采用比较直观的方式,使用opencv内置的级联检测器来寻找人脸的大致区域,用外接矩形来描述。按照数据驱动的思路,通过学习训练使我们的系统能够学习人脸外界矩形与人脸跟踪特征之间的几何关系detector_offset向量,然后利用该向量对人脸参考形状矩阵reference进行仿射变换,获得外界矩形区域内的人脸特征点。

接下来,我们首先介绍训练的步骤,然后展示我们的训练结果。

训练过程:

1.      载入样本标注点ft_data及形状模型数据shape_model

2.      设置参数向量p,构造人脸子空间坐标点集合作为人脸特征的参考点集

3.      调用trian函数,学习外界矩形与人脸特征点之间的几何关系detector_offset


train函数入参:

  data:ft_data对象实例,包含了手工标注信息

  fname:级联分类器名称(比如:haarcascade_frontalface_alt.xml)

  ref:参考形状矩阵,在人脸子空间的k种投影点集

  mirror:镜像样本图像标记

  visi:训练过程可视化标记

  frac:有效特征点比率阈值

 

    这里train函数的目标是获取detector_offset向量,该向量的作用是将之前训练得到的形状模型以合理的方式镶嵌到人脸上。detector_offset向量通过外接矩形的width和该区域内手工标注点集合pt的重心计算得到。具体过程如下:

(1)    加载级联分类器、形状参考矩阵

(2)    对手工标注的每一幅图片,使用级联分类器搜索人脸区域

(3)    判断人脸的外接矩形内是否包含足够多的标注点(防止错误学习)

(4)    如果包含足够的标注点,则按照如下公式计算

重心:

                 

     计算每一幅图像的offset,构成平面坐标集合(X,Y)及缩放比例集合Z:






对X、Y、Z集合分别按升序排序,去各自的中值作为最终的detector_offset(Xm,Ym,Zm

具体实现代码:

[cpp]  view plain  copy
  1. //====================================================================  
  2. void  
  3. face_detector::  
  4. train(ft_data &data,  
  5.       const string fname,  
  6.       const Mat &ref,  
  7.       const bool mirror,  
  8.       const bool visi,  
  9.       const float frac,  
  10.       const float scaleFactor,  
  11.       const int minNeighbours,  
  12.       const Size minSize)  
  13. {  
  14.   //载入级联分类器  
  15.   detector.load(fname.c_str());   
  16.   detector_fname = fname;   
  17.   reference = ref.clone();  
  18.   vector<float> xoffset(0),yoffset(0),zoffset(0);  
  19.   for(int i = 0; i < data.n_images(); i++)  
  20.   {  
  21.     //获取每一张训练图片  
  22.     Mat im = data.get_image(i,0);   
  23.     if(im.empty())  
  24.        continue;  
  25.     //获取训练图片对应的标注点  
  26.     vector<Point2f> p = data.get_points(i,false);   
  27.     int n = p.size();  
  28.     Mat pt = Mat(p).reshape(1,2*n);  
  29.     vector<Rect> faces;  
  30.     Mat eqIm;   
  31.     //直方图均衡化  
  32.     equalizeHist(im,eqIm);  
  33.     //人脸检测  
  34.     detector.detectMultiScale(eqIm,faces,scaleFactor,minNeighbours,0  
  35.                   |CV_HAAR_FIND_BIGGEST_OBJECT  
  36.                   |CV_HAAR_SCALE_IMAGE,minSize);  
  37.     if(faces.size() >= 1)  
  38.     {  
  39.       if(visi)  
  40.       {  
  41.         //框出人脸区域  
  42.         Mat I; cvtColor(im,I,CV_GRAY2RGB);  
  43.         for(int i = 0; i < n; i++)circle(I,p[i],1,CV_RGB(0,255,0),2,CV_AA);  
  44.         rectangle(I,faces[0].tl(),faces[0].br(),CV_RGB(255,0,0),3);  
  45.         imshow("face detector training",I); waitKey(10);   
  46.       }  
  47.       //check if enough points are in detected rectangle  
  48.       if(this->enough_bounded_points(pt,faces[0],frac))  
  49.       {  
  50.         Point2f center = this->center_of_mass(pt);   
  51.         float w = faces[0].width;  
  52.         xoffset.push_back((center.x - (faces[0].x+0.5*faces[0].width ))/w);  
  53.         yoffset.push_back((center.y - (faces[0].y+0.5*faces[0].height))/w);  
  54.         zoffset.push_back(this->calc_scale(pt)/w);  
  55.       }  
  56.     }  
  57.     if(mirror)  
  58.     {  
  59.       im = data.get_image(i,1); if(im.empty())continue;  
  60.       p = data.get_points(i,true);  
  61.       pt = Mat(p).reshape(1,2*n);  
  62.       equalizeHist(im,eqIm);  
  63.       detector.detectMultiScale(eqIm,faces,scaleFactor,minNeighbours,0  
  64.                   |CV_HAAR_FIND_BIGGEST_OBJECT  
  65.                 |CV_HAAR_SCALE_IMAGE,minSize);  
  66.       if(faces.size() >= 1){  
  67.         if(visi)  
  68.         {  
  69.           Mat I; cvtColor(im,I,CV_GRAY2RGB);  
  70.           for(int i = 0; i < n; i++)circle(I,p[i],1,CV_RGB(0,255,0),2,CV_AA);  
  71.           rectangle(I,faces[0].tl(),faces[0].br(),CV_RGB(255,0,0),3);  
  72.           imshow("face detector training",I); waitKey(10);  
  73.         }  
  74.         //check if enough points are in detected rectangle  
  75.         if(this->enough_bounded_points(pt,faces[0],frac))  
  76.         {  
  77.           Point2f center = this->center_of_mass(pt); float w = faces[0].width;  
  78.           xoffset.push_back((center.x - (faces[0].x+0.5*faces[0].width ))/w);  
  79.           yoffset.push_back((center.y - (faces[0].y+0.5*faces[0].height))/w);  
  80.           zoffset.push_back(this->calc_scale(pt)/w);  
  81.         }  
  82.       }  
  83.     }  
  84.   }  
  85.   //choose median value,选取集合中值  
  86.   Mat X = Mat(xoffset),Xsort,Y = Mat(yoffset),Ysort,Z = Mat(zoffset),Zsort;  
  87.   cv::sort(X,Xsort,CV_SORT_EVERY_COLUMN|CV_SORT_ASCENDING); int nx = Xsort.rows;  
  88.   cv::sort(Y,Ysort,CV_SORT_EVERY_COLUMN|CV_SORT_ASCENDING); int ny = Ysort.rows;  
  89.   cv::sort(Z,Zsort,CV_SORT_EVERY_COLUMN|CV_SORT_ASCENDING); int nz = Zsort.rows;  
  90.   detector_offset = Vec3f(Xsort.fl(nx/2),Ysort.fl(ny/2),Zsort.fl(nz/2));   
  91.   return;  
  92. }  

      训练过程如下,一共9幅样本图,每幅图首先利用opencv级联分离检索人脸位置,然后在样本图像上标出标注点。接下来判断矩形内标注点的数量是否合理,最后计算我们的偏移量detector_offset。下图是我们的样本图像、标注点、人脸位置示意图:




      经过上述训练后,我们将序列化存储:级联分类器名称、偏移向量、形状参考点集。接下来,在展示训练结果之前,先介绍下detect函数,如何将我们训练的矩形与特征点的偏移关系套用到测试图像上。

detect函数,输入一副人脸测试图片,根据训练结果,在该人脸图像上标准人脸特征点。具体操作过程:

(1)    彩色图像转化成灰度图像,并进行直方图均衡化

(2)    利用Opencv的级联分类器检测人脸位置

(3)    根据人脸外界矩形,结合detector_offset和参考形状模型,重新计算标注点的坐标

这步公式比较简单不列了,但作者上边和这里设计的意图需要自己体会下,关键代码:

[cpp]  view plain  copy
  1. //predict face placement  
  2.   Rect R = faces[0]; //人脸外界矩形  
  3.   Vec3f scale = detector_offset*R.width;  
  4.   int n = reference.rows/2;   
  5. vector<Point2f> p(n);  
  6.   for(int i = 0; i < n; i++){  
  7.     //scale[2]代表缩放比例  
  8.     p[i].x = scale[2]*reference.fl(2*i  ) + R.x + 0.5 * R.width  + scale[0];  
  9.     p[i].y = scale[2]*reference.fl(2*i+1) + R.y + 0.5 * R.height + scale[1];  
  10.   }  
  11.   return p;  

     展示训练的结果如下,任老头子拿出来晒晒,好肥:


       从上图来看,我们的训练使得人脸特征点基本标注都正确了。本节内容说白了就是训练一个外界矩形与标注点的几何关系,然后让之后的测试图像人脸特征点得到合理标注。这里需要细细体会的是,老外几何关系detector_offset的数学设计!!现在讲了第一帧图像的处理方法,接下来在第四节介绍后续帧,人脸检测与特征点标注如何实现。

四、人脸跟踪实现步骤及代码

      本文的人脸跟踪问题,就是寻找一种高效、健壮的方法,将多个独立的人脸特征通过之前训练的几何依赖关系联合起来,实现精确跟踪每幅图像中人脸特征的几何位置。也许有人会问,在已经用Opencv级联分类器检测人脸了,再考虑几何依赖关系是否有意义。下面两幅图像,分别是带与不带几何依赖的人脸跟踪效果图。


      上面对比的结果清晰地展示了人脸特征之间空间内部依赖关系的优势(这里因为pathc_model已经训练了很多人脸特征,由于每帧变化不大,仅仅依靠这些特征点就能产生no dependency的结果了,只是如果有了空间内部依赖关系,每个点的跟踪效果会更好)。

      图像对比非常明显,造成这种差异的原因:仅按照检测到人脸特征的位置进行跟踪会导致过度噪声。因为每个人脸特征跟踪时,采用模版匹配法,即便在正确的位置,该区域图像在人脸模版上的反馈,也有可能不是最佳的。无论是图像噪声、光照变化、还是表情变化,解决人脸特征”模版匹配式跟踪”局限性的唯一方法,就是借助每个人脸特征之间的几何关系。

     那么这个几何依赖关系到底在人脸跟踪时怎么做呢?我们将人脸特征提取的结果投影到形状模型的线性子空间(shape model),也就是最小化原始点集到其在人脸子空间最接近合理形状分布的投影点集的距离。(就是说,把通过模版匹配检测到的原始点集A投影到人脸子空间产生新的点集B,再按照某种约束规则,通过对A迭代变化,使得A’到B的距离最小)。

     这么做的好处:在用融合了几何关系的人脸特征模版匹配法跟踪人脸时,即便空间噪声满足高斯近乎于高斯分布,其检测效果也“最像“人脸的形状。

上一节我们学习了如何初始化第一帧中的人脸特征点模型,接下来我们要搞明白如何使用上一帧或第一帧的人脸特征点来估计当前帧的人脸特征点,达到跟踪的目的。我们先来认识以下3个类:

fps_timer类:计算程序运算的速度XX帧/秒,在face_tracker类中track函数调用。

face_tracker_params类:完成face_tracker中基本参数的初始化、序列化存储,包括搜索区域集合,最大迭代次数,级联分类器参数等等。

face_tracker类:人脸跟踪的核心模块,也是本次介绍的重点,包含train和track两个部分。

 

    人脸跟踪的训练过程face_tracker::train,其实就是简单的把以往的训练数据重新打包序列化保存,包括shape_model、patch_model、face_detector。

[cpp]  view plain  copy
  1. int main(int argc,char** argv)  
  2. {  
  3.     //create face tracker model  
  4.     face_tracker tracker;  
  5.     tracker.smodel = load_ft<shape_model>("shape.xml");  
  6.     tracker.pmodel = load_ft<patch_models>("patch.xml");  
  7.     tracker.detector = load_ft<face_detector>("detector.xml");  
  8.   
  9.     //save face tracker  
  10.     save_ft<face_tracker>("tracker.xml",tracker);   
  11. return 0;  
  12. }  

    人脸跟踪的track函数,拥有两种功能。当tracking标志位为fasle时,程序属于构建模型(detectmode)阶段,为第一帧或下一帧图像初始化的人脸特征,所用的技术就是上一节所讲的;当tracking标志位为true时,则根据上一帧人脸特征点的位置估计下一帧的人脸特征,这个操作主要由fit函数完成。

[cpp]  view plain  copy
  1. int  
  2. face_tracker::  
  3. track(const Mat &im,const face_tracker_params &p)  
  4. {  
  5.   //convert image to greyscale  
  6.   Mat gray;   
  7.   if(im.channels()==1)  
  8.      gray = im;  
  9.   else  
  10.      cvtColor(im,gray,CV_RGB2GRAY);  
  11.   
  12.   //initialise,为第一帧或下一帧初始化人脸特征  
  13.   if(!tracking)  
  14.     points = detector.detect(gray,p.scaleFactor,p.minNeighbours,p.minSize);  
  15.   if((int)points.size() != smodel.npts())  
  16.     return 0;  
  17.   
  18.   //fit,通过迭代缩小的搜索范围,估计当前帧中的人脸特征点  
  19.   for(int level = 0; level < int(p.ssize.size()); level++)  
  20.     points = this->fit(gray,points,p.ssize[level],p.robust,p.itol,p.ftol);  
  21.   
  22.   //set tracking flag and increment timer  
  23.   tracking = true;   
  24.   timer.increment();   
  25.   return 1;  
  26. }  

     face_tracker::fit函数的主要功能:给定一帧图像及上一帧人脸特征点集,在当前图像上搜索该点集附近的人脸特征,并产生新的人脸特征点集。

fit函数入参:

image:当前帧灰度图像

init:上一帧人脸特征点集(几何位置)

ssize:搜索区域大小

robust:标志位,决定是否采用robustmodel fitting流程,应对人脸特征的孤立点

itol:robust modelfitting迭代上限

ftol:迭代收敛判断阈值

 

返回值:

pts:在给定的搜索区域大小后,当前帧中人脸特征位置点集

[cpp]  view plain  copy
  1. //==========================================================================  
  2. vector<Point2f>  
  3. face_tracker::  
  4. fit(const Mat &image,  
  5.     const vector<Point2f> &init,  
  6.     const Size ssize,  
  7.     const bool robust,  
  8.     const int itol,  
  9.     const float ftol)  
  10. {  
  11.   int n = smodel.npts();//number of points in shape model  
  12.   assert((int(init.size())==n) && (pmodel.n_patches()==n));  
  13.   smodel.calc_params(init); vector<Point2f> pts = smodel.calc_shape();  
  14.   
  15.   //find facial features in image around current estimates  
  16.   vector<Point2f> peaks = pmodel.calc_peaks(image,pts,ssize);  
  17.   
  18.   //optimise  
  19.   if(!robust){  
  20.     smodel.calc_params(peaks); //compute shape model parameters          
  21.     pts = smodel.calc_shape(); //update shape  
  22.   }else{  
  23.     Mat weight(n,1,CV_32F),weight_sort(n,1,CV_32F);  
  24.     vector<Point2f> pts_old = pts;  
  25.     for(int iter = 0; iter < itol; iter++){  
  26.       //compute robust weight  
  27.       for(int i = 0; i < n; i++)weight.fl(i) = norm(pts[i] - peaks[i]);  
  28.       cv::sort(weight,weight_sort,CV_SORT_EVERY_COLUMN|CV_SORT_ASCENDING);  
  29.       double var = 1.4826*weight_sort.fl(n/2); if(var < 0.1)var = 0.1;  
  30.       pow(weight,2,weight); weight *= -0.5/(var*var); cv::exp(weight,weight);   
  31.   
  32.       //compute shape model parameters      
  33.       smodel.calc_params(peaks,weight);  
  34.         
  35.       //update shape  
  36.       pts = smodel.calc_shape();  
  37.         
  38.       //check for convergence  
  39.       float v = 0; for(int i = 0; i < n; i++)v += norm(pts[i]-pts_old[i]);  
  40.       if(v < ftol)breakelse pts_old = pts;  
  41.     }  
  42.   }return pts;  
  43. }  

上面代码中,有两个函数:

    shape_mode::calc_param,为了得到合理的人脸子空间投影,我们需要求参数向量p,该函数通过人脸子空间投影坐标集合pts与人脸特征空间标准基V,计算得到参数向量

    patch_models::calc_peaks,根据人脸子空间点集在当前图像内搜索人脸特征,并产生新的人脸特征位置估计

 

    calc_param函数:计算参数向量p时可分为两种投影:simpleprojection和scale projection

如果权值为空,则采用简单投影,由于V是标准正交基:


否则对每个被投影点设置尺度权值,采用尺度投影,利用opencv提供的奇异值分解法求解非齐次线性系统:

(1)    遍历每个原始点集,取出联合分布矩阵V2n*k(人脸模型)对应的矩形区域宽k,高2,起始点(0,2i),存入矩阵v2*k

(2)    Opecnv函数solve求解非齐次线性方程,求p


 其中,,w为每个点的权重,p是需要求解的向量。(至于权值,老外在后面robust model fitting流程有用到。由于H和g是对每种表情模式作累加的,所以我猜想权值w的作用是控制每个人脸特征点对每种表情模式的影响,即只要哪些点就能显著表达对应表情)

最后,无论哪种投影,都经过clamp函数处理,根据c_factor个标准差e约束调整参数向量p,防止人脸投影失真。

[cpp]  view plain  copy
  1. //===============================================================  
  2. void   
  3. shape_model::  
  4. calc_params(const vector<Point2f> &pts,const Mat weight,const float c_factor)  
  5. {  
  6.   int n = pts.size(); assert(V.rows == 2*n);  
  7.   Mat s = Mat(pts).reshape(1,2*n); //point set to vector format  
  8.   if(weight.empty())p = V.t()*s;   //simple projection  
  9.   else{                            //scaled projection  
  10.     if(weight.rows != n){cout << "Invalid weighting matrix" << endl; abort();}  
  11.     int K = V.cols; Mat H = Mat::zeros(K,K,CV_32F),g = Mat::zeros(K,1,CV_32F);  
  12.     for(int i = 0; i < n; i++){  
  13.       Mat v = V(Rect(0,2*i,K,2)); float w = weight.fl(i);  
  14.       H += w*v.t()*v; g += w*v.t()*Mat(pts[i]);  
  15.     }  
  16.     solve(H,g,p,DECOMP_SVD);  
  17.   }this->clamp(c_factor);          //clamp resulting parameters  
  18. }  

[另外,calc_param与calc_shape,如果排除权值,那么是一对互逆的操作]

 

calc_peaks函数:

     通过上面的函数我们得到设置投影范围的参数向量p,然后再调用calc_shape函数就可以得到人脸子空间中的特征点pts。现在我们要在每个人脸子空间特征点附近搜索包含人脸特征的区域,最终为下一帧生成新的特征估计。这也是为什么,我们只需在第一帧或者跟踪失败时,才需要调用Opencv级联分类器重新定位人脸的原因。

calc_peaks函数入参:

image:当前包含人脸的灰度图像

pts:前一帧估计的人脸特征点集在人脸子空间投影坐标集合

ssize:搜索区域窗口大小

返回值:

  当前帧人脸特征估计的点集


[cpp]  view plain  copy
  1. //========================================================================  
  2. vector<Point2f>   
  3. patch_models::  
  4. calc_peaks(const Mat &im,  
  5.        const vector<Point2f> &points,  
  6.        const Size ssize)  
  7. {  
  8.   int n = points.size(); assert(n == int(patches.size()));  
  9.   Mat pt = Mat(points).reshape(1,2*n);  
  10.   Mat S = this->calc_simil(pt);// 计算当前点集到人脸参考模型的变化矩阵  
  11.   Mat Si = this->inv_simil(S); //对矩阵S求逆  
  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.     Mat R = patches[i].calc_response(I,false);  
  23.     Point maxLoc; minMaxLoc(R,0,0,0,&maxLoc);  
  24.     pts[i] = Point2f(pts[i].x + maxLoc.x - 0.5*ssize.width,  
  25.              pts[i].y + maxLoc.y - 0.5*ssize.height);  
  26.   }return this->apply_simil(S,pts);  
  27. }  

上面代码片段中,介绍以下函数:

(1)    apply_simil函数,对点集points按照Si进行仿射变化(将人脸特征子空间中的坐标经过仿射变换转成图像空间中的坐标,或者反过来)

(2)    calc_response函数,在灰度图像上搜索人脸特征(团块图像)的匹配位置,核心技术是Opencv API:matchTemplate模版匹配函数(图像I,模版T,匹配结果,算法标记),这里采用的算法是CV_TM_CCOEFF_NORMED,标准相关匹配宏。具体做法就是在原始图像上滑动模版窗口,在一次移动一个像素,最后在每个像素点上的匹配度量值R(x):



w,h为模版T的宽和高

(3)    minMaxLoc函数(数组 ,最小值,最大值,最小值坐标,最大值坐标):寻找矩阵中的最大最小值的位置,这里在矩阵R中寻找最大值,即最佳匹配位置。不需要关注的,API内直接填0即可。

 

calc_peak总体说来,利用上一帧坐标构造人脸特征的搜索区域,借助之前训练得到的团块模型,在搜索区域内进行模版匹配,找到最优匹配点,作为新一帧人脸特征点的坐标。完整操作过程如下:

(1)    计算前一帧人脸特征坐标到人脸参考模型坐标的仿射变换 S2*3

(2)    计算上述仿射变化的逆矩阵 Si 2*3

(3)    通过逆矩阵Si将人脸特征子空间中的坐标还原成图像帧中的坐标

(4)    遍历所有点,在原始图像中搜索每个人脸特征模版图像匹配的坐标点

(5)    利用(4)中的坐标,修正人脸特征估计点位置

(6)    利用仿射变化矩阵,再次将图像中的坐标投影到人脸特征子空间中,作为下一帧的人脸特征坐标估计

 

     在对每帧图像进行人脸跟踪时,track函数都会通过fit函数迭代产生多个人脸子空间坐标集合,并且每次迭代的时候,搜索区域都在减小。在迭代过程中,可能会产生很多孤立的特征点(孤立点,我认为是模版匹配时得到人脸特征错误估计点,因为本文没有一种机制保证R(X)的反馈一定包含人脸特征)。为了得到更精确的人脸跟踪效果,如果存在孤立点时,仍采用简单投影simple projection,会严重影响跟踪效果。因此,老外在计算投影参数calc_param时引入了权重,搞了一套robust model fitting流程,特意去除孤立点。

 

权值的计算:

pts,上一帧人脸特征子空间估计点集;

peaks,当前帧人脸特征子空间投影点集(模版匹配);


     以上公式,本节一开始有提到,想要表达:前后两帧估计点集之差服从高斯分布,即空间噪声满足高斯近乎于高斯分布。咱有了权值后,就可以计算带权值的参数向量,然后更新投影。循环退出的条件,循环次数达到最大,或者前后两次重新计算的投影之间的距离满足阈值ftol。

 

还有两个问题:

a.      为什么用一个逐渐缩小的搜索窗口,多次模版匹配人脸特征?

由于采用模版匹配法,通过多次在尺度不断减小的窗口中寻找人脸特征,可以使得估计点更好的表达人脸特征所在的位置(毕竟是俺像素遍历,在R(X)中挑最大值,多挑几次总是不错的,越挑越细)

 

b.      如何理解通常情况下,随着搜索范围的减小,孤立点会被自动排除(robust=false)?

因此在模版匹配时,挑选R(x)的最大值作为当前特征点,即使由于噪声啥的导致了错误,在下次搜索时,搜索窗口变小了,该错误点附近图像的反馈R(x)一定会变小,所以可以被剔除。这里假设模版匹配总能找到收敛的位置,如果实在离谱了,那么请按”d”键,重新初始化人脸检测器把。

 

接下来,完整展示跟踪结果:

[cpp]  view plain  copy
  1. #include "ft.hpp"  
  2. #include "face_tracker.hpp"  
  3. #include <opencv2/highgui/highgui.hpp>  
  4. #include <iostream>  
  5. #define fl at<float>  
  6. //===========================================================================  
  7. void  
  8. draw_string(Mat img,                       //image to draw on  
  9.             const string text)             //text to draw  
  10. {  
  11.     Size size = getTextSize(text,FONT_HERSHEY_COMPLEX,0.6f,1,NULL);  
  12.     putText(img,text,Point(0,size.height),FONT_HERSHEY_COMPLEX,0.6f,  
  13.         Scalar::all(0),1,CV_AA);  
  14.     putText(img,text,Point(1,size.height+1),FONT_HERSHEY_COMPLEX,0.6f,  
  15.         Scalar::all(255),1,CV_AA);  
  16. }  
  17.   
  18. //========================================================================  
  19. int main(int argc,char** argv)  
  20. {  
  21.     //load detector model  
  22.     Mat im;  
  23.     face_tracker tracker = load_ft<face_tracker>("tracker.xml");  
  24.   
  25.     //create tracker parameters  
  26.     face_tracker_params p;   
  27.     p.robust = false;  
  28.     p.ssize.resize(3);  
  29.     p.ssize[0] = Size(21,21);  
  30.     p.ssize[1] = Size(11,11);  
  31.     p.ssize[2] = Size(5,5);  
  32.   
  33.     //open video stream  
  34.     VideoCapture cam;   
  35.     namedWindow("face tracker");  
  36.   
  37.     cam.open("test.avi");  
  38.     if(!cam.isOpened()){  
  39.         cout << "Failed opening video file." << endl  
  40.             << usage << endl; return 0;  
  41.     }  
  42.     //detect until user quits  
  43.   
  44.     while(cam.get(CV_CAP_PROP_POS_AVI_RATIO) < 0.999999){           
  45.         cam >> im;   
  46.         if(tracker.track(im,p))  
  47.             tracker.draw(im);  
  48.         draw_string(im,"d - redetection");  
  49.         tracker.timer.display_fps(im,Point(1,im.rows-1));  
  50.         imshow("face tracker",im);  
  51.         int c = waitKey(10);  
  52.         if(c == 'q')  
  53.             break;  
  54.         else if(c == 'd')  
  55.             tracker.reset();  
  56.     }  
  57.     destroyWindow("face tracker");   
  58.     cam.release();   
  59.     return 0;  
  60. }  









     老外也比较实在,当人脸跟踪失败的时候,你只要按d,重新复位人脸跟踪器即可。。。缺点,优点也很明显,只能一个次跟踪一个人,对于侧脸效果很差

 

五、写了这么多,我们到底还能做什么(心得与拓展)

    分了4个批次终于写完了,我想应该表达了老外80%-90%的意思。注意我的训练图片太少,并且受限于opencv级联分类器的选择(人脸检测宏CV_HAAR_FIND_BIGGEST_OBJECT),所以对于侧脸的跟踪效果不好。

后面的网友可以使用老外的提供的强大数据库,并挑选opencv合理的人脸分类器,应该可以满足侧脸跟踪的要求。


所有代码下载地址:

 http://download.csdn.net/detail/jinshengtao/8555713


表情识别:

有人问到这个事情,我是这么想的:

模仿PCA人脸检测算法:http://blog.csdn.net/jinshengtao/article/details/18599165

在那个文章里,我把每个人脸图像转化成一维向量,多个图像这么转换后就得到训练集,还记那个36000*20的矩阵嘛?然后利用PCA算法,提取主成份,构造平均脸什么的。最后,将测试集图像也投影到平均脸的空间里,计算二者的距离,挑距离最小的作为最终匹配。

这里也可以这么模仿。现在可以比较精确的跟踪每个人脸的特征点了,我们把每幅图像对应的人脸特征点集也搞成一维的,然后挑选多个表情充分独立的图像所对应的点集构成训练集,一样采用PCA提取主成份,搞个投影空间。然后表情识别,无非就是算距离,给标签罢了。

 

至于,行人动作识别,这个有难度,需要查文献。毕竟行人的动作幅度可比人脸表情幅度大多了,非常容易跟丢。

 

最后附上一个叫“大嘴说图”的网友的连接,他罗列了“人脸器官精确定位/人脸特征点的跟踪”的主流算法及代码文档资源(ASM,活动形状模型),有兴趣的朋友可以拓展下。

 

http://blog.sina.com.cn/s/blog_ebbe6d790102vmez.html

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值