精华内容
下载资源
问答
  • 图像风格迁移
    千次阅读
    2021-12-23 15:12:26

    1. 风格迁移入门
    我们首先选取一幅图像作为基准图像,也可以将其称为内容图像,然后选取另一幅或多幅图像作为我们希望获取相应风格的图像,也可以将其称为风格图像。图像风格迁移算法就是在保证内容图像的完整性的前提下,将风格图像的风格融入内容图像中,使得内容图像的原始风格最后发生了转变,最终输出的图像呈现的将是输入的内容图像的内容和风格图像风格之间的理想融合。
    图像风格迁移实现的难点就是如何有效地提取一张图像的风格。和传统的图像风格提取方法不同,我们在基于神经网络的图像风格迁移算法中使用卷积神经网络来完成对图像风格的提取。

    2. pytorch实战
    首先我们需要获取一张内容图片和一张风格图片;然后定义两个度量值,一个度量叫做内容度量值,另一个叫做风格度量值,其中的内容度量值用于衡量图片之间的内容差异程度,风格度量值用于衡量图片之间的风格差异程度;最后,简历神经网络模型,对内容图片中的内容和风格图片的风格进行提取,以内容图片为基准将其输入建立的模型中,并不断调整内容度量值和风格度量值,让他们趋近最小,最后输出的图片就是内容与风格融合的图片。

    2.1 图像的内容损失
    内容度量值可以使用均方误差作为损失函数,在代码中定义的图像内容损失如下:

    class Content_loss(torch.nn.Module):
    	def __init__(self,weight,targrt):
    		super(Content_loss, self).__init__()
    		self.weight = weight
    		self.target = target.detach()*weight
    		self.loss_fn = torch.nn.MSELoss()
    		
    	def forward(self, input):
    		self.loss = self.loss_fn(input*self.weight, self.target)
    		return input
    	def backward(self):
    		self.loss.backward(retain_graph = True)
    		return self.loss
    

    以上代码中的target是通过卷积获取到的输入图像中的内容,weight是我们设置的一个权重参数,用来控制内容和风格对最后合成图像的影响程度;input代表输入图像,target.detach()用于对提取到的内容进行锁定,不需要进行梯度;forward函数用于计算输入图像和内容图像之间的损失值;backward函数根据计算得到的损失值进行后向传播,并返回损失值。

    2.2 图像的风格损失
    风格度量同样使用均方误差作为损失函数,代码如下:

    class Style_loss(torch.nn.Module):
    	def __init__(self,weight,target):
    		super(Style_loss, self).__init__():
    		self.weight = weight
    		self.target = target
    		self.loss_fn = torch.nn.MSELoss()
    		self.gram = Gram_matrix()
    	def forward(self,input):
    		self.Gram = self.gram(input.clone())
    		self.Gram.mul_(self.weight)
    		self.loss = self.loss_fn(self.Gram, self.target)
    		return input
    	def backward(self):
    		self.loss.backward(retain_graph = True)
    		return self.loss
    

    风格损失计算的代码基本和内容损失计算的代码相似,不同之处是在代码中引入了一个Gram_matri类定义的实例参与风格损失的计算,这个类的代码如下:

    class Gram_matrix(torch.nn.Module):
    	def forward(self,input):
    		a,b,c,d = input.size()
    		feature = input.view(a*b,c*d)
    		gram = torch.mm(feature, feature.t())
    		return gram.div(a*b*c*d)
    

    以上代码实现的是格拉姆矩阵的功能。我们通过卷积神经网络提取了风格图片的风格,这些风格其实是由数字组成的,数字的大小代表了图中风格的突出程度,而Gram矩阵是矩阵的内积运算,在运算过后输入到该矩阵的特征图中的大的数字会变得更大,相当于图片的风格被放大了,放大的风格在参与损失计算,便能够对最后的合成图片产生更大的影响。

    2.3 模型搭建和参数优化
    在定义好内容损失和风格损失的计算方法之后,我们还需要搭建一个自定义的模型,并将这两部分内容融入模型中。我们首先要做的是欠以一个卷积神经网络的特征提取部分,即卷积相关的部分,代码如下:

    cnn = models.vgg16(pretrained = True).features
    
    content_layer = ["Conv_3"]
    style_layer = ["Conv_1","Conv_2","Conv_3","Conv_4"]
    
    content_losses = []
    style_losses = []
    
    conten_weight = 1
    style_weight = 1000
    

    以上代码迁移了一个vgg16架构的卷积神经网络模型的特征提取部分,然后定义了content_layer和style_layer,分别制定了我们需要在整个卷积过程中的哪一层提取内容,以及在哪一层提取风格。content_loss和style_loss是两个用于保存内容损失和风格损失的列表;content_weight和style_weight指定了内容损失和风格损失对我们最后得到的融合图片的影响权重。
    搭建图像风格迁移模型的代码如下:

    new_model = torch.nn.Sequential()
    model = copy.deepcopy(cnn)
    gram = gram_matrix()
    
    if use_gpu:
    	new_model = new_model.cuda()
    	gram = gram.cuda()
    index = 1
    for layer in list(model)[:8]:
    	if isinstance(layer, torch.nn.Conv2d):
    		name = "Conv_"+str(index)
    		new_model.add_module(name,layer)
    		for name in content_layer:
    			target = new_model(content_img).clone()
    			content_loss = Content_loss(content_weight, target)
    			new_model.add_module("content_loss_"+str(index),content_loss)
    			content_losses.append(content_loss)
    
    		if name in style_layer:
    			target = new_model(style_img).clone()
    			target = gram(target)
    			style_loss = Style_loss(style_weight, target)
    			new_model.add_module("style_loss_"+str(index), style_loss)
    			style_losses.append(style_loss)
    	
    	if isinstance(layer, torch.nn.ReLU):
    		name = "ReLU_"+str(index)
    		new_model.add_module(name, layer)
    		index = index + 1
    	if isinstance(layer, torch.nn.MaxPool2d):
    		name = "MaxPool_"+str(index)
    		new_model.add_module(name, layer)
    

    以上代码中,for layer in list(model)[:8]指明了我们仅仅用到迁移模型特征提取部分的前8层,,因为我们的内容提取和风格提取在前8层就已经完成了。然后建立一个空的模型,使用torch.nn.Module类的add_module方法向空的模型中加入指定的层次模块,最后得到我们自定义的图像风格迁移模型。add_module方法传递的参数分别是层次的名字和模块,该模块是使用isinstance实例检测函数得到的,而名字是对应的层次。在定义好模型之后可以对其进行打印输出。
    接下来是参数优化部分的代码。

    input_image = content_img.clone()
    parameter = torch.nn.Parameter(input_img.data)
    optimizer = torch.optim.LBFGS([parameter])
    

    在以上代码中使用的优化函数是optim.LBFGS,原因是这个模型中需要优化的损失值有多个并且规模较大,使用该优化函数可以取得更好的效果。

    2.4 训练新定义的卷积神经网络
    完成模型的搭建和优化函数定义之后,就可以开始进行模型的训练和参数的优化,代码如下:

    epoch_n = 300
    
    epoch = [0]
    while epoch[0]<epoch_n:
    	def closure():
    		optimizer.zero_grad()
    		style_score = 0
    		content_score = 0
    		parameter.data.clamp_(0,1)
    		new_model(parameter)
    		for sl in style_losses:
    			style_score += sl.backward()
    		
    		for cl in content_losses:
    			content_score += cl.backward()
    		
    		epoch[0]+=1
    		if epoch[0]%50 ==0:
    			print("Epoch:{} Style score:{:.4f} Content Score:{:.4f}".format(epoch[0],style_score.data,content_score.data))
    		return style_score+content_score
    	optimizer.step(closure)
    

    2.5 完整图像风格迁移代码

    import torch
    import torchvision
    from torchvision import transforms, models
    from PIL import Image
    import matplotlib.pyplotlib.pyplot as plt
    from torch.autograd import Variable
    import copy
    
    %matplotlib inline
    
    transform = transforms.Compose([transforms.Scale([224,224]),transforms.ToTensor()])
    
    def loading(path = None):
    	img = Image.open(path)
    	img = transform(img)
    	img = img.unsqueeze(0)
    	return img
    
    content_img = loading("images/4.jpg")
    content_img = Variable(content_img).cuda()
    style_img = loading("images/1.jpg")
    style_img = Variable(style_img).cuda()
    
    class Content_loss(torch.nn.Module):
    	def __init__(self,weight,targrt):
    		super(Content_loss, self).__init__()
    		self.weight = weight
    		self.target = target.detach()*weight
    		self.loss_fn = torch.nn.MSELoss()
    		
    	def forward(self, input):
    		self.loss = self.loss_fn(input*self.weight, self.target)
    		return input
    	def backward(self):
    		self.loss.backward(retain_graph = True)
    		return self.loss
    class Gram_matrix(torch.nn.Module):
    	def forward(self,input):
    		a,b,c,d = input.size()
    		feature = input.view(a*b,c*d)
    		gram = torch.mm(feature, feature.t())
    		return gram.div(a*b*c*d)
    class Style_loss(torch.nn.Module):
    	def __init__(self,weight,target):
    		super(Style_loss, self).__init__():
    		self.weight = weight
    		self.target = target
    		self.loss_fn = torch.nn.MSELoss()
    		self.gram = Gram_matrix()
    	def forward(self,input):
    		self.Gram = self.gram(input.clone())
    		self.Gram.mul_(self.weight)
    		self.loss = self.loss_fn(self.Gram, self.target)
    		return input
    	def backward(self):
    		self.loss.backward(retain_graph = True)
    		return self.loss
    use_gpu = torch.cuda.is_available()
    cnn = models.vgg16(pretrained = True).features
    
    if use_gpu:
    	cnn = cnn.cuda()
    model = copy.deepcopy(cnn)
    
    content_layer = ["Conv_3"]
    style_layer = ["Conv_1","Conv_2","Conv_3","Conv_4"]
    
    content_losses = []
    style_losses = []
    
    conten_weight = 1
    style_weight = 1000
    
    new_model = torch.nn.Sequential()
    model = copy.deepcopy(cnn)
    gram = gram_matrix()
    
    if use_gpu:
    	new_model = new_model.cuda()
    	gram = gram.cuda()
    index = 1
    for layer in list(model)[:8]:
    	if isinstance(layer, torch.nn.Conv2d):
    		name = "Conv_"+str(index)
    		new_model.add_module(name,layer)
    		for name in content_layer:
    			target = new_model(content_img).clone()
    			content_loss = Content_loss(content_weight, target)
    			new_model.add_module("content_loss_"+str(index),content_loss)
    			content_losses.append(content_loss)
    
    		if name in style_layer:
    			target = new_model(style_img).clone()
    			target = gram(target)
    			style_loss = Style_loss(style_weight, target)
    			new_model.add_module("style_loss_"+str(index), style_loss)
    			style_losses.append(style_loss)
    	
    	if isinstance(layer, torch.nn.ReLU):
    		name = "ReLU_"+str(index)
    		new_model.add_module(name, layer)
    		index = index + 1
    	if isinstance(layer, torch.nn.MaxPool2d):
    		name = "MaxPool_"+str(index)
    		new_model.add_module(name, layer)
    
    input_image = content_img.clone()
    parameter = torch.nn.Parameter(input_img.data)
    optimizer = torch.optim.LBFGS([parameter])
    
    epoch_n = 300
    
    epoch = [0]
    while epoch[0]<epoch_n:
    	def closure():
    		optimizer.zero_grad()
    		style_score = 0
    		content_score = 0
    		parameter.data.clamp_(0,1)
    		new_model(parameter)
    		for sl in style_losses:
    			style_score += sl.backward()
    		
    		for cl in content_losses:
    			content_score += cl.backward()
    		
    		epoch[0]+=1
    		if epoch[0]%50 ==0:
    			print("Epoch:{} Style score:{:.4f} Content Score:{:.4f}".format(epoch[0],style_score.data,content_score.data))
    		return style_score+content_score
    	optimizer.step(closure)
    

    3. 小结
    本章展示的是比较基础的图像风格迁移算法,每次训练只能对其中的一种风格进行迁移,如果要进行其他风格的迁移,则需要调整模型重新训练。

    更多相关内容
  • 实用代码 04 图像风格迁移实用代码 04 图像风格迁移实用代码 04 图像风格迁移实用代码 04 图像风格迁移实用代码 04 图像风格迁移实用代码 04 图像风格迁移实用代码 04 图像风格迁移实用代码 04 图像风格迁移实用代码...
  • 单张图片的图像风格迁移,包含图像已训练完成的数据模型,可以进行输出图片的质量选择,进行照片,背景图风格转换学习。
  • OpenCV图像风格迁移所用模板-Candy 使用方法: import cv2 image_file = 'xxx.jpg' #目标文件 model = 'Candy.t7' #模板文件 net = cv2.dnn.readNetFromTorch('models/' + model) image = cv2.imread('...
  • 图像风格迁移技术是计算机视觉中的重点技术,传统的图像风格迁移技术采 用手工演算的方式,计算过程复杂,计算时间漫长,图像风格迁移效果不理想。 随着人工智能技术在计算机视觉领域的应用逐步广泛,一些艺术风格...
  • 实现基于深度卷集神经网络的图像风格迁移的程序,采用python语言编写代码
  • 图像风格迁移原始论文完整实现代码,可以实现内容图片和风格图片的转化,https://blog.csdn.net/kevinoop/article/details/79827782 这个博客有代码详细介绍。
  • 实用代码 30 快速图像风格迁移实用代码 30 快速图像风格迁移实用代码 30 快速图像风格迁移实用代码 30 快速图像风格迁移实用代码 30 快速图像风格迁移实用代码 30 快速图像风格迁移实用代码 30 快速图像风格迁移实用...
  • OpenCV图像风格迁移所用模板文件之starry_night.t7 使用代码: import cv2 image_file = xxx.jpg' #目标文件 model = 'starry_night.t7' #模板文件 net = cv2.dnn.readNetFromTorch('models/' + model) ...
  • 图像风格迁移

    万次阅读 多人点赞 2021-09-24 13:35:20
    风格迁移指的是两个不同域中图像的转换,具体来说就是提供一张风格图像,将任意一张图像转化为这个风格,并尽可能保留原图像的内容


    前言

    在这里插入图片描述

    风格迁移指的是两个不同域中图像的转换,具体来说就是提供一张风格图像,将任意一张图像转化为这个风格,并尽可能保留原图像的内容(否则就成了艺术创作了…)

    图像的风格迁移只是目的,实现图像风格迁移的手段是多种多样,所谓条条大路通罗马。个人根据时间线将其概况为三种大类别。传统的图像风格迁移;基于神经网络的图像风格迁移和基于对抗生成网络的图像迁移。

    至于图像风格迁移可以用来做什么,大多数情况下可以实现类似于滤镜的图像处理效果;基于对抗生成网络实现的风格迁移甚至可以实现语义上的迁移(橘子变苹果,马变斑马,卡车变汽车以及一些侵犯肖像权的不好变换…)。总的来说,这些多种多样的变换不仅好玩儿,也是数据集扩充的另一个思路和手段。


    一、传统的图像风格迁移(Traditional style transfer)

    时间线:2015年前30到40年间

    1.1计算机图形学领域和计算机视觉领域(Computer Graphics&Computer Vision)

    传统的图像风格迁移可以从两个角度来说:计算机图形学领域和计算机视觉领域
    防止混淆,这里简单介绍两者的区别(顺带提一下电子图像处理)

    计算机图形学Computer Graphics,简称 CG 。输入的是对虚拟场景的描述,通常为多边形数组,而每个多边形由三个顶点组成,每个顶点包括三维坐标、贴图坐标、rgb 颜色等。输出的是图像,即二维像素数组。

    计算机视觉Computer Vision,简称 CV。输入的是图像或图像序列,通常来自相机、摄像头或视频文件。输出的是对于图像序列对应的真实世界的理解,比如检测人脸、识别车牌。

    电子图像处理Digital Image Processing,简称 DIP。输入的是图像,输出的也是图像。Photoshop 中对一副图像应用滤镜就是典型的一种图像处理。常见操作有模糊、灰度化、增强对比度等。

    这三者于人工智能的总结如下图所示:
    在这里插入图片描述

    1.2非真实感图形学(Non-photorealistic graphics)和纹理迁移(texture transfer)

    下面回归正题:
    图形风格转移在计算机图形学领域其实就是非真实感图形学(Non-photorealistic graphics)
    其中大体又可以分为三种方法:笔触渲染的方法(Stroke-based Rendering)、基于图像类比的方法(Image Analogy)、基于图像滤波的方法(Image Filtering)。但是这些方法都没有大规模落地。

    首先,基于笔触的方法在设计前需要确定某一风格,不能简单的扩展到其他风格的迁移;基于图像类别的方法需要成对的数据集,然而采集成对的数据集在现实场景下几乎是不可能的,打个比方,我们要采集晴天和雨天两种风格下的猫咪,不可能采集到两种图片分别在晴天和雨天下猫咪的姿态一模一样;最后,通过图像滤波的方式速度快,效果稳定,可满足工业界落地的需求,但是滤波器的值是算法工程师不断调整出来的,十分的耗时耗力,而且模拟出来的风格种类也是有限的。这就是为什么这些方法没有大规模工业落地的原因。然后,隔壁的计算机视觉领域的进展也不理想。

    在计算机视觉领域,风格迁移被视为纹理合成的扩展问题,当时甚至连名字都没有,更多的叫法是纹理迁移(texture tansfer)因为风格其实也可以看做一种纹理,假如在合成纹理图的时候刻意的保留一些语义信息(即输入图的内容信息),那就得到了风格迁移的结果。这一方法没有流行起来的原因是当时纹理迁移的是基于像素的底层图像特征,并没有过多的考虑语义信息,因此图像的迁移结果并不理想。

    其实,我们可以将图像迁移看做图像纹理提取和图像重建两个步骤,2015年前,仅在图像纹理合成上有些许成就,但在图像重建领域考虑的并不周全。但是随着深度学习的飞速发展,基于神经网络的图像迁移方法有了巨大的进步,以下的介绍都是基于神经网络的图像迁移方法。该课题在效果上看上去是一个image-to-image,似乎属于DIP领域,可实际应用的越是计算机视觉领域的知识和方法,具体来说是神经网络知识和深度学习方法。

    二、基于神经网络的风格转换(Neural style transfer)

    时间线:2015年后

    在上一小节说道,我们可以通过纹理建模方法(Visual Texture Modelling),主要研究如何表示一种纹理,是纹理合成技术的核心。

    以往的纹理合成方法大致可以分为两大类:
    (1)基于统计分布的参数化纹理建模方法(Parametric Texture Modelling with Summary Statistics)
    (2)基于 MRF 的非参数化纹理建模方法(Non-parametric Texture Modelling with MRFs)。

    基于统计分布的参数化方法主要将纹理建模为 N 阶统计量,而基于 MRF 的方法一个经典套路是用 patch 相似度匹配进行逐点合成。纹理建模方法的相关研究解决了图像风格化迁移的第一个大问题:如何对风格图中的风格特征进行建模和提取。然而,如何将风格和内容混合然后还原成一个相应的风格化结果呢?这就到了另一个领域——图像重建(Image Reconstruction)了。

    图像重建算法其实也可以分为两类:
    (a)基于在线图像优化的慢速图像重建方法(Slow Image Reconstruction based on Online Image Optimisation)
    (b)基于离线模型优化的快速图像重建方法(Fast Image Reconstruction based on Offline Model Optimisation)

    第一类图像重建的方法是在图像像素空间做梯度下降来最小化目标函数。这一类算法的过程可以理解为:由随机噪声作为起始图,然后不断迭代改变图片的所有像素值来寻找一个目标结果图 x’ 。由于每个重建结果都需要在像素空间进行迭代优化很多次,这种方式是很耗时的,占用的计算资源以及需要的时间开销很大。为了加速这一过程,一个直接的想法是设计一个前向网络,用数据驱动的方式,喂给它很多训练数据去提前训练它,训练的目标就是给定一个输入,这个训练好的网络只需要一次前向就能输出一张重建结果图像。即第二类方法 。

    基于神经网络的风格迁移就是以上面的四大类方法进行排列组合。

    便于理解,这里可以将图像风格迁移想象成一个虚拟画家,这个画家的目的就是学习并创作各种风格的图像,我们的目的就是让这个画家具备智能,以下会穿插画家的类比,方便理解。

    2.1 基于在线图像优化的慢速图像风格化迁移算法(Slow Neural Method Based On Online Image Optimisation)

    2.1.1 基于统计分布的参数化慢速风格化迁移算法(Parametric Slow Neural Method with Summary Statistics)这一类方法是基于在线图像优化的慢速图像重建方法和基于统计分布的参数化纹理建模方法结合而来的,即上面提到的(1)+(a)。

    Neural style transfer的开山之作,祖师爷 Gatys 在2015年提出的‘Texture Synthesis Using Convolutional Neural Networks’就是基于此方法的。如下图所示,输入是噪声底图,约束是Style Loss和Content Loss分别用来优化风格图像的风格和目标图像的内容。
    在这里插入图片描述

    简单说便是输入一张随机噪声构成的底图,通过计算Style Loss和Content Loss,迭代update底图,使其风格纹理上与Style Image相似,内容上与原照片相似。正常的训练过程是通过loss反向传播更新网络参数,这里则是用一个已经训练好的VGG16作为backbone,锁住参数,更新输入的底图。类比画家作画的话,随机噪声就是画家的画纸,网络提取更新的内容特征相当于画家打的线稿,风格特征则是画家写真的风景对象(晴天,雨天,雪天等等各种风格)。可以想象,这种方式是十分耗时的,而且每张结果都需要长时间的训练(就想作家作画每一张都需要很长时间一样)。但是好处是由于长时间的训练,每幅画的效果还是很不错的。

    具体说,论文用 Gram 矩阵来对图像中的风格进行建模和提取,再利用慢速图像重建方法,让重建后的图像以梯度下降的方式更新像素值,使其 Gram 矩阵接近风格图的 Gram 矩阵(即风格相似),然后,用VGG网络提取的高层feature map来表征图像的内容信息,通过使 VGG 网络对底图的提取的高层feature map接近目标图高层的feature map来达到内容相似,实际应用时候经常再加个总变分 TV 项来对结果进行平滑,最终重建出来的结果图就既拥有风格图的风格,又有内容图的内容。

    额外提一下,Gram矩阵是论文的核心思想,是一种基于统计分布的参数化纹理建模方法,作者发现Gram矩阵可以很好的提取图像风格,具体原理这里就不展开叙述了。

    再讲一下如何提取图像的内容特征,这里使用VGG网络高层特征表达目标图像的内容特征。得益于对神经网络黑盒特性的不断研究,学者们发现,神经网络的中间层提取到的图像特征是不一样的,越靠近输入层的中间层提取到的特殊是浅层特征(即,点,线,色块等低级特征);越靠近输出层的中间层提取到的特征是高级特征(例如,边,角,轮廓等)因此,图像的内容信息可以使用神经网络提取到的高级特征来表达(实际上,Gram矩阵是对神经网络提取的浅层特征做变换得到的,用来表示风格)。

    总结一下,这篇开山之作的算法虽然生成的图片看起来很不错,但是仍存在以下问题:

    由于每次迁移都要对网络进行训练,速度是非常慢的,无法实现实时迁移;
    应用在照片上进行风格迁移,会出现失真的情况;

    针对第一个问题,Johnson提出的fast-neural -style在本文网络模型前增加一个转换网络,转换网络的输入是内容图片,输出是风格迁移图片。而本文的网络模型称为损失网络,用于计算损失。为每个风格图片训练一个网络,这样在测试时,给定一张内容图片,只需要一次前向过程即可得到生成图片。这对第二个问题,康奈尔大学和Adobe公司合作推出了一篇文章:Deep Photo Style Transfer,通过对损失函数进行改进,使得可以在照片之间进行风格迁移且不失真。

    当然,基于统计分布的参数化纹理建模方法不只Gram矩阵一种,才疏学浅这里就不一一介绍了。

    2.1.2 基于MRF的非参数化慢速风格化迁移算法(Non-parametric Slow Neural Method with MRFs)

    另外一类慢速风格化算法就是利用基于 MRF 的非参数化纹理建模方法对风格信息进行建模了,即第二篇开篇提到的(2)+(a)。代表性工作由浙大出身、德国美茵茨大学 Postdoc Chuan Li 学长完成并发表于 CVPR 2016的Combining Markov Random Fields and Convolutional Neural Networks for Image Synthesis

    其核心思想是提出了一个取代 Gram 损失的新的 MRF 损失。思路与传统的 MRF 非参数化纹理建模方法相似,即先将风格图和重建风格化结果图分成若干 patch,然后对于每个重建结果图中的 patch,去寻找并逼近与其最接近的风格 patch。

    与传统 MRF 建模方法不同之处在于,以上操作是在 CNN 特征空间中完成的。另外还需要加一个祖师爷 Gatys 提出的内容损失来保证不丢失内容图中的高层语义信息。

    这种基于 patch 的风格建模方法相比较以往基于统计分布的方法的一个明显优势在于,当风格图不是一幅艺术画作,而是和内容图内容相近的一张摄影照片(Photorealistic Style),这种基于 patch 匹配(patch matching)的方式可以很好地保留图像中的局部结构等信息。

    至此,我们创作的虚拟画家已经学会了如何作画,前提是需要提供给它两张图片,一张告诉它画什么,一张告诉它画什么风格。关键是这个画家的工作方式是一幅一幅的进行从头作画,看上去工作效率并不是很高。

    2.2 基于离线模型优化的快速图像风格化迁移算法(Fast Neural Method Based On Offline Model Optimisation)

    本节算法主要为了解决上一小节的算法速度慢这一缺点。核心思想就是利用基于离线模型优化的快速图像重建方法来节省时间。具体来说就是预训练一个前向网络,使得图像经过一次前向计算就可以得到图像重建结果,在依据各自约束来优化这一结果。根据一个训练好的前向网络能够学习到多少个风格作为分类依据,这里可以将这一类算法再细分为单模型单风格(PSPM)、单模型多风格(MSPM)和单模型任意风格(ASPM)的快速风格化迁移算法

    类比画家来说就是我们的画家工作效率太低了,我们希望它更智能,希望它画的更快。

    2.2.1 PSPM的快速风格迁移(Per-Style-Per-Model Fast Neural Method)
    主要想法是针对每一风格图,我们去特定的训练处一个前向网络,这样当测试的时候,我们只需要向模型中输入一张图,经过一次前向计算就可以得到输出结果。这种方法按照纹理建模的方式不同其实又可以分成基于统计分布的参数化纹理建模和基于MRF的非参数化纹理建模两类,即第二节开篇提到的(1)+(b)和(2)+(b)。

    (1)+(b)这一小类算法代表性工作主要有两个,一个由斯坦福的 Justin Johnson 和李飞飞教授提出的 Perceptual Losses for Real-Time Style Transfer and Super-Resolution;另一个由俄罗斯成立不久的 Skolkovo 科技研究所的 Ulyanov 提出的Texture Networks: Feed-forward Synthesis of Textures and Stylized Images

    李飞飞的模型如下图所示
    在这里插入图片描述
    相比于祖师爷Gatys提出的模型相比,输入不再是噪声底图而是目标图像,并增加了一个autoencoder形状的前向网络来拟合风格转移的过程,其他思想相同:损失函数仍是用另一个神经网络(一般是VGG)提取的content loss+style loss,并统称perceptual loss,纹理建模使用Gram矩阵来表达。

    Ulyanov的模型与李飞飞提出的相似,不同之处在于两个工作的具体网络框架设计不同,一个基于当时最新的残差网络设计的,一个是设计了多尺度的网络。Ulyanov后来又在 CVPR 2017 上对其之前的工作做了改进,他们发现 Instance Normalization 比 Batch Normalization 能够更快、更好地使模型达到收敛(其实就是把 batch normalization 的 batch size 设成 1)。Instance Normalization 的 idea 最早是在论文Revisiting Batch Normalization For Practical Domain Adaptation 中提出的 ,即文章中的 Adaptive Batch Normalization (AdaBN)。

    第二小类(2)+(b)基于 MRF 的快速 PSPM 风格化算法由Chuan Li提出的Precomputed Real-Time Texture Synthesis with Markovian Generative Adversarial Networks属于此类方法,他们将自己之前提出的基于 patch 的慢速风格化算法进行了加速。同样是训练一个前向网络,Chuan Li 进一步利用 GAN 中的判别网络的想法来取代他们慢速风格化算法中的 patch 匹配(patch matching)过程。这一思想在清华出身的朱俊彦发表的论文 Image-to-Image Translation with Conditional Adversarial Networks 中被进一步延伸和发展。

    总结一下,使用类比画家作画的思维描述,不管是(1)+(b)还是(2)+(b),(b)其实就是一种类似颜色模板的东东,不管勾勒的线稿是什么,都使用这种模板进行上色。比如这种模板可能是白雪皑皑的天气,不管画中的内容是什么,最终的结果都是统一风格的——雪中风景图。在模型角度来说,这个颜色模板就是训练出来的前项网络,勾勒的线稿就是Gram矩阵或者MFR所表达的特征,至于到底是使用Gram提取内容特征还是MFR提取内容特征还是其他的方式仅仅是方法不同而已,相当于画家作画的方式可能是油画,是素描,是水彩等等,只要能勾勒出风景的内容信息即可。使用模板进行创作的速度自然比画家一张一张的进行作画要快得多。至此,我们的画家已经学会如何快速作画了,当然,获得这个本领的前提是要对它进行提前训练。

    2.2.3 MSPM 的快速风格转移(Mutil-Style-Per-Model Fast Neural Method)
    楼上的单模型快速风格转化迁移算法对于每一个风格都要训练一个模型;这在风格类别较小的情况下是可行的,但是让所需风格类别是成千上万种时,就需要成千上万个模型,这是工业落地所不能接受的。

    实际上,把多个风格整合到一个模型上的需求是合理的。类比来说,一名画家不应该只会画冬天的画,难道我要创作四季的风景还需要四名画家吗?这就显的有点人工智障而非智能了。。。。。从理论上来说也是合理的,例如一个树的四季风景图,不管是春夏秋冬,风格图总有相似的地方,就树的例子来说,相似的是树干,树的树干在一年四季变换是较小的,变换大的只是枝叶而已。因此,只需要发掘不同风格的相似部分就可以把不同风格整合到一个模型中。换句话说,即使风格不同,结果图中也有相似部分,因此对每个风格去训练一个模型是一件冗余的,不太合理的浪费资源的事情。这个思路的解决方案就是发掘不同风格网络之间的共享部分,然后对新的风格只去改变其有差别的部分,并保持共享部分不变

    这就是 Google Brain 的众大佬们研究出来的一个 MSPM 算法的基本思路 A Learned Representation for Artistic Style。他们发现在训练好的一个风格化网络基础上,只通过在 Instance Norlization 层上做一个仿射变换(他们起了个名字叫 Conditional Instance Normalization,简称 CIN),就可以得到一个具有完全不同风格的结果。但是CIN 层为什么有效的理由现在也没有严格的推导证明。一个大概的解释是 CIN 能够进行一种 style normalization,能够将图像中的风格直接 normalize 成另外一种风格。另一个由陈冬冬提出的 MSPM 法 StyleBank: An Explicit Representation for Neural Image Style Transfer 与 Google Brain 这篇思路有异曲同工之妙,核心思想为把风格化网络中间的几层单独拎出来(文章中起了个名字叫 StyleBank 层),与每个风格进行绑定,对于每个新风格只去训练中间那几层,其余部分保持不变

    上面俩工作的共同点都是把网络的一部分拿出来与每个风格进行绑定,从而实现 MSPM,虽然随着风格的增加,模型大小也会增大,即使远远低于正比关系。所以呢,另外有一些研究者想,能不能试试完全用一个网络,看它能不能学到多个风格。这时候需要考虑的问题是既然只用一个网络,那就需要给网络一个信号,我们需要风格化成哪一个风格。这一思路最早由 Amazon AI 的张航 在 2017 年 3 月提出Multi-style Generative Network for Real-time Transfer。 该算法的核心思想是把通过 VGG 网络提取到的风格特征与风格化网络中的多个尺度的中间层的 feature map 通过提出的 Inspiration Layer 结合在一起,相当于将风格特征作为信号输入到网络中来决定要风格化成哪一个风格。最终算法的效果非常显著。另外除了把风格特征作为信号外,另一个选择是把图像像素作为信号输入进去风格化网络。这一想法的可行性在浙大李一君的工作中得到了证明 Diversified Texture Synthesis With Feed-Forward Networks首先将每一张风格图与一个随机产生的噪声图进行绑定,然后将噪声图与风格化网络中间层的 feature map 进行 concat,作为网络进行风格选择的信号

    2.2.3 ASPM快速风格化迁移算法(Arbitrary-Style-Per-Model Fast Neural Method)

    有了上面的介绍,现在已经可以利用深度学习做到使用一个模型迁移多个风格了,美中不足的是,对于新的一组风格,我们仍需要额外的训练时间开销

    类比来说,至此,我们使用深度学习创造出来的一个画家就已经初具智能了。但是天赋不足,每次学习新的画风时都需要练习很长时间。于是,我们考虑是否可以创造出一个天赋异禀的画家,对于新的风格,只需要看一眼就可以理解并创作而不需要长时间的练习呢?

    果然,科学的探索是神奇且永无止境的,这种 Zero-shot Fast Style Transfer 的单模型任意风格转换( ASPM)的畅想最早由多伦路大学的天才博士陈天奇解决。他提出的 *Fast Patch-based Style Transfer of Arbitrary Style算法是基于 patch 的,可以归到基于 MRF 的非参数化 ASPM 算法。基本思想是在 CNN 特征空间中,找到与内容 patch 匹配的风格 patch 后,进行内容 patch 与风格 patch 的交换(Style Swap),之后用快速图像重建算法的思想对交换得到的 feature map 进行快速重建。但由于 style swap 需要一定的时间开销,还没有达到实时的效果。

    第一篇能达到实时的 ASPM 算法由康奈尔的大牛 Xun Huang 提出 Arbitrary Style Transfer in Real-time with Adaptive Instance Normalization。其工作主要受到 MSPM 的 CIN 层启发,提出一个 Adaptive Instance Normalization (AdaIN)。AdaIN 的输入是通过 VGG 提取的风格和内容特征,用数据驱动的方式,通过在大规模风格和内容图上进行训练,让 AdaIN 能够直接将图像中的内容 normalise 成不同的风格。这一工作录用为 ICCV 2017 的 Oral。另外一个数据驱动的 ASPM 方法是Exploring the Structure of a Real-time, Arbitrary Neural Artistic Stylization Network。可以看做是前一篇的一个 follow-up 的工作,既然通过改变 CIN 层中仿射变换的参数,就可以得到不同的 style,换言之,只要任意给一个风格,我们只需要知道他的 CIN 层中的仿射变换的参数就可以了。沿着这个思路,Google Brain 的研究者设计和训练了一个 style prediction network 去专门预测每个 style 的仿射变换的参数,style prediction network 需要大规模 style 和 content 图来进行训练。这个方法的缺点也很明显,数据驱动的方式不可避免地导致风格化效果与训练数据集中 style 的种类和数量非常相关

    由以上数据驱动 ASPM 算法的局限性,李一君学长进一步思考能不能用一种不需要学习训练的方式(style learning-free),而是单纯使用一系列特征变换来进行 ASPM 风格迁移 Universal Style Transfer via Feature Transforms。李一君发现在 VGG 提取的特征上用 ZCA whitening transform 能够把一张图片的风格信息抹去,而保留原有高级语义信息,之后应用 coloring transform 将风格图的颜色进行迁移,即可重建出效果不错的风格化结果。这一工作发表于 NIPS 2017 上,也是很少见的一篇 NIPS 上发表的 application 类的文章,足见学术界对 Neural Style Transfer 的关注。

    至此,我们已经可以使用深度学习创作出一名天赋异禀的画家了。

    三, 基于对抗生成网络的风格迁移(Style transfer based on GAN)

    时间线: 2017年后

    3.1 对抗生成网络(adversarial generative network)

    严格来说,基于GAN的风格迁移其实是属于基于神经网络的风格迁移范围之内的,因为GAN网络本质上就是神经网络,只是由于其巧妙的loss函数设计方法,使得GAN网络的效果在图像生成领域一马当先,其研究也极为火热,近些年已有开山立派之态。

    为了对比来说GAN网络和神经网络的异同,这里重新贴一下祖师爷Gatys的模型在这里插入图片描述
    这里在贴一张普通GAN网络的模型
    在这里插入图片描述
    相同点显而易见,输入都是噪声图像,输出也是一张图像,经过的网络都是神经网络。不同之处则在于loss的设计方面,Gatys模型的loss函数是使用VGG网络去提取风格图片和目标图片的特征做label;提取输出图片的特征做结果,并比较两者的差异作为loss。而Gan网络的loss设计比较巧妙,Gan网络由生成器和判别器两个网络组成,生成器负责对输入图片进行重建;其重建结果与真实数据集一起送入判别网络进行判断,判别网络负责分辨生成器的输出结果到底是来自真实数据集的真实图片还是来自其本身的生成图片。这时,巧妙的事情就发生了,生成器会努力使自己生成的图片骗过判别器,方法自然是生成的图片越真实越容易欺骗判别器;判别器则会努力分辨输入到底是真实的还是生成的,其反馈来指导生成器生成更真实的图片,从而形成一种动态博弈,结果是输出的图片在内容和风格上都类似于真实数据集。

    这时,因为对GAN网络并没有内容和风格上的约束,因此其输出结果更像是艺术创作,只不过由于loss设计的巧妙性,其模型创作出来的结果非常的逼真,甩了其他图片生成模型好几条街。由于篇幅原因,对普通GAN网络不再展开解释,好奇的小伙伴可以自行搜索,尤其是对抗损失的理解。这对理解下面的GAN变体很有帮助。

    3.2 CycleGAN(Unpaired Image-to-ImageTranslation using Cycle-Consistent Adversarial Networks)

    现在要通过GAN网络实现风格迁移需要考虑的事情就是如何为GAN网络加上内容约束和风格约束了,著名的CycleGan变通过自己巧妙的loss设计实现了这一点。

    上图的原始GAN网络可以进行如下的简化描述
    在这里插入图片描述
    输入x 经过生成器 G 得到输出Y ,判别器用来负责判别Y是取自真实数据集还是生成的图像。通过这一约束,使得生成器生成的图片越来越真实,但是由于没有内容和风格约束,所以模型的输出结果是不可预测的,这更像是艺术创作。就像给画家一张白纸,说你去画画吧,最后只评判画家画的画写不写实,因此画家可以随意创作,这要保证作品看起来真实就好。那么如何添加约束?

    首先, 要做风格转换,就必须要有两套不同风格的数据集,例如春天数据集和冬天数据集;GAN网络的输入也不再是随机噪声,而是其中一个数据集,这里使用春天数据集,经过生成器后重建的结果图片与冬天数据集一起作为判别器的输入,让判别器来判断该冬天风格图是否真实,从而指导生成器生成的图片是冬天风格且真实的。这样这个生成器G就通过优化对抗loss就学习到了一种由春天风格到冬天风格的一种映射,可以将任意一张输入图片转换为冬天风格的。这里的风格约束与Gatys的思路就完全不同了,一个是通过不断优化结果图与风格图Gram矩阵的距离作为loss;一个是通过对抗损失的动态博弈最为loss。值得一提的是,由于对抗损失只是为了学习不同风格域间的映射,因此两个数据集里的数据不必是成对的,(通俗的理解是风格作为纹理特征的延伸是一种低级特征,内容作为语义信息是一种高级特征。当我们只希望从不同数据集学习风格映射时,只需要保证不同数据集风格一致即可,比如一个数据集都是冬天,另一个数据集都是夏天,对数据集中具体照片的内容没有要求,是猫是狗是啥姿态无所谓,即可以是非配对的)这一点很重要因为现实中不同风格下成对的数据是很难很难获得的

    但是,仅仅使用两个数据集运用GAN网络是不能达到风格迁移的目的的,原因有二:其一,没有内容约束,生成的结果无法预测,失去了风格迁移的意义(我们希望风格迁移后的语义信息是不变的);其二,GAN网络的训练倾向于模式崩塌(Model collapse),这里不展开解释,简单来说就是,当生成器发现生成某一张图片可以轻易的骗过判别器时,其他的生成结果就会倾向类似于该照片。即当生成器发现一个好的解时,就疯狂输出类似于这个解的结果,不愿意去做其他的尝试,但是,关键在于该解并不一定有较好的视觉效果并不一定是我们期望的结果,只是因为该解的loss较优,生成器才做此决定,而我们希望生成器多去尝试,以找到最优解(这里隐含一点,个人认为loss只是指导,并不代表loss最优,结果的视觉效果就是最优的)。

    言归正传,CycleGan如果进行内容约束?如下图所示
    在这里插入图片描述
    对输出结果进行一次反变换来对比原始输入和反变换后的输出间的差异即可进行内容上的约束
    类比语言翻译:我的目的是将汉语风格的X “神经网络真奇妙~” 变换成英语Y。生成器G相当于翻译机,经过在这里插入图片描述过程可以有多个翻译结果:例如 1,“神经net very fun” 2,“neural network 真好玩” 3,“neural network very fantastic” 4,“I am very handsome” 判断器D的任务是判断哪个结果更像英语,因此判断器给出的结果可能是3,也可能是4,因为它们都是英语,但是,显然4是不对的。如何进行约束呢?只需要再进行一次反变换,判断其结果和原始输出之间的差别即可。
    经过在这里插入图片描述过程,我的得到3的结果是神经网络真奇妙,与原始输入结果(神经网络真奇妙)的距离为0,4的结果是我真帅与原始结果的距离就很大了。这变达到了内容约束的目的。将后半部分一翻转就是一个圆,如下图所示,这就是CycleGan名字的由来。
    在这里插入图片描述
    总结一下,CycleGan使用两个不同风格的数据集,通过优化对抗损失来到底风格约束的目的,这一过程需要生成器和判别器两个网络组成;CycleGan又通过一次反向变化来达到内容约束的目的,同样,这一过程也需要生成器和判别器两个网络组成。因此CycleGan的网络组成部分是两个生成器和两个判别器四部分,loss组成部分是对抗loss和原始输入与反变换后输出的内容距离loss两部分组成

    由于GAN网络在图像生成领域的效果卓越,因此也广泛应用于风格转换,各种基于GAN的变体都能达到风格转换的效果。CycleGan是其中非常有创新性的代表模型,与其同期发表的DiscoGAN和DualGAN于CycleGAN有异曲同工之妙,可以说是孪生兄弟,篇幅原因就不展开介绍了,最后放一些cyclegan的效果图瞻仰一下吧
    在这里插入图片描述
    顺便提一下CycleGan的问题,CycleGAN 存在一种情况,是它能学会把输入的某些部分藏起来,然后在输出的时候再还原回来。比如下面这张图:
    在这里插入图片描述
    可以看到,在经过第一个生成器的时候,屋顶的黑色斑点不见了,但是在经过第二个生成器之后,屋顶的黑色斑点又被还原回来了。这其实意味着,第一个生成器并没有遗失掉屋顶有黑色斑点这一讯息,它只是用一种人眼看不出的方式将这一讯息隐藏在输出的图片中(例如黑点数值改得非常小),而第二个生成器在训练过程中也学习到了提取这种隐藏讯息的方式。那生成器隐藏讯息的目的是什么呢?其实很简单,隐藏掉一些破坏风格相似性的“坏点”会更容易获得判别器的高分,而从判别器那拿高分是生成器实际上的唯一目的。

    3.3 StarGAN(StarGAN: Unified Generative Adversarial Networks for Multi-Domain Image-to-Image Translation)

    仔细思考一下,上面的CycleGAN其实是一个单模型单风格的风格迁移方式PSPM),甚至一个模型中就需要四个网络结构(两个生成器,两个判别器)。有时候我们可能希望图像能在n个风格下互转,比如春夏秋冬四季风格,安装CycleGAN的思想,我们就需要24个网络结构(12个生成器+12判别器)如下图:在这里插入图片描述
    很明显这需要训练的生成器太多了,那可不可以像第二章Neurol Style Transfer提到的那样实现单模型多风格的风格迁移呢(MSPM)?于是StarGAN被提出来了。

    StarGAN 在设计的时候就希望只用一个生成器去实现所有 domain 之间的互转。
    对于判别器,它的输入是一张图片,它需要去鉴别出这张图片是真实图片还是生成器产生的图片,同时它还需要分辨出这张输入的图片来自哪种风格的。
    对于生成器,它有两个输入,分别是目标风格和输入图片,它的输出是一张符合目标风格的图片。

    首先风格的定义在 StarGAN 中是用一个 vector 表示的,相当于一种label用来区分不同风格。这与CycleGAN不同,CycleGAN压根就没有这个东东,因为不需要,具体解释就是CycleGAN是单风格转变,类比画家,CycleGAN画家只会画一种冬天风格的画,所以只需要告诉它“你去画画吧” , 它就可以开始工作了。但是,当这个画家既会画冬天也会画春天时,如果还只告诉他“你去画画吧”。它就傻了…这时候必须指明画冬天还是画春天,即,需要给它一个风格信号。举个例子:
    在这里插入图片描述
    那如果一个风格表示为 00101 00000 10,就意味着这是一个 CelebA 的 label,并且
    这个风格是棕发年轻女性,如果希望要转移到的风格是黑发年轻男性,那么训练
    过程如下图所示:在这里插入图片描述
    首先原始图片被丢入这个生成器中,目标风格是 10011(黑发年轻男性),然后生成器就会产生一个黑发年轻男性的图片,接着这张图片又会再次被丢进这个生成器中,但是目标风格改成了 00101(棕发年轻女性),然后生成器就会产生一个棕发年轻女性的图片,StarGAN 希望这张图片和原始的输入的棕发年轻女性的图片尽可能相似,此处用到的loss 就是 CycleGAN 当中的 loss。另外一方面,第一次生成器产生的黑发年轻男性图会传给判别器,判别器一方面要判断这张图的真假,另外一方面还要判断出这张图的风格,如果判断结果是 10011 的话就证明这张图的效果是好的。
    综上就是 StarGAN 的实现过程,它能实现多种风格之间的互转,并且模型比较小巧且高效

    四,自动驾驶数据集的扩充(外传)

    一般来说,晴天的数据集是比较好采集的,但是极端天气下的数据是稀有的,例如暴雪,暴雨,风暴,雷暴等等。这时,可以通过风格迁移来扩充稀有数据集。个人认为,综上述方法,基于神经网络的PSPM快速离线建模方法和基于GAN的CycleGAN是首先值得尝试的方法(验证ing…)


    比较懒。。文章中提到的论文就列链接了,名字都比较全,感兴趣的小伙伴自己查就好。另外,附一些链接,都是一些写的不错了关于图像风格迁移的文章
    https://www.sohu.com/a/260408837_166433
    https://zhuanlan.zhihu.com/p/40322927
    https://zhuanlan.zhihu.com/p/57564626
    https://mp.weixin.qq.com/s/iV-OXiKF1jgAhSmX4QUIXw
    https://zhuanlan.zhihu.com/p/26332365

    展开全文
  • 图像风格迁移代码.zip

    2020-11-03 16:45:22
    图像风格迁移源代码,jupyter文件有注释,附带VGG16。基于keras进行了实现。 程序中使用到了VGG16的预训练模型vgg16_weights_tf_dim_ordering_tf_kernels_notop.h5(已放入zip包),第一次调用会自动下载,如果下载...
  • 图像风格迁移等。尤其是第一个topic,这是一个新颖且十分有意思的课题,涉及到人物图像建模、人物匹配、多源数据融合、相关数据集构建等核心想法与技术,期待上述资源能为相关研究人员打开思路,在该领域上持续进行...
  • 图像风格迁移-数据集

    2021-03-05 06:58:34
    暂无描述 GBDT原理.md
  • 基于VGG19的图像风格迁移,如果没有vgg-19文件 运行utils代码是会下载。 在styles文件夹中选择更改要迁移的图,包含了风格图片,内容图片替换成自己要进行操作的图片即可。是可以直接运行跑通的。有疑问的话可以留言...
  • 针对该问题,不同于基于特征或公共子空间的域适应方法,提出一种基于图像风格迁移的解决方法。具体地,基于CycleGAN网络改进得到Face-CycleGAN,在保持身份属性的前提下,对现有带标签数据进行风格迁移,使其在背景...
  • 使用VGG19迁移学习实现图像风格迁移 这是一个使用预训练的VGG19网络完成图片风格迁移的项目,使用的语言为python,框架为tensorflow。 给定一张风格图片A和内容图片B,能够生成具备A图片风格和B图片内容的图片C。此...
  • 实现图片风格迁移,让输出图像具备模板图像风格
  • 基于深度学习的图像风格迁移研究综述.pdf
  • 基于循环生成对抗网络的图像风格迁移.pdf
  • 图像风格迁移实战

    千次阅读 2022-03-31 15:42:18
    最近看了一些基于深度学习的Style Transfer, 也就是风格...一、图像风格迁移(Neural Style)简史 可以参考文章: 图像风格迁移(Neural Style)简史 二、实战 所谓风格迁移,其实就是提供一幅画(Reference tye imag

    最近看了一些基于深度学习的Style Transfer, 也就是风格迁移相关的paper,感觉挺有意思的。

    所谓风格迁移,其实就是提供一幅画(Reference style image),将任意一张照片转化成这个风格,并尽量保留原照的内容(Content)。之前比较火的修图软件Prisma就提供了这个功能

    一、图像风格迁移(Neural Style)简史

    可以参考文章:

    图像风格迁移(Neural Style)简史

    二、实战

    所谓风格迁移,其实就是提供一幅画(Reference tye image),将任意一张照片转化成这个风格,并尽量保留原照的内容(Conent。之前比较火的修图软件Prisma就提供了这个功能。我觉得这一说法可以改成风格迁移,将一张图的风格迁移到另一张图片上,也可以理解为生成问题,根据两种图片,生成第三种(风格)图片,具体看怎么理解怎么做吧《(不喜勿喷,纯个人观点)。比如下图,把一张图片的风格"迁移"到另一张图片上。论文地址:thtp slani oag odi1508.065762.pd然而,原始的风格迁移的速度是非常慢的。在GPU上,生成一张图片都需要10分钟左右,而如果只使用CPU而不使用GPU运行程序,甚至需要几个小时。这个时间还会驰着图片尺寸的增大而迅速增大。这其中的原因在于,在原始的风格迁移过程中,把生成图片的过程当做一个训练的过程。每生成一张图片,都相当于要训练一次模型,这中间可能会迭代几百几千次。如果你了解过一点机器学习的知识,就会知道,从头训练一个模型要比执行一个已经训练好的模型要责时太多。而这也正是原始的风格迁移速度缓慢的原因。(出处: https://zhuanlan.zhihu.com/p/24383274 )
     

    下面就开始我们的风格迁移:

    首先我们要引入一些工具包:

    这些工具包都可以通过pycharm直接安装或者通过pip install来安装。

    import tensorflow_hub as hub
    import tensorflow as tf
    import matplotlib.pyplot as plt
    import numpy as np

     接下来,我们要确定我们的风格迁移的步骤,首先是我们要拿到一个原始图片(记得修改一下路径):

    #原始图片
    img=plt.imread('./picture/xxx证件照.jpg')
    img=img/255.
    #plt.imshow(img)
    #plt.show()
    #print(img)

     这里随便拿一张网络证件照为例子:

     接下来需要拿到风格图片,同样我们可以在网络上下载:

    这里需要注意,我们的网络模型对输入的图像的初始大小(像素)有要求,所以使用了resize方法。

    #风格图片
    style_img=plt.imread('./picture/彩色.jpg')
    style_img=style_img/255.
    style_img = tf.image.resize(style_img, (256, 256))

     同样我们从网上下载几张图片:

     

     很出名的星空和向日葵。

    接下来就需要到最关键的风格迁移部分:

    #风格迁移
    
    hub_model = hub.load("C:\\Users\\Administrator\\Desktop\\imagenet")

     诶,很多小伙伴就要问了,怎么最难的部分最简单啊,就一行代码,哈哈哈哈,那是因为我们已经把需要的东西下载到本地了,就是很厉害的风格迁移的模型(别人已经做好了)。其实真实的代码应该是:

    hub_model = hub.load('https://tfhub.dev/google/magenta/arbitrary-image-stylization-v1-256/2')

     但是,很多小伙伴翻不了墙,加载不了模型,而且,即使你可以翻墙,程序运行时候加载模型真的会很慢很慢,所以这里推荐大家直接下载好模型,直接去用就行了!记得修改本地地址就行。

    下载的东西我已经准备好了,在这里,大家直接去下载就行了:

    tensorflowhub中的风格迁移模型-机器学习文档类资源-CSDN下载

    下面是官网的界面:

    而你需要的是复制下面那一行绿色的:

     好的,加载完模型,我们就可以训练,然后输出了:

    # 把输入规范一下,
    # 改变维度
    before_img_ = before_img[np.newaxis,:,:,:]
    style_img_ = style_img[np.newaxis,:,:,:]
    
    
    # 传入的是Tensor对象
    before_img_ = tf.convert_to_tensor(before_img_,dtype=tf.float32)
    style_img_ = tf.convert_to_tensor(style_img_,dtype=tf.float32)
    
    
    
    
    outputs = hub_model(before_img_,style_img_)
    
    # 输出有趣的图片[[[]]]
    # print(outputs[0][0])
    
    
    
    # 创建子图
    plt.subplot(1,3,1)
    plt.xlabel('before')
    plt.xticks([])
    plt.yticks([])
    plt.imshow(before_img)
    
    plt.subplot(1,3,2)
    plt.xlabel('style')
    plt.xticks([])
    plt.yticks([])
    plt.imshow(style_img)
    
    plt.subplot(1,3,3)
    plt.xlabel("after")
    plt.xticks([])
    plt.yticks([])
    plt.imshow(outputs[0][0])
    
    
    plt.show()
    
    
    
    # 图片的保存
    X = (outputs[0][0]) * 255
    print(X)
    # 将X转化为Tensor对象
    img = tf.cast(X,dtype=tf.uint8)
    # 编码回图片,二进制
    img = tf.image.encode_png(img)
    
    print(img)
    # 图片保存的路径
    save_path = './data/1.jpg'
    
    # 文件的保存
    with tf.io.gfile.GFile(save_path,'wb') as file:
        file.write(img.numpy())

     最后得到的图片:

    哈哈哈哈,是不是很好玩,还有点好看嘞! 

    喜欢的小伙伴点赞加关注哦! 

    展开全文
  • 基于深度学习的图像风格迁移算法研究与实现.pdf
  • 基于图像风格迁移的人脸识别域适应方法.pdf
  • 基于深度学习的图像风格迁移研究进展.pdf
  • 人工智能-项目实践-迁移学习-使用VGG19迁移学习实现图像风格迁移 使用VGG19迁移学习实现图像风格迁移 这是一个使用预训练的VGG19网络完成图片风格迁移的项目,使用的语言为python,框架为tensorflow。 给定一张...
  • 基于亮度与色度信息的深度学习图像风格迁移算法研究.pdf
  • 风格迁移中有两类图片:一类是`风格图片`,通常是一些艺术家的作品,往往具有明显的艺术家风格,包括色彩、线条、轮廓等;另一类是`内容图片`,这些图片往往来自现实世界,如个人摄影等。利用风格迁移能够将内容图片...


    风格迁移,又称为风格转换。只需要给定原始图片,并选择艺术家的风格图片,就能把原始图片转换成具有相应艺术家风格的图片。图像的风格迁移始于2015年Gatys的论文“Image Style Transfer Using Convolutional Neural Networks”,所做的工作就是由一张内容图片和一张风格图片进行融合之后,得到经风格渲染之后的合成图片。示例如下:

    图像分割迁移


    1. 风格迁移原理介绍

    风格迁移中有两类图片:一类是风格图片,通常是一些艺术家的作品,往往具有明显的艺术家风格,包括色彩、线条、轮廓等;另一类是内容图片,这些图片往往来自现实世界,如个人摄影等。利用风格迁移能够将内容图片转换成具有艺术家风格的图片。

    Gatys等人提出的方法被称为Neural Style,但是他们在实现上过于复杂。Justin Johnson等提出了一种快速实现风格迁移的算法,称为Fast Neural Style。当用Fast Neural Style训练好一个风格的模型之后,通常只需要GPU运行几秒,就能生成对应的风格迁移效果。

    Fast Neural Style 和Neural Style主要有以下两点区别
    (1)Fast Neural Style针对每一个风格图片训练一个模型,而后可以反复使用,进行快速风格迁移。Neural Style不需要专门训练模型,只需要从噪声中不断地调整图像的像素值,指导最后得到结构,速度较慢,需要十几分钟到几十分钟不等。
    (2)普遍认为Neural Style生成的图片的效果会比Fast Neural Style的效果好。

    这里主要介绍Fast Neural Style的实现。
    要产生效果逼真的风格迁移图片,有两个要求:

    1. 要生成的图片在内容、细节上尽可能地与输入的内容图片相似;
    2. 要生成的图片在风格上尽可能地与风格图片相似。

    相应地,定义两个损失content lossstyle loss,分别用来衡量上述两个指标。

    • content loss 比较常用的做法是采用逐像素计算差值,又称pixel-wise loss,追求生成的图片和原始图片逐像素的差值尽可能小。但是这种方法有诸多不合理之处,Justin提出了一种更好的计算content loss的方法,称为perceptual loss。不同于pixel-wise loss计算像素层面的差异,perceptual loss计算的是图像在更高层语义层次上的差异。使用预训练好的神经网络的高层作为图片的知觉特征,进而计算二者的差异值作为perceptual loss。

    在进行风格迁移时,并不要求生成图片的像素和原始图片中的每一个像素都一样,追求的是生成图片和原图片具有相同的特征。

    一般使用Gram矩阵来表示图像的风格特征。对于每一张图片,卷积层的输出形状为 C × H × W C\times H\times W C×H×W,C是卷积核的通道数,一般称为有C个卷积核,每个卷积核学习图像的不同特征。每一个卷积核输出的 H × W H\times W H×W代表这张图像的一个feature map,可以认为是一张特殊的图像——原始彩色图像可以看作RGB三个feature map拼接组成的特殊feature map。通过计算每个feature map之间的相似性,就可以得到图像的风格特征。对于一个 C × H × W C\times H\times W C×H×W的feature maps F F F,Gram Matrix的形状为 C × C C\times C C×C,其第 i , j i,j i,j个元素 G i , j G_{i,j} Gi,j的计算方式如下:
    G i , j = ∑ k F i k F j k G_{i,j}=\sum_{k}F_{ik}F_{jk} Gi,j=kFikFjk
    其中 F i k F_{ik} Fik代表第i个feature map的第k个像素点。
    需要注意的是:

    • Gram Matrix的计算采用了累加的形式,抛弃了空间信息。
    • Gram Matrix的结果与feature maps F的尺度无关,只与通道数有关。无论H,W的大小如何,最后Gram Matrix的形状都是C×C。
    • 对于一个 C × H × W C\times H\times W C×H×W的feature maps,可以通过调整形状和矩阵乘法快速计算它的Gram Matrix,即先将F调整为 C × ( H W ) C\times (HW) C×(HW)的二维矩阵,然后再计算 F ⋅ F T F\cdot F^T FFT,结果就是Gram Matrix。

    实践证明利用Gram Matrix表征图像的风格特征在风格迁移、纹理合成等任务中表现十分出众。总之:

    • 神经网络的高层输出可以作为图像的知觉特征描述
    • 神经网络的高层输出的Gram Matrix可以作为图像的风格特征描述。
    • 风格迁移的目标是使生成图片和原图片的知觉特征尽可能相似,并且和风格图片的风格特征尽可能地相似。

    2. Fast Neural Style网络结构

    Fast Neural Style专门涉及了一个网络用来进行风格迁移,输入原图片,网络将自动生成目标图片。如下图所示:
    Fast Neural Style的结构概览
    整个网络是由两部分组成:Image transformation networkLoss Netwrok

    • Image Transformation network是一个deep residual conv netwrok,用来将输入图像(content image)直接transform为带有style的图像;
    • 而loss network参数是fixed的,这里的loss network和 A Neural Algorithm of Artistic Style 中的网络结构一致,只是参数不做更新(neural style的weight也是常数,不同的是像素级loss和per loss的区别,neural style里面是更新像素,得到最后的合成后的照片),只用来做content loss 和style loss的计算,这个就是所谓的perceptual loss,

    一个是生成图片的网络,就是图片中前面那个,主要用来生成图片,其后面的是一个VGG网络,主要是提取特征,其实就是用这些特征计算损失的,我们训练的时候只训练前面这个网络,后面的使用基于ImageNet训练好的模型,直接做特征提取。

    如上图所示, x x x是输入图像,在风格迁移任务中 y c = x y_c=x yc=x y s y_s ys是风格图片,Image Transform Net f W f_W fW是我们涉及的风格迁移网络,针对输入的图像 x x x,能够返回一张新的图像 y ^ \hat{y} y^ y ^ \hat{y} y^在图像内容上与 y c y_c yc相似,但在风格上与 y s y_s ys相似。损失网络(loss network)不用训练,只是用来计算知觉特征和风格特征。损失网络采用ImageNet上预训练好的VGG-16
    VGG16
    VGG16的网络结构

    网络从左到右有5个卷积块,两个卷积块之间通过MaxPooling层区分,每个卷积块有2~3个卷积层,每一个卷积层后面都跟着一个ReLU激活曾。其中relu2_2表示第2个卷积块的第2个卷积层的激活层(ReLU)输出。

    Fast Neural Style的训练步骤如下:
    (1)输入一张图片x到 f W f_W fW中,得到结果 y ^ \hat{y} y^
    (2)将 y ^ \hat{y} y^ y c y_c yc(其实就是x)输入到loss network(VGG-16)中,计算它在relu3_3的输出,并计算它们之间的均方误差作为content loss。
    (3)将 y ^ \hat{y} y^ y s y_s ys(风格图片)输入到loss network中,计算它在relu1_2,relu2_2,relu3_3和relu4_3的输出,再计算它们的Gram Matrix的均方误差作为style loss。
    (4)两个损失相加,并反向传播。更新 f W f_W fW的参数,固定loss network不动。
    (5)跳回第一步,继续训练 f W f_W fW


    先了解全卷积网络的结构。输入是图片,输出也是图片,对这种网络一般实现为一个全部都是卷积层而没有全连接层的网络结构。对于卷积层,当输入feature map(或者图片)的尺寸为 C i n × H i n × W i n C_{in}\times H_{in}\times W_{in} Cin×Hin×Win,卷积核有 C o u t C_{out} Cout个,卷积核尺寸为 K K K,padding大小为 P P P、步长为 S S S时,输出的feature maps的形状为 C o u t × H o u t × W o u t C_{out}\times H_{out}\times W_{out} Cout×Hout×Wout,其中
    H o u t = f l o o r ( H i n + 2 ∗ P − K ) / S + 1 H_{out}=floor(H_{in}+2\ast P-K)/S+1 Hout=floor(Hin+2PK)/S+1
    W o u t = f l o o r ( W i n + 2 ∗ P − K ) / S + 1 W_{out}=floor(W_{in}+2\ast P-K)/S+1 Wout=floor(Win+2PK)/S+1
    如果输入图片的尺寸是3×256×256,第一层卷积的卷积核大小为3,padding为1,步长为2,通道数为128,那么输出的feature map形状,按照上述公式计算结果就是:
    H o u t = f l o o r ( 256 + 2 ∗ 1 − 3 ) / 2 + 1 = 128 H_{out} = floor(256+2\ast 1-3)/2+1=128 Hout=floor(256+213)/2+1=128
    W o u t = f l o o r ( 256 + 2 ∗ 1 − 3 ) / 2 + 1 = 128 W_{out} =floor(256+2\ast 1-3)/2+1=128 Wout=floor(256+213)/2+1=128
    所以最后的输出是 C o u t × H o u t × W o u t = 128 × 128 × 128 C_{out}\times H_{out}\times W_{out}=128\times 128\times 128 Cout×Hout×Wout=128×128×128,即尺度缩小一半,通道数增加。如果把步长由2改成1,则输出的形状就是128×256×256,即尺度不变,只是通道数增加。

    除了卷积层之外,还有一种叫做转置卷积层(Transposed Convolution),也有人称之为反卷积(DeConvolution),它可以简单地看成是卷积操作的逆运算。对于卷积操作,当步长大于1时,执行的是类似下采样的操作,而对于转置卷积,当步长大于1时,执行的是类似于上采样的操作。全卷积网络的一个重要优势在于对输入的尺寸没有要求,这样在进行风格迁移时就能够接受不同分辨率的图片。


    论文中提到的风格迁移结构全部由卷积层、Batch Normalization和激活层组成,不包含全连接层,这里我们不使用Batch Normalization,取而代之的是Instance Normalization。

    Instance Normalization和Batch Normalization的唯一区别就在于InstaneNorm只对每一个样本求均值和方差,而BatchNorm则会对一个batch中所有的样本求均值。
    例如对于一个B×C×H×W的tensor,在Batch Normalization中计算均值时,就会计算B×H×W个数的均值,共有C个均值,而Instance Normalization会计算H×W个数的均值,即共有B×C个均值。
    

    卷积神经网络风格迁移训练过程
    如上图所示,最左侧的两张图片(input image)一张是作为内容输入,一张是作为风格输入,分别经过VGG16的5个block,由浅及深可以看出,得到的特征图(feature map)的高和宽逐渐减小,但是深度是逐渐加大,Gatys为了更直观地让人看到每个block提取到的特征,所以做了一个trick,即特征重建,把提取到的特征做了一个可视化。但是可以看出,**对于内容图片特征的提取在很大程度上是保留了原图的信息,但是对于风格图片来说,基本上看不出原图的样貌,而是可以粗略的认为提取到了风格。这是为什么呢?**原来对于这两张图片做的特征提取处理是不一样的,在下一张图就可以看出。
    风格图片和内容图片的处理差异
    两侧的图片分别是风格图片,记为 a → \overrightarrow{a} a ,和内容图片 p → \overrightarrow{p} p ,同时还需要有第三张随机产生的噪声图片,需要不断地在噪声图片上迭代,直到得到结合了内容和风格的合成图片。内容图片 p → \overrightarrow{p} p 经过VGG16网络的5个block会在每层都得到feature map,记为 P l P^l Pl,即第l个block得到的特征,噪声图片 x → \overrightarrow{x} x 经过VGG16网络的5个block得到的特征图记为 F l F^l Fl
    对于内容损失,只取Conv4_2层的特征,计算内容图片特征和噪声图片特征之间的欧式距离,公式为:
    L c o n t e n t ( p → , x → , l ) = 1 2 ∑ i , j ( F i j l − P i j l ) 2 \mathcal{L}_{content}(\overrightarrow{p},\overrightarrow{x}, l)=\frac{1}{2}\sum_{i,j}(F_{ij}^l-P_{ij}^l)^2 Lcontent(p ,x ,l)=21i,j(FijlPijl)2

    对于风格损失,计算方法有些不同。根据上面已知,噪声图片 x → \overrightarrow{x} x 经过VGG16网络的5个block得到的特征记为 F l F^l Fl F l F^l Fl的gram矩阵记为 G l G^l Gl,风格图片 a → \overrightarrow{a} a 得到的特征图,再计算gram矩阵后得到的内容记为 A l A^l Al,之后计算 G l G^l Gl A l A^l Al之间的欧式距离,其中gram矩阵的公式为:
    KaTeX parse error: Can't use function '$' in math mode at position 9: G_{ij}^l$̲=\sum_k F_{ik}^…
    风格损失的公式为:
    E l = 1 4 N l 2 M l 2 ∑ i , j ( G i j l − A i j l ) 2 E_l=\frac{1}{4N_l^2M_l^2}\sum_{i,j}(G_{ij}^l-A_{ij}^l)^2 El=4Nl2Ml21i,j(GijlAijl)2
    公式之前的系数是标准化操作,即除以面积的平方。
    需要注意的是,在计算风格损失时,5个block提取的特征都用来计算了,而计算内容损失时,实际上只用了第四个block提取的特征。这是因为每个block提取到的风格特征都是不一样的,都参与计算可以增加风格的多样性,而内容图片每个block提取到的特征相差不大,所以只取一个就好。
    总损失即为内容损失和风格损失的线性和,改变α和β的比重可以调整内容和风格的占比。
    L t o t a l ( p → , a → , x → ) = α L c o n t e n t ( p → , x → ) + β L s t y l e ( a → , x → ) \mathcal{L}_{total}(\overrightarrow{p}, \overrightarrow{a}, \overrightarrow{x})=\alpha \mathcal{L}_{content}(\overrightarrow{p}, \overrightarrow{x})+\beta \mathcal{L}_{style}(\overrightarrow{a}, \overrightarrow{x}) Ltotal(p ,a ,x )=αLcontent(p ,x )+βLstyle(a ,x )

    代码中还使用了一个trick,总loss的计算还会加上一个total variation loss用来降噪,让合成的图片看起来更加平滑。

    最后需要注意的是,Gatys计算出的total loss是对噪声图片 x → \overrightarrow{x} x 求偏导,而Johnson计算出的loss是对自定义网络的权重w求偏导。

    3. 用PyTorch实现风格迁移

    数据集下载地址:https://pjreddie.com/projects/coco-mirror/
    COCO数据集下载地址

    3.1 首先看看如何使用预训练的VGG。

    class Vgg16(nn.Module):
        def __init__(self, requires_grad=False):
            super(Vgg16, self).__init__()
            vgg_pretrained_ft = vgg16(pretrained=False)
            vgg_pretrained_ft.load_state_dict(torch.load("vgg16-397923af.pth"))
            vgg_pretrained_features = nn.Sequential(*list(vgg_pretrained_ft.features.children()))
            self.slice1 = nn.Sequential()
            self.slice2 = nn.Sequential()
            self.slice3 = nn.Sequential()
            self.slice4 = nn.Sequential()
    
            for x in range(4):
                self.slice1.add_module(str(x), vgg_pretrained_features[x])
            for x in range(4, 9):
                self.slice2.add_module(str(x), vgg_pretrained_features[x])
            for x in range(9, 16):
                self.slice3.add_module(str(x), vgg_pretrained_features[x])
            for x in range(16, 23):
                self.slice4.add_module(str(x), vgg_pretrained_features[x])
    
            if not requires_grad:
                for param in self.parameters():
                    param.requires_grad = False
    
        def forward(self, X):
            h = self.slice1(X)
            h_relu1_2 = h
            h = self.slice2(h)
            h_relu2_2 = h
            h = self.slice3(h)
            h_relu3_3 = h
            h = self.slice4(h)
            h_relu4_3 = h
            vgg_outputs = namedtuple('VggOutputs', ['relu1_2', 'relu2_2', 'relu3_3', 'relu4_3'])
            result = vgg_outputs(h_relu1_2, h_relu2_2, h_relu3_3, h_relu4_3)
            return result
    

    在风格迁移网络中,需要获得中间层的输出,因此需要修改网络的前向传播过程,将相应层的输出保存下来。同时有很多层不需要,可以删除以节省内容占用。

    **在torchvision中, VGG的实现由两个nn.Sequential对象组成,一个是features,包含卷积、激活和MaxPool层,用来提取图片特征;另一个是classifier,包含全连接等,用来分类。**可以通过vgg.features直接获得对应的nn.Sequential对象。这样在前向传播时,当计算完指定层的输出后,就将结果保存于一个list中,然后再使用namedtuple进行名称绑定,这样可以通过output.relu1_2访问第一个元素,更为方便和直观。当然也可以利用layer.register_forward_hook的方式获取相应层的输出。

    3.2 接下来要实现风格迁移网络

    实现风格迁移网络参考了Pytorch的官方示例,其结构总结起来有以下几点:

    • 先下采样,后上采样,使计算量变小
    • 使用残差结构使网络变深
    • 边缘补齐的方式不再是传统的补0,而是采用一种被称为Reflection Pad的补齐策略:上下左右反射边缘的像素进行补齐。
    • 上采样不再使用传统的ConvTransposed2d,而是先用Upsample,然后用Conv2d,这样做避免Checkerboard Artifacts现象。
    • Batch Normalization全部改成Instance Normalization。
    • 网络中没有全连接层,线性操作是卷积,因此对输入和输出的尺寸没有要求。

    对于常出现的网络结构,可以实现为nn.Module对象,作为一个特殊的层。因此,将Conv,UpConv和残差块都实现为一个特殊的层:

    # -*- coding: utf-8 -*-#
    
    # ----------------------------------------------
    # Name:         transformer_net.py
    # Description:
    # Author:       PANG
    # Date:         2022/6/27
    # ----------------------------------------------
    class ConvLayer(nn.Module):
        """
        add ReflectionPad for Conv
        默认的卷积的padding操作是补0,这里使用边界反射填充
        """
    
        def __init__(self, in_channels, out_channels, kernel_size, stride):
            super(ConvLayer, self).__init__()
            reflection_padding = int(np.floor(kernel_size / 2))
            self.reflection_pad = nn.ReflectionPad2d(reflection_padding)
            self.conv2d = nn.Conv2d(in_channels, out_channels, kernel_size, stride)
    
        def forward(self, x):
            out = self.reflection_pad(x)
            out = self.conv2d(out)
            return out
    
    
    class UpsampleConvLayer(nn.Module):
        """
        默认的卷积的padding操作是补0,这里使用边界反射填充
        先上采样,然后做一个卷积(Conv2d),而不是采用ConvTranspose2d
        """
    
        def __init__(self, in_channels, out_channels, kernel_size, stride, upsample=None):
            super(UpsampleConvLayer, self).__init__()
            self.upsample = upsample
            reflection_padding = int(np.floor(kernel_size / 2))
            self.reflection_pad = nn.ReflectionPad2d(reflection_padding)
            self.conv2d = nn.Conv2d(in_channels, out_channels, kernel_size, stride)
    
        def forward(self, x):
            x_in = x
            if self.upsample:
                x_in = nn.functional.interpolate(x_in, mode='nearest', scale_factor=self.upsample)
            out = self.reflection_pad(x_in)
            out = self.conv2d(out)
            return out
    
    
    class ResidualBlock(nn.Module):
        """
        introduced in: https://arxiv.org/abs/1512.03385
        recommended architecture: http://torch.ch/blog/2016/02/04/resnets.html
        """
    
        def __init__(self, channels):
            super(ResidualBlock, self).__init__()
            self.conv1 = ConvLayer(channels, channels, kernel_size=3, stride=1)
            self.in1 = nn.InstanceNorm2d(channels, affine=True)
            self.conv2 = ConvLayer(channels, channels, kernel_size=3, stride=1)
            self.in2 = nn.InstanceNorm2d(channels, affine=True)
            self.relu = nn.ReLU()
    
        def forward(self, x):
            residual = x
            out = self.relu(self.in1(self.conv1(x)))
            out = self.in2(self.conv2(out))
            out = out + residual
            return out
    
    
    class TransformerNet(nn.Module):
        def __init__(self):
            super(TransformerNet, self).__init__()
            # 下卷积层
            self.initial_layers = torch.nn.Sequential(
                ConvLayer(3, 32, kernel_size=9, stride=1),
                nn.InstanceNorm2d(32, affine=True),
                nn.ReLU(True),
    
                ConvLayer(32, 64, kernel_size=3, stride=2),
                torch.nn.InstanceNorm2d(64, affine=True),
                torch.nn.ReLU(True),
    
                ConvLayer(64, 128, kernel_size=3, stride=2),
                torch.nn.InstanceNorm2d(128, affine=True),
                torch.nn.ReLU(True)
            )
    
            # Residual layers(残差层)
            self.res_layers = torch.nn.Sequential(
                ResidualBlock(128),
                ResidualBlock(128),
                ResidualBlock(128),
                ResidualBlock(128),
                ResidualBlock(128)
            )
    
            # Upsampling layers(上采样层)
            self.upsample_layers = torch.nn.Sequential(
                UpsampleConvLayer(128, 64, kernel_size=3, stride=1, upsample=2),
                torch.nn.InstanceNorm2d(64, affine=True),
                torch.nn.ReLU(True),
    
                UpsampleConvLayer(64, 32, kernel_size=3, stride=1, upsample=2),
                torch.nn.InstanceNorm2d(32, affine=True),
                torch.nn.ReLU(True),
    
                ConvLayer(32, 3, kernel_size=9, stride=1)
            )
    
        def forward(self, X):
            y = self.initial_layers(X)
            y = self.res_layers(y)
            y = self.upsample_layers(y)
            return y
    

    在TransformerNet中包含三个部分:下采样的卷积层,深度残差层和上采样的卷积层。实现时充分利用了nn.Sequential,避免在forward中重复写代码。

    搭建完网络之后,需要实现一些工具函数,例如gram_matrix。

    from PIL import Image
    
    IMAGENET_MEAN = [0.485, 0.456, 0.406]
    IMAGENET_STD = [0.229, 0.224, 0.225]
    
    
    def load_image(filename, size=None, scale=None):
        img = Image.open(filename).convert('RGB')
        if size is not None:
            img = img.resize((size, size), Image.ANTIALIAS)
        elif scale is not None:
            img = img.resize((int(img.size[0] / scale), int(img.size[1] / scale)), Image.ANTIALIAS)
        return img
    
    
    def save_image(filename, data):
        img = data.clone().clamp(0, 255).numpy()
        img = img.transpose(1, 2, 0).astype('uint8')
        img = Image.fromarray(img)
        img.save(filename)
    
    
    def gram_matrix(y):
        """
        输入形状b, c, h, w
        输出形状b, c, c
        :param y: image
        :return: gram matrix
        """
        (b, ch, h, w) = y.size()
        features = y.view(b, ch, w * h)
        features_t = features.transpose(1, 2)
        gram = features.bmm(features_t) / (ch * h * w)
        return gram
    
    
    def normal_batch(batch):
        """
        输入: b, ch, h, w 0~255, 是一个Variable
        输出: b, ch, h, w 大约-2~2, 是一个Variable
        :param batch:
        :return:
        """
        mean = batch.new_tensor(IMAGENET_MEAN).view(-1, 1, 1)
        std = batch.new_tensor(IMAGENET_STD).view(-1, 1, 1)
        batch = batch.div_(255.0)
        return (batch - mean) / std
    

    当将上述网络定义的工具和函数都实现之后,就开始训练网络了。

    def train(args):
        device = torch.device('cuda' if args.cuda else 'cpu')
        np.random.seed(args.seed)
        torch.manual_seed(args.seed)
    
        # 数据加载
        transform = transforms.Compose([
            transforms.Resize(args.image_size),
            transforms.CenterCrop(args.image_size),
            transforms.ToTensor(),
            transforms.Lambda(lambda x: x.mul(255))
        ])
        train_dataset = datasets.ImageFolder(args.dataset, transform)
        train_loader = DataLoader(train_dataset, batch_size=args.batch_size)
        # 转换网络
        transformer = TransformerNet().to(device)
        optimizer = Adam(transformer.parameters(), args.lr)
        mse_loss = torch.nn.MSELoss()
        # VGG16
        vgg = Vgg16(requires_grad=False).to(device)
        # 获取风格图片的数据
        style_transform = transforms.Compose([
            transforms.ToTensor(),
            transforms.Lambda(lambda x: x.mul(255))
        ])
        style = utils.load_image(args.style_image, size=args.style_size)
        style = style_transform(style)
        style = style.repeat(args.batch_size, 1, 1, 1).to(device)
    
        feature_style = vgg(utils.normal_batch(style))
        gram_style = [utils.gram_matrix(y) for y in feature_style]
    
        for e in range(args.epochs):
            # 训练
            agg_content_loss = 0
            agg_style_loss = 0
            count = 0
            transformer.train()
            for batch_id, (x, _) in enumerate(train_loader):
                n_batch = len(x)
                count += n_batch
                optimizer.zero_grad()
    
                x = x.to(device)
                y = transformer(x)
    
                y = utils.normal_batch(y)
                x = utils.normal_batch(x)
    
                features_y = vgg(y)
                features_x = vgg(x)
    
                # 计算content_loss, 只用到了relu2_2
                content_loss = args.content_weight * mse_loss(features_y.relu2_2, features_x.relu2_2)
    
                # style loss同时用到了4层输出
                style_loss = 0
                for ft_y, gm_s in zip(features_y, gram_style):
                    gm_y = utils.gram_matrix(ft_y)
                    style_loss += mse_loss(gm_y, gm_s[:n_batch, :, :])
                style_loss *= args.style_weight
    
                # 反向传播,更新梯度,这里只更新transformer的参数,不更新VGG16的
                total_loss = content_loss + style_loss
                total_loss.backward()
                optimizer.step()
    
                # 损失平滑
                agg_content_loss += content_loss.item()
                agg_style_loss += style_loss.item()
    
                if (batch_id + 1) % args.log_interval == 0:
                    mesg = "{}\tEpoch {}:\t[{}/{}]\tcontent: {:.6f}\tstyle: {:.6f}\ttotal: {:.6f}".format(
                        time.ctime(), e + 1, count, len(train_dataset),
                                      agg_content_loss / (batch_id + 1),
                                      agg_style_loss / (batch_id + 1),
                                      (agg_content_loss + agg_style_loss) / (batch_id + 1)
                    )
                    print(mesg)
                if args.checkpoint_model_dir is not None and (batch_id + 1) % args.checkpoint_interval == 0:
                    transformer.eval().cpu()
                    ckpt_model_filename = "ckpt_epoch_" + str(e) + "_batch_id_" + str(batch_id + 1) + ".pth"
                    ckpt_model_path = os.path.join(args.checkpoint_model_dir, ckpt_model_filename)
                    torch.save(transformer.state_dict(), ckpt_model_path)
                    transformer.to(device).train()
    
        # 保存模型
        transformer.eval().cpu()
        save_model_filename = "epoch_" + str(args.epochs) + "_" + str(time.ctime()).replace(' ', '_') + "_" + str(
            args.content_weight) + "_" + str(args.style_weight) + ".model"
        save_model_path = os.path.join(args.save_model_dir, save_model_filename)
        torch.save(transformer.state_dict(), save_model_path)
    
        print("\nDone, trained model saved at", save_model_path)
    

    这里训练用的图片是MS COCO 2014 training的数据集,大约包含8万张图片,13GB。
    训练完成之后,要加载预训练好的模型对指定的图片进行风格迁移的操作。代码如下:

    def stylize(args):
        device = torch.device('cuda' if args.cuda else 'cpu')
        # 图片处理
        content_image = utils.load_image(args.content_image, scale=args.content_scale)
        content_transform = transforms.Compose([
            transforms.ToTensor(),
            transforms.Lambda(lambda x: x.mul(255))
        ])
    
        content_image = content_transform(content_image)
        content_image = content_image.unsqueeze(0)
    
        if args.model.endswith('.onnx'):
            output = stylize_onnx(content_image, args)
        else:
            with torch.no_grad():
                # 模型
                style_model = TransformerNet()
                state_dict = torch.load(args.model)
                # remove saved deprecated running_* keys in InstanceNorm from the checkpoint
                for k in list(state_dict.keys()):
                    if re.search(r'in\d+\.running_(mean|var)$', k):
                        del state_dict[k]
                # 风格迁移与保存
                style_model.load_state_dict(state_dict).to(device).eval()
                if args.export_onnx:
                    assert args.export_onnx.endswith('.onnx'), "Export model file should end with .onnx"
                    output = torch.onnx._export(
                        style_model, content_image, args.export_onnx, opset_version=11
                    ).cpu()
                else:
                    output = style_model(content_image).cpu()
                    utils.save_image(args.output_image, output[0])
    

    参考资料

    [1] 深度学习-VGG16原理详解
    [2] 机器学习进阶笔记之二 | 深入理解Neural Style
    [3] NEURAL TRANSFER USING PYTORCH
    [4] PyTorch官方示例

    展开全文
  • ​不知道大家使用过一款叫...这个就是图像风格迁移,直观来看,就是将一副图片的“风格”转移到另一幅图片,而保持它的内容不变。一般我们将内容保持不变的图称为内容图,content image,把含有我们想要的风格的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 14,633
精华内容 5,853
关键字:

图像风格迁移