欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

基于tensorflow的图像风格迁移原理与实现

程序员文章站 2022-04-08 14:57:30
...

一、什么是风格迁移?

 苹果APP有一个图片编辑器的软件叫Prisma,可以把我们拍的平时的照片转换成像漫画一样的样式,这个APP中给我们提供了各式各样的艺术图片,
 我们可以任意选择自己的拍的图片,选择喜欢的漫画图、或者说是不同风格的图,进行转化。这样自己拍的图片就风格化了 ,就像是一幅画一样了。
 下面就先用Prisma对风格迁移做一个简单的描述:如下图,我选择了不同的风格,生成的图片效果也就不一样。现在我们应该就都知道了,风格迁移
 简单来说,就是把一幅图像的风格“迁移”到现有图像上,现有图像的内容还是原来的内容 ,就想我的图片上,水杯还是水杯根本没变化,只是整个
 图片的“画风”或者说风格发生了变化,可以说成了一个漫画杯子了。

基于tensorflow的图像风格迁移原理与实现基于tensorflow的图像风格迁移原理与实现![基于tensorflow的图像风格迁移原理与实现

二、风格迁移是如何实现的呢?

风格迁移是先由Gatys在2016年CVPR论文中提出来的,论文的题目是“ Image Style Transfer Using Convolutional Neural Networks”,咱们可以顺着大佬的论文拜读一下,自然就明白风格迁移是怎么实现的了
首先作者先把两张图片都是输入到了VGG-19 网络里面,然后把每个卷积层出来的结果都列出来了,然后作者得出了一个
结论:神经网络的浅层多提取出来的是图像的像素信息,而深层更多提取的是物体的位置信息、布局等
基于tensorflow的图像风格迁移原理与实现
接下来我们看一下Gatys在风格迁移中做了哪些工作:

首先先把一张内容图P输入到VGG19 网络,想要提取这幅图像的P的内容信息。因为之前已经得出了结论“神经网络的浅层多提取出来的是图像的像素信息,而深层更多提取的是物体的位置信息、布局等”,也就是说在提取content特征时,不同层的表达效果是不一样的,文章在提取内容图像的特征时采用高层特征,如下图的右侧可以看出,取的是第四层,高层会很大程度上保留原图的内容特征。然后把一个白噪声图片x也输入到了VGG-19网络里面,就是下图那个灰黑色的小图片,然后同样也在第四个卷积层取出,p和x在第四层卷积层所有的feature map求一个均方差,作为内容的损失值。为什么要这样做呢?这是两幅图片相当于内容上的“差”,我们是想通过后期迭代优化,使其X白噪声图像中的内容与p越来越一样。这是内容损失值的计算公式Lcontent
基于tensorflow的图像风格迁移原理与实现
再来看看怎么样能得到一幅图像的风格呢?
提到图像的风格,我们就得说一下什么是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的图像风格迁移原理与实现
基于tensorflow的图像风格迁移原理与实现

三、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的,可以自己更改。
实现效果如下:基于tensorflow的图像风格迁移原理与实现