基于卷积网络的图像风格迁移原理及实现

本文详细介绍了图像风格迁移的概念,涉及卷积神经网络的各个组件(输入层、卷积层、激励层、池化层、全连接层),以及风格迁移的基本流程,包括损失函数(内容损失和风格损失)、格拉姆矩阵的应用。通过实例展示了如何使用CNN进行风格迁移并优化图像。
摘要由CSDN通过智能技术生成
1.1 图像风格迁移

        图像风格迁移是指把图像A的风格变成图像B的风格。例如把梵高的星空的风格用作于画蒙娜丽莎的微笑。这里利用卷积神经网络成功实现图像的风格迁移。下图是一个风格迁移的例子。

2 卷积神经网络简介
2.1 输入层

该层是对输入的图像数据进行预处理,使其满足后续处理的要求,包括:

       去均值:其目的是把图像数据在坐标轴上以原点为中心,如下图所示

        归一化:输入的内容图片和风格图片的取值范围可能不同,会产生误差。归一化就是把两者的取值范围都变成0-1,如下图所示。 

        其他处理:如改变输入的图像的数据类型,改变大小等,目的是方便所选择的方法在后续的处理 

        下面给出使用的部分预处理代码。

#图像预处理,主要是把图像变为合适的tensor
def preprocess(img, size=512):
    transform = T.Compose([
        T.Scale(size), #对图像进行等比缩放,最小边长度为size
        T.ToTensor(), #转化为torch.tensor
        #标准化,每个通道内的数减去通道内平均值,然后除以标准差
        # 中心化,减去平均值,使得样本的的中心在坐标原点
        T.Normalize(mean=SQUEEZENET_MEAN.tolist(),
                    std=SQUEEZENET_STD.tolist()), 
                                                                                       
        #将tensor升高一维度,如a.shape = (1, 2), 
        T.Lambda(lambda x: x[None]), a[None].shape = (1, 1, 2)
    ])
    return transform(img)

#反处理,跟上面相反
def deprocess(img): 
    transform = T.Compose([
        T.Lambda(lambda x: x[0]), #降维
        T.Normalize(mean=[0, 0, 0], std=[1.0 / s for s in SQUEEZENET_STD.tolist()]), #相当于乘标准差
        T.Normalize(mean=[-m for m in SQUEEZENET_MEAN.tolist()], std=[1, 1, 1]), #相当于加回原本的平均值
        T.Lambda(rescale), #
        T.ToPILImage(),
    ])
    return transform(img)

#归一化,把范围都变成0-1,减少各维度数据取值范围的差异而带来的干扰
def rescale(x): 
    low, high = x.min(), x.max()
    x_rescaled = (x - low) / (high - low) 
    return x_rescaled

#计算x, y的相对误差
def rel_error(x,y): 
    return np.max(np.abs(x - y) / (np.maximum(1e-8, np.abs(x) + np.abs(y))))


2.2 卷积层

该层是卷积神经网络最重要的一部分。通过卷积层可以提取图像的特征。

       为什么卷积可以提取图像特征?

       某一图像与对应特征的卷积进行点积会得到较大的值,而对于不能反应特征的卷积得到的点积会非常小,那么对应的卷积就能反应特征值。事先准备好的卷积,通过与原图像点积得到的结果,能提取、反应出该图像的某一特征。

       通过下面的表格简单解释一下。看上面两张表,左是卷积权重,很明显代表一条斜线,右是图像的二维表示,两者的点积是40。而下面的两张表,右的图像变成方向相反的斜线,两者点积是0。所以该卷积可以提取、反应向着右下角的斜线这一特征。

100010000
010001000
001000100
000100010

————————————————————————————————    

100000010
010000100
001001000
000110000

        这里使用的卷积神经网络包含了多个卷积层,每层的权重参数是固定不变的,每层只关注特定的特征,这些所有的卷积层就可以把图像的特征提取完全。一般来说层数越高提取的特征是抽象的轮廓、大小等,层数低的提取出局部的细节文理。卷积层也可以认为是一个神经网络,但它是已经训练好的,只是把它当作工具提取特征。

#用squeezeNet轻量级卷积神经网络提取特征,每经过一层网络就将结果存到features
#每层网络的输出提取的是不同层次的特征
def extract_features(x, cnn):
    
    features = []
    prev_feat = x
    for i, module in enumerate(cnn._modules.values()):
        # 使用上一层训练的结果作为这一层的输入
        next_feat = module(prev_feat)
        features.append(next_feat) 
        # 让下一层训练这一层得到的结果
        prev_feat = next_feat
    return features
2.3 激励层

        卷积层输出结果做非线性映射。一般都会采用ReLU,它的特点是收敛快,求梯度简单,但较脆弱。这里没有过多讲述的必要。

2.4 池化层

        池化层夹在连续的卷积层中间, 用于压缩数据和参数的量,减小过拟合。这里输入的是图像,那么池化层的最主要作用就是压缩图像。这里只是简单介绍一下池化层。

       池化层的具体作用:

        ⑴特征不变性,也就是我们在图像处理中经常提到的特征的尺度不变性。图像压缩时去掉的信息只是一些无关紧要的信息,而留下的信息则是具有尺度不变性的特征,是最能表达图像的特征。例如把一张狗的图片压缩后,我们仍然能看出这是狗。

        ⑵特征降维,我们知道一幅图像含有的信息是很大的,特征也很多,但是有些信息对于我们做图像任务时没有太多用途或者有重复,我们可以把这类冗余信息去除,把最重要的特征抽取出来。

        ⑶在一定程度上防止过拟合,更方便优化。

2.5 全连接层

        和传统神经网络的神经元结构几乎是一样的,由多层神经元构成,两层之间所有神经元都有权重连接。在这里进行前后传递、更新参数,从而完成任务目标。

3 更细节的介绍

        介绍完卷积神经网络,再简单的介绍一下风格迁移的基本流程。当训练开始时,根据内容图片和噪声,生成一张噪声图片。将噪声图片传送给网络,计算loss,再根据loss调整噪声图片。将调整后的图片发给网络,重新计算loss,再调整,再计算,直到达到指定迭代次数或者效果达到预期要求,这时,噪声图片已兼具内容图片的内容和风格图片的风格,进行保存即可。

3.1 格拉姆矩阵

        这里使用gram矩阵计算图像三个通道取得的特征值的内积。gram 矩阵的每个值可以说是代表三个通道的互相关程度。对于一个大小为F(m, n, c)的图像,先变换格式变成大小为F(m*n, c)的矩阵,那么它的格拉姆矩阵就是:Gram[i,j]=\sum F_{i,k}*F_{j,k},其中k是通道数,Gram矩阵在一定程度上可以体现图片的风格。

3.2 损失函数

       风格迁移的损失分为内容损失和风格损失,分别对应噪声图片与内容、风格图片在内容、风格上的差异。内容损失较简单,就是两图像对应点的L2范数乘上权重的和,如 ∑w[i, j] * (F[i, j] – G[i, j])2  。风格损失要用到卷积层的结果,卷积层已经提取了图片的各种特征,我们再对每层的卷积的结果求取格拉姆矩阵。多层的风格损失是单层风格损失的加权累加。那么风格损失就是两图像的格拉姆矩阵的L2范数乘上权重的和,如\sum w[i,j]*(Gram1[i,j]-Gram2[i,j])^{2}。最终的loss就是内容损失 + 风格损失。根据反向传播,调整参数使得loss最小,那么噪声图片就是得到的最终结果。这里我们额外增加了一个图像自身的光滑损失,用于判断噪声图像是否足够光滑,避免生成有割裂感的图像。

        下面是损失函数的相关代码。

#内容损失很简单,就是将生成图像和内容图像均表示为torch.tensor
#shape是一样的,都是(3, height, width),然后求L2范数
# 在内容方面的相似度,这个内容相似度一般是用于比较内容图片和结果图片
def content_loss(content_weight, content_current, content_target):
    
    N, C, H, W = content_current.shape
    L = content_weight * (content_target - content_current).pow(2).sum()
    
    return L
# 风格损失:比较风格图片和结果图片的风格
def style_loss(feats, style_layers, style_targets, style_weights):
    
    style_loss = 0

    #feats为生成图像所有卷积层的输出
    #风格图像的某些层的格拉姆矩阵为style_targets,层的索引位于style_layers
    #计算特定层(style_layers)生成图像与风格图像的Gram Matrix差值的L2范数并加权求和
    for i in range(len(style_layers)):
        l = style_layers[i]
        G = gram_matrix(feats[l])
        # 风格损失:两个格拉姆矩阵的L范数
        # 多层的风格损失是单层风格损失的加权累加。
        style_loss += style_weights[i] * ((G - style_targets[i]) ** 2).sum()
        
    return style_loss

4 成果展示

      下面是生成噪声图像并不断迭代更新的过程的代码。

def style_transfer(content_image, style_image, image_size, style_size, content_layer, content_weight,
                   style_layers, style_weights, tv_weight, init_random = False):
    
    
    # Extract features for the content image
    #读取内容图像并提取特征
    content_img = preprocess(PIL.Image.open(content_image), size=image_size)
    content_img_var = Variable(content_img.type(dtype))
    feats = extract_features(content_img_var, cnn)
    content_target = feats[content_layer].clone()

    # Extract features for the style image
    #读取风格图像并提取特征,把某些层(style_layers, 函数参数)的输出的格拉姆矩阵作为style_targets
    style_img = preprocess(PIL.Image.open(style_image), size=style_size)
    style_img_var = Variable(style_img.type(dtype))
    feats = extract_features(style_img_var, cnn)
    style_targets = []
    for idx in style_layers:
        style_targets.append(gram_matrix(feats[idx].clone()))

    # Initialize output image to content image or nois
    #初始化一张噪声图像
    if init_random:
        img = torch.Tensor(content_img.size()).uniform_(0, 1)
    else:
        img = content_img.clone().type(dtype)

    # We do want the gradient computed on our image!
    img_var = Variable(img, requires_grad=True)

    # Set up optimization hyperparameters
    #初始学习率与学习率衰减参数设定
    initial_lr = 3.0
    decayed_lr = 0.1
    decay_lr_at = 180

    # Note that we are optimizing the pixel values of the image by passing
    # in the img_var Torch variable, whose requires_grad flag is set to True
    #Adam优化器
    optimizer = torch.optim.Adam([img_var], lr=initial_lr)
    
    #显示内容图像和风格图像,这里的content_img已经经过预处理,为torch.tensor,所以需要deprocess还原为图像
    f, axarr = plt.subplots(1,2)
    axarr[0].axis('off')
    axarr[1].axis('off')
    axarr[0].set_title('Content Source Img.')
    axarr[1].set_title('Style Source Img.')
    axarr[0].imshow(deprocess(content_img.cpu()))
    axarr[1].imshow(deprocess(style_img.cpu()))
    plt.show()
    plt.figure()
    
    #迭代
    for t in range(200):
        #截断函数,将不在[-1.5, 1.5]范围的数限制在这个范围,小于-1.5就变为-1.5,超过1.5就变为1.5(我也不知道为什么要这么做)
        if t < 190:
            img.clamp_(-1.5, 1.5)
        optimizer.zero_grad()

        #提取目标图像的特征
        feats = extract_features(img_var, cnn)
        
        # Compute loss
        #计算各种损失并反向传播
        c_loss = content_loss(content_weight, feats[content_layer], content_target)
        s_loss = style_loss(feats, style_layers, style_targets, style_weights)
        t_loss = tv_loss(img_var, tv_weight) 
        loss = c_loss + s_loss + t_loss
        
        loss.backward()

        # Perform gradient descents on our image values
        if t == decay_lr_at:
            optimizer = torch.optim.Adam([img_var], lr=decayed_lr)
        optimizer.step()

        if t % 10 == 0:
            print('Iteration {}'.format(t))
            plt.axis('off')
            plt.imshow(deprocess(img.cpu()))
            plt.show()
    print('Iteration {}'.format(t))
    plt.axis('off')
    plt.imshow(deprocess(img.cpu()))
    plt.show()

        接下来展示一下代码结果,这里每50次展示一下生产的噪声图像。

        根据结果,大概进行两百次操作就可以较好地实现图片的风格迁移。

卷积神经网络参考文献:

机器学习算法之——卷积神经网络(CNN)原理讲解 - 知乎 (zhihu.com)

卷积神经网络:(二)风格迁移——原理部分_不同物理特性风格迁移-CSDN博客

格拉姆矩阵(Gram matrix)详细解读 - 知乎 (zhihu.com)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值