基于tensorflow的图像风格迁移原理与实现
一、什么是风格迁移?
苹果APP有一个图片编辑器的软件叫Prisma,可以把我们拍的平时的照片转换成像漫画一样的样式,这个APP中给我们提供了各式各样的艺术图片,
我们可以任意选择自己的拍的图片,选择喜欢的漫画图、或者说是不同风格的图,进行转化。这样自己拍的图片就风格化了 ,就像是一幅画一样了。
下面就先用Prisma对风格迁移做一个简单的描述:如下图,我选择了不同的风格,生成的图片效果也就不一样。现在我们应该就都知道了,风格迁移
简单来说,就是把一幅图像的风格“迁移”到现有图像上,现有图像的内容还是原来的内容 ,就想我的图片上,水杯还是水杯根本没变化,只是整个
图片的“画风”或者说风格发生了变化,可以说成了一个漫画杯子了。
![
二、风格迁移是如何实现的呢?
风格迁移是先由Gatys在2016年CVPR论文中提出来的,论文的题目是“ Image Style Transfer Using Convolutional Neural Networks”,咱们可以顺着大佬的论文拜读一下,自然就明白风格迁移是怎么实现的了
首先作者先把两张图片都是输入到了VGG-19 网络里面,然后把每个卷积层出来的结果都列出来了,然后作者得出了一个
结论:神经网络的浅层多提取出来的是图像的像素信息,而深层更多提取的是物体的位置信息、布局等。
接下来我们看一下Gatys在风格迁移中做了哪些工作:
首先先把一张内容图P输入到VGG19 网络,想要提取这幅图像的P的内容信息。因为之前已经得出了结论“神经网络的浅层多提取出来的是图像的像素信息,而深层更多提取的是物体的位置信息、布局等”,也就是说在提取content特征时,不同层的表达效果是不一样的,文章在提取内容图像的特征时采用高层特征,如下图的右侧可以看出,取的是第四层,高层会很大程度上保留原图的内容特征。然后把一个白噪声图片x也输入到了VGG-19网络里面,就是下图那个灰黑色的小图片,然后同样也在第四个卷积层取出,p和x在第四层卷积层所有的feature map求一个均方差,作为内容的损失值。为什么要这样做呢?这是两幅图片相当于内容上的“差”,我们是想通过后期迭代优化,使其X白噪声图像中的内容与p越来越一样。这是内容损失值的计算公式Lcontent
再来看看怎么样能得到一幅图像的风格呢?
提到图像的风格,我们就得说一下什么是Gram矩阵?
Gram矩阵
格拉姆矩阵可以看做feature之间的偏心协方差矩阵(即没有减去均值的协方差矩阵),在feature map中,每个数字都来自于一个特定滤波器在特定位置的卷积,因此每个数字代表一个特征的强度,而Gram计算的实际上是两两特征之间的相关性,哪两个特征是同时出现的,哪两个是此消彼长的等等,同时,Gram的对角线元素,还体现了每个特征在图像中出现的量,因此,Gram有助于把握整个图像的大体风格。有了表示风格的Gram Matrix,要度量两个图像风格的差异,只需比较他们Gram Matrix的差异即可。
一般来说浅层网络提取的是局部的细节纹理特征,深层网络提取的是更抽象的轮廓、大小等信息。这些特征总的结合起来表现出来的感觉就是图像的风格,由这些特征向量计算出来的的Gram矩阵,就可以把图像特征之间隐藏的联系提取出来,也就是各个特征之间的相关性高低。为了获得所有这些通道的相互关系,我们需要计算一些称为 gram矩阵的东西,我们将使用 gram 矩阵来测量通道之间的相关程度,这些通道随后将作为风格本身的度量。如果两个图像的特征向量的Gram矩阵的差异较小,就可以认定这两个图像风格是相近的。格拉姆矩阵用于度量各个维度自己的特性以及各个维度之间的关系。内积之后得到的多尺度矩阵中,对角线元素提供了不同特征图各自的信息,其余元素提供了不同特征图之间的相关信息。这样一个矩阵,既能体现出有哪些特征,又能体现出不同特征间的紧密程度。
Gatys把风格图像a也输入到了VGG-19网络里面,把每一层得出的feature map 求出它们的Gram矩阵,一共是5层卷积,求得5个Gram矩阵,X也做同样的操作,然后把它们两个Gram矩阵求均方差,得到的值当做风格损失。x的每一层的Gram矩阵都会和a风格图像的每一层的Gran矩阵一起计算均方差EL,然后由这个EL根据权重w计算得到????_style,权重w用来表达各层特征的重要性,这个损失就是用来描述风格的差异。最后,迭代更新x,其实就是对总的loss求导,然后乘以步长,得到的就是更新的大小。因此x就不断在网络中循环更新,直到达到好的效果。
三、tensorflow代码实现
from PIL import Image
import numpy as np
import scipy.io
import tensorflow as tf
from functools import reduce
import scipy.misc
#$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
#*******************LOAD PICTURE*********************
def loadpic():
imgcon = Image.open("111.jpg")
imgsty = Image.open("time1.jpg")
datacon = imgcon.getdata()
datasty = imgsty.getdata()
datacon = np.reshape(datacon, (300, 300, 3))
datasty = np.reshape(datasty, (300, 300, 3))
datacon = np.array(datacon)
datasty = np.array(datasty)
'''
datacon = scipy.misc.imread("111.jpg").astype(np.float)
datasty = scipy.misc.imread("time1.jpg").astype(np.float)
'''
return datacon, datasty
#********************CONSTRUCT VGG***********************
def vggnet(input_image):
predata = scipy.io.loadmat('imagenet-vgg-verydeep-19.mat')
weights = predata['layers'][0]
def conv_layer(bottom, num):
kernels, bias = weights[num][0][0][0][0]
kernels = np.transpose(kernels, (1, 0, 2, 3))
bias = bias.reshape(-1)
cvnt = tf.nn.conv2d(bottom, tf.constant(kernels), strides = (1, 1, 1, 1), padding = 'SAME')
ret = tf.nn.bias_add(cvnt, bias)
return ret
def pool_layer(bottom):
ret = tf.nn.avg_pool(bottom, ksize = (1, 2, 2, 1), strides = (1, 2, 2, 1), padding = 'SAME')
return ret
def relu_layer(bottom):
ret = tf.nn.relu(bottom)
return ret
vgg = {}
vgg['conv1_1'] = conv_layer(input_image, 0)
vgg['relu1_1'] = relu_layer(vgg['conv1_1'])
vgg['conv1_2'] = conv_layer(vgg['relu1_1'], 2)
vgg['relu1_2'] = relu_layer(vgg['conv1_2'])
vgg['pool1'] = pool_layer(vgg['relu1_2'])
vgg['conv2_1'] = conv_layer(vgg['pool1'], 5)
vgg['relu2_1'] = relu_layer(vgg['conv2_1'])
vgg['conv2_2'] = conv_layer(vgg['relu2_1'], 7)
vgg['relu2_2'] = relu_layer(vgg['conv2_2'])
vgg['pool2'] = pool_layer(vgg['relu2_2'])
vgg['conv3_1'] = conv_layer(vgg['pool2'], 10)
vgg['relu3_1'] = relu_layer(vgg['conv3_1'])
vgg['conv3_2'] = conv_layer(vgg['relu3_1'], 12)
vgg['relu3_2'] = relu_layer(vgg['conv3_2'])
vgg['conv3_3'] = conv_layer(vgg['relu3_2'], 14)
vgg['relu3_3'] = relu_layer(vgg['conv3_3'])
vgg['conv3_4'] = conv_layer(vgg['relu3_3'], 16)
vgg['relu3_4'] = relu_layer(vgg['conv3_4'])
vgg['pool3'] = pool_layer(vgg['relu3_4'])
vgg['conv4_1'] = conv_layer(vgg['pool3'], 19)
vgg['relu4_1'] = relu_layer(vgg['conv4_1'])
vgg['conv4_2'] = conv_layer(vgg['relu4_1'], 21)
vgg['relu4_2'] = relu_layer(vgg['conv4_2'])
vgg['conv4_3'] = conv_layer(vgg['relu4_2'], 23)
vgg['relu4_3'] = relu_layer(vgg['conv4_3'])
vgg['conv4_4'] = conv_layer(vgg['relu4_3'], 25)
vgg['relu4_4'] = relu_layer(vgg['conv4_4'])
vgg['pool4'] = pool_layer(vgg['relu4_4'])
vgg['conv5_1'] = conv_layer(vgg['pool4'], 28)
vgg['relu5_1'] = relu_layer(vgg['conv5_1'])
vgg['conv5_2'] = conv_layer(vgg['relu5_1'], 30)
vgg['relu5_2'] = relu_layer(vgg['conv5_2'])
vgg['conv5_3'] = conv_layer(vgg['relu5_2'], 32)
vgg['relu5_3'] = relu_layer(vgg['conv5_3'])
vgg['conv5_4'] = conv_layer(vgg['relu5_3'], 34)
vgg['relu5_4'] = relu_layer(vgg['conv5_4'])
return vgg
#***********************STYLIZE IMAGE************************
def main():
infodata = scipy.io.loadmat('imagenet-vgg-verydeep-19.mat')#导入VGG-19 mat文件
mean = infodata['normalization'][0][0][0]
mean = np.mean(mean, axis=(0, 1))
datacon, datasty = loadpic()#把内容图和风格图都读进来
shape = (1,) + datacon.shape
infocon = {}
infosty = {}
compute_content = 'relu4_2'#内容图去的是卷积层的第四层
compute_style = ('relu1_1', 'relu2_1', 'relu3_1', 'relu4_1', 'relu5_1')#风格图去的是所有卷积层
weightcon = 5
weightsty = 0.2
iterations = 1000
g = tf.Graph()
with g.as_default(), tf.Session() as sess:
tempimage = tf.placeholder('float', shape = shape)
tempvgg = vggnet(tempimage)#内容图喂给VGG-19 网络
qqq = np.array([datacon - mean])
infocon[compute_content] = tempvgg[compute_content].eval(feed_dict = {tempimage: qqq})
g = tf.Graph()
with g.as_default(), tf.Session() as sess:
tempimage = tf.placeholder('float', shape = shape)
tempvgg = vggnet(tempimage)#风格图像喂给VGG-19 网络
ppp = np.array([datasty - mean])
for layer in compute_style:
temp_style = tempvgg[layer].eval(feed_dict = {tempimage: ppp})
temp_style = np.reshape(temp_style, (-1, temp_style.shape[3]))
infosty[layer] = np.matmul(temp_style.T, temp_style) / temp_style.size#实现求每层的Gram矩阵
g = tf.Graph()
with g.as_default(), tf.Session() as sess:
#initial = np.random.normal(size = shape, scale = 0.2)
ini = tf.random_normal(shape) * 0.1#随机生成白噪声图像
via = tf.Variable(ini)
center_vgg = vggnet(via)#白噪声图像 输入到VGG-19网络里面去
#tf.nn.l2_loss是计算的是每一个元素的平方之后相加最后除以2
#计算内容损失的公式
losscon = weightcon * (tf.nn.l2_loss(center_vgg[compute_content] - infocon[compute_content]) * 2 / infocon[compute_content].size)
#losssty = 0.0
losssty = [] #放损失的
for layer in compute_style:
'''
ttt = np.array([initial])
temp_center = center_vgg[layer].eval(feed_dict = {via: ttt[0]})
temp_center = np.reshape(temp_center, (-1, temp_center.shape[3]))
center_gram = np.matmul(temp_center.T, temp_center) / temp_center.size
'''
temp_center = center_vgg[layer]
_, height, width, number = map(lambda i: i.value, temp_center.get_shape())# number相当于就是图像经过VGG网络的深度,即feature map 的个数
sss = height * width * number
tempnet = tf.reshape(temp_center, (-1, number))# 把好多成的feature map 重新构造成的矩阵,用来求下面的gram矩阵的
center_gram = tf.matmul(tf.transpose(tempnet), tempnet) / sss #求Gram矩阵,白噪声图像的
style_gram = infosty[layer]
#jjj = tf.cast(tf.nn.l2_loss(center_gram - style_gram) * 2 / style_gram.size, tf.float32)
#losssty = tf.add(losssty, jjj)
uuu = tf.nn.l2_loss(center_gram - style_gram) * 2 / style_gram.size#白噪声gram矩阵对风格图像的Gram矩阵求得均方差
losssty.append(uuu)
fi_losssty = reduce(tf.add, losssty)#把风格的损失累加起来
total_loss = losscon + weightsty * fi_losssty#总的损失计算
desceding = tf.train.AdamOptimizer(0.2).minimize(total_loss)#梯度下降进行优化损失
with tf.Session() as sess:
sess.run(tf.initialize_all_variables())
for i in range(iterations):
desceding.run()
if i == iterations - 1:
this_loss = total_loss.eval()
output = via.eval()
#output = np.reshape(output, (300, 300, 3))
output = output.reshape(shape[1:])
output = output + mean
yield (
output
)
#output = np.uint8(output*255)
#!output = np.clip(output, 0, 255).astype(np.uint8)
#image_output = Image.fromarray(output)
#scipy.misc.imsave(save_path, image_output)
#!scipy.misc.imsave(save_path, output)
#image_output.show()
#**************************SAVE RESULT*****************************
def output():
save_path = "./rst.jpg"
for via in main():
via = np.clip(via, 0, 255).astype(np.uint8)
scipy.misc.imsave(save_path, via)
output()
注:
需要把VGG-19预训练模型的.mat 文件导入到和同一目录文件下,而且要给定风格图和内容图,同样在同一目录下,即可实现
风格迁移;风格图和内容图采用300X300的,可以自己更改。
实现效果如下:
推荐阅读