看了之前做的基于眨眼检测的活体识别,其中利用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中,主要原理如下:
设两个点集
得到转换参数。
该原理的论文为: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,若
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