人脸对齐算法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 , 给定一个初始形状S^0 (通常是在训练集计算得到的平均形状)。每一级输出的是根据输入图像得到的偏移估计 ΔS,那么每一级都会更准确的预测脸上 Landmark 的位置
    S^{t+1}=S^t + r_t(\phi(I, S^t))
    其中,S^tS^{t+1}分别表示第tt+1级预测的人脸形状(即所有关键点集合),r_t表示回归函数。
    在级联形状回归的框架下,由于特征提取方法\phi以及回归函数r_t的选择不同,而延伸出了一系列的人脸特征点对齐方法,如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算法框图

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

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


      DAN中各级的CNN结构
  • 标准形状规范化
    前文可知,除第一级外,之后各级输入的人脸图像都是经过对齐后的人脸图像(对齐于S^0),这种规范化操作一定程度上确保了DAN的旋转不变性。对齐的操作通过计算第t级输出S^{t-1}相对于平均形状S^0的仿射矩阵T^t(旋转r,平移t),因此相应的输出也是对齐后的估计,映射到原人脸图像上,需要进行逆变换:
    S^t = T_t^{-1}(T_t(S^{t-1}+ΔS))

  • 关键点热度图
    热度图的生成是基于某一像素点到各关键点的距离来计算的:


    关键点热度图

结果

训练

  • 数据集
    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)
    loss =\frac{||S-S'||}{d_{ipd}}
  • 预处理
    • 通过图像旋转, 平移和尺度变换进行训练即数据增广
    • 训练集上(增广前)计算平均形状和样本的均值\mu和标准差std
    • 训练时,网络的输入是规范化的112x112灰度图: to gray, crop to 112 and (I-\mu)/std
  • 代码实现
    作者开源的官方代码是基于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]

参考

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容