论文:BeautyGAN: Instance-level Facial Makeup Transfer with Deep Generative Adversarial Network
官网:http://liusi-group.com/projects/BeautyGAN
Github:https://github.com/Honlan/BeautyGAN
论文提出了一种基于GAN的方式的化妆迁移的方法BeautyGAN,效果优于传统的Cycle-GAN。
主要贡献:
- 基于GAN实现了自动的换装,实验表明,BeautyGAN不仅高效,而且生成质量优于目前最好的方法。
- 通过在局部区域应用像素级别的直方图loss,取得了实例级别的风格变换。该实例级别的变换策略也可以应用于风格迁移,属性变换等其他任务。
- 贡献了3834张高清图片的换妆数据集Makeup Transfer(MT)
网络结构:
生成器G为两个输入和两个输出的网络结构,中间的模块共享权重。生成器G中还使用了IN(instance normalization)模块。生成器的输入图片大小为256*256,输出图片大小也是256*256。
判别器D为70*70的PatchGANs。
损失函数:
假设未化妆图片为A,A ⊂ RH×W ×3
化妆图片为B,B ⊂ RH×W ×3
整个换妆问题可以定义为,
Isrc表示需要换妆的人脸图片,表示ID图,
Iref表示化好妆的参考图,
IBsrc表示将原图Isrc进行了图B的操作,即化妆操作,也就是我们真正需要的输出结果。
IAref表示将参考图Iref进行了图片A的操作,即去妆操作
BeautyGAN整体loss由4部分loss组成,对抗loss(adversarial loss),循环GAN loss( cycle consistency loss),感知loss( perceptual loss) ,换妆约束loss(makeup constrain loss) 。
其中,α = 1, β = 10,γ = 0.005
对抗loss(adversarial loss):
由于生成模型有2个输出组成,所以判别器也是有2个组成,DA和DB。
由于训练过程中,该loss是基于log函数的loss,很容易出现负值,因此,使用MSE loss对DA和DB分别进行优化。
为了使得判别器的训练更加平稳,这里还引入了普归一化spectral normalization,
σ(W ) 表示w的归一化操作。h表示每一层的输入。
循环GAN loss( cycle consistency loss):
整个的训练过程,先通过输入的图片(Isrc,Iref)生成妆容风格互换后的图片G(Isrc,Iref)。然后将互换妆容风格的图片再输入生成器中,就会将妆容风格又互换回来G(G(Isrc,Iref)),也就是说经过2次互换,又回到了原始的输入图片。
Cycle loss的目的就是保证2次换妆后的输出和原始输入一样。
其中,dist表示L1或者L2。
感知loss( perceptual loss):
Flijk 表示模型的第l层,位置<j,k>处的第i个滤波器。
换妆约束loss(makeup constrain loss):
首先使用PSPNet 这样的分割模型,对人脸区域进行分割,即Face parsing 。可以分别提取出嘴巴,眼睛,人脸这3个部位。然后分别对这3个部位进行直方图Histogram loss的计算。
其中,λl = 1, λs =1, λf = 0.1
◦ 表示elemetwise的乘法操作,
item表示{lips, shadow, f ace}
为什么要进行Face parsing操作?
- 背景和头发区域的像素和换妆是没有关系的。
- 人脸换妆不仅是一个全局的风格变换,更是人脸不同区域的独立风格的变换。
First, pixels in background and hairs have no relationship with makeup. If we do not separate them apart, they
will disturb the correct color distribution. Second, facial makeup is beyond a global style but a collection of several independent styles in different cosmetics regions.
为什么要使用Histogram loss,而不是MSE loss?
If we directly adopt MSE loss on pixel-level histograms of two images, the gradient will be zero, owning to the indicator function, thus makes no contribution to optimization process. Therefore, we adopt histogram matching strategy that generates a ground truth remapping image in advance.
Makeup Transfer(MT) 数据集:
该数据集一共包含3834张图片,其中1115 张没有化妆,2719 张有化妆。图片大小为361*361。从里面随机选出100张未化妆的,250张化妆的作为测试集。并且附带分割的mask图片。
工程化:
模型ckpt转化为pb
import tensorflow as tf
from tensorflow.python.framework import graph_util
def freeze_graph(input_checkpoint,output_graph):
'''
:param input_checkpoint:
:param output_graph: PB模型保存路径
:return:
'''
# 指定输出的节点名称,该节点名称必须是原模型中存在的节点
# 直接用最后输出的节点,可以在tensorboard中查找到,tensorboard只能在linux中使用
input= ["X","Y"]
output_node_names = "generator/xs"
saver = tf.train.import_meta_graph(input_checkpoint + '.meta', clear_devices=True)
graph = tf.get_default_graph() # 获得默认的图
input_graph_def = graph.as_graph_def() # 返回一个序列化的图代表当前的图
#print(input_graph_def)
with tf.Session() as sess:
saver.restore(sess, input_checkpoint) #恢复图并得到数据
output_graph_def = graph_util.convert_variables_to_constants( # 模型持久化,将变量值固定
sess=sess,
input_graph_def=input_graph_def,# 等于:sess.graph_def
output_node_names=output_node_names.split(","))# 如果有多个输出节点,以逗号隔开
with tf.gfile.GFile(output_graph, "wb") as f: #保存模型
f.write(output_graph_def.SerializeToString()) #序列化输出
print("%d ops in the final graph." % len(output_graph_def.node)) #得到当前图有几个操作节点
freeze_graph('./model/model',"model.pb")
转化完后,模型从356M降为28.4M。
有了这个基本的网络结构,就可以进行训练代码复现,本人复现后的代码,还有一些小问题,待完善后跟新,
Anyway,the devil in the details.
推理端调用pb模型:
import os,sys
import cv2
import numpy as np
import tensorflow as tf
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
class BeautyGAN():
def __init__(self):
cur_dir = os.path.dirname(os.path.abspath(__file__))
model_file = "./model.pb"
self.graph = self.load_graph(model_file)
with self.graph.as_default():
self.inputsX = self.graph.get_tensor_by_name("import/X:0")
self.inputsY = self.graph.get_tensor_by_name("import/Y:0")
self.result = self.graph.get_tensor_by_name("import/generator/xs:0")
self.session = tf.Session(graph=self.graph)
def load_graph(self, model_file):
graph = tf.Graph()
graph_def = tf.GraphDef()
with open(model_file, "rb") as f:
graph_def.ParseFromString(f.read())
with graph.as_default():
tf.import_graph_def(graph_def)
return graph
def interface(self,inputX,inputY):
inputX = cv2.resize(inputX[:,:,::-1], (256, 256),interpolation=cv2.INTER_CUBIC)
inputY = cv2.resize(inputY[:,:,::-1], (256, 256),interpolation=cv2.INTER_CUBIC)
array_X = (np.array(inputX)/(255*1.0)-0.5)*2.0
samples_features_X = array_X.reshape([-1,256,256,3])
array_Y = (np.array(inputY)/(255*1.0)-0.5)*2.0
samples_features_Y = array_Y.reshape([-1,256,256,3])
feed = {self.inputsX: samples_features_X,self.inputsY: samples_features_Y}
predict_result = self.session.run(self.result, feed_dict=feed)
predict_result =np.array(((predict_result +1)/2*255),np.uint8).squeeze()[:,:,::-1]
return predict_result
if __name__=="__main__":
bgan=BeautyGAN()
imageX=cv2.imread("./imgs/no_makeup/vSYYZ306.png")
imageY=cv2.imread("./imgs/makeup/vFG756.png")
predict_result=bgan.interface(imageX,imageY)
cv2.imwrite("predict_result.jpg",predict_result)
实验结果: