超越面部旋转:全局和局部感知GAN生成逼真的和身份保留正脸图像
Beyond Face Rotation: Global and Local Perception GAN for Photorealistic and
Identity Preserving Frontal View Synthesis
一.摘要
背景:
单一人脸图像的逼真正面视图合成在人脸识别领域有着广泛的使用。
数据驱动的深度学习方法被提出,但是由于侧脸图片数据集的收集和标记难度,所以正脸和侧脸的数据严重不平衡。
GAN在二维数据分布建模方面的突出能力,极大地提高了超分辨率等许多不适定的底层视觉问题。
思路启发来源:
当我们试图去合成一个图像的时候,我们先根据我们的先验知识和观察到的轮廓来推断正面人脸的整体结构,然后我们注意力到局部区域,在那里所有的面部细节都被填满。
从人脸的对称结构中得到灵感,提出了一种对称损失来填充遮挡部分。
本文的创新点:
(网络)提出了two-pathway 生成对抗网络(TP-GAN)。通过同时感知全局结构和局部纹理的转换来合成逼真的正脸视图。
(结构)提出了四个标记定位块网络,除了在常用的全局编码解码网络的基础上还注意局部纹理。
(损失)为了更好的约束这个问题,引入了对抗损失,对称损失、身份保留损失、感知损失来保留个体最显著的面部结构。将数据分布中的先验知识(对抗训练)和人脸领域知识(对称损失和身份保持损失)结合起来,精确地恢复了将三维物体投影到二维图像空间中所固有的丢失信息。
综合损失函数 利用正脸特征分布和预训练识别深度人脸模型,指导从侧脸到正脸视图进行身份保留推理。
与先前主要依赖中间特征进行识别的深度学习方法不同,本文的方法直接利用合成的身份保持图像进行人脸识别和属性估计等后续任务。
效果:实验结果表明,该方法不仅能给出令人信服的感知结果,而且在大姿态人脸识别方面的性能也优于现有方法。
二.介绍
解决姿态变化的方法:
① 学习姿态不变的特征。
传统的方法:经常使用Gabor ,haar,LBP去考虑局部畸变,然后采用度量学习技术实现姿态不变。
深度学习方法:经常使用池化操作和triplet loss ,contrastIve loss 去确保对每一个大的类间变化的不变性。但是这些方法不能处理大姿态问题。
②利用合成技术:从大量姿态人脸图像中恢复一个正脸的视图,然后使用这些恢复的人脸图像进行人脸识别。
传统的方法:通常使用3D几何变换来呈现一个正脸视图。
问题:这些方法在小姿态人脸归一化中表现好,但是在大姿态下纹理严重丢失,性能下降。
深度学习方法:提出了一种基于深度学习的数据驱动人脸复原方法。
问题:虽然结果很好,但合成的图像有时缺乏精细的细节,在较大的姿态下往往是模糊的,因此他们只使用中间特征进行人脸识别。合成的图像仍然不能很好地执行其他面部分析任务,如取证和属性估计。
从优化的角度来讲:从不完整的侧脸图像中恢复出正脸视图是定义不明确的问题,如果不考虑先验知识或者约束条件,则存在多个解。因此,恢复结果的质量在很大程度上依赖于训练过程中利用的先前的或约束条件。先前的工作在训练过程中通常采用成对的监督很少引入约束,所以容易产生模糊的效果。 从侧脸图像IP合成正面人脸是一个高度非线性的变换。由于这些过滤器是在人脸图像的所有空间位置上共享的,我们认为仅仅使用一个全局网络无法学习既适合旋转人脸又能精确恢复局部细节的过滤器。我们将传统方法中两种通路结构的成功转化为基于深度学习的框架,并引入了仿人的两种通路生成器来进行正面视图的合成。
三.结构:
采用Two Pathway Generator。
local pathway:用于解决人脸的细节问题,输入侧脸的四个特征图像块:分别是 两个眼睛、鼻子、嘴巴。输出正脸的对应四个图像块
global pathway:用于生产人脸大的结构,缺少细节,输入完整的侧脸图像输出完整的模糊的正脸图像
一个GAN网络和一个分类网络,GAN网络有两部分组成,一个全局的GAN生成一个正脸轮廓没有具体眼睛嘴巴鼻子的,和四个局部的GAN分别生成左眼 右眼 鼻子 嘴巴。然后把他们两个结合起来。
分类网络:分类网络就是一个简单的CNN,判别生成的人脸是哪个人。
网络结构代码:
局部生成器代码:
class LocalPathway(nn.Module):
def __init__(self,use_batchnorm = True,feature_layer_dim = 64 , fm_mult = 1.0):
super(LocalPathway,self).__init__()
n_fm_encoder = [64,128,256,512]
n_fm_decoder = [256,128]
n_fm_encoder = emci(n_fm_encoder,fm_mult) 进行点乘操作
n_fm_decoder = emci(n_fm_decoder,fm_mult)
#encoder sequential是一个时序容器,会以他们传入的顺序添加到容器中
self.conv0 = sequential( conv( 3 , n_fm_encoder[0] , 3 , 1 , 1 , "kaiming" , nn.LeakyReLU(1e-2) , use_batchnorm) ,
ResidualBlock(n_fm_encoder[0] , activation = nn.LeakyReLU() ) )
self.conv1 = sequential( conv( n_fm_encoder[0] , n_fm_encoder[1] , 3 , 2 , 1 , "kaiming" , nn.LeakyReLU(1e-2) , use_batchnorm) ,
ResidualBlock(n_fm_encoder[1] , activation = nn.LeakyReLU() ) )
self.conv2 = sequential( conv( n_fm_encoder[1] , n_fm_encoder[2] , 3 , 2 , 1 , "kaiming" , nn.LeakyReLU(1e-2) , use_batchnorm) ,
ResidualBlock(n_fm_encoder[2] , activation = nn.LeakyReLU() ) )
self.conv3 = sequential( conv( n_fm_encoder[2] , n_fm_encoder[3] , 3 , 2 , 1 , "kaiming" , nn.LeakyReLU(1e-2) , use_batchnorm) ,
ResidualBlock(n_fm_encoder[3] , activation = nn.LeakyReLU() ) )
#decoder
self.deconv0 = deconv( n_fm_encoder[3] , n_fm_decoder[0] , 3 , 2 , 1 , 1 , "kaiming" , nn.ReLU() , use_batchnorm)
self.after_select0 = sequential( conv( n_fm_decoder[0] + self.conv2.out_channels , n_fm_decoder[0] , 3 , 1 , 1 , 'kaiming' , nn.LeakyReLU() , use_batchnorm ) , ResidualBlock( n_fm_decoder[0] , activation = nn.LeakyReLU() ) )
self.deconv1 = deconv( self.after_select0.out_channels , n_fm_decoder[1] , 3 , 2 , 1 , 1 , "kaiming" , nn.ReLU() , use_batchnorm)
self.after_select1 = sequential( conv( n_fm_decoder[1] + self.conv1.out_channels , n_fm_decoder[1] , 3 , 1 , 1 , 'kaiming' , nn.LeakyReLU() , use_batchnorm ) , ResidualBlock( n_fm_decoder[1] , activation = nn.LeakyReLU() ) )
self.deconv2 = deconv( self.after_select1.out_channels , feature_layer_dim , 3 , 2 , 1 , 1 , "kaiming" , nn.ReLU() , use_batchnorm)
self.after_select2 = sequential( conv( feature_layer_dim + self.conv0.out_channels , feature_layer_dim , 3 , 1 , 1 , 'kaiming' , nn.LeakyReLU() , use_batchnorm ) , ResidualBlock( feature_layer_dim , activation = nn.LeakyReLU() ) )
self.local_img = conv( feature_layer_dim , 3 , 1 , 1 , 0 , None , None , False )
局部特征学习的前向传播
def forward(self,x):
conv0 = self.conv0( x )
conv1 = self.conv1( conv0 )
conv2 = self.conv2( conv1 )
conv3 = self.conv3( conv2 )
deconv0 = self.deconv0( conv3 )
after_select0 = self.after_select0( torch.cat([deconv0,conv2], 1) ) #对上次输出和编码环节的第二步卷积点乘
deconv1 = self.deconv1( after_select0 )
after_select1 = self.after_select1( torch.cat([deconv1,conv1] , 1) )
deconv2 = self.deconv2( after_select1 )
after_select2 = self.after_select2( torch.cat([deconv2,conv0], 1 ) )
local_img = self.local_img( after_select2 )
assert local_img.shape == x.shape , "{} {}".format(local_img.shape , x.shape)
return local_img , deconv2
局部特征融合:
输出的是四个局部标记块的特征 带有坐标和标签
将不同部位的特征块 进行填充 填充为128*128 然后将四个部位拼接为一张图
class LocalFuser(nn.Module):
def __init__(self ):
super(LocalFuser,self).__init__()
def forward( self , f_left_eye , f_right_eye , f_nose , f_mouth):
EYE_W , EYE_H = 40 , 40 # 确定特征块的大小
NOSE_W , NOSE_H = 40 , 32
MOUTH_W , MOUTH_H = 48 , 32
IMG_SIZE = 128
f_left_eye = torch.nn.functional.pad(f_left_eye , (39 - EYE_W//2 - 1 ,IMG_SIZE - (39 + EYE_W//2 - 1) ,40 - EYE_H//2 - 1, IMG_SIZE - (40 + EYE_H//2 - 1))) #对每一个特征块进行填充
f_right_eye = torch.nn.functional.pad(f_right_eye,(86 - EYE_W//2 - 1 ,IMG_SIZE - (86 + EYE_W//2 - 1) ,39 - EYE_H//2 - 1, IMG_SIZE - (39 + EYE_H//2 - 1)))
f_nose = torch.nn.functional.pad(f_nose, (64 - NOSE_W//2 - 1 ,IMG_SIZE - (64 + NOSE_W//2 -1) ,64 - NOSE_H//2- 1, IMG_SIZE - (64 + NOSE_H//2- 1)))
f_mouth = torch.nn.functional.pad(f_mouth, (65 - MOUTH_W//2 -1 ,IMG_SIZE - (65 + MOUTH_W//2 -1),89 - MOUTH_H//2-1, IMG_SIZE - (89 + MOUTH_H//2-1)))
return torch.max( torch.stack( [ f_left_eye , f_right_eye , f_nose , f_mouth] , dim = 0 ) , dim = 0 )[0] #输出仅有四个特征组合的五官特征图
全局生成网络结构图:
损失:
1.像素损失:
我们在多个位置采用像素L1损失,以促进多尺度图像内容的一致性
像素损失是在全局输出,标记定位块网络和最后的融合输出处被计算的。为了便于深度监督,还增加了对多尺度生成器全局编码器输出的约束。
缺点:将导致过于平滑的合成结果。
优点:是加速优化和优越性能的重要组成部分。
它直接算合成的人脸与真实正脸每个点像素值的绝对差值。这个loss在三个地方起作用,一个是预测局局部区域例如左眼的时候,一个是全局的,还有是全局和局部合成一个最终正脸的时候。如果光训练这一个loss,要训练到收敛我觉得是比较难的。因为每个点的像素值都能影响到loss,训练过程中指挥棒比较分散。
像素损失代码:
Pixeljwise_loss:#计算合成的人脸与真实的正脸每个像素点之间的绝对差值
Pixeljwise_loss的作用有三处
l1_loss = torch.nn.L1Loss().cuda() #输入x和目标y之间差的绝对值的平均值
pixelwise_128_loss = l1_loss( img128_fake , batch['img_frontal']) #不同size的合成图片之间的loss
pixelwise_64_loss = l1_loss( img64_fake , batch['img64_frontal'])
pixelwise_32_loss = l1_loss( img32_fake , batch['img32_frontal'])
pixelwise_loss = config.loss['weight_128'] * pixelwise_128_loss + config.loss['weight_64'] * pixelwise_64_loss + config.loss['weight_32'] * pixelwise_32_loss #全局损失综合
eyel_loss = l1_loss( le_fake , batch['left_eye_frontal'] ) #局部loss
eyer_loss = l1_loss( re_fake , batch['right_eye_frontal'] )
nose_loss = l1_loss( nose_fake , batch['nose_frontal'] )
mouth_loss = l1_loss( mouth_fake , batch['mouth_frontal'] )
pixelwise_local_loss = eyel_loss + eyer_loss + nose_loss + mouth_loss #局部损失综合
2.对称损失
对称是人脸的固有特征。利用这一领域知识作为先验知识,对合成图像施加对称约束,可以有效地缓解自遮挡问题,从而大大提高大姿态情况下的性能。
定义了两个空间的对称性损失,原始像素空间和拉普拉斯图像空间 它们对光照的变化具有鲁棒性。我们有选择地翻转输入,以便遮挡的部分都在右侧。只有遮挡部分在右侧,才会具有对称损失。
对称损失的作用:
通过鼓励一个对称结构生成真实的图像。
通过提供额外的反向传播梯度来缓解极端姿态下的自遮挡,从而加速了TP-GAN的收敛。
问题:由于光照改变或者本质的纹理差异,像素值在大多数时候并不是完全对称。
局部区域内的像素差异是一致的,在不同的光照下,一个点沿着所有方向的梯度基本上是保留的,因此,拉普拉斯空间对光照变化的鲁棒性更强,对人脸结构的是知识性更强。由于预测的是正脸,正脸是对称的,左右对称位置上的像素应该相同。
对称损失代码:
Summetry_loss:
#有选择的翻转输入使得遮挡在右侧
inv_idx128 = torch.arange(img128_fake.size()[3]-1, -1, -1).long().cuda()
img128_fake_flip = img128_fake.index_select(3, Variable( inv_idx128))
img128_fake_flip.detach_()
#对不同size 的翻转
inv_idx64 = torch.arange(img64_fake.size()[3]-1, -1, -1).long().cuda()
img64_fake_flip = img64_fake.index_select(3, Variable( inv_idx64))
img64_fake_flip.detach_()
inv_idx32 = torch.arange(img32_fake.size()[3]-1, -1, -1).long().cuda()
img32_fake_flip = img32_fake.index_select(3, Variable( inv_idx32))
img32_fake_flip.detach_()
#计算左右对称位置像素值的差
symmetry_128_loss = l1_loss( img128_fake , img128_fake_flip )
symmetry_64_loss = l1_loss( img64_fake , img64_fake_flip )
symmetry_32_loss = l1_loss( img32_fake , img32_fake_flip )
symmetry_loss = config.loss['weight_128'] * symmetry_128_loss + config.loss['weight_64'] * symmetry_64_loss + config.loss['weight_32'] * symmetry_32_loss #对称损失综合
3.对抗损失
对抗损失作为一个监督,推动合成图像去reside多个正脸图像。它可以防止模糊效应,产生好的结果,让合成的人脸跟真实的人脸更接近。
对抗损失代码:
D_loss :
#torch.mean(input)float 返回输入张量所有元素的均值
#判断 合成的图片和真实正脸图片的损失
alpha = torch.rand( batch['img_frontal'].shape[0] , 1 , 1 , 1 ).expand_as(batch['img_frontal']).pin_memory().cuda(async = True) #扩充维度
interpolated_x = Variable( alpha * img128_fake.detach().data + (1.0 - alpha) * batch['img_frontal'].data , requires_grad = True)
out = D(interpolated_x)
dxdD = torch.autograd.grad( outputs = out , inputs = interpolated_x , grad_outputs = torch.ones(out.size()).cuda() , retain_graph = True , create_graph = True , only_inputs = True )[0].view(out.shape[0],-1)
gp_loss = torch.mean( ( torch.norm( dxdD , p = 2 ) - 1 )**2 )
adv_D_loss = - torch.mean( D( batch['img_frontal'] ) ) + torch.mean( D( img128_fake.detach() ) )
L_D = adv_D_loss + config.loss['weight_gradient_penalty'] * gp_loss
G_loss:
cross_entropy_loss = cross_entropy( G_encoder_outputs , batch['label'] ) #用于识别任务的交叉熵loss
L_syn = config.loss['weight_pixelwise']*pixelwise_loss + config.loss['weight_pixelwise_local'] * pixelwise_local_loss + config.loss['weight_symmetry']*symmetry_loss + config.loss['weight_adv_G']*adv_G_loss + config.loss['weight_identity_preserving']*ip_loss + config.loss['weight_total_varation']*tv_loss #综合损失
L_G = L_syn + config.loss['weight_cross_entropy']*cross_entropy_loss #生成器的损失
Adv_D_loss:
adv_D_loss = - torch.mean( D( batch['img_frontal'] ) ) + torch.mean( D( img128_fake.detach() ) )
Adv_G_loss:
adv_G_loss = - torch.mean( D(img128_fake) )
4.身份保留损失
在“生成识别”框架的开发中,在综合正面视图图像的同时保持身份是最关键的部分。我们利用最初提出的感知损失帮助我们的模型获得身份保持能力。
在Light CNN的最后两层的激活中定义身份保留损失。
其中Wi, Hi为最后第i层的空间维数,i从1到2,代表的是分类网络的最后两层卷积层,身份保留损失去预测真实的图片和深度特征空间之间的最小距离。由于Light CNN是经过预先训练的,可以对成千上万的身份进行分类,所以它可以捕捉到最突出的特征或人脸结构来进行身份识别。因此,利用这一损失来加强保持身份的正面视图综合是完全可行的。
这个损失的作用是让合成的人脸还是本人,而不是合成了另一个人。对抗损失是保证合成的人脸逼真,保留身份损失相当于更进一步,不光逼真,还是本人
绝对值符号里面的两项相减,说的是真实正脸和合成正脸走CNN前向传播得到的feature map。
它的物理意义是相对像素级的损失,是高级语义损失,由于CNN的高层值代表更抽象的语义信息,因而这个损失能保证合成的人脸主体特征的正确性,进而保证了合成人脸的身份。
Ip_loss:
#真实的正脸
feature_frontal , fc_frontal = feature_extract_model( batch['img_frontal'] )
#合成的正脸
feature_predict , fc_predict = feature_extract_model( img128_fake )
mse = torch.nn.MSELoss().cuda()
ip_loss = mse( feature_predict , feature_frontal.detach() )
5.感知损失
在生成图像领域,产生了一个非常重要的idea,那就是可以将卷积神经网络提取出的feature,作为目标函数的一部分,将真实图片卷积得到的feature与生成图片卷积得到的feature作比较,使得高层信息(内容和全局结构)接近,也就是感知的意思 使得待生成的图片与目标图片在语义上更加相似(相对于Pixel级别的损失函数)。
感知损失代码:
#将真实图片卷积得到的特征图和生成图片卷积得到的特征图做比较
tv_loss = torch.mean( torch.abs( img128_fake[:,:,:-1,:] - img128_fake[:,:,1:,:] ) ) + torch.mean( torch.abs( img128_fake[:,:,:,:-1] - img128_fake[:,:,:,1:] ) )
6.综合目标函数
cross_entropy_loss = cross_entropy( G_encoder_outputs , batch['label'] ) #用于识别任务的交叉熵loss
L_syn = config.loss['weight_pixelwise']*pixelwise_loss + config.loss['weight_pixelwise_local'] * pixelwise_local_loss + config.loss['weight_symmetry']*symmetry_loss +config.loss['weight_adv_G']*adv_G_loss + config.loss['weight_identity_preserving']*ip_loss + config.loss['weight_total_varation']*tv_loss
四.实验
细节:
图像大小1281283作为输入
在一个具有750000+的大数据集MultiPIE上进行评估;在姿态、光照和表情变化下进行人脸识别。训练集的输入:侧脸和正脸训练对,侧脸有几个种类,90度,60度,45度等。对输入图片进行了预处理,通过关键点检测,把左眼右眼鼻子嘴巴都抠出来,做成了侧脸左眼正脸左眼训练对,侧脸鼻子,正脸鼻子训练对,以此类推。
light-cnn 作为特征提取网络,在MS-CELEB-1M上训练,MultiPIE的原始图像上进行微调。TP-GAN的训练 batch-size为10,学习率为0.0001,损失函数的权重为a=0.001,λ1=0.3,λ2=0.001,λ3=0.003,λ4=0.0001
使用LFW数据集,用于测试只在Multi-PIE上训练的TP-GAN模型
TP-GAN人脸合成的优势:
与其他方法相比,TP-GAN在生成真实感合成时具有较好的保真性。
利用Ladv和Lip的先验知识进行数据驱动建模,不仅可以对整体面部结构产生错觉,还可以对被遮挡的耳朵、脸颊和前额产生一致性的错觉。
它还完美地保留了原始头像中观察到的面部特征,如眼镜和发型。
TP-GAN能够在LFW数据集中忠实地合成具有更精细细节和更好全局形状的正面视图图像。