【论文阅读】关键点检测——3FabRec: Fast Few-shot Face alignment by Reconstruction


主要作为自己的笔记,期待有良好的学术交流,如果你也做这篇论文的电子笔记,请在留言区附上链接,蟹蟹٩(‘ω’)و

论文链接: CVF
论文链接: Arxiv
作者及团队:Bjoern Browatzki 韩国高丽大学
会议及时间:CVPR 2020
code:Github

数据集

Affectnet
VGGnet
300-W

总览

在这里插入图片描述
The rightmost box shows results from testing the framework trained on only the 10 images of the middle box with original faces (top row), reconstructed faces via the autoencoder (middle row), and confidence heatmaps (bottom row).

3FabRec的技术框架包括三个部分:
无监督训练阶段(左边框):在大量的无标注人脸数据集上,通过对抗自编码器训练一个低维隐空间;
监督学习(中间框):随后,在少量的标注人脸数据上进行有监督训练;
测试集结果(右边框):显示测试结果,中间阶段只用10幅图像进行训练后模型的测试结果,这些图像分别是原始面孔(顶部行)、通过自动编码器重构的面孔(中间行)和置信度热图(底部行)。

人脸关键点检测存在的挑战

1、人工标注不准确;
在这里插入图片描述
2、数据集标注格式、数量不一致;
在这里插入图片描述
3、图像变化量大。遮挡、角度等;
在这里插入图片描述
4、有标注的图像量少,未标注的图像量大;
在这里插入图片描述

1. Introduction

人脸关键点的准确、鲁棒定位是追踪、情感分析和人脸识别等诸多人脸处理应用的关键步骤。现有的有监督脸部关键点检测方法需要大量训练集,并且由于参数量巨大,容易对特定数据集产生过拟合现象。
作者提出一种半监督方法,其关键思想在于从大量的已有未标注人脸图像中生成生成隐式人脸知识。
首先,完全无监督阶段,通过一个低维的人脸嵌入(face embedding)来训练一个对抗性自动编码器以重建人脸。
其次,监督阶段,将解码器与传输层交错,以重新处理彩色图像的生成,以预测landmark热图。
3FabRec只要非常小的训练数据集,低至10幅图像,便可以保证预测的准确性,另外由于交错层只向解码器添加少量参数,推理在GPU上以几百帧每秒的速度运行。

2. Related Work

相关工作太多不想一一翻译,这里主要解释文章涉及的技术。

Heatmap

热图(Heatmap)有点像红外成像图,温度高的地方就很红,温度低的部分就呈现蓝色。同理,我们使用热力图可以以权重的形式来展现,神经网络对图片的哪一部分激活值最大。
从最后一层softmax节点出发,进行反向传播,对最后一层卷积层求得梯度,然后对每一张特征图求出均值,最后我们取出最后一层卷积层的激活值,与前面我们对梯度特征图的均值进行相乘,这个过程可以理解为,每个通道的重要程度与我们卷积激活值进行相乘,就相当于是一个加权操作。最后根据这个乘积值生成一个热力图,与原图进行叠加,得到了可视化的CAM图。
在这里插入图片描述
本论文中热图网络直接回归出每一类关键点的概率,在一定程度上每一个点都提供了监督信息,网络能够较快的收敛,同时对每一个像素位置进行预测能够提高关键点的定位精度。

SSIM损失

SSIM(结构相似)损失函数: 考虑了亮度 (luminance)、对比度 (contrast) 和结构 (structure)指标,类似于人类视觉感知。一般而言,SSIM得到的结果会比L1,L2的结果更有细节。
在这里插入图片描述
对于两个DxD的图像块,他们的SSIM始终小于1;1表示完全相似。其中 u x u_x ux u y u_y uy是图像块所有像素的平均值, σ x σ_x σx σ y σ_y σy是图像像素值的方差;
SSIM实际上是三个比较部分的乘积:
图像照明度比较部分:
在这里插入图片描述
图像对比度比较部分:
在这里插入图片描述
图像结构比较部分:
在这里插入图片描述
其中
在这里插入图片描述
SSIM相当于将数据进行归一化后,分别计算图像块照明度(图像块的均值),对比度(图像块的方差)和归一化后的像素向量这三者相似度,并将三者相乘。
MSE距离:
在这里插入图片描述
上图左侧为原图,中间为把灰度值调整为原来 0.9 的图,右侧为高斯模糊后的图。我们人眼明显感觉到中间的图比右边的图清晰,然而 MSE 距离显示,右侧的图与原图的距离远小于中间的图与原图的距离,即右侧的图质量比中间的高。
SSIM距离:
在这里插入图片描述
中间单纯调节亮度的图片和原图的相似性大于高斯模糊后的图,符合人类的感受
我们发现单纯调节亮度后,中间的图和原图的相似度仍然是 0.99 ,而高斯模糊后的图,和原图的相似性只有 0.85,SSIM 比 MSE 效果要好。
SSIM小于1,作为损失函数时如下公式:
在这里插入图片描述

3. Methods

3.1. Our approach

从无监督的方法开始,利用了包含在大型人脸数据集(如用于人脸识别)中的关于人脸形状的隐式知识。这个知识被捕获在一个自编码器框架的低维隐空间中。重要的是,该自动编码器还具有生成功能,即在训练时,它的任务是根据相应的隐向量重建人脸。之所以要完成这一步,是因为接下来的监督阶段实现了一个混合重建管道,该管道使用生成器和交叉传输层来重建人脸和概率关键点热图。因此,隐向量空间的变化将被映射到标注数据集上训练的关键点的位置。鉴于第一个无监督阶段已经捕获了关于面部外观和脸型的知识,这些信息将在第二个有监督阶段迅速明确,允许跨多个数据集进行泛化,并实现low-shot和few-shot的训练。
在这里插入图片描述

3.2. Unsupervised face representation

无监督训练阶段参考论文[1]的四个损失函数,部分公式作者没有在文中给出,需要看参考原文[1]及查看代码。
a. L r e c \mathcal{L}_{rec} Lrec:重建损失( reconstruction loss),基于像素的 L 1 L1 L1 损失。该公式截图来自参考文献[1] L r e c \mathcal{L}_{rec} Lrec是编码器和生成器之间的 L 1 L1 L1 损失, x x x为原始图像, G ( E ( x ) ) G(E(x)) G(E(x))为自编码器生成的图像;
在这里插入图片描述

'部分代码'
def loss_recon(X, X_recon, reduction='mean'):
    diff = torch.abs(X - X_recon) * 255
    l1_dist_per_img = diff.reshape(len(X), -1).mean(dim=1)
    return __reduce(l1_dist_per_img, reduction)

b. L e n c \mathcal{L}_{enc} Lenc:编码特征损失( encoding feature loss),确保创建平滑连续的隐空间。该公式截图来自参考文献[1],判别器 D z D_z Dz将先验分布 p ∗ ∼ p ( z ) p^∗ ∼ p(z) pp(z)强加到 z z z上(从代码来看 z z z服从正态分布)。这里是 E ( x ) E(x) E(x)而不是 G ( E ( x ) ) G(E(x)) G(E(x)),是在控制编码器,利用简单的线性神经网络,eps是防止log(a)中a值为0。loss_D_z = - torch.mean(torch.log(D_real + eps) + torch.log(1 - D_fake + eps)),“-”符号是因为D网络torch.sigmoid(self.lin3(x)),sigmoid后得出的结果为小数log(小数)<0;
在这里插入图片描述
在这里插入图片描述

'部分代码'
eps = 1e-8
#################
    def update_encoding(self, z_sample):
        stats = {}
        # Discriminator
        if self.iter_in_epoch % 4 == 0:
            z_real = self.enc_rand_like(z_sample).to(device)
            D_real = self.saae.D_z(z_real)
            D_fake = self.saae.D_z(z_sample.detach())
            loss_D_z = -torch.mean(torch.log(D_real + eps) + torch.log(1 - D_fake + eps))
            loss_D_z.backward()
            self.optimizer_D_z.step()
            stats['loss_D_z'] = loss_D_z.item()

        # Encoder gaussian loss
        if self.iter_in_epoch % 2 == 0:
            D_fake = self.saae.D_z(z_sample)
            loss_E = -torch.mean(torch.log(D_fake + eps))
            loss_E.backward(retain_graph=True)
            stats['loss_E'] = loss_E.item()
        return stats
#################
		self.D_z = D_net_gauss(self.z_dim).cuda()
#################
class D_net_gauss(nn.Module):
    def __init__(self, z_dim, N=1000):
        super(D_net_gauss, self).__init__()
        self.lin1 = nn.Linear(z_dim, N)
        self.lin2 = nn.Linear(N, N)
        self.lin3 = nn.Linear(N, 1)

    def forward(self, x):
        x = F.dropout(self.lin1(x), p=0.2, training=self.training)
        x = F.relu(x)
        x = F.dropout(self.lin2(x), p=0.2, training=self.training)
        x = F.relu(x)
        return torch.sigmoid(self.lin3(x))

c. L a d v \mathcal{L}_{adv} Ladv:对抗特征损失( adversarial feature loss),推动编码器 E 和生成器 G 产生高保真度的重建,仅使用图像重建损失会导致图像模糊。该公式截图来自参考文献[1]
在这里插入图片描述

'部分代码'
        if  self.iter_in_epoch % self.args.update_D_freq == 0:
            self.saae.D.zero_grad()
            err_real = self.saae.D(X_target)
            err_fake = self.saae.D(X_recon.detach())
            err_fake = err_fake[sklearn.utils.shuffle(range(len(err_fake)))]
            assert(len(err_real) == len(X_target))
            loss_D = -torch.mean(torch.log(err_real + eps) + torch.log(1.0 - err_fake + eps))
            if with_gen_loss:
                err_fake_gen = self.saae.D(X_gen.detach())
                loss_D_gen = -torch.mean(torch.log(err_real + eps) + torch.log(1.0 - err_fake_gen + eps))
                loss_D = loss_D*(1-w_gen) + loss_D_gen*w_gen
                stats.update({'loss_D_rec': loss_D.item(), 'loss_D_gen': loss_D_gen.item()})
            if train:
                loss_D.backward()
                self.optimizer_D.step()
            stats.update({'loss_D': loss_D.item(), 'err_real': err_real.mean().item()})
        
        # update E
        if self.iter_in_epoch % self.args.update_E_freq == 0:
            self.saae.D.zero_grad()
            set_requires_grad(self.saae.D, False)
            err_G_random = self.saae.D(X_recon)
            loss_G_rec = -torch.mean(torch.log(err_G_random + eps))
            if with_gen_loss:
                err_G_gen = self.saae.D(X_gen)
                loss_G_gen = -torch.mean(torch.log(err_G_gen + eps))
                loss_G = loss_G_rec*(1-w_gen) + loss_G_gen*w_gen
                stats.update({'loss_G_rec': loss_G_rec.item(), 'loss_G_gen': loss_G_gen.item()})
            else:
                loss_G = loss_G_rec
            set_requires_grad(self.saae.D, True)
            stats.update({'loss_G': loss_G.item(), 'err_fake': loss_G.mean().item()})
            return stats, loss_G

#############
		self.D = Discriminator().cuda()
#############
class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()
        ndf = 32
        nc = 3
        self.main = [
            # input is (nc) x 64 x 64
            nn.Conv2d(nc, ndf, 4, 2, 1, bias=False),
            nn.LeakyReLU(0.2, inplace=True),
            # state size. (ndf) x 32 x 32
            nn.Conv2d(ndf, ndf * 2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 2),
            nn.LeakyReLU(0.2, inplace=True),
            # state size. (ndf*2) x 16 x 16
            nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 4),
            nn.LeakyReLU(0.2, inplace=True),
            # state size. (ndf*4) x 8 x 8
            nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 8),
            nn.LeakyReLU(0.2, inplace=True),

            nn.Conv2d(ndf*8, ndf * 8, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 8),
            nn.LeakyReLU(0.2, inplace=True),

            # nn.Conv2d(ndf*8, ndf * 8, 4, 2, 1, bias=False),
            # nn.BatchNorm2d(ndf * 8),
            # nn.LeakyReLU(0.2, inplace=True),

            # state size. (ndf*8) x 4 x 4
            nn.Conv2d(ndf * 8, 1, 4, 1, 0, bias=False),

            nn.AdaptiveAvgPool2d((1, 1))
        ]
        self.main.append(nn.Sigmoid())
        self.main = nn.Sequential(*self.main)

    def forward(self, input):
        output = self.main(input)
        return output.view(-1, 1).squeeze(1)

d. L c s \mathcal{L}_{cs} Lcs:结构图像损失(structural image loss),由于预测 landmark 位置直接来自重建的面部元素的位置,因此在训练自动编码器时,主要重点在于准确重建这些特征。通过用新的结构化图像损失 L c s \mathcal{L}_{cs} Lcs代替[1]中使用的产生图像损失 L g e n \mathcal{L}_{gen} Lgen,将一些生成能力与重建精度进行了权衡。

结构图像损失: 为了惩罚那些没有很好地将面部结构与输入图像对齐的重建,增加了一个基于SSIM图像相似性度量的结构图像损失,为了对不能将面部结构与输入图像很好地对齐的重建进行惩罚,基于SSIM 图像相似性度量添加了结构图像损失,该损失改善了高频图像元素的对准,并且对由对抗性图像损失引入的高频噪声施加了惩罚。 L c s \mathcal{L}_{cs} Lcs​还可以充当正则化器,稳定对抗训练。
该度量可测量两个图像a和b之间的对比度c(a,b)和相关性s(a,b) ,其 σ a \sigma_a σa σ b \sigma_b σb表示图像a和b之间强度方差, σ a b \sigma_{ab} σab表示它们的协方差,常数 c = 25 5 0.01 c = 255^{0.01} c=2550.01增加了分母的稳定性。这个计算在图像上 k × k k×k k×k 个窗口上运行:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

			'部分代码'
            #######################
            # Reconstruction loss
            #######################
            loss_recon = aae_training.loss_recon(X_target, X_recon)
            loss = loss_recon * self.args.w_rec 
            iter_stats['loss_recon'] = loss_recon.item()

            #######################
            # Structural loss
            #######################
            cs_error_maps = None
            if self.args.with_ssim_loss or eval:
                store_cs_maps = self._is_printout_iter(eval) or eval  # get error maps for visualization
                loss_ssim, cs_error_maps = aae_training.loss_struct(X_target, X_recon, self.ssim,
                                                                    calc_error_maps=store_cs_maps)
                loss_ssim *= self.args.w_ssim
                loss = 0.5 * loss + 0.5 * loss_ssim
                iter_stats['ssim_torch'] = loss_ssim.item()

全自编码器目标函数:
在这里插入图片描述
其中 λ e n c \lambda_{enc} λenc​和 λ a d v \lambda_{adv} λadv=1.0, λ r e c \lambda_{rec} λrec ≈ 1.0, λ c s \lambda_{cs} λcs​≈60.0

3.3. Supervised landmark discovery

对于关键点检测,关注的不是生成RGB图像,而是包含关键点概率图的L通道图像(每个关键点一个通道)。这可以看作是一种样式转换的形式,在这种形式中,生成的人脸的外观被转换为允许我们读取关键点位置的表示。因此,之前在彩色图像生成中隐含的关于脸型的信息现在变得明确了。我们的目标是在不丢失从大量(未标记)图像中提取的面部知识的情况下创建这种转移,因为可用来进行关键点预测的经标注的数据集规模只是其中的一小部分,而且存在不精确和内容不一致的人工标注。为此,我们引入附加的、交叉的传输层到生成器G。

3.3.1 Interleaved transfer layers

训练关键点生成:首先冻结自编码器的所有参数。然后,在生成器(这里是AE的生成器,监督阶段是基于非监督阶段进行改进的)的ResNet层中交叉使用 3 × 3 3\times 3 3×3卷积层。 每个交叉传输层(ITL)产生与原始ResNet层相同数量的输出通道。ResNet 层产生的激活函数被ITL层转换,并输入到下一个block中。==ITL的主要作用就是提取生成器每一层卷积层的关键特性(本文是关于热图的位置)==最后一层卷积层映射到 L-channel 的热图(L = 要预测的landmarks 数)。 这种方法通过重新使用预训练的自编码器权重,为生成器增加了足够的灵活性以生成新的热图输出。

给定一张有标注的人脸图像 x x x, H i H_i Hi l i l_i li(第 i i i个landmark )的GT (Ground Truth) 热图。在关键点训练和推理过程中,第一个倒置ResNet block层对编码图像 z = E ( x ) z = E ( x ) z=E(x) 进行卷积操作,然后传递到第一个ITL层,接着到下一个冻结的倒立ResNet block层,这样ResNet和ITL的完整级联可以重建界标热图 H ^ \hat{H} H^

landmark heatmap 损失函数使用的是 L 2 L_2 L2损失:
在这里插入图片描述
landmark 的位置:
在这里插入图片描述

3.3.2 Encoder finetuning

一旦ITL层的训练达到收敛,可以执行一个可选的微调步骤。为此,对编码器 E E E进行解冻,使ITL层与编码器进行串联优化(图2)。
在这里插入图片描述

由于更新仅基于关键点损失( landmark errors),因此这将推动 E E E对输入面部进行编码,以便将面部特征更精确地放置在重建的面部中。 同时,其他属性(如性别,肤色或照度)可能会被删除,因为这些属性与关键点预测任务无关。 由于生成器保持不变,充当了正则化项(regularizer)的作用,并限制了编码器的灵活性,所以避免了过拟合。结构风险最小化等价于正则化。结构风险在经验风险上加上表示模型复杂度的正则化项(regularizer)或罚项(penalty term)

4. Experiments

4.2. Experimental settings

4.2.1 Unsupervised autoencoder training

训练细节:

使用 VGGFace2(1.8 million faces) & AffectNet (228k images) 数据训练对抗性自编码器,输入和输出都是 128 × 128 128\times128 128×128,训练50个epoch,batchsize=100;直到收敛,训练编码器和解码器,输入图像大小为 256 × 256 256\times256 256×256,提高生成图像的保真度,训练50个epoch,batchsize=50;

4.2.2 Supervised landmark training

将人脸大小调整到 256 × 256 256\times256 256×256,根据 landmark 坐标,使用 σ = 7 \sigma=7 σ=7生成ground truth heatmaps,并且使用了数据增强方法对人脸数据进行扩充。

4.3. Qualitative results

经过训练的生成器能够从一个低维(99D)的潜在特征向量 z z z中生成人脸如图3所示,随机生成带有landmark 热图的人脸。

在这里插入图片描述
通过在人脸嵌入之间插入和观察生成图像中的面部结构(如嘴角)以高度一致的方式构建来进一步说明隐含的脸型知识,图4展示了隐含人脸知识的可视化结果,说明了两点,首先,面部结构实际上是在低维表示 z z z中编码的。其次,此信息可以转换为像素强度(即彩色图像)的2D映射,同时保持与原始编码的高度相关性。

在这里插入图片描述
图5显示了具有挑战性的图像上的重建质量,可以看到,pipeline 将尝试重建一个全脸尽可能给定的输入,去除遮挡和化妆。但是如第5列所示,当过度极端情况下, lamdmark 的预测是不准确的。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

4.6. Ablation studies

4.6.1 Effects of ITLs

为了了解在ITL中关键点信息的学习位置,图6显示了在使用所有四层和减少上层子集时关键点热图的重构结果。可以看到,最高的图层只有非常局部的信息(主要集中在眼睛和嘴巴上),而较低的图层可以添加关于轮廓的信息——特别是在第2层下面。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

5.Conclusion

使用3FabRec,我们已经证明,在大量人脸上进行无监督的生成式训练会捕获有关人脸形状的隐式信息,从而仅需进行最少的有监督的后续训练就可以解决关键点定位。这种范例使我们的方法在本质上更强大,可以防止对特定的训练数据集过拟合以及对抗人类注释的可变性。3FabRec的关键要素可以概括为使用对抗自编码器,该自编码器可从低维潜在空间重建高质量的面孔,并在生成器阶段添加 low-overhead,ITL,以将面孔重建迁移到关键点热图重建。
结果表明,该自动编码器能够很容易地从其未标记的训练集归纳为不可见数据集的数据。这仅从训练集的几个百分比提供了对训练的泛化,并且仍然仅从少数带注释的图像产生可靠的结果——远远低于迄今为止文献中报道的任何结果。与此同时,同时,由于推理仅通过ResNet18进行两次正向传递,因此我们的方法比其他高度准确的方法具有更高的运行时性能。

附加结果图

图7显示了从潜在空间(顶部四行)的随机采样中生成的人脸,以及预测的地标热图(底部四行),使用的是论文中的最终架构(在VGGFace2和AffectNet上使用256x256px进行训练)。注意到,这些面部具有很高的视觉质量,并且在面部外观(姿势、表情、发型、配饰)上有很大的变异性。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

参考博客:
https://blog.csdn.net/john_bh/article/details/106721494
https://www.tensorinfinity.com/paper_164.html
https://blog.csdn.net/Kevin_cc98/article/details/79028507

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值