原图像中的68个关键点是怎么获取_dlib人脸关键点代码解析

介绍

看了之前做的基于眨眼检测的活体识别,其中利用dlib提取68个人脸关键点,通过眼睛关键点提取到人眼做眨眼识别。dlib提取68个人脸关键点是基于One Millisecond Face Alignment with an Ensemble of Regression Trees这篇论文实现的,以gbdt为基础,下面先介绍下算法原理,做下源码解析

算法原理

输入标注人脸关键点的图像数据,先将脸提取处理,由于脸的尺寸不一,所以利用仿射变换将人脸关键点仿射到单位空间,统一尺寸和坐标系。将数据的人脸关键点做下平均,作为初始人脸形状,基于这个初始形状再进行残差计算拟合人脸关键点。

首先在初始关键点的范围内随机采样像素作为对应的特征像素点。特征像素点选择最接近的初始关键点作为anchor,并计算偏差。当前像素点通过旋转、转换、伸缩后的坐标系要与初始关键点(关键点的平均位置)接近,即最小化之间的距离平方,得到最优变换tform。tform作用于偏差,加上自身的位置信息,得到当前关键点的特征像素点。

得到特征像素点后开始构建残差树,计算出当前关键点与目标关键点的偏差。通过特征像素点,利用退火的方法选择多个分割点,进行左右树划分,选择最小化划分后的偏差为最优分割点。分割样本,基于样本的平均残差更新当前关键点位置。回到上一步骤,重新选择出特征关键点,拟合下一颗残差树,最终综合所有残差树的结果得到关键点位置。

源码分析

分析训练代码,训练函数在dlib/image_processing/shape_predictor_trainer.h中,函数入口如下:

shape_predictor 

先做数据检测,判断数据中没有一个关键点是缺失。

const 

populate_training_sample_shapes这个函数主要将训练数据的人脸目标关键点仿射到单位空间,即人脸框的左上角坐标为(0,0),左下角为(0,1),右上角为(1,0),右下角为(1,1),标注的关键点仿射到对应的坐标系中。然后将所有的关键点对应的坐标取均值mean_shape。一半的样本当前关键点设置为mean_shape,另一半当前关键点则通过随机获取一个样本的目标关键点乘上(0.1,1.1)之间的随机数加上mean_shape取得。对于每个样本的缺失关键点则设置为对应的mean_shape值,返回mean_shape,即initial_shape。

把人脸关键点仿射到同一坐标系下,避免不同尺寸带来的影响。

randomly_sample_pixel_coordinates这个函数主要选择一组(容量为级联器的级数,默认为10)在initial_shape坐标范围内随机点(随机点数量为特征池数量,默认为400)作为特征像素坐标点返回。

之后准备构造森林,循环构建森林,循环get_cascade_depth()次,开始时选择瞄点,计算对应的偏差。

create_shape_relative_encoding

create_shape_relative_encoding这个函数在dlib/image_processing/shape_predictor.h中,根据特征像素坐标点(pixel_coordinates[cascade])每个点,在initial_shape中选择最接近的点,点的序列号存放在anchor_idx中,deltas存放的是特征像素坐标点和anchor_idx对应关键点的距离偏差。

上面已经做了尺寸归一化,故在人脸关键点周围进行随机采样提取特征点。

并行抽取出特征像素值,代码如下:

parallel_for

extract_feature_pixel_values这个函数在dlib/image_processing/shape_predictor.h中,首先基于最小均方差计算initial_shape和current_shape之间的转换参数,代码如下:

const 

find_tform_between_shapes调用了find_tform_between_shapes函数,在dlib/geometry/point_transforms.h中,主要原理如下:

设两个点集

,转换参数R(rotation),t(translation),c(scaling) ,最小化下式

得到转换参数。

该原理的论文为:Least-squares estimation of transformation parameters between two point patterns by Umeyama。对于两个点集x,y,x通过旋转,变换和缩放等操作后拟合y。最小化平方差即最佳拟合。

利用转换参数校正特征像素点坐标偏差,加上当前的像素点坐标得到校正后的采样点,代码如下:

point 

我们知道,偏差+初始关键点位置=特征像素点位置(对应于初始关键点)。而tform使得当前像素点转换到初始关键点,那么tform*偏差+当前关键点位置=特征像素点位置(这个特征点是对应于当前关键点)

如果采样点在人脸框内在取对应像素点强度值,否则为0,得到特征像素值。

取得对应的特征像素值后,开始构建gbdt树,总共500课,代码如下:

for (unsigned long i = 0; i < get_num_trees_per_cascade_level(); ++i)
{
     forests[cascade].push_back(make_regression_tree(tp, samples, pixel_coordinates[cascade]));
}

make_regression_tree先计算每个样本目标关键点和当前关键点的残差,并求和。

parallel_for

构建树并分裂节点

for 

generate_split这个函数先选取num_test_splits个切割测试点

for 

每个切割测试点选择规则为:随机从特征像素点选择两个点,计算出距离dist,选择随机数r,若

大于r,则接受这两个点,同时随机取得阈值,构成切割测试点,代码如下:
impl

以退火方法选择接受概率。

对每个切割测试点,分成块并行,对于每个测试点,如果样本点之间的像素特征值差值大于测试点阈值,在分到左树,否则分到右树。代码如下:

parallel_for

对基于每个切割点分类后的树,选择能让下式最大值作为最有切割点:

代码如下:

for 

返回最优分割点。

最优分割点的原理如下:

我们的目标是最小化下式:

其中

是每个图关键点的残差,而
是对应子树的残差均值,即

故有

故最小化

实际上就是最大化

基于分割点交换数据得到左右树。设计的很巧妙,以数组存储节点,交互节点数据即可。

template

最后,以叶子节点的平均残差作为梯度,得到残差树。不断迭代得残差树森林,保存初始关键点,残差树和特征像素点(对应于初始特征点)的信息。

推断过程代码如下:

加载完模型后,基于残差树修正初始关键点得到最终的人脸关键点

template 

参考文献

One Millisecond Face Alignment with an Ensemble of Regression Trees

Least-squares estimation of transformation parameters between two point patterns by Umeyama

你可能感兴趣的:(原图像中的68个关键点是怎么获取_dlib人脸关键点代码解析)