tensorflow实现图像风格迁移

Tensorflow实现图像风格迁移

前言

什么是艺术?就拿一副油画来说,我认为需要考察两个方面:一是作品所描绘主题目标(自然,有时会进行高维抽象,但是想表达的事物本身是不变的);二是作者想附着在事物之上的感情色彩,即色彩搭配,风格等等,用信息学的角度来说这幅画传达了两个向量信息,即:事物本身与作者想带给我们的主观感受。

那么,我们是否可以将A图像的风格迁移到B图像上呢?于是,风格迁移算法应运而生:

我妹妹的爱犬coco(宠爱脸 ( ̄︶ ̄)↗)


图像风格迁移的原理

图像风格迁移是指:将图像A的风格转换到图像B中去,得到新的图像,取个名字为new B,其中new B中既包含图像B的内容,也包含图像A的风格。如下图所示:

经过160次迭代后:

更多死侍艺术风格:

图像从左到右,从上到下依次为:

  • ‘The Starry Night’ by Vincent van Gogh (1889)
  • Pattern by Olga Drozdova (201x?)
  • ‘The Harlequin’s Carnival’ by Joan Miró (1924-1925)
  • ‘Metamorphosis of Narcissus’ by Salvador Dalí (1937)**

原理简介

风格迁移算法的成功,主要基于以下两点:

  1. 两张图像经过预训练好的分类网络,若提取出的高维特征( high-level )之间的欧氏距离越小,则这两张图像内容越相似
  2. 两张图像经过与训练好的分类网络,若提取出的低维特征( low-level )在数值上基本相等,则这两张图像风格越相似,换句话说,两张图像相似等价于二者特征 Gram 的矩阵具有较小的弗罗贝尼乌斯范数。

基于这两点,就可以设计合适的损失函数优化网络。

原理解读

对于深度网络来讲,深度卷积分类网络具有良好的特征提取能力,不同层提取的特征具有不同的含义,每一个训练好的网络都可以视为是一个良好的特征提取器,另外,深度网络由一层层的非线性函数组成,可以视为是复杂的多元非线性函数,此函数完成输入图像到输出的映射,因此,完全可以使用训练好的深度网络作为一个损失函数计算器。

Gram 矩阵的数学形式如下: G_j(x)=A*A^T

Gram矩阵实际上是矩阵的内积运算,在风格迁移算法中,其计算的是feature map之间的偏心协方差,在feature map 包含着图像的特征,每个数字表示特征的强度,Gram矩阵代表着特征之间的相关性,因此,Gram矩阵可以用来表示图像的风格,因此可以通过Gram矩阵衡量风格的差异性。

论文

网络框架分为两部分,其一部分是图像转换网络 T (image transfrom net)和预训练好的损失计算网络VGG-16(loss network),图像转换网络 T 以内容图像 x 为输入,输出风格迁移后的图像 y^{'} ,随后内容图像 y_c (也即是 x ),风格图像 y_s ,以及 y^{'} 输入vgg-16计算特征,损失计算如下:

内容损失:

l^{φ;j}_{feat}(^ y; y) = \frac{1} {C_jH_jW_j} ||φ_j(y^{'}) − φ_j(y)||^2 , 其中 φ 代表深度卷积网络VGG-16

感知损失如下:

l^{φ;j}_{style}(^ y; y) = ||G_j(y^{'}) − G_j(y)||^2_F ,其中G是Gram矩阵,计算过程如下:

G^φ_j(x)_{c^{'}, c} = ||G^φ_j(y^{'}) - G^φ_j(y)||

总损失定义如下:

Loss_{total} = \gamma _1 l_{feat} + \gamma _2 l_{style}

风格迁移代码实现

这个模型不困难。 但是需要注意,它与大多数卷积模型有三个方面的不同:

  • 在一般的模型中,我们导入数据并用它来训练变量 ,但是我们一般不修改原始输入。 对于这个模型,你有两个固定的输入:内容图像和风格图像,在输入之前我们要对图像进行处理,它将被训练成为最终的艺术品。

  • TensorFlow程序的两个阶段之间没有明确的区别:组装图并执行它。 所有3个输入(内容图像,样式图像和可训练输入)具有相同的尺寸,并作为同一计算的输入,以提取相同的特征集。 为了避免我们不得不多次组装同一个子图,我们将为它们中的所有三个使用一个变量。 该变量已经为你定义为:

    1
    2
    3
    4
    self.input_img = tf.get_variable('in_img',
    shape=([1, self.img_height, self.img_width,3]),
    dtype=tf.float32,
    initializer=tf.zeros_initializer())

    知道你在想什么,“你的意思是说,所有三个输入共享相同的变量? TF如何知道它正在处理哪个输入?“你记得在TF中我们有变量的赋值操作。当我们需要做一些将内容图像作为输入的计算时,我们首先将内容图像分配给该变量,依此类推。您必须这样做来计算内容丢失(因为它取决于内容图像)和样式丢失(因为它取决于样式图像)。

  • 在这项任务中,需要用到转移学习:我们使用为这项任务训练的另一项任务的权数。我们将使用VGG-19(一种19层的卷积网络)物体识别任务已经训练过的权重和偏差来提取样式转移的内容和样式图层。关于这个模型的更多上下文,你可以在这里阅读它。我们只会将它们的权重用于卷积图层。 Gatys等人的论文建议Average Pooling总比Max Pooling要好,所以我们必须自己Pooling。

在这个模型中,有几个步骤:

  • 第1步:定义模型
  • 第2步:创建损失函数
  • 第3步:创建优化器
  • 第4步:创建摘要以监控您的培训过程
  • 第5步:训练你的模型

这个模型主要基于tf的slim模块,slim封装的很好,调用起来比较方便。接下来分为网络结构,损失函数,以及训练部分分别做介绍。

网络结构

1
2
3
4
5
6
7
8
9
slim = tf.contrib.slim
# 定义卷积,在slim中传入参数
def arg_scope(weight_decay=0.0005):
with slim.arg_scope([slim.conv2d, slim.fully_connected, slim.conv2d_transpose],
activation_fn=None,
weights_regularizer=slim.l2_regularizer(weight_decay),
biases_initializer=tf.zeros_initializer()):
with slim.arg_scope([slim.conv2d, slim.conv2d_transpose], padding='SAME') as arg_sc:
return arg_sc

接下来就是图像转换网络结构部分,仿照上图,不过这里有一个trick,就是在输入之前对图像做padding,经过网络后再把padding的部分去掉,防止迁移后出现边缘效应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def gen_net(imgs, reuse, name, is_train=True):
imgs = tf.pad(imgs, [[0, 0], [10, 10], [10, 10], [0, 0]], mode='REFLECT')
with tf.variable_scope(name, reuse=reuse) as vs:
# encoder : three convs layers
out1 = slim.conv2d(imgs, 32, [9, 9], scope='conv1')
out1 = relu(instance_norm(out1))
out2 = slim.conv2d(out1, 64, [3, 3], stride=2, scope='conv2')
out2 = instance_norm(out2)
# out2 = relu(img_scale(out2, 0.5))
out2 = slim.conv2d(out2, 128, [3, 3], stride=2, scope='conv3')
out2 = instance_norm(out2)
# out2 = relu(img_scale(out2, 0.5))
# transform
out3 = res_module(out2, 128, name='residual1')
out3 = res_module(out3, 128, name='residual2')
out3 = res_module(out3, 128, name='residual3')
out3 = res_module(out3, 128, name='residual4')
# decoder
out4 = img_scale(out3, 2)
out4 = slim.conv2d(out4, 64, [3, 3], stride=1, scope='conv4')
out4 = relu(instance_norm(out4))
# out4 = img_scale(out4, 128)
out4 = img_scale(out4, 2)
out4 = slim.conv2d(out4, 32, [3, 3], stride=1, scope='conv5')
out4 = relu(instance_norm(out4))
# out4 = img_scale(out4, 256)
out = slim.conv2d(out4, 3, [9, 9], scope='conv6')
out = tf.nn.tanh(instance_norm(out))
variables = tf.contrib.framework.get_variables(vs)
out = (out + 1) * 127.5
height = out.get_shape()[1].value # if is_train else tf.shape(out)[0]
width = out.get_shape()[2].value # if is_train else tf.shape(out)[1]
out = tf.image.crop_to_bounding_box(out, 10, 10, height-20, width-20)
# out = tf.reshape(out, imgs_shape)
return out, variables

其中instance_norm是归一化部分[5],res_module是残差块,image_scale是采样部分,scale因子是2表示上采样,特征图扩大2倍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def img_scale(x, scale):
weight = x.get_shape()[1].value
height = x.get_shape()[2].value
try:
out = tf.image.resize_nearest_neighbor(x, size=(weight*scale, height*scale))
except:
out = tf.image.resize_images(x, size=[weight*scale, height*scale])
return out
# net = slim.conv2d(net, 4096, [1, 1], scope='fc7')
def res_module(x, outchannel, name):
with tf.variable_scope(name_or_scope=name):
out1 = slim.conv2d(x, outchannel, [3, 3], stride=1, scope='conv1')
out1 = relu(out1)
out2 = slim.conv2d(out1, outchannel, [3, 3], stride=1, scope='conv2')
out2 = relu(out2)
return x+out2
def instance_norm(x):
epsilon = 1e-9
mean, var = tf.nn.moments(x, [1, 2], keep_dims=True)
return tf.div(tf.subtract(x, mean), tf.sqrt(tf.add(var, epsilon)))

风格迁移代码的使用

fast-neural-style

这是一种快速通道,使用已经训练好的.yml,快速实现风格的嵌套。

要从模型“wave.ckpt-done”生成样本,请运行:

1
python eval.py --model_file models/<your path for mask.ckpt-done> --image_file img/<your image file path>.jpg

my_url:

1
python eval.py --model_file models/wave.ckpt-done --image_file img/Hepburn.jpg

输出为: generated/res.jpg.

您可以从百度硬盘下载所有7个训练模型。训练模型来自致远何的分享,十分感谢。

组态 样式 样品
wave.yml img img
cubist.yml img img
denoised_starry.yml img img
mosaic.yml img img
scream.yml img img
feathers.yml img img
udnie.yml img img

训练模型

要从头开始训练模型,首先应该从Tensorflow Slim 下载VGG16模型。解压缩文件vgg_16.ckpt。然后将其复制到预训练的文件夹/:

1
2
3
cd <this repo>
mkdir pretrained
cp <your path to vgg_16.ckpt> pretrained/

然后下载COCO数据集。请解压缩它,你将有一个名为“train2014”的文件夹,其中包含许多原始图像。然后创建一个符号链接:

1
2
cd <this repo>
ln -s <your path to the folder "train2014"> train2014

训练“波浪”的模型:

1
python train.py -c conf/wave.yml

(可选)使用tensorboard:

1
tensorboard --logdir models/wave/

检查点将写入“models / wave /”。

用钱砸我,不要停!