圖像風格遷移——《A Neural Algorithm of Artistic Style》

之前看到別人玩圖像風格遷移,感覺挺有意思的,趁着空下來的時間自己玩了一下。還是沿着老方法,先看一下論文,然後跑跑程序。論文看的是最基礎的《A Neural Algorithm of Artistic Style》,程序嘛,當然不是笨妞自己寫的,跑了keras安裝文件夾下examples裏面的例子害羞


1. 論文概括

這篇論文寫得很容易懂,雖然連笨妞這麼囉嗦的人都覺得有點囉嗦。原本想直接翻譯的,但是,實際核心內容並不多,就直接概括一下吧。

作者的目的是想把藝術大師的名畫的畫風遷移到普通的圖片上,使機器也可以畫名畫。這就涉及到兩方面的圖片——畫風圖片(一般是名畫)、內容圖片(我們想畫的內容)。

具體實現的方式用框圖表示出來是這樣的


實際操作是通過不帶全連接層的vgg網絡分別抽取內容圖、畫風圖以及生成圖(就是最後你畫的“名畫”)的特徵圖,然後分別用內容特徵和生成特徵圖計算內容損失,用畫風圖和生成圖計算風格損失,將兩個損失合起來,作爲總體損失,用總體損失來計算生成圖的梯度然後更新生成圖。生成圖最初初始化爲白噪聲圖。vgg用imagenet預訓練模型。


作者的意圖是想通過CNN分別抽取內容圖的特徵圖、名畫的特徵圖。然後用內容特徵圖做出目標內容;用畫風特徵圖做出目標畫風。根據生成圖的內容與目標內容的差異來“畫”(優化)內容;用生成圖與目標畫風的差異來“畫”(優化)風格。(下面這句爲個人理解)這樣一來,上面框圖中最上面的網絡和最下面的網絡實際只跑一次,但中間網絡要跑N次,但這N次跑前向和後向,後向不會優化網絡參數,只會優化生成圖。

特徵圖可以選擇vgg 5個卷積模塊中任何一個模塊的仁義卷積層的輸出,實際畫風提取了多個卷積層的輸出。內容直接用特徵圖表示;畫風采用每個特徵圖自己與自己的格拉姆矩陣表示,通常將多個卷積層提取的多個特徵圖作格拉姆矩陣後疊加。

格拉姆矩陣是計算相關關係的,個人以爲計算特徵圖自己的格拉姆矩陣是提取像素與像素之間的關聯,以這種關聯性來表示風格。

論文中實際方法是這樣的:


內容的損失和基於生成圖的梯度

其中Fij和Pij分別表示生成圖像和原始內容圖像中的像素值。


畫風損失和基於生成圖的梯度

畫風損失——名畫的格拉姆矩陣和要生成圖的格拉姆矩陣之間的距離總和


畫風基於生成圖的梯度


總損失



以總損失作爲目標優化生成圖。


2. 實際操練

實際操作過程中,可以分成以下幾步:

A. 圖像預處理(內容圖、畫風圖)、生成圖佔位符定義

B. 預訓練模型加載

C. 三張圖跑網絡

D. 內容損失計算

E.格拉姆矩陣計算

F. 風格損失計算

G. 損失和梯度彙總

H. 設置迭代計算圖

I.選擇生成圖優化方法

J. 開始迭代


程序如下:

from __future__ import print_function
from keras.preprocessing.image import load_img, img_to_array
from scipy.misc import imsave
import numpy as np
from scipy.optimize import fmin_l_bfgs_b
import time
import argparse

from keras.applications import vgg19
from keras import backend as K

import sys

#解析參數的定義和添加
paser = parser = argparse.ArgumentParser(description='Neural style transfer with Keras.')
parser.add_argument('--base_image_path',  type=str, default='image/boy1.jpg', required=False,
                    help='Path to the image to transform.')
parser.add_argument('--style_reference_image_path', default='image/style/style_6.jpg', type=str, required=False,
                    help='Path to the style reference image.')
parser.add_argument('--result_prefix', default='boy_style_transfer', type=str, required=False,
                    help='Prefix for the saved results.')
parser.add_argument('--iter', type=int, default=10, required=False,
                    help='Number of iterations to run.')
parser.add_argument('--content_weight', type=float, default=0.025, required=False,
                    help='Content weight.')
parser.add_argument('--style_weight', type=float, default=1.0, required=False,
                    help='Style weight.')
parser.add_argument('--tv_weight', type=float, default=1.0, required=False,
                    help='Total Variation weight.')

#自定義參數的設置
args = parser.parse_args(['--base_image_path', 'image/spring.jpg'])
#args = parser.parse_args()
base_image_path = args.base_image_path
style_reference_image_path = args.style_reference_image_path
result_prefix = args.result_prefix
iterations = args.iter

# these are the weights of the different loss components
total_variation_weight = args.tv_weight
style_weight = args.style_weight
content_weight = args.content_weight

# dimensions of the generated picture.
width, height = load_img(base_image_path).size
img_nrows = 400
img_ncols = int(width * img_nrows / height)

#圖像預處理
def preprocess_image(image_path):
    #讀入圖像,並轉化爲目標尺寸。
    img = load_img(image_path, target_size=(img_nrows, img_ncols))
    img = img_to_array(img)
    img = np.expand_dims(img, axis=0) #3
    #vgg提供的預處理,主要完成(1)去均值(2)RGB轉BGR(3)維度調換三個任務。
    img = vgg19.preprocess_input(img)
    return img

#圖像後處理
def deprocess_image(x):
    if K.image_dim_ordering() == 'th':
        x = x.reshape((3, img_nrows, img_ncols))
        x = x.transpose((1, 2, 0))
    else:
        x = x.reshape((img_nrows, img_ncols, 3))
   
    x[:, :, 0] += 103.939
    x[:, :, 1] += 116.779
    x[:, :, 2] += 123.68
    # 'BGR'->'RGB'
    x = x[:, :, ::-1]
    x = np.clip(x, 0, 255).astype('uint8')
    return x


#讀入內容圖和風格圖,預處理,幷包裝成變量。這裏把內容圖和風格圖都做成尺寸相同的了,有點不靈活。
base_image = K.variable(preprocess_image(base_image_path))
style_reference_image = K.variable(preprocess_image(style_reference_image_path))

#給目標圖片定義佔位符,目標圖像與resize後的內容圖大小相同。
if K.image_dim_ordering() == 'th':
    combination_image = K.placeholder((1, 3, img_nrows, img_ncols))
else:
    combination_image = K.placeholder((1, img_nrows, img_ncols, 3))

#將三個張量串聯到一起,形成一個形如(3,3,img_nrows,img_ncols)的張量
#三張圖一同喂入網絡中,以batch的形式
input_tensor = K.concatenate([base_image, style_reference_image, combination_image], axis=0)

#加載vgg19預訓練模型,模型由imagenet預訓練。去掉模型的全連接層。
model = vgg19.VGG19(input_tensor=input_tensor,
                    weights='imagenet', include_top=False)
print('Model loaded.')

# get the symbolic outputs of each "key" layer (we gave them unique names).
outputs_dict = dict([(layer.name, layer.output) for layer in model.layers])

#計算特徵圖的格拉姆矩陣,格拉姆矩陣算兩者的相關性,這裏算的是一張特徵圖的自相關。
#個人理解爲是像素或者區域間的相關性。用這個相關性來代表風格是的表示。
def gram_matrix(x):
    assert K.ndim(x) == 3
    if K.image_data_format() == 'channels_first':
        features = K.batch_flatten(x)
    else:
        features = K.batch_flatten(K.permute_dimensions(x, (2, 0, 1)))
    gram = K.dot(features, K.transpose(features))
    return gram

#計算風格圖的格拉姆矩陣
#計算生成圖的格拉姆矩陣
#計算風格圖與生成圖之間的格拉姆矩陣的距離,作爲風格loss
def style_loss(style, combination):
    assert K.ndim(style) == 3
    assert K.ndim(combination) == 3
    S = gram_matrix(style)
    C = gram_matrix(combination)
    channels = 3
    size = img_nrows * img_ncols
    return K.sum(K.square(S - C)) / (4. * (channels ** 2) * (size ** 2))

#內容圖和生成圖之間的距離作爲內容loss
def content_loss(base, combination):
    return K.sum(K.square(combination - base))

#計算變異loss(不太明白)
def total_variation_loss(x):
    assert K.ndim(x) == 4
    if K.image_data_format() == 'channels_first':
        a = K.square(x[:, :, :img_nrows - 1, :img_ncols - 1] - x[:, :, 1:, :img_ncols - 1])
        b = K.square(x[:, :, :img_nrows - 1, :img_ncols - 1] - x[:, :, :img_nrows - 1, 1:])
    else:
        a = K.square(x[:, :img_nrows - 1, :img_ncols - 1, :] - x[:, 1:, :img_ncols - 1, :])
        b = K.square(x[:, :img_nrows - 1, :img_ncols - 1, :] - x[:, :img_nrows - 1, 1:, :])
    return K.sum(K.pow(a + b, 1.25))

#以第5卷積塊第2個卷積層的特徵圖爲輸出。
loss = K.variable(0.)
layer_features = outputs_dict['block5_conv2']
#抽取內容特徵圖和生成特徵圖
base_image_features = layer_features[0, :, :, :]
combination_features = layer_features[2, :, :, :]
#計算內容loss
loss += content_weight * content_loss(base_image_features,
                                      combination_features)

feature_layers = ['block1_conv1', 'block2_conv1',
                  'block3_conv1', 'block4_conv1',
                  'block5_conv1']
#抽取風格圖和生成圖每個卷積塊第一個卷積層輸出的特徵圖
#並逐層計算風格loss,疊加在到loss中
for layer_name in feature_layers:
    layer_features = outputs_dict[layer_name]
    style_reference_features = layer_features[1, :, :, :]
    combination_features = layer_features[2, :, :, :]
    sl = style_loss(style_reference_features, combination_features)
    loss += (style_weight / len(feature_layers)) * sl
#疊加生成圖像的變異loss
loss += total_variation_weight * total_variation_loss(combination_image)

# 計算生成圖像的梯度
grads = K.gradients(loss, combination_image)

#output[0]爲loss,剩下的是grad
outputs = [loss]
if isinstance(grads, (list, tuple)):
    outputs += grads
else:
    outputs.append(grads)
#定義需要迭代優化過程的計算圖。
#前面三張圖一起跑了一次網絡只是抽取特徵而已,而這裏定義了真正訓練過程的計算圖。
#計算圖的前向傳導以生成圖爲輸入,以loss和grad爲輸出,反過來就是優化過程。
#在初始化之前,生成圖仍然只是佔位符而已。
f_outputs = K.function([combination_image], outputs)

#同時取出loss和grad
def eval_loss_and_grads(x):
    if K.image_data_format() == 'channels_first':
        x = x.reshape((1, 3, img_nrows, img_ncols))
    else:
        x = x.reshape((1, img_nrows, img_ncols, 3))
    outs = f_outputs([x])
    loss_value = outs[0]
    if len(outs[1:]) == 1:
        grad_values = outs[1].flatten().astype('float64')
    else:
        grad_values = np.array(outs[1:]).flatten().astype('float64')
    return loss_value, grad_values

#這個類在前面同時計算出loss和grad的基礎上,通過不同的函數分別獲取loss和grad。
#原因在於scipy優化函數通過不同的函數獲取loss和grad。
class Evaluator(object):

    def __init__(self):
        self.loss_value = None
        self.grads_values = None

    def loss(self, x):
        assert self.loss_value is None
        loss_value, grad_values = eval_loss_and_grads(x)
        self.loss_value = loss_value
        self.grad_values = grad_values
        return self.loss_value

    def grads(self, x):
        assert self.loss_value is not None
        grad_values = np.copy(self.grad_values)
        self.loss_value = None
        self.grad_values = None
        return grad_values

evaluator = Evaluator()

# run scipy-based optimization (L-BFGS) over the pixels of the generated image
# so as to minimize the neural style loss
#x的初始化,初始化成內容圖
#爲什麼不是論文中的白噪聲圖呢?不解。
x = preprocess_image(base_image_path)

#迭代優化過程
for i in range(iterations):
    print('Start of iteration', i)
    start_time = time.time()
    #使用L-BFGS-B算法優化
    #不斷被優化的是x,也就是生成圖。
    x, min_val, info = fmin_l_bfgs_b(evaluator.loss, x.flatten(),
                                     fprime=evaluator.grads, maxfun=20)
    print('Current loss value:', min_val)
    # save current generated image
    #圖像後處理
    img = deprocess_image(x.copy())
    #保存圖像
    fname = result_prefix + '_at_iteration_%d.png' % i
    imsave(fname, img)
    end_time = time.time()
    print('Image saved as', fname)
    print('Iteration %d completed in %ds' % (i, end_time - start_time))


3. 效果


內容圖


名畫


生成圖,經過10次迭代後的結果


4. 個人感想

說實話,笨妞並不覺得這個“畫”得好,也不覺得意義有多大(也可能是我迭代次數太少,或者使用的方法太低級)。

笨妞使用的這個模型有一個很大的弊端:內容圖和畫風圖的尺寸和目標圖尺寸不能相差太遠,尤其是不能太小,不然出來的效果圖會有模糊感。




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章