人脸对齐算法DAN--Deep Alignment Network: A convolutional neural network for robust face alignment

摘要

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--Deep Alignment Network: A convolutional neural network for robust face alignment_第1张图片
    DAN算法框图

    参考上图,DAN中每一级的输入输出介绍如下:

    • 第一级的输入是一张人脸图像(可以是人脸检测后crop得到的人脸图像),经过CNN网络,加上平均形状,得到该级的形状估计,
    • 第二级中,首先利用对人脸图像和进行矫正变换(计算相对于的仿射矩阵,作用于二者上),得到矫正后的人脸图像和形状,并根据生成关键点热度图,然后将,以及第一级全连接层的输出 三者在通道轴上进行拼接,以次作为第二级的输入,经过CNN网络输出该级的更细致的预测。
    • 之后的级联都可以看作是第二级的堆叠(即:上一级的全连接层, 经上一级输出的关键点热度图,矫正后的人脸图像作为输入,输出该级的估计)
      此外,DAN中每一级采用的CNN网络结构都是一样的,即VGG16的mini版,各级的输入是112x112的灰度图,输出是1x136的关键点坐标,详细结构如下:


      人脸对齐算法DAN--Deep Alignment Network: A convolutional neural network for robust face alignment_第2张图片
      DAN中各级的CNN结构
  • 标准形状规范化
    前文可知,除第一级外,之后各级输入的人脸图像都是经过对齐后的人脸图像(对齐于),这种规范化操作一定程度上确保了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]

参考

级联回归
人脸关键点对齐方法综述

你可能感兴趣的:(人脸对齐算法DAN--Deep Alignment Network: A convolutional neural network for robust face alignment)