摘要
cvpr2017 作品, 是级联形状回归(Cascaded Shape Regressor)人脸对齐框架的CNN实现。算法级联了多级回归器,每一级的输出是相对于上一级的偏移量。通过增加人脸关键点热度图,可以使得每级的输入是整个人脸图像,与之前的局部区域图像定位某个关键点的方法相比,增加了人脸的全局信息。另外,第一级的输入是一个平均形状(mean shape calculated on trainset),此后每一级的输入包含3个部分:由上一级回归的关键点对齐后的输入图像,关键点热度图以及上一级的最后一层特征图(featuremap)。每一级的CNN网络都是VGG16。另外,作者还开源了theano 代码。
方法
-
CSR人脸对齐思想
CSR将人脸特征点看作是一个从人脸的表观到人脸形状(由人脸的特征点组成的向量)的回归过程,通过不断的迭代直到回归到最优的特征点位置上。 即对于一个输入 I , 给定一个初始形状 (通常是在训练集计算得到的平均形状)。每一级输出的是根据输入图像得到的偏移估计 ,那么每一级都会更准确的预测脸上 Landmark 的位置
其中, 和分别表示第 和级预测的人脸形状(即所有关键点集合),表示回归函数。
在级联形状回归的框架下,由于特征提取方法以及回归函数的选择不同,而延伸出了一系列的人脸特征点对齐方法,如SDM, LBF, DCNN等,其详细介绍参见我的人脸对齐技术综述文章 --《人脸关键点对齐》, 此处简要介绍如下:- RCPR(2013 ICCV 加州理工学院 Xavier P.Burgos-Artizzu ) 直接就是针对CPR在部分遮挡情况下,性能不佳进行改进,提出同时预测人脸形状和特征点是否被遮挡的状态。
- SDM(2031cpr) 输入的是SIFT特征,通过牛顿下山法求解非线性优化问题
- ESR(2012cvpr,MSRA孙剑组) 采用2层级联boost回归
- ERT(2014cvpr) dlib中采用的算法,基于随机数的实现,与LBF类似
- DRMF 输入的是HOG特征,回归函数SVR回归函数
- LBF(2013cvpr) 用随机森林模型在局部区域学习稀疏的二值化特征
- DCNN(2015) 使用CNN来作为回归函数
-
CNN作为关键点回归器
在DAN算法中,作者在每一级的形状回归中都采用了一个深度神经网络(CNN)来进行特征的提取和点坐标的回归。与之前CSR方法的主要区别在于: DAN通过引入关键点热度图,使得每一级CNN回归网络的输入图像都可以是整个人脸图像,而非某个关键点周围的局部小块。
参考上图,DAN中每一级的输入输出介绍如下:- 第一级的输入是一张人脸图像(可以是人脸检测后crop得到的人脸图像),经过CNN网络,加上平均形状,得到该级的形状估计,
- 第二级中,首先利用对人脸图像和进行矫正变换(计算相对于的仿射矩阵,作用于二者上),得到矫正后的人脸图像和形状,并根据生成关键点热度图,然后将,以及第一级全连接层的输出 三者在通道轴上进行拼接,以次作为第二级的输入,经过CNN网络输出该级的更细致的预测。
-
之后的级联都可以看作是第二级的堆叠(即:上一级的全连接层, 经上一级输出的关键点热度图,矫正后的人脸图像作为输入,输出该级的估计)
此外,DAN中每一级采用的CNN网络结构都是一样的,即VGG16的mini版,各级的输入是112x112的灰度图,输出是1x136的关键点坐标,详细结构如下:
标准形状规范化
前文可知,除第一级外,之后各级输入的人脸图像都是经过对齐后的人脸图像(对齐于),这种规范化操作一定程度上确保了DAN的旋转不变性。对齐的操作通过计算第级输出相对于平均形状的仿射矩阵(旋转r,平移t),因此相应的输出也是对齐后的估计,映射到原人脸图像上,需要进行逆变换:
-
关键点热度图
热度图的生成是基于某一像素点到各关键点的距离来计算的:
结果
训练
- 数据集
DAN采用的数据集是300W,- 训练集:afw + helen/trainset + lfpw/trainset
- 测试集:CommonSet(helen/test + lfpw/testset) 和ChallengeSet(ibug)两种情况
- 损失函数
DAN采用比均方误差和(Sum of Squared Errors)更为公平的误差-由眼珠距归一化的点对点距离(the landmark location rror normalized by the distance between the pupils)
- 预处理
- 通过图像旋转, 平移和尺度变换进行训练即数据增广
- 训练集上(增广前)计算平均形状和样本的均值和标准差std
- 训练时,网络的输入是规范化的112x112灰度图: to gray, crop to 112 and
- 代码实现
作者开源的官方代码是基于theano, 在工程里作者也给出了网友用tensorflow实现的两个版本,但两个版本在训练集准备上以及训练代码上各自有难懂的地方。此外,本人目前在汇总tensorflow框架下的人脸关键点算法,基于现有代码也对DAN进行了整理,使得训练样本的预处理更加简单灵活,训练代码易于阅读理解,其中网络结构部分的代码如下,完整代码见我的github工程:
class MultiVGG:
def __init__(self, mean_shape, num_lmk=68, stage=1, img_size=112, channel=1, name='multivgg'):
self.name = name
self.channel = channel
self.img_size = img_size
self.stage = stage
self.num_lmk = num_lmk
self.mean_shape = tf.constant(mean_shape, dtype=tf.float32)
def __str__(self):
return "dan_vgg_%d" % self.img_size
def _vgg_model(self, x, is_training=True, name="vgg"):
"""
basic vgg model
:param x:
:param is_training:
:param name:
:return:
"""
with tf.variable_scope(name):
conv1 = vgg_block(x, 2, 64, is_training=is_training)
conv2 = vgg_block(conv1, 2, 128, is_training=is_training)
conv3 = vgg_block(conv2, 2, 256, is_training=is_training)
conv4 = vgg_block(conv3, 2, 512, is_training=is_training)
pool4_flat = tf.contrib.layers.flatten(conv4)
dropout = tf.layers.dropout(pool4_flat, 0.5, training=is_training)
fc1 = tf.layers.dense(dropout, 256, activation=tf.nn.relu,
kernel_initializer=tf.glorot_uniform_initializer())
fc1 = tcl.batch_norm(fc1, is_training=is_training)
return fc1
def __call__(self, x, s1_istrain=False, s2_istrain=False):
"""
:param x: tensor of shape [batch, 112, 112, 1]
:param s1_istrain:
:return:
"""
# todo: fc -> avgglobalpool
with tf.variable_scope(self.name):
with tf.variable_scope('Stage1'):
s1_fc1 = self._vgg_model(x, s1_istrain)
s1_fc2 = tf.layers.dense(s1_fc1, N_LANDMARK * 2, activation=None)
s1_out = s1_fc2 + self.mean_shape
with tf.variable_scope('Stage2'):
affine_param = TransformParamsLayer(s1_out, self.mean_shape)
affined_img = AffineTransformLayer(x, affine_param)
last_out = LandmarkTransformLayer(s1_out, affine_param)
heatmap = LandmarkImageLayer(last_out)
featuremap = tf.layers.dense(s1_fc1,
int((IMGSIZE / 2) * (IMGSIZE / 2)),
activation=tf.nn.relu,
kernel_initializer=tf.glorot_uniform_initializer())
featuremap = tf.reshape(featuremap, (-1, int(IMGSIZE / 2), int(IMGSIZE / 2), 1))
featuremap = tf.image.resize_images(featuremap, (IMGSIZE, IMGSIZE), 1)
s2_inputs = tf.concat([affined_img, heatmap, featuremap], 3)
s2_inputs = tf.layers.batch_normalization(s2_inputs, training=s2_istrain)
# vgg archive
s2_fc1 = self._vgg_model(s2_inputs, s2_istrain)
s2_fc2 = tf.layers.dense(s2_fc1, N_LANDMARK * 2)
s2_out = LandmarkTransformLayer(s2_fc2 + last_out, affine_param, inverse=True)
Ret_dict = {}
Ret_dict['S1_Ret'] = s1_out
Ret_dict['S2_Ret'] = s2_out
Ret_dict['S2_InputImage'] = affined_img
Ret_dict['S2_InputLandmark'] = last_out
Ret_dict['S2_InputHeatmap'] = heatmap
Ret_dict['S2_FeatureUpScale'] = featuremap
return Ret_dict
@property
def trainable_vars(self):
return [var for var in tf.trainable_variables() if "Stage%d" % self.stage in var.name]
@property
def vars(self):
return [var for var in tf.global_variables() if self.name in var.name]