什么是神经风格迁移

图像风格迁移一直是cv领域的一个难题
传统图像处理中有很多针对该问题的非参数化算法,这些算法的局限都在于只能提取图像的低层特征

2015年Gatys等人提出了基于神经网络的图像风格迁移算法,即神经风格迁移
神经风格迁移使用预训练的深度CNN提取图像高层特征独立构造出了图像的内容/风格表示
该算法表现十分优异,使得图像风格迁移在神经网络研究中流行起来

论文链接:

A neural algorithm of artistic style

Image Style Transfer Using Convolutional Neural Networks

自己训练的模型效果如下

neuralStyleTransfer

内容与风格的提取

深度内容表达

以VGG为例,为了将深度CNN模型每一层学得的特征可视化
我么可以将一张随机噪声图和一张正常照片同时输入网络,计算网络第l层输出的两张特征图之间的loss,再对噪声图进行梯度下降

即设$\vec{p},\vec{x}$分别为输入的原图和噪声图,$P^l,F^l$分别为他们在网络第$l$层的输出特征图,其尺寸为$(H_l,W_l,C_l)$,则有square-error

对$F^l$求导有

之后即可通过反向传播对$\vec{x}$进行梯度下降

使用该方法对VGG不同层学得的特征可视化的结果如图中下侧所示

neuralStyleTransfer2

从图中可知网络越高层学得的表达对图像内容(content)越敏感,即捕获了输入图像中的物体以及它们的布局等,而并不限制每个像素的色值

因此我们将CNN网络高层的输出特征图称为图像的内容表达(content representation)

深度风格表达

与内容表达不同,图像的风格表达通过计算网络不同层的特征图以及特征图不同通道的相关性得到,而这种相关性通过gram矩阵获得

设网络第$l$层输出的特征图尺寸为$(H_l,W_l,C_l)$,则gram矩阵$G^l\in \mathbb{R}^{C_l\times C_l}$,计算公式为

也即$G^l_{i,j}$为特征图通道$i$和通道$j$的内积

实际中操作中可以先将$F^l$转置,将通道维度提前,再将后两维展平,使其尺寸为$(C_l,H_lW_l)$,记为$F^{l’}$,则$G^l=F^{l’}F^{l’^{T}}$

同样通过对噪声图进行梯度下降可以将VGG每层学得的风格表达可视化

即设$\vec{a},\vec{x}$分别为输入的原图和噪声图,$A^l,G^l$分别为他们在网络第$l$层的gram矩阵,则有mean-squared损失

由于风格损失还应考虑不同层的输出特征图的相关性,所以最终风格损失为

其中$w_l$为对应第$l$层风格损失的权重

包含不同层的风格损失的可视化结果如上图中上侧所示,从左至右依次包含 (a)conv1_1 (b)conv1_1 conv2_1 (c)conv1_1 conv2_1 conv3_1 (d) conv1_1 conv2_1 conv3_1 conv4_1
(e) conv1_1 conv2_1 conv3_1 conv4_1 conv5_1

可以发现包含的不同层次越多,风格损失的效果就更好

实现风格迁移

根据上述理论,可以通过同时最小化定义在某一层上的内容损失和定义在多层上的风格损失,并对一张噪声图进行梯度下降实现神经风格迁移

neuralStyleTransfer3

以下为代码实现

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
""" Neural Style Transfer
paper: A neural algorithm of artistic style
see: https://arxiv.org/pdf/1508.06576.pdf

paper: Image Style Transfer Using Convolutional Neural Networks
see: https://openaccess.thecvf.com/content_cvpr_2016/papers/Gatys_Image_Style_Transfer_CVPR_2016_paper.pdf

Implementation reference: https://keras.io/examples/generative/neural_style_transfer/
"""

import tensorflow as tf
from tensorflow.keras.models import Model
from tensorflow.keras.optimizers import RMSprop
from tensorflow.keras.applications import vgg19
import tensorflow.keras.backend as K
import numpy as np
import cv2

def gram_matrix(x):
x = K.permute_dimensions(x, (2, 0, 1))
x = K.reshape(x, (K.shape(x)[0], -1))
gram = K.dot(x, K.transpose(x))
return gram

def style_loss(style, combination):
gram_style = gram_matrix(style)
gram_combination = gram_matrix(combination)

h, w, c = style.get_shape().as_list()
fac = 4.0 * ((h * w) ** 2) * (c ** 2)

return K.sum(K.square(gram_style - gram_combination)) / fac


def content_loss(content, combination):
return K.sum(K.square(content - combination)) / 2.0


def calc_loss(outputs):
content_weight = 2e-4
style_weight = 1

content_layer_name = 'block5_conv2'
style_layer_names = [
"block1_conv1",
"block2_conv1",
"block3_conv1",
"block4_conv1",
"block5_conv1",
]

loss = K.variable(0.)

features = outputs[content_layer_name]
loss = loss + content_weight * content_loss(features[0, :, :, :], features[2, :, :, :])

for layer_name in style_layer_names:
features = outputs[layer_name]
sloss = style_loss(features[1, :, :, :], features[2, :, :, :])

loss = loss + style_weight / len(style_layer_names) * sloss

return loss


def preprocessImage(fpath, shape):
img = cv2.imread(fpath, 1)
img = cv2.resize(img, shape)

img = np.expand_dims(img, axis=0).astype(np.float32)
img = img / 127.5 - 1

return img

def saveImage(img, fpath):
img = (img + 1) * 127.5
img = np.clip(img, 0, 255).astype("uint8")
cv2.imwrite(fpath, img)

def getFeatureExtractor():
vgg = vgg19.VGG19(include_top=False, weights='imagenet')
outputs_dict = dict([(layer.name, layer.output) for layer in vgg.layers])
return Model(vgg.inputs, outputs_dict)

def train(img_content, img_style, img_combination):
extractor = getFeatureExtractor()
optimizer = RMSprop()

epochs = 8000
for step in range(1, epochs + 1):
with tf.GradientTape() as tape:
inputs = K.concatenate([img_content, img_style, img_combination], axis=0)
outputs = extractor(inputs)
loss = calc_loss(outputs)

grad = tape.gradient(loss, img_combination)

optimizer.apply_gradients([(grad, img_combination)])

if step % 1000 == 0:
print("epoch {}: loss={:.4f}".format(step, loss))
saveImage(img_combination.numpy()[0], './epoch{}.png'.format(step))

fpath_content = '../input/styletransfer/content.jpg'
fpath_style = '../input/styletransfer/style.jpg'

h, w = cv2.imread(fpath_content, 0).shape
shape = (w, h)

img_content = K.variable(preprocessImage(fpath_content, shape))
img_style = K.variable(preprocessImage(fpath_style, shape))
img_combination = K.variable(preprocessImage(fpath_content, shape))

train(img_content, img_style, img_combination)