彻底学会并实现有趣的图像风格迁移,一篇文章就够了

学会并实现有趣的图像风格迁移,一篇文章就够了

结果演示

先给大家展示一下成功后的结果:
在这里插入图片描述
在这里插入图片描述

准备工作

安装anaconda

下载地址:https://www.anaconda.com/download/

安装pytorch

conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/
conda config --set show_channel_urls yes
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/pytorch/
conda install pytorch torchvision cudatoolkit=10.1

下载VGG19数据集

因为之间用pytorch下载比较缓慢所以我们从下面找到VGG19模型的下载地址,把文件下载到C:\Users\你的用户名\.cache\torch\checkpoints\路径下。

Resnet:
model_urls = {
    'resnet18': 'https://download.pytorch.org/models/resnet18-5c106cde.pth',
    'resnet34': 'https://download.pytorch.org/models/resnet34-333f7ec4.pth',
    'resnet50': 'https://download.pytorch.org/models/resnet50-19c8e357.pth',
    'resnet101': 'https://download.pytorch.org/models/resnet101-5d3b4d8f.pth',
    'resnet152': 'https://download.pytorch.org/models/resnet152-b121ed2d.pth',
}

inception:
model_urls = {
    # Inception v3 ported from TensorFlow
    'inception_v3_google': 'https://download.pytorch.org/models/inception_v3_google-1a9a5a14.pth',
}

Densenet:

model_urls = {
    'densenet121': 'https://download.pytorch.org/models/densenet121-a639ec97.pth',
    'densenet169': 'https://download.pytorch.org/models/densenet169-b2777c0a.pth',
    'densenet201': 'https://download.pytorch.org/models/densenet201-c1103571.pth',
    'densenet161': 'https://download.pytorch.org/models/densenet161-8d451a50.pth',
}

Alexnet:
model_urls = {
    'alexnet': 'https://download.pytorch.org/models/alexnet-owt-4df8aa71.pth',
}

vggnet:
model_urls = {
    'vgg11': 'https://download.pytorch.org/models/vgg11-bbd30ac9.pth',
    'vgg13': 'https://download.pytorch.org/models/vgg13-c768596a.pth',
    'vgg16': 'https://download.pytorch.org/models/vgg16-397923af.pth',
    'vgg19': 'https://download.pytorch.org/models/vgg19-dcbb9e9d.pth',
    'vgg11_bn': 'https://download.pytorch.org/models/vgg11_bn-6002323d.pth',
    'vgg13_bn': 'https://download.pytorch.org/models/vgg13_bn-abd245e5.pth',
    'vgg16_bn': 'https://download.pytorch.org/models/vgg16_bn-6c64b313.pth',
    'vgg19_bn': 'https://download.pytorch.org/models/vgg19_bn-c79401a0.pth',
}

原理

论文推荐

有兴趣的可以看这些论文:
Very Deep Convolutional Networks for Large-Scale Image Recognition
A Neural Algorithm of Artistic Style
Demystifying Neural Style Transfer

VGG19

VGG19叫VGG19是因为它有19个层,其的结构对应于E列,conv3代表卷积核是3x3的,后面的数字代表卷积核的数目。从图中可以看到一共有5组卷积,下面我们用到的conv3_1简写,代表第三组的第一个卷积层。
在这里插入图片描述

内容与风格

要进行图像的风格迁移,首先便是找到什么是图像的内容,什么是图像的风格,还有就是,图像的内容和风格是否可以相互剥离。
下图中下方的图片‘conv1_1’ (a), ‘conv2_1’ (b), ‘conv3_1’ ( c), ‘conv4_1’ (d)和 ‘conv5_1’ (e) 分别表示VGG19网络中对应的卷积层基于内容进行重建得到图片。
下图中上面的图片‘conv1 1’ (a), ‘conv1 1’ and ‘conv2 1’ (b), ‘conv1 1’, ‘conv2 1’ and ‘conv3 1’ (c ),‘conv1 1’, ‘conv2 1’, ‘conv3 1’ and ‘conv4 1’ (d), ‘conv1 1’, ‘conv2 1’, ‘conv3 1’, ‘conv4 1’and ‘conv5 1’ (e) 分别表示VGG网络中对应的卷积层基于风格进行重建得到的图片。这将以递增的比例创建与给定图像样式匹配的图像,同时丢弃场景的整体布置信息。

网络中的高层根据对象及其在输入图像中的排列来捕获高级内容,但不限制重建的确切像素值(图中d,e)。相比之下,从较低层进行的重建仅可以再现原始图像的精确像素值(图中a,b,c)。我们可以看到在利用(a,b,c)层进行重建时可以几乎完美的恢复图片原来的样子。

层级选择

由于卷积层和池化层更能反映图像的特征,所以我们选取卷积层和池化层的数值来计算代价而不选取全连接层。我们使用的VGG19网络一共有16个卷积层和5个池化层,我们将从中挑选若干层来计算代价。

代价函数

代价函数分为以下两个部分
J c o n t e n t ( C , G ) J_{content}(C,G) Jcontent(C,G)第一部分被称作内容代价,这是一个关于内容图片和生成图片的函数,它是用来度量生成图片 G G G的内容与内容图片 C C C的内容有多相似。
J s t y l e ( S , G ) J_{style}(S,G) Jstyle(S,G)第二部分被称作风格代价,这是一个关于风格图片和生成图片的函数,它是用来度量生成图片 G G G的内容与风格图片 S S S的内容有多相似。
J ( G ) = α J c o n t e n t ( C , G ) + β J s t y l e ( S , G ) J(G)=\alpha J_{content}(C,G)+\beta J_{style}(S,G) J(G)=αJcontent(C,G)+βJstyle(S,G)
最后我们用两个超参数 α \alpha α β \beta β来来确定内容代价和风格代价,两者之间的权重用两个超参数来确定。一般情况下,为了突出我们的风格效果 α β = 1 × 1 0 − 3 或 者 1 × 1 0 − 4 \frac{\alpha}{\beta}=1\times 10^{-3}或者1\times 10^{-4} βα=1×1031×104
我们只要确定了代价函数 J ( G ) J(G) J(G),我们的目标就是最小化代价函数。因此就可以通过以下的方法,利用梯度下降的方式来更新我们的图片: G = G − ∂ J ( G ) ∂ G G=G-\frac{\partial J(G)}{\partial G} G=GGJ(G)

内容代价函数

现在你需要衡量一个内容图片和一个生成图片他们在内容上的相似度,我们令这个 a [ l ] [ C ] a^{[l][C]} a[l][C] a [ l ] [ G ] a^{[l][G]} a[l][G](其中 a [ l ] [ C ] a^{[l][C]} a[l][C]代表 C C C图片的第 l l l层),代表这两个图片的指定层的激活函数值。如果这两个激活值大小接近,那么就意味着两个图片的内容相似。
J c o n t e n t ( C , G ) = λ ∣ ∣ a [ l ] [ C ] − a [ l ] [ G ] ∣ ∣ 2 J_{content}(C,G)=\lambda||a^{[l][C]}-a^{[l][G]}||^2 Jcontent(C,G)=λa[l][C]a[l][G]2
其中的 λ = 1 2 \lambda=\frac{1}{2} λ=21(论文里是这么写的),但是实际上由于我们可以调节 α 和 β \alpha和\beta αβ的值所以这个值不是特别重要。
按照论文中的方法我们选取 ‘conv1_1’ (a), ‘conv2_1’ (b), ‘conv3_1’ (c), ‘conv4_1’ (d) and ‘conv5_1’(e)这五层来计算内容代价。

Gram矩阵

A A T 就 是 A 的 G r a m 矩 阵 AA^T就是A的Gram矩阵 AATAGramGram矩阵实际上可看做是不同特征之间的偏心协方差矩阵(即没有减去均值的协方差矩阵),在每一个特征层中,每一个数字都来自于一个特定卷积核在特定位置的卷积,因此每个数字就代表一个特征的强度,而Gram矩阵的计算的实际上是两两特征之间的相关性,哪两个特征是同时出现的,哪两个是此消彼长的等等,同时,Gram矩阵的对角线元素,还体现了每个特征在图像中出现的量,因此,Gram矩阵有助于把握整个图像的大体风格。有了表示风格的Gram矩阵,要度量两个图像风格的差异,只需比较他们Gram矩阵的差异即可。

风格代价函数

为了获取输入图片的风格表示,我们使用某些相同层的不同信道的神经元的输出激活值(卷积后的矩阵)的相关性来代表图像的风格。提取神经元之间的相关性是生物学上合理的计算,例如,由初级视觉系统中的所谓复杂细胞实现。同样,我们可以构建与给的输入图像风格匹配的图像来产生我们要产生的图像的风格。

S S S图片的每个位置的风格函数如下,其中 G [ l ] [ S ] G^{[l][S]} G[l][S]是一个 n C [ l ] × n C [ l ] n_C^{[l]}\times n_C^{[l]} nC[l]×nC[l]的矩阵, a i j k [ l ] [ S ] a_{ijk}^{[l][S]} aijk[l][S]表示 i i i k k k k k k信道的数值。简单来说就是不同的信道之间的对应位置两两相乘。
G k k ′ [ l ] [ S ] = ∑ i = 1 n H [ l ] ∑ j = 1 n W [ l ] a i j k [ l ] [ S ] a i j k ′ [ l ] [ S ] G_{kk'}^{[l][S]}=\sum_{i=1}^{n_H^{[l]}}\sum_{j=1}^{n_W^{[l]}}a_{ijk}^{[l][S]}a_{ijk'}^{[l][S]} Gkk[l][S]=i=1nH[l]j=1nW[l]aijk[l][S]aijk[l][S] G G G图片的每个位置的风格函数对应如下:
G k k ′ [ l ] [ G ] = ∑ i = 1 n H [ l ] ∑ j = 1 n W [ l ] a i j k [ l ] [ G ] a i j k ′ [ l ] [ G ] G_{kk'}^{[l][G]}=\sum_{i=1}^{n_H^{[l]}}\sum_{j=1}^{n_W^{[l]}}a_{ijk}^{[l][G]}a_{ijk'}^{[l][G]} Gkk[l][G]=i=1nH[l]j=1nW[l]aijk[l][G]aijk[l][G]我们算出来的风格矩阵如下:
J s t y l e [ l ] ( S , G ) = 1 ( 2 n H [ l ] n W [ l ] n C [ l ] ) 2 ∑ k ∑ k ′ ( G k k ′ [ l ] [ S ] − G k k ′ [ l ] [ G ] ) 2 J_{style}^{[l]}(S,G)=\frac{1}{(2n_H^{[l]}n_W^{[l]}n_C^{[l]})^2}\sum_{k}\sum_{k'}(G_{kk'}^{[l][S]}-G_{kk'}^{[l][G]})^2 Jstyle[l](S,G)=(2nH[l]nW[l]nC[l])21kk(Gkk[l][S]Gkk[l][G])2
按照论文中的方法我们可以选取这些层来计算我们的图像风格。

  1. ‘conv1_1’
  2. ‘conv1_1’ and ‘conv2_1’
  3. ‘conv1_1’, ‘conv2_1’ and ‘conv3_1’
  4. ‘conv1_1’, ‘conv2_1’, ‘conv3_1’ and ‘conv4_1’
  5. ‘conv1_1’, ‘conv2_1’, ‘conv3_1’, ‘conv4_1’ and ‘conv5_1’

为什么Gram矩阵可以代表风格(高能预警)

如果你只是想实现代码的话那么可以直接看之后的代码实现部分,不会有任何影响。
如果你想细致的了解可以看我的这篇博客

代码实现

引入头文件

from __future__ import division
from torchvision import models  # 计算机视觉工具包
from torchvision import transforms
from PIL import Image
import argparse
import torch
import torchvision
import torch.nn as nn
import numpy as np
import cv2
%matplotlib inline

import matplotlib.pyplot as plt

选择GPU

# 将torch.Tensor分配到的设备的对象。torch.device包含一个设备类型(‘cpu’或‘cuda’)和可选的设备序号。
# 我们应该优先选取gpu因为cpu真的跑的很慢
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

读取数据

# Image.NEAREST :低质量
# Image.BILINEAR:双线性
# Image.BICUBIC :三次样条插值
# Image.ANTIALIAS:高质量
def load_image(image_path, transform=None, max_size=None, shape=None):
    image = Image.open(image_path)
    if max_size:
        scale = max_size / max(image.size)
        size= np.array(image.size) * scale
        image = image.resize(size.astype(int), Image.ANTIALIAS)  # 重新设定大小,设定ANTIALIAS,即抗锯齿
         
    if shape:
        image = image.resize(shape, Image.LANCZOS)  # 利用插值算法缩放图像
        
    if transform:
        image = transform(image).unsqueeze(0)  # 再看torch.unsqueeze()这个函数主要是对数据维度进行扩充。给指定位置加上维数为一的维度,比如原本有个三行的数据(3),在0的位置加了一维就变成一行三列(1,3)
        
    return image.to(device)


transform = transforms.Compose([  # 把多个步骤整合到一起
    transforms.ToTensor(),  # 把一个取值范围是[0,255]的PIL.Image 转换成 Tensor
    transforms.Normalize(mean=[0.485, 0.456, 0.406],  # ImageNet的标准操作,因为训练VGG19这个数据的时候就是这样处理的
                        std=[0.229, 0.224, 0.225])
]) # 来自ImageNet的mean和variance
base = 'C:/Users/46002/Desktop/'
content = load_image(base + "jpg/content3.jpg", transform, shape=[720,480])
style = load_image(base + "jpg/style2.jpg", transform, shape=[720, 480])

显示风格图片

unloader = transforms.ToPILImage()  # reconvert into PIL image
# 将张量的每个元素乘上255
# 将张量的数据类型有FloatTensor转化成Uint8
# 将张量转化成numpy的ndarray类型
# 对ndarray对象做permute (1, 2, 0)的操作
# 利用Image下的fromarray函数,将ndarray对象转化成PILImage形式
# 输出PILImage

plt.ion()

def imshow(tensor, title=None):
    # 从cpu –> gpu,使用data.cuda()即可。
    # 若从gpu –> cpu,则使用data.cpu()。
    image = tensor.cpu().clone()  # 复制
    image = image.squeeze(0)      # 去掉第0维
    image = unloader(image)  # 
    plt.imshow(image)
    if title is not None:
        plt.title(title)
    plt.pause(0.001) # 图片停留多久,如果你用的pycharm调的久一点


plt.figure()
imshow(style[0], title='Image')
# content.shape

在这里插入图片描述

处理目标图片

# 把两个照片合在一起,可以达到显示一部分风格图片内容的效果
def get_compose_img(ori_img, ano_img, alpha):
    return (ori_img + alpha * ano_img)/(1 + alpha)
# 为图片增加噪声
def get_noise_img(mode, ori_img, alpha):
    if(mode == 'rand'):
        noise_img = torch.rand(ori_img.shape)
    elif(mode == 'gaussian'):
        noise_img = torch.randn(ori_img.shape)
    return ori_img + alpha.cuda() * noise_img.cuda()

建立网络

根据我们从论文里学到的知识,一般是从conv1_1,conv2_1…,conv_5_1里选,对于的分别为0 5 10 19 28 层

from torchsummary import summary
summary(vgg, input_size=(3, 1080, 720))

报错的可以pip install torchsummary
在这里插入图片描述

class VGGNet(nn.Module):
    def __init__(self):
        super(VGGNet, self).__init__()
        self.select = ['0', '5', '10', '19', '28']  # 我们选出知道的层数
        self.vgg = models.vgg19(pretrained=True).features  # 读取模型
        
    def forward(self, x):
        features = []
        for name, layer in self.vgg._modules.items():
            x = layer(x)
            if name in self.select:
                features.append(x)
        return features


target = content.clone()  # 初始化和content一样的内容
target = get_noise_img('gaussian', target, torch.tensor(0.2))
# target = get_compose_img(target, style, 0.2)
target.requires_grad = True  # 是否求梯度:是(表明需要进行训练)
# target = torch.rand(content.shape).to(device).requires_grad_(True)
optimizer = torch.optim.Adam([target], lr=0.003, betas=[0.9, 0.999])
vgg = VGGNet().to(device).eval()  # eval() 函数用来执行一个字符串表达式,并返回表达式的值。

进行训练

大概需要训练30、40分钟。如果报错Cuda out of memary,可能是由于你的图片太大导致显存不够(这个没办法,只能调整图片大小了),也有可能是之前的程序占了内存没有释放(看这里解决)。
如果你看了gram矩阵的原理,那么你就可以知道,我们之前的方式只是用了一种特殊的多项式核函数核函数,我们常用的核函数还有高斯核函数等。我们都可以尝试一下,看看效果。

def gaussian(a, b, sigma):
    return torch.exp(-torch.norm(a - b) ** 2 / (2 * sigma ** 2))


def getGaussian(G, sigma):
    C = G.shape[0]
    t = torch.from_numpy(np.zeros((C, C)))
    for i in range(C):
        for j in range(C):
            t[i, j] = gaussian(G[i, :], G[j, :], sigma).item()
    return t

# 开始优化图片
total_step = 2000
style_weight = 10000.
options = np.array([[1, 1, 0, 0, 0], [1, 1, 1, 1, 1]])
sigma = 1
mode = 'Polynomial kernel'
# mode = 'Linear kernel'  # 这个没做,有兴趣的可以自己试一试
# mode = 'Gaussian kernel'  # 这个跑的特别慢
print(options[1, 1])
for step in range(total_step):
    # 在进行训练的时候对内存开销很大,一不小心内存满了就会炸,所以需要限制图片的大小
    # 对于显存也有要求可能户报错
    target_features = vgg(target)
    content_features = vgg(content)
    style_features = vgg(style)

    style_loss = 0  # 风格差距
    content_loss = 0  # 内容差距

    pos = -1
    for G, C, S in zip(target_features, content_features, style_features):
        pos = pos + 1
        if (options[0, pos]):
            content_loss += torch.mean((G - C) ** 2)
        m, c, h, w = G.size()
        if (options[1, pos]):
            G = G.view(c, h * w)  # 把原先tensor中的数据按照行优先的顺序排成c个一维的数据
            S = S.view(c, h * w)  #
            if (mode == 'Polynomial kernel'):
                G = torch.mm(G, G.t())
                S = torch.mm(S, S.t())
            elif (mode == 'Gaussian kernel'):
                G = getGaussian(G, sigma)
                S = getGaussian(S, sigma)
            # 计算gram matrix
            # torch.mean(input) 输出input 各个元素的的均值
            # 这里为什么要求均值, 是因为这样可以简化过程
            if (mode == 'Gaussian kernel'):
                style_loss += torch.mean((G - S) ** 2) / (c * h * w)
            elif (mode == 'Polynomial kernel'):
                style_loss += torch.mean((G - S) ** 2) / (c * h * w)  # 风格损失函数1
    loss = (1/(1 + style_weight)) * content_loss + (style_weight/(1 + style_weight)) * style_loss  # 求出最终的损失函数

    # 更新target
    optimizer.zero_grad()  # 梯度初始化为零
    loss.backward()  # 反向传播求梯度
    optimizer.step()  # 进行更新

    if step % 10 == 0:
        print("Step [{}/{}], Content Loss: {:.4f}, Style Loss: {:.4f}"
              .format(step, total_step, content_loss.item(), style_loss.item()), loss)
        if step % 50 == 0:  # 保存中间图片,看看训练多少次比较好看
            denorm = transforms.Normalize((-2.12, -2.04, -1.80), (4.37, 4.46, 4.44))  # 前面操作的反向操作
            img = target.clone().squeeze()
            img = denorm(img).clamp_(0, 1)  # clamp 与clamp_ ,有下划线的表示修改并付给自身,无下划线的表示需要返回处理后的值
            vutils.save_image(img, './src/target15' + 'step' + str(step) + '.jpg')

打印并保存最终图片

denorm = transforms.Normalize((-2.12, -2.04, -1.80), (4.37, 4.46, 4.44))  # 前面操作的反向操作
img = target.clone().squeeze()
img = denorm(img).clamp_(0, 1)  # clamp 与clamp_ ,有下划线的表示修改并付给自身,无下划线的表示需要返回处理后的值
plt.figure()
imshow(img, title='Target Image')
vutils.save_image(img, 'target15.jpg')
plt.show()
  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值