实现图片的风格转换
一、实验介绍
1.1 实验内容
上一节课我们介绍了经典的CNN模型 VGG
,以及图像风格迁移算法的基本原理。本节课我们将使用另外一个经典的模型 GoogLenet
来实现我们的项目(这是由于环境的限制,用 googlenet可以更快的完成我们的风格转换
),如果你完成了上节课的作业,那么你应该基本了解了 GoogleNet
的原理。在本节实验的算法展示中,我希望你可以将代码与上节课所讲解公式对照着看来帮助自己更好地理解,现在让我们开始本节实验,完成后你就可以亲自动手实现任意图片的风格转换。
1.2 实验知识点
- style-transfer
- 风格迁移算法代码详解
- 实现图片的任意风格转换
1.3 实验环境
- python 2.7、pip、numpy
- caffe
- Xfce终端
1.4 实验思路
我们可以先整理一下思路,从前面几节课的学习我们可以知道训练一个模型,我们需要准备:
- 一个训练好的神经网络
官方参考的是Vgg16、Vgg19、Caffenet、Googlenet四类
- 一张风格图像
style image
,用来计算它的风格representation - 一张内容图像
content image
,用来计算它的内容representation, - 一张噪声图像
result image
,用来迭代优化一般都是拿Content内容图来做,Caffe里面默认也是拿内容图来作为底图
- 一个loss函数,用来获得loss
- 一个求反传梯度的计算图,用来依据loss获得梯度修改图片
Caffe有明确的forward和backward,会帮我们自动计算
二、实验步骤
2.1 style-transfer
2.1.1 简介
style-tansfer是 "A Neural Algorithm of Artistic Style"[2]
的基于pyCaffe的实现。
caffe的官方完美的支持Python语言的兼容,提供的接口就是pyCaffe
style-tansfer提供了将一个输入图像的艺术风格转移到另一个输入图像的方法,即图像风格迁移。
在这个算法中,神经网络操作由Caffe处理,使用numpy和scipy执行损耗最小化和其他杂项矩阵运算, L-BFGS用于最小化操作(关于L-BFGS)。
2.1.2 获取源码
我们可以从Github上 An implementation of "A Neural Algorithm of Artistic Style"[3]
获取style-transfer源码
启动终端
$ cd /home/shiyanlou
$ wget http://labfile.oss.aliyuncs.com/courses/861/style-transfer-master.zip
$ mv style-transfer-master style-transfer
2.2 核心代码
在本实验中,所有的操作都在style.py
中完成。
首先让我们来看一下 /home/shiyanlou/style-transfer/style.py
中的核心算法:
2.2.1 矩阵计算
def _compute_reprs(net_in, net, layers_style, layers_content, gram_scale=1):
"""
首先正向传播计算出各层feature map,再将特征矩阵保存返回
"""
(repr_s, repr_c) = ({}, {})
net.blobs["data"].data[0] = net_in
net.forward()
for layer in set(layers_style)|set(layers_content):
F = net.blobs[layer].data[0].copy()
F.shape = (F.shape[0], -1)
repr_c[layer] = F
if layer in layers_style:
repr_s[layer] = sgemm(gram_scale, F, F.T)
return repr_s, repr_c
其中的网络权重的定义:
GOOGLENET_WEIGHTS = {"content": {"conv2/3x3": 2e-4,
"inception_3a/output": 1-2e-4},
"style": {"conv1/7x7_s2": 0.2,
"conv2/3x3": 0.2,
"inception_3a/output": 0.2,
"inception_4a/output": 0.2,
"inception_5a/output": 0.2}}
内容用了2层,风格用了5层。我们可以用key()来获取层名:
layers_style = weights["style"].keys()
layers_content = weights["content"].keys()
2.2.2 梯度处理
def _compute_style_grad(F, G, G_style, layer):
"""
完成风格梯度以及loss的计算
"""
(Fl, Gl) = (F[layer], G[layer])
c = Fl.shape[0]**-2 * Fl.shape[1]**-2
El = Gl - G_style[layer]
loss = c/4 * (El**2).sum()
grad = c * sgemm(1.0, El, Fl) * (Fl>0)
return loss, grad
"""
完成内容梯度以及loss的计算
"""
def _compute_content_grad(F, F_content, layer):
Fl = F[layer]
El = Fl - F_content[layer]
loss = (El**2).sum() / 2
grad = El * (Fl>0)
return loss, grad
2.2.3 噪声图像拟合
def _make_noise_input(self, init):
"""
制造最开始的噪声输入,但是我们默认使用 `content` 作噪声图像。我们就是在这个上面进行不断的拟合
"""
# 指定维度并在傅立叶域中创建网格
dims = tuple(self.net.blobs["data"].data.shape[2:]) + \
(self.net.blobs["data"].data.shape[1], )
grid = np.mgrid[0:dims[0], 0:dims[1]]
# 创建噪声的频率表示
Sf = (grid[0] - (dims[0]-1)/2.0) ** 2 + \
(grid[1] - (dims[1]-1)/2.0) ** 2
Sf[np.where(Sf == 0)] = 1
Sf = np.sqrt(Sf)
Sf = np.dstack((Sf**int(init),)*dims[2])
# 应用并使用 ifft 规范化
ifft_kernel = np.cos(2*np.pi*np.random.randn(*dims)) + \
1j*np.sin(2*np.pi*np.random.randn(*dims))
img_noise = np.abs(ifftn(Sf * ifft_kernel))
img_noise -= img_noise.min()
img_noise /= img_noise.max()
# 预处理
x0 = self.transformer.preprocess("data", img_noise)
return x0
2.2.4 风格转换
def transfer_style(self, img_style, img_content, length=512, ratio=1e5,
n_iter=512, init="-1", verbose=False, callback=None):
# 假设卷积层的输入是平方的
orig_dim = min(self.net.blobs["data"].shape[2:])
# 缩放图像
scale = max(length / float(max(img_style.shape[:2])),
orig_dim / float(min(img_style.shape[:2])))
img_style = rescale(img_style, STYLE_SCALE*scale)
scale = max(length / float(max(img_content.shape[:2])),
orig_dim / float(min(img_content.shape[:2])))
img_content = rescale(img_content, scale)
# 计算表示风格的特征矩阵
self._rescale_net(img_style)
layers = self.weights["style"].keys()
net_in = self.transformer.preprocess("data", img_style)
gram_scale = float(img_content.size)/img_style.size
G_style = _compute_reprs(net_in, self.net, layers, [],
gram_scale=1)[0]
# 计算表示内容的特征矩阵
self._rescale_net(img_content)
layers = self.weights["content"].keys()
net_in = self.transformer.preprocess("data", img_content)
F_content = _compute_reprs(net_in, self.net, [], layers)[1]
# 噪声图像输入
if isinstance(init, np.ndarray):
img0 = self.transformer.preprocess("data", init)
elif init == "content":
img0 = self.transformer.preprocess("data", img_content)
elif init == "mixed":
img0 = 0.95*self.transformer.preprocess("data", img_content) + \
0.05*self.transformer.preprocess("data", img_style)
else:
img0 = self._make_noise_input(init)
# 计算数据边界
data_min = -self.transformer.mean["data"][:,0,0]
data_max = data_min + self.transformer.raw_scale["data"]
data_bounds = [(data_min[0], data_max[0])]*(img0.size/3) + \
[(data_min[1], data_max[1])]*(img0.size/3) + \
[(data_min[2], data_max[2])]*(img0.size/3)
# 参数优化
# 使用L-BFGS-B算法可以最小化 loss function 而且空间效率较高。
grad_method = "L-BFGS-B"
reprs = (G_style, F_content)
minfn_args = {
"args": (self.net, self.weights, self.layers, reprs, ratio),
"method": grad_method, "jac": True, "bounds": data_bounds,
"options": {"maxcor": 8, "maxiter": n_iter, "disp": verbose}
}
# 迭代优化
self._callback = callback
minfn_args["callback"] = self.callback
if self.use_pbar and not verbose:
self._create_pbar(n_iter)
self.pbar.start()
res = minimize(style_optfn, img0.flatten(), **minfn_args).nit
self.pbar.finish()
else:
res = minimize(style_optfn, img0.flatten(), **minfn_args).nit
return res
2.3 实现
2.3.1 pycaffe环境布置
因为github上的代码是基于pycaffe的,所以需要配置环境
$ cd /home/shiyanlou/style-transfer
$ sudo gedit style.py
在 import caffe
之前加入python和pycaffe的环境变量
sys.path.append("/opt/caffe/python")
sys.path.append("/opt/caffe/python/caffe")
当然,待会转换图片时我们需要一个进度条的显示,这里我们使用 python progressbar
。
$ sudo pip install progressbar
2.3.2 下载模型
本实验用到的是googlenet,如果你完成了上节课的作业,那么你一定很清楚 googlenet 的结构了,其实和 VGG 一样,我们只需帮他们当作一个辅助工具,这是大牛们用巨大的数据集(ImageNet)帮我们训练好的可以准确提取图片特征的神经网络模型。下载完成之后需要将bvlc_googlenet.caffemodel放到指定路径下 /style-transfer/models/googlenet
。
$ cd models/googlenet
$ wget http://labfile.oss.aliyuncs.com/courses/861/bvlc_googlenet.caffemodel
2.3.3 准备训练
一个可以使用的训练好的模型文件夹有三样东西 style-transfer/models/googlenet
- deploy.prototxt
- ilsvrc_2012_mean.npy
- bvlc_googlenet.caffemodel
-
deploy.prototxt
网格配置文件你可以通过如下指令来观察网格的结构
#和第二节一样的操作 $ cd /opt/caffe/python $ sudo apt-get install python-pydot $ sudo apt-get insall graphviz $ python draw_net.py /home/shiyanlou/style-transfer/models/googlenet/deploy.prototxt ~/Desktop/googlenet.png $ cd ~/Desktop $ display googlenet.png
ilsvrc_2012_mean.npy
均值文件(caffe中使用的均值数据格式是binaryproto,如果我们要使用python接口,或者我们要进行特征可视化,可能就要用到python格式的均值文件了)。图片减去均值后,再进行训练和测试,会提高速度和精度。因此,一般在各种模型中都会有这个操作。那么这个均值怎么来的呢,实际上就是计算所有训练样本的平均值,计算出来后,保存为一个均值文件,在以后的测试中,就可以直接使用这个均值来相减,而不需要对测试图片重新计算。
bvlc_googlenet.caffemodel
训练好的神经网络模型。
2.3.4 参数解析
我们来看看 /home/shiyanlou/style-transfer/style.py
中关于参数的设定,具体代码在88行。
$ python style.py -s <style_image> -c <content_image> -m <model_name> -g 0
主要参数解析
- -s,风格图位置
- -c,内容图位置
- -m,模型位置
- -g,什么模式
-1为CPU,0为单个GPU,1为两个GPU
。
其他默认或不必须参数
parser.add_argument("-r", "--ratio", default="1e4", type=str, required=False, help="style-to-content ratio")
非必要,转化比率α/β
,有默认值
parser.add_argument("-n", "--num-iters", default=512, type=int, required=False, help="L-BFGS iterations")
非必要,有默认值,表示迭代次数
2.3.5 开始转换
$ cd /home/shiyanlou/style-transfer
$ python style.py -s images/style/starry_night.jpg -c images/content/nanjing.jpg -m googlenet -g -1 -n 20
我们使用的是梵高“星空”的风格,需要转换风格的图片是nanjing.jpg,使用googlenet模型及CPU,训练20次的结果,如果操作正确的话,训练的过程应该如下图所示。

这里的警告其实我们不用在意,这只是提醒我们转换后的图片与原始图片的相对坐标系发生了变换。
2.3.6 生成风格转换图片
我们可以在 /home/shiyanlou/style-transfer/images/style
中查看风格图;
在 /home/shiyanlou/style-transfer/images/content
中查看内容图。


在 /home/shiyanlou/style-transfer/outputs/
就可以看到我们训练完成得到的效果图,图片的名字包含我们使用的模型、转化比率、迭代次数。

我们可以通过更改 style.py
来设置图像输出的尺寸大小,例如你自己的照片图像大小是1024*500 ,更改输出length=1024,可以获得与原始图像一致的尺寸。不更改的话,程序中默认输出是512宽度,和输入原始图像一致的宽长比。
parser.add_argument("-l", "--length", default=1024, type=float, required=False, help="maximum image length")
def transfer_style(self, img_style, img_content, length=1024, ratio=1e4,
n_iter=512, init="-1", verbose=False, callback=None):
六、实验总结
至此,本门课程的学习结束,我们完成了一个很有趣的深度学习的项目。通过利用大牛训练的神经网络模型,还有我们自定义的loss函数,我们成功的让电脑学会了大师的绘画技巧,实现了以后是不是觉得其实并没有自己想象的这么难?
最近风格迁移的概念被炒的很火,Prisma也成了朋友圈的装逼利器,但是当我们真正理解了算法背后的原理,其实这也就是只纸老虎。最后,拿着自己画出来的图片去朋友圈装逼吧,记得屏蔽那些学过深度学习的人哦!
七、课后习题
- 请增加迭代次数和转换比率,看看转换风格后的图片效果是否会有巨大的提升?
- 将风格图片和内容图片换成自己选择的图片,看看效果如何?
八、参考文献
[3] An implementation of "A Neural Algorithm of Artistic Style" by L. Gatys, A. Ecker, and M. Bethge.