灵感
我最近迷上了图雅老师的套马杆,美中不足的就是她总是“躲汉子”,或许是因为这个汉子不够帅不能吸引到她?那今天我就动手做一个帅气的“套马的汉子”。准备工作很简单,去下载一张帅气的男生图片以及一张马的图片就可以了。
风格迁移
1.简述
风格迁移顾名思义就是将一个A图像的风格迁移到另一个B图像上混合在一起;最终的效果是产生一个看起来还是B图像的内容,但是用了A图像风格的新的图像。
在深度学习火热之前,解决这个问题往往是通过提取风格图像的纹理特征以及内容图像的纹理特征,说到底这其实是基于统计的方法。这种传统的方法效果不太好,而且得到的模型泛化能力有限,往往只能用于特定的图像上。
到了深度学习火热时代,随着各种深层次效果更好的卷积神经网络(CNN)模型提出,就有了很好的特征提取器。这个时候创新点就出现了,运用深度学习的方法代替传统的纹理特征提取方法作为图像的特征提取器,这也就是论文中最重要的思想,下面是论文中将内容图像与不同艺术风格的作品结合的结果
2.重点
前面已经提出了使用卷积神经网络代替传统的局部纹理特征提取,但是纹理特征与图像风格之间存在着关系才决定了风格迁移是否可行。
再来看看论文中提到的方法
使用VGG19作为特征提取器,并且用平均池化代替最大池化,获得更有吸引力的结果;
损失函数用的还是均方误差,然后经过反向传播梯度下降训练网络参数,使原图像与生成的图像在内容上更相似;在风格上相似也就是使得原始图像与生成图像的格拉姆矩阵之间的均方距离最近。
然后总的损失函数为
是内容和风格的权重因子,不同的权重比率就会得到不同的图像结果。
横着对比就是不同比率对应着不同的结果图。
3.代码demo
使用tensorflow创建一些有意思的风格迁移图片
import tensorflow as tf import IPython.display as display import matplotlib.pyplot as plt import matplotlib as mpl import numpy as np import PIL.Image import time import functools import warnings mpl.rcParams['figure.figsize'] = (12,12) mpl.rcParams['axes.grid'] = False plt.rcParams['font.sans-serif']='SimHei' plt.rcParams['axes.unicode_minus']=False warnings.filterwarnings('ignore') tf.__version__
# 从tensor转为图像 def tensor_to_image(tensor): tensor = tensor*255 tensor = np.array(tensor, dtype=np.uint8) if np.ndim(tensor)>3: assert tensor.shape[0] == 1 tensor = tensor[0] return PIL.Image.fromarray(tensor) # 加载本地图片 def load_img(img_path): # 最大像素值 max_dim = 512 # 读取图像(dtype=string),解码(从string变为tensor)并修改数据类型 img = tf.io.read_file(img_path) img = tf.image.decode_image(img, channels=3) img = tf.image.convert_image_dtype(img, tf.float32) shape = tf.cast(tf.shape(img)[:-1], tf.float32) long_dim = max(shape) # 规范化 scale = max_dim / long_dim new_shape = tf.cast(shape * scale, tf.int32) img = tf.image.resize(img, new_shape) img = img[tf.newaxis, :] return img # 可视化图片 def imshow(image, title=None): if len(image.shape) > 3: image = tf.squeeze(image, axis=0) plt.imshow(image) if title: plt.title(title) 复制代码
3.1 使用tensorflow-hub快速实现图像风格迁移
首先,我们引入内容图像和风格图像并可视化一下
content_image = load_img('./帅哥.jpeg') style_image0 = load_img('./党.jpeg') style_image1 = load_img('./艺术.jpeg') style_image2 = load_img('./马.jpeg') plt.subplot(2,2, 1) imshow(content_image, '内容图片') plt.subplot(2,2, 2) imshow(style_image0, '风格图片1') plt.subplot(2,2, 3) imshow(style_image1, '风格图片2') plt.subplot(2,2,4) imshow(style_image2, '风格图片3') 复制代码
当我们使用tensorflow-hub的模型时,有可能会出现因为网络问题(无法科学上网)加载失败,这个时候我们需要把链接修改到.cn版本的网站中,具体的tensorflow-hub链接在这里,然后去找到你所需要的模型。
# 使用tensorflow_hub制作风格迁移 import tensorflow_hub as hub hub_module = hub.load('https://hub.tensorflow.google.cn/google/magenta/arbitrary-image-stylization-v1-256/2') for i in range(0,3): stylized_image=hub_module(tf.constant(content_image),tf.constant(locals()['style_image'+str(i)]))[0] tensor_to_image(stylized_image).show() 复制代码
三种不同风格的图像如下,我分别叫它“爱党的孩子”,“迷幻的帅哥”,“套马的汉子”
3.2 自己动手实现风格迁移
在这里,我们需要使用VGG19中的部分网络作为我们的图像特征提取器,并且根据论文设置好网络的损失函数以及优化方法,构建自己的风格迁移网络。
开始,先看看VGG19中的网络结构,并且选出其中我们需要的部分
# 除去最后三个全连接层,只要特征提取部分网络 vgg = tf.keras.applications.VGG19(include_top=False, weights='imagenet') # 查看各网络层 for layer in vgg.layers: print(layer.name) 复制代码
从这些卷积层中选出我们的内容提取和风格提取
# 内容层:提取图像特征 content_layers = ['block5_conv2'] # 多个风格层,每一层的输出可能是我们需要的某种风格 style_layers = ['block1_conv1', 'block2_conv1', 'block3_conv1', 'block4_conv1', 'block5_conv1'] num_content_layers = len(content_layers) num_style_layers = len(style_layers) 复制代码
# 根据上面的定义创建我们自己的VGG层 def vgg_layers(layer_names): vgg = tf.keras.applications.VGG19(include_top=False, weights='imagenet') vgg.trainable = False outputs = [vgg.get_layer(name).output for name in layer_names] model = tf.keras.Model([vgg.input], outputs) return model 复制代码
# 风格特征提取器 style_extractor = vgg_layers(style_layers) # 风格特征 style_outputs = style_extractor(style_image0*255) #查看每层输出的统计信息 for name, output in zip(style_layers, style_outputs): print(name) print(" shape: ", output.numpy().shape) print(" min: ", output.numpy().min()) print(" max: ", output.numpy().max()) print(" mean: ", output.numpy().mean()) print() 复制代码
图像的内容由中间feature maps(特征图)的值表示。
事实证明,图像的风格可以通过不同 feature maps (特征图)上的平均值和相关性来描述。 通过在每个位置计算 feature (特征)向量的外积,并在所有位置对该外积进行平均,可以计算出包含此信息的 Gram 矩阵。 对于特定层的 Gram 矩阵,具体计算方法如下所示:
这可以使用tf.einsum
函数来实现,有关于tf.einsum的使用可以看tf.einsum的使用
# 根据上面的计算公式得到Gram矩阵 def gram_matrix(input_tensor): # 分子 result = tf.einsum('bijc,bijd->bcd', input_tensor, input_tensor) input_shape = tf.shape(input_tensor) # 分母 num_locations = tf.cast(input_shape[1]*input_shape[2], tf.float32) # 结果 return result/(num_locations) 复制代码
# 创建风格内容模型 class StyleContentModel(tf.keras.models.Model): def __init__(self, style_layers, content_layers): super(StyleContentModel, self).__init__() self.vgg = vgg_layers(style_layers + content_layers) self.style_layers = style_layers self.content_layers = content_layers self.num_style_layers = len(style_layers) self.vgg.trainable = False def call(self, inputs): "Expects float input in [0,1]" inputs = inputs*255.0 preprocessed_input = tf.keras.applications.vgg19.preprocess_input(inputs) outputs = self.vgg(preprocessed_input) style_outputs, content_outputs = (outputs[:self.num_style_layers], outputs[self.num_style_layers:]) style_outputs = [gram_matrix(style_output) for style_output in style_outputs] content_dict = {content_name:value for content_name, value in zip(self.content_layers, content_outputs)} style_dict = {style_name:value for style_name, value in zip(self.style_layers, style_outputs)} return {'content':content_dict, 'style':style_dict} 复制代码
# 实例化风格内容模型,提取图片的风格、内容特征 extractor = StyleContentModel(style_layers, content_layers) # 内容图像的提取特征结果 results = extractor(tf.constant(content_image)) print('Styles:') for name, output in sorted(results['style'].items()): print(" ", name) print(" shape: ", output.numpy().shape) print(" min: ", output.numpy().min()) print(" max: ", output.numpy().max()) print(" mean: ", output.numpy().mean()) print() print("Contents:") for name, output in sorted(results['content'].items()): print(" ", name) print(" shape: ", output.numpy().shape) print(" min: ", output.numpy().min()) print(" max: ", output.numpy().max()) print(" mean: ", output.numpy().mean()) 复制代码
# 风格图像的结果 style_targets = extractor(style_image2)['style'] content_targets = extractor(content_image)['content'] # 我们生成的风格迁移图像 image = tf.Variable(content_image) # 维持像素值在0-1之间 def clip_0_1(image): return tf.clip_by_value(image, clip_value_min=0.0, clip_value_max=1.0) # 定义优化器 这里使用Adam,版本高的也可以用LBFGS opt = tf.optimizers.Adam(learning_rate=0.02, beta_1=0.99, epsilon=1e-1) # 需要tensorflow>=2.5 # import tensorflow_probability as tfp # opt_lbfgs = tfp.optimizer.lbfgs_minimize() # 内容和风格的权重 style_weight=1e-2 content_weight=1e4 # 总的损失,这部分对应论文方法中的Ltotal def style_content_loss(outputs): style_outputs = outputs['style'] content_outputs = outputs['content'] style_loss = tf.add_n([tf.reduce_mean((style_outputs[name]-style_targets[name])**2) for name in style_outputs.keys()]) # 赋予风格损失权重 style_loss *= style_weight / num_style_layers content_loss = tf.add_n([tf.reduce_mean((content_outputs[name]-content_targets[name])**2) for name in content_outputs.keys()]) # 赋予内容损失权重 content_loss *= content_weight / num_content_layers loss = style_loss + content_loss return loss 复制代码
# 定义训练 @tf.function() @tf.autograph.experimental.do_not_convert() def train_step(image): with tf.GradientTape() as tape: outputs = extractor(image) loss = style_content_loss(outputs) grad = tape.gradient(loss, image) opt.apply_gradients([(grad, image)]) image.assign(clip_0_1(image)) 复制代码
为了减少生成图像的噪音,使得生成图像更平滑并且保留内容图像的边缘信息,引入全变分损失。
关于全变分损失 Total Variation Loss,是用来图像修复复原和去噪音的,具体思想如下
所以根据这个思想,我们修改之前的 loss = style_loss + content_loss
,加上一项全变分损失的正则化约束,全变分损失在tensorflow中可以用tf.image.total_variation(image)
来计算
# 去除高频误差 def high_pass_x_y(image): x_var = image[:,:,1:,:] - image[:,:,:-1,:] y_var = image[:,1:,:,:] - image[:,:-1,:,:] return x_var, y_var # 全变分损失的权重 total_variation_weight=50 @tf.function() def train_step(image): with tf.GradientTape() as tape: outputs = extractor(image) loss = style_content_loss(outputs) loss += total_variation_weight*tf.image.total_variation(image) grad = tape.gradient(loss, image) opt.apply_gradients([(grad, image)]) image.assign(clip_0_1(image)) return loss # 内容图片 image = tf.Variable(content_image) epochs = 10 steps_per_epoch = 120 step = 0 for n in range(epochs): for m in range(steps_per_epoch): step += 1 loss=train_step(image) print(".", end='') if m%50==0: display.display(loss) display.clear_output(wait=True) display.display(tensor_to_image(image)) print("Train step: {}".format(step)) # 保存图片 file_name = 'stylized-image.png' tensor_to_image(image).save(file_name) 复制代码
最终自己动手生成的套马的汉子和使用tensorflow-hub生成的进行对比
- tensorflow-hub版本
自己做出的
相比起来,自己生成的眼睛更模糊,而且颜色不协调,衣服纹路不清晰。最重要的是背景里极其不搭的绿色,套马的汉子威武强壮怎么可能会绿绿的呢?
查看一下引入的hub_module,发现用的是InceptionV3作为图片的特征提取器,于是我就试着用InceptionV3进行特征提取再做风格迁移,结果效果十分不理想,不仅新图像没有学到风格,并且内容图像也变得模糊不清。
更改了多次的权重比例以及特征选择层,效果依旧很差。准备仔细看看tensorflow-hub中模型的具体实现,下载了tensorflow-hub中的模型,解压打开准备读取.pb文件,但是解析一直报错无法解决。唉,下次试着用GAN来做风格迁移到时候再对比一次效果。
那最后,不知道这样的汉子图雅老师还躲不躲呢?