StyleTransfer

以下示例受TensorFlow 官方教程的启发,同时也参考了这篇博客文章。使用 CNTK 框架进行风格迁移的另一个优秀示例可以在这里找到。这是关于艺术风格迁移的原始论文。

风格迁移的主要思想如下:

  • 从白噪声开始,我们尝试优化当前图像 $x$,以最小化某个损失函数
  • 损失函数由三个部分组成 $\mathcal{L(x)} = \alpha\mathcal{L}_c(x,i) + \beta\mathcal{L}_s(x,s)+\gamma\mathcal{L}_t(x)$
    • $\mathcal{L}_c$ - 内容损失 - 表示当前图像 $x$ 与原始图像 $i$ 的接近程度
    • $\mathcal{L}_s$ - 风格损失 - 表示当前图像 $x$ 与风格图像 $s$ 的接近程度
    • $\mathcal{L}_t$ - 总变分损失(在我们的示例中不会考虑) - 确保生成的图像是平滑的,即表示图像 $x$ 的相邻像素之间的均方误差

这些损失函数需要以巧妙的方式设计,例如风格损失应对应于图像风格的相似性,而不是实际内容。为此,我们将比较一个 CNN 的一些深层特征层,这些特征层会对图像进行分析。

让我们先加载几张图片:

In [1]:
!mkdir -p images
!curl https://cdn.pixabay.com/photo/2016/05/18/00/27/franz-marc-1399594_960_720.jpg > images/style.jpg
!curl https://upload.wikimedia.org/wikipedia/commons/thumb/b/bd/Golden_tabby_and_white_kitten_n01.jpg/1280px-Golden_tabby_and_white_kitten_n01.jpg > images/image.jpg
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  210k  100  210k    0     0  2670k      0 --:--:-- --:--:-- --:--:-- 2670k
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  131k  100  131k    0     0   459k      0 --:--:-- --:--:-- --:--:--  459k
In [30]:
import cv2
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
import tensorflow as tf
from tensorflow.keras.applications.vgg16 import preprocess_input
import IPython.display as display

让我们加载这些图像并将它们调整为 $512\times512$。同时,我们将生成结果图像 img_result 作为一个随机数组。

In [31]:
img_size = 256

def load_image(fn):
  x = cv2.imread(fn)
  return cv2.cvtColor(x, cv2.COLOR_BGR2RGB)

img_style = load_image('images/style.jpg')
img_content = load_image('images/image.jpg')
img_content = img_content[:,200:200+857,:]
img_content = cv2.resize(img_content,(img_size,img_size))
img_style = img_style[:,200:200+671,:]
img_style = cv2.resize(img_style,(img_size,img_size))

img_result = np.random.uniform(size=(img_size,img_size,3))

matplotlib.rcParams['figure.figsize'] = (12, 12)
matplotlib.rcParams['axes.grid'] = False

fig,ax = plt.subplots(1,3)
ax[0].imshow(img_content)
ax[1].imshow(img_style)
ax[2].imshow((255*img_result).astype(int))
plt.show()
Notebook 输出图像

要计算风格损失和内容损失,我们需要在由CNN提取的特征空间中进行操作。我们可以使用不同的CNN架构,但为了简单起见,在我们的案例中我们将选择预训练于ImageNet的VGG-19。

In [32]:
vgg = tf.keras.applications.VGG16(include_top=False, weights='imagenet')
vgg.trainable = False

让我们来看看模型架构:

In [33]:
vgg.summary()
Model: "vgg16"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_3 (InputLayer)        [(None, None, None, 3)]   0         
                                                                 
 block1_conv1 (Conv2D)       (None, None, None, 64)    1792      
                                                                 
 block1_conv2 (Conv2D)       (None, None, None, 64)    36928     
                                                                 
 block1_pool (MaxPooling2D)  (None, None, None, 64)    0         
                                                                 
 block2_conv1 (Conv2D)       (None, None, None, 128)   73856     
                                                                 
 block2_conv2 (Conv2D)       (None, None, None, 128)   147584    
                                                                 
 block2_pool (MaxPooling2D)  (None, None, None, 128)   0         
                                                                 
 block3_conv1 (Conv2D)       (None, None, None, 256)   295168    
                                                                 
 block3_conv2 (Conv2D)       (None, None, None, 256)   590080    
                                                                 
 block3_conv3 (Conv2D)       (None, None, None, 256)   590080    
                                                                 
 block3_pool (MaxPooling2D)  (None, None, None, 256)   0         
                                                                 
 block4_conv1 (Conv2D)       (None, None, None, 512)   1180160   
                                                                 
 block4_conv2 (Conv2D)       (None, None, None, 512)   2359808   
                                                                 
 block4_conv3 (Conv2D)       (None, None, None, 512)   2359808   
                                                                 
 block4_pool (MaxPooling2D)  (None, None, None, 512)   0         
                                                                 
 block5_conv1 (Conv2D)       (None, None, None, 512)   2359808   
                                                                 
 block5_conv2 (Conv2D)       (None, None, None, 512)   2359808   
                                                                 
 block5_conv3 (Conv2D)       (None, None, None, 512)   2359808   
                                                                 
 block5_pool (MaxPooling2D)  (None, None, None, 512)   0         
                                                                 
=================================================================
Total params: 14,714,688
Trainable params: 0
Non-trainable params: 14,714,688
_________________________________________________________________

让我们定义一个函数,以便我们从VGG网络中提取中间特征:

In [34]:
def layer_extractor(layers):
    outputs = [vgg.get_layer(x).output for x in layers]
    model = tf.keras.Model([vgg.input],outputs)
    return model 

内容损失#

内容损失 用于衡量当前图像 $x$ 与原始图像的接近程度。它通过查看卷积神经网络(CNN)的中间特征层,并计算平方误差。在第 $l$ 层的内容损失定义为: $$ \mathcal{L}c = {1\over2}\sum{i,j} (F_{ij}^{(l)}-P_{ij}^{(l)})^2 $$ 其中 $F^{(l)}$ 和 $P^{(l)}$ 分别表示第 $l$ 层的特征。

In [35]:
content_layers = ['block4_conv2'] 
content_extractor = layer_extractor(content_layers)

content_target = content_extractor(preprocess_input(tf.expand_dims(img_content,axis=0)))

def content_loss(img):
    z = content_extractor(preprocess_input(tf.expand_dims(255*img,axis=0))) 
    return 0.5*tf.reduce_sum((z-content_target)**2)

现在我们将实现风格迁移的主要技巧——优化。我们将从随机图像开始,然后使用 TensorFlow 优化器调整这张图像,以最小化内容损失。

重要提示:在我们的案例中,所有计算都是通过支持 GPU 的 TensorFlow 框架进行的,这使得代码在 GPU 上运行效率更高。

In [63]:
img = tf.Variable(img_result)
opt = tf.optimizers.Adam(learning_rate=0.002, beta_1=0.99, epsilon=1e-1)

clip = lambda x : tf.clip_by_value(x,clip_value_min=0,clip_value_max=1)

def optimize(img,loss_fn):
    with tf.GradientTape() as tape:
        loss = loss_fn(img)
    grad = tape.gradient(loss,img)
    opt.apply_gradients([(grad,img)])
    #img.assign(tf.clip_by_value(img,clip_value_min=0,clip_value_max=1))

def train(img,loss_fn,epochs=10,steps_per_epoch=100):
    for _ in range(epochs):
        display.clear_output(wait=True)
        plt.imshow((255*clip(img)).numpy().astype(int))
        plt.show()
        for _ in range(steps_per_epoch):
            optimize(img,loss_fn=loss_fn)

train(img,content_loss)
Notebook 输出图像

练习:尝试在网络中使用不同的层,看看会发生什么。你也可以尝试同时优化多个层,但需要稍微修改一下 content_loss 的代码。

风格损失#

风格损失是风格迁移的核心思想。我们比较的不是实际的特征,而是它们的 Gram 矩阵,定义为 $$G=A\times A^T$$。

Gram 矩阵类似于相关矩阵,它展示了某些滤波器之间的依赖关系。风格损失是从不同层计算的损失之和,这些损失通常会乘以加权系数。

风格迁移的总损失函数是 内容损失风格损失 的总和。

In [64]:
def gram_matrix(x):
  result = tf.linalg.einsum('bijc,bijd->bcd', x, x)
  input_shape = tf.shape(x)
  num_locations = tf.cast(input_shape[1]*input_shape[2], tf.float32)
  return result/(num_locations)

style_layers = ['block1_conv1','block2_conv1','block3_conv1','block4_conv1']

def style_extractor(img):
    return [gram_matrix(x) for x in layer_extractor(style_layers)(img)]

style_target = style_extractor(preprocess_input(tf.expand_dims(img_style,axis=0)))

def style_loss(img):
    z = style_extractor(preprocess_input(tf.expand_dims(255*img,axis=0)))
    loss = tf.add_n([tf.reduce_mean((x-target)**2) 
                           for x,target in zip(z,style_target)])
    return loss / len(style_layers)

综合起来#

我们将定义 total_loss 函数来计算综合损失,并执行优化:

In [65]:
def total_loss(img):
    return 2*content_loss(img)+style_loss(img)

img.assign(img_result)

train(img,loss_fn=total_loss)
Notebook 输出图像

下面的代码执行实际的损失优化。请注意,即使使用 GPU,优化也需要相当长的时间。您可以多次运行下面的单元格以改善结果。

添加变化损失#

变化损失可以减少图像中的噪点,通过最小化相邻像素之间的差异来实现。

我们还将从原始内容图像开始优化,这样可以在图像中保留更多的内容细节,而不会使内容损失函数变得复杂。不过,我们会添加一些噪点。

In [95]:
def variation_loss(img):
  img = tf.cast(img,tf.float32)
  x_var = img[ :, 1:, :] - img[ :, :-1, :]
  y_var = img[ 1:, :, :] - img[ :-1, :, :]
  return tf.reduce_sum(tf.abs(x_var)) + tf.reduce_sum(tf.abs(y_var))
  
def total_loss_var(img):
    return content_loss(img)+150*style_loss(img)+30*variation_loss(img)

img.assign(clip(np.random.normal(-0.3,0.3,size=img_content.shape)+img_content/255.0))

train(img,loss_fn=total_loss_var)
Notebook 输出图像
In [94]:
cv2.imwrite('result.jpg',(img.numpy()[:,:,::-1]*255))
(256, 256, 3)
True
In [ ]:

免责声明
本文档使用AI翻译服务 Co-op Translator 进行翻译。尽管我们努力确保翻译的准确性,但请注意,自动翻译可能包含错误或不准确之处。应以原始语言的文档作为权威来源。对于关键信息,建议使用专业人工翻译。我们对因使用此翻译而引起的任何误解或误读不承担责任。