《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

具体实现代码:

//====================================================================
void
face_detector::
train(ft_data &data,
      const string fname,
      const Mat &ref,
      const bool mirror,
      const bool visi,
      const float frac,
      const float scaleFactor,
      const int minNeighbours,
      const Size minSize)
{
  //载入级联分类器
  detector.load(fname.c_str());
  detector_fname = fname;
  reference = ref.clone();
  vector<float> xoffset(0),yoffset(0),zoffset(0);
  for(int i = 0; i < data.n_images(); i++)
  {
    //获取每一张训练图片
    Mat im = data.get_image(i,0);
	if(im.empty())
	   continue;
	//获取训练图片对应的标注点
    vector<Point2f> p = data.get_points(i,false);
	int n = p.size();
    Mat pt = Mat(p).reshape(1,2*n);
    vector<Rect> faces;
	Mat eqIm;
	//直方图均衡化
	equalizeHist(im,eqIm);
	//人脸检测
    detector.detectMultiScale(eqIm,faces,scaleFactor,minNeighbours,0
                  |CV_HAAR_FIND_BIGGEST_OBJECT
                  |CV_HAAR_SCALE_IMAGE,minSize);
    if(faces.size() >= 1)
	{
      if(visi)
	  {
	    //框出人脸区域
		Mat I; cvtColor(im,I,CV_GRAY2RGB);
		for(int i = 0; i < n; i++)circle(I,p[i],1,CV_RGB(0,255,0),2,CV_AA);
		rectangle(I,faces[0].tl(),faces[0].br(),CV_RGB(255,0,0),3);
		imshow("face detector training",I); waitKey(10);
      }
      //check if enough points are in detected rectangle
      if(this->enough_bounded_points(pt,faces[0],frac))
	  {
		Point2f center = this->center_of_mass(pt);
		float w = faces[0].width;
		xoffset.push_back((center.x - (faces[0].x+0.5*faces[0].width ))/w);
		yoffset.push_back((center.y - (faces[0].y+0.5*faces[0].height))/w);
		zoffset.push_back(this->calc_scale(pt)/w);
      }
    }
    if(mirror)
	{
      im = data.get_image(i,1); if(im.empty())continue;
      p = data.get_points(i,true);
      pt = Mat(p).reshape(1,2*n);
      equalizeHist(im,eqIm);
      detector.detectMultiScale(eqIm,faces,scaleFactor,minNeighbours,0
                  |CV_HAAR_FIND_BIGGEST_OBJECT
                |CV_HAAR_SCALE_IMAGE,minSize);
      if(faces.size() >= 1){
		if(visi)
		{
		  Mat I; cvtColor(im,I,CV_GRAY2RGB);
		  for(int i = 0; i < n; i++)circle(I,p[i],1,CV_RGB(0,255,0),2,CV_AA);
		  rectangle(I,faces[0].tl(),faces[0].br(),CV_RGB(255,0,0),3);
		  imshow("face detector training",I); waitKey(10);
		}
		//check if enough points are in detected rectangle
		if(this->enough_bounded_points(pt,faces[0],frac))
		{
		  Point2f center = this->center_of_mass(pt); float w = faces[0].width;
		  xoffset.push_back((center.x - (faces[0].x+0.5*faces[0].width ))/w);
		  yoffset.push_back((center.y - (faces[0].y+0.5*faces[0].height))/w);
		  zoffset.push_back(this->calc_scale(pt)/w);
		}
      }
    }
  }
  //choose median value,选取集合中值
  Mat X = Mat(xoffset),Xsort,Y = Mat(yoffset),Ysort,Z = Mat(zoffset),Zsort;
  cv::sort(X,Xsort,CV_SORT_EVERY_COLUMN|CV_SORT_ASCENDING); int nx = Xsort.rows;
  cv::sort(Y,Ysort,CV_SORT_EVERY_COLUMN|CV_SORT_ASCENDING); int ny = Ysort.rows;
  cv::sort(Z,Zsort,CV_SORT_EVERY_COLUMN|CV_SORT_ASCENDING); int nz = Zsort.rows;
  detector_offset = Vec3f(Xsort.fl(nx/2),Ysort.fl(ny/2),Zsort.fl(nz/2));
  return;
}

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

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

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

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

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

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

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

//predict face placement
  Rect R = faces[0]; //人脸外界矩形
  Vec3f scale = detector_offset*R.width;
  int n = reference.rows/2;
vector<Point2f> p(n);
  for(int i = 0; i < n; i++){
    //scale[2]代表缩放比例
    p[i].x = scale[2]*reference.fl(2*i  ) + R.x + 0.5 * R.width  + scale[0];
    p[i].y = scale[2]*reference.fl(2*i+1) + R.y + 0.5 * R.height + scale[1];
  }
  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。

int main(int argc,char** argv)
{
	//create face tracker model
	face_tracker tracker;
	tracker.smodel = load_ft<shape_model>("shape.xml");
	tracker.pmodel = load_ft<patch_models>("patch.xml");
	tracker.detector = load_ft<face_detector>("detector.xml");

	//save face tracker
	save_ft<face_tracker>("tracker.xml",tracker);
return 0;
}

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

int
face_tracker::
track(const Mat &im,const face_tracker_params &p)
{
  //convert image to greyscale
  Mat gray;
  if(im.channels()==1)
     gray = im;
  else
     cvtColor(im,gray,CV_RGB2GRAY);

  //initialise,为第一帧或下一帧初始化人脸特征
  if(!tracking)
    points = detector.detect(gray,p.scaleFactor,p.minNeighbours,p.minSize);
  if((int)points.size() != smodel.npts())
    return 0;

  //fit,通过迭代缩小的搜索范围,估计当前帧中的人脸特征点
  for(int level = 0; level < int(p.ssize.size()); level++)
    points = this->fit(gray,points,p.ssize[level],p.robust,p.itol,p.ftol);

  //set tracking flag and increment timer
  tracking = true;
  timer.increment();
  return 1;
}

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

fit函数入参:

image:当前帧灰度图像

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

ssize:搜索区域大小

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

itol:robust modelfitting迭代上限

ftol:迭代收敛判断阈值

 

返回值:

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

//==========================================================================
vector<Point2f>
face_tracker::
fit(const Mat &image,
    const vector<Point2f> &init,
    const Size ssize,
    const bool robust,
    const int itol,
    const float ftol)
{
  int n = smodel.npts();//number of points in shape model
  assert((int(init.size())==n) && (pmodel.n_patches()==n));
  smodel.calc_params(init); vector<Point2f> pts = smodel.calc_shape();

  //find facial features in image around current estimates
  vector<Point2f> peaks = pmodel.calc_peaks(image,pts,ssize);

  //optimise
  if(!robust){
    smodel.calc_params(peaks); //compute shape model parameters
    pts = smodel.calc_shape(); //update shape
  }else{
    Mat weight(n,1,CV_32F),weight_sort(n,1,CV_32F);
    vector<Point2f> pts_old = pts;
    for(int iter = 0; iter < itol; iter++){
      //compute robust weight
      for(int i = 0; i < n; i++)weight.fl(i) = norm(pts[i] - peaks[i]);
      cv::sort(weight,weight_sort,CV_SORT_EVERY_COLUMN|CV_SORT_ASCENDING);
      double var = 1.4826*weight_sort.fl(n/2); if(var < 0.1)var = 0.1;
      pow(weight,2,weight); weight *= -0.5/(var*var); cv::exp(weight,weight); 

      //compute shape model parameters
      smodel.calc_params(peaks,weight);

      //update shape
      pts = smodel.calc_shape();

      //check for convergence
      float v = 0; for(int i = 0; i < n; i++)v += norm(pts[i]-pts_old[i]);
      if(v < ftol)break; else pts_old = pts;
    }
  }return pts;
}

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

    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,防止人脸投影失真。

//===============================================================
void
shape_model::
calc_params(const vector<Point2f> &pts,const Mat weight,const float c_factor)
{
  int n = pts.size(); assert(V.rows == 2*n);
  Mat s = Mat(pts).reshape(1,2*n); //point set to vector format
  if(weight.empty())p = V.t()*s;   //simple projection
  else{                            //scaled projection
    if(weight.rows != n){cout << "Invalid weighting matrix" << endl; abort();}
    int K = V.cols; Mat H = Mat::zeros(K,K,CV_32F),g = Mat::zeros(K,1,CV_32F);
    for(int i = 0; i < n; i++){
      Mat v = V(Rect(0,2*i,K,2)); float w = weight.fl(i);
      H += w*v.t()*v; g += w*v.t()*Mat(pts[i]);
    }
    solve(H,g,p,DECOMP_SVD);
  }this->clamp(c_factor);          //clamp resulting parameters
}

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

 

calc_peaks函数:

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

calc_peaks函数入参:

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

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

ssize:搜索区域窗口大小

返回值:

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

//========================================================================
vector<Point2f>
patch_models::
calc_peaks(const Mat &im,
       const vector<Point2f> &points,
       const Size ssize)
{
  int n = points.size(); assert(n == int(patches.size()));
  Mat pt = Mat(points).reshape(1,2*n);
  Mat S = this->calc_simil(pt);// 计算当前点集到人脸参考模型的变化矩阵
  Mat Si = this->inv_simil(S); //对矩阵S求逆
  vector<Point2f> pts = this->apply_simil(Si,points);
  for(int i = 0; i < n; i++){
    Size wsize = ssize + patches[i].patch_size(); Mat A(2,3,CV_32F);
    A.fl(0,0) = S.fl(0,0); A.fl(0,1) = S.fl(0,1);
    A.fl(1,0) = S.fl(1,0); A.fl(1,1) = S.fl(1,1);
    A.fl(0,2) = pt.fl(2*i  ) -
      (A.fl(0,0) * (wsize.width-1)/2 + A.fl(0,1)*(wsize.height-1)/2);
    A.fl(1,2) = pt.fl(2*i+1) -
      (A.fl(1,0) * (wsize.width-1)/2 + A.fl(1,1)*(wsize.height-1)/2);
    Mat I; warpAffine(im,I,A,wsize,INTER_LINEAR+WARP_INVERSE_MAP);
    Mat R = patches[i].calc_response(I,false);
    Point maxLoc; minMaxLoc(R,0,0,0,&maxLoc);
    pts[i] = Point2f(pts[i].x + maxLoc.x - 0.5*ssize.width,
             pts[i].y + maxLoc.y - 0.5*ssize.height);
  }return this->apply_simil(S,pts);
}

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

(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”键,重新初始化人脸检测器把。

 

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

#include "ft.hpp"
#include "face_tracker.hpp"
#include <opencv2/highgui/highgui.hpp>
#include <iostream>
#define fl at<float>
//===========================================================================
void
draw_string(Mat img,                       //image to draw on
			const string text)             //text to draw
{
	Size size = getTextSize(text,FONT_HERSHEY_COMPLEX,0.6f,1,NULL);
	putText(img,text,Point(0,size.height),FONT_HERSHEY_COMPLEX,0.6f,
		Scalar::all(0),1,CV_AA);
	putText(img,text,Point(1,size.height+1),FONT_HERSHEY_COMPLEX,0.6f,
		Scalar::all(255),1,CV_AA);
}

//========================================================================
int main(int argc,char** argv)
{
	//load detector model
	Mat im;
	face_tracker tracker = load_ft<face_tracker>("tracker.xml");

	//create tracker parameters
	face_tracker_params p;
	p.robust = false;
	p.ssize.resize(3);
	p.ssize[0] = Size(21,21);
	p.ssize[1] = Size(11,11);
	p.ssize[2] = Size(5,5);

	//open video stream
	VideoCapture cam;
	namedWindow("face tracker");

	cam.open("test.avi");
	if(!cam.isOpened()){
		cout << "Failed opening video file." << endl
			<< usage << endl; return 0;
	}
	//detect until user quits

	while(cam.get(CV_CAP_PROP_POS_AVI_RATIO) < 0.999999){
		cam >> im;
		if(tracker.track(im,p))
			tracker.draw(im);
		draw_string(im,"d - redetection");
		tracker.timer.display_fps(im,Point(1,im.rows-1));
		imshow("face tracker",im);
		int c = waitKey(10);
		if(c == 'q')
			break;
		else if(c == 'd')
			tracker.reset();
	}
	destroyWindow("face tracker");
	cam.release();
	return 0;
}

     老外也比较实在,当人脸跟踪失败的时候,你只要按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

时间: 2024-08-15 09:51:24

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

《Master Opencv...读书笔记》非刚性人脸跟踪 I

      最近又开始学习<MasteringOpenCV系列>之前没看的部分,依旧是英文版.这次主要研究"非刚性人脸跟踪"(non-rigid face tracking),业余时间较少分几次写完吧.      首先谈谈什么是非刚性人脸跟踪.它是对每帧视频图像中人脸特征稠密数据集合的估计.非刚性人脸跟踪侧重于不同脸部表情或不同人物的脸部特征相对距离的变化.它和一般的人脸检测与跟踪算法不同,它不仅仅是找到每一帧中人脸的位置,它还要找到人脸五官的组态关系.[可以作用表情识别]

《Master Opencv...读书笔记》非刚性人脸跟踪 II

    上一篇博文中,我们了解了系统的功能和模块,明确了需要采集哪些类型的样本点及利用类的序列化的保存方式.这次将介绍几何约束模块,通过统计形态分析法(Statistical Shape Analysis, SSA),利用样本点建立对形状的描述,然后对描述的形状建立点分布模型,并从中学习统计参数,完成对形状的建模.该模块在后续跟踪阶段将被用来约束和剔除不合理的特征点. 本次讲解目录:  facial geometry是什么?程序主要数据结构介绍  普氏分析是什么?作用是什么?怎么实现?[代码分析

《Master Opencv...读书笔记》非刚性人脸跟踪 III

   上篇文章中,我们获得了人脸的各种表情模式,也就是一堆标注点的形变参数.这次我们需要训练一中人脸特征(团块模型),它能够对人脸的不同部位(即"标注点")分别进行描述,作为后面人脸跟踪.表情识别的区分依据.本次博文的主要内容: a.      介绍下人脸特征检测器大概有哪些类别 b.      详细介绍随机梯度法,并介绍在人脸团块特征提取时的应用 c.      为了提高训练/跟踪的健壮性,利用上一讲对输入的图像进行大小.角度的约束   人脸特征检测器综述       人脸特征检测与

《Master Opencv...读书笔记》卡通化效果移植到android系统

昨天CSDN博客抽风了.这是第7次编辑这个文件了,之前老是提交失败!     声明 1.电脑比较坑爹,前置摄像头坏掉了. 2.卡通化效果运行比较慢,老外的书上说是,人每触摸一下屏幕,才生成一张卡通化效果的图片 因此,为了简便期间,我就只对一副图像进行卡通化效果. 原理什么的见前面的文章,本文的目的,是熟悉ndk和jni   环境需求: eclipse juno ndk(r9) android sdk 4.4 api 19 opencv 2.4.7 android版本 cygwin 准备工作: 1

《Master Opencv...读书笔记》图像特征点匹配

这是本书的第三章,本文主要关注其中的特征点匹配及去除失配点的方法. 主要功能:对统一物体拍了两张照片,只是第二张图片有选择和尺度的变化.现在要分别对两幅图像提取特征点,然后将这些特征点匹配,使其尽量相互对应 下面,本文通过采用surf特征,分别使用Brute-force matcher和Flann-based matcher对特征点进行相互匹配: 第一段代码摘自opencv官网的教程: // test2.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h&quo

《Mastering Opencv ...读书笔记系列》车牌识别(II)

   继上一篇文章后,现在要做的就是从车牌图像上使用optical character recognition算法将字符提取出来.对于每一块被检测的车牌,使用带监督的神经网络机器学习算法来识别字符. 本文内容: 1.字符分割  2.神经网络训练方法 3.使用神经网络预测字符 一.字符分割[OCR Segment] 在使用神经网络对每个字符进行预测之前,我们必须从车牌图像中扣取改字符图片,因此有如下步骤: 本文的输入图像为上一篇文章的车牌: a.二值化车牌 b.求轮廓 c.求最小外接矩形 d.用纵

《Mastering Opencv ...读书笔记系列》车牌识别(I)

一.ANPR简介:   Automatic Number Plate Recognition (ANPR),,是一种使用Optical Character Recognition (OCR)和其他分割.检测方法来读取汽车注册牌照的算法.最好的ANPR算法结果是由红外线照相机拍摄图片得到的.因为车牌的特殊材质,夜间会有逆反射效果,看不清车牌.但是现在我们不使用IR图片,我们使用常规图片,这样就增加了我们检测错误和识别错误的等级,以显示我们的算法有多牛逼[老外的意思,有逆反射的图片我没试过].下面给

《Mastering Opencv读书笔记》第一章 实现图像卡通效果

这本书和配套代码网上都有得下载. 要实现书中的效果,只要三步:1.使用拉普拉斯算子提取轮廓  2.使用双边滤波器对图像进行平滑 3.根据第一步得到的轮廓模版图,将第二步的结果拷贝过去[填充轮廓图中全白的部分] 由于我的笔记本摄像头坏了,故我的程序读取的是手机拍摄的视频. 下面给出我整理的两段代码: 1.边缘提取: // GetMySketch.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h" #include<iostream> #in

服务的协作:服务间的消息传递——《微服务设计》读书笔记

很多开发者都表示他们基于HTTP的API是RESTful的.但是,如同Fielding在他的博客中所说,这些API可能并不都是RESTful的.Leonard Richardson为REST定义了一个成熟度模型,具体包含以下4个层次(摘自IBM): 第一个层次(Level 0)的 Web 服务只是使用 HTTP 作为传输方式,实际上只是远程方法调用(RPC)的一种具体形式.SOAP 和 XML-RPC 都属于此类. 第二个层次(Level 1)的 Web 服务引入了资源的概念.每个资源有对应的标