Image translation (风格转换)

Image Style Transfer

图像转换(Style transfer)一直是个让人感到新颖的主题,本文利用CNN(Convolutional Neural Networks)的方式进行图片的风格转换,并藉由调整参数来决定原图像与转换风格后的相似程度,细节将在本文陆续说明,这里分享自己实作上的过程与结果

简介

图像转换(Style transfer)最早可追溯到2015年Gatys 等人所发表的 A Neural Algorithm of Artistic Style,
他们所採用的方式是利用VGG(Visual Geometry Group)模型进行图像的特徵提取,关键在于提取出来的特徵分为content 和 style features,所谓 content是指一张图像的大致轮廓,而style是指图像中更细节的资讯(像是纹理、对比度、方向性等),因此只要将原图像的content成分取出,搭配欲产生的风格照片之style进行结合,透过loss函数的设计在这两着间达成平衡,便能合成出具有content和style成分的图象。

技术与原理

整个模型的主要核心在于如何体取出图像中的content和style的特徵,接着透过增加图像预处理(image preprocessing),以及尝试不同的模型架构、learning rate的选择、调整loss参数达到最佳的合成效果


(引用自参考资料[3])

图像预处理

这里尝试先将图像进行缩放和归一化,使用pytorch中的transform套件进行缩放,并转为tensor的形式,接着在image 的部分即是将原图像套用到transform定义好的缩放方式,并从原来的(512,512,3)在第0维上新增一个维度,形成(1,512,512,3)的四维向量,目的是为了方便后续进行特徵(features)的堆叠。另外这里的(512,512,3)分别代表图像的512x512的像素及RGB三颜色(通道数)。

def image_loader(path,is_cuda=False):    image=Image.open(path)    loader=transforms.Compose([transforms.Resize((512,512)),transforms.ToTensor()])    image=loader(image).unsqueeze(0)    return image.to(device,torch.float)

模型架构

首先从模型架构来说,我採用VGG19的预训练(pre-training)模型,直接利用前一大段的CNN架构来加快模型收敛时间。这里只保留VGG前30层[3],其中把有需要处理的层别其对应的索引值分别为'1','3','8','13','20','29'(relu1_1,relu1_2,relu2_2,relu3_2,relu4_2,relu5_2)来提取特徵,原因是希望特徵在线性激活后更譨购抓出图像中重要的部分,依序取出图像的特徵(features)并存在feature box中,直观上可以想像为了让机器学会辨识一张图像的特徵(例如:纹理、边缘等等资讯),在VGG模型中透过不同层滤波器(filter)所产生的不同特徵图,又称为feature map,而feature box就是收集这些feature map的过程。如下示意图


(引用自参考资料[4])

class VGG(nn.Module):    def __init__(self):        super(VGG,self).__init__()        self.layer_names= ['3','8','13','20']         #Since we need only the 5 layers in the model so we will be dropping all the rest layers from the features of the model        self.model=models.vgg19(pretrained=True).features[:29] #model will contain the first 29 layers            # x holds the input tensor(image) that will be feeded to each layer    def forward(self,x):        features=[]        # features={}        for layer_num,layer in enumerate(self.model):            #activation of the layer will stored in x            x=layer(x)            #appending the activation of the selected layers and return the feature array            if (str(layer_num) in self.layer_names):                features.append(x)            return features

Content features

由于VGG卷积层能够有效提取出各层特徵,并将图像转换为四维向量层层堆叠形成features map,接着进一步定义content loss为原图与content image的均方误差(MSE),而这里之所以採用MSE的理由是希望计算如下

其中的content image 会预先採用複製原图的方式,并迭代更新content loss。

实作代码如下:

def calc_content_loss(gen_feat,orig_feat):    content_l=torch.mean((gen_feat-orig_feat)**2) #*0.5    return content_l

Style features

在计算图像的style时,採用余弦相似性(Cosine similarity)来计算图像本身的"相似性",若将图像的各特徵向量化后,那么要评估任意向量间的相似度会变得非常有用。因此,当考虑任意两向量在向量空间中,可透过计算向量的内积来知道,当两向量成90度时,内积为零,意味此两向量彼此毫无相关。


(引用自参考资料[1])


(引用自参考资料[7])

将上述提及的cosine similarity推广到图像处理,相当于进一步计算图像的特徵相关性分布,而这个分布形成的二维方阵称作格拉姆矩阵(Gram matrix),细节可参考[6]
这里提到的gram matrix是指针对图像在不同通道、像素下(nw,nh,nc)进行相关性(correlation)的计算,也就是说从Gram matrix中的数值大小,能够看出合成图和原图在那些特徵的关係强弱,具体公式如下

对于style loss 的计算,同样计算和原图的均方误差(MSE)估算变异量。代码中使用torch.mm 将先前每一层所储存的feature map进行矩阵相乘运算

代码如下

def calc_style_loss(gen,style):    #Calculating the gram matrix for the style and the generated image    batch,channel,height,width=gen.shape    G=torch.mm(gen.view(channel,height*width),gen.view(channel,height*width).t())    A=torch.mm(style.view(channel,height*width),style.view(channel,height*width).t())    style_l=torch.mean((G-A)**2) #/(4*channel*(height*width)**2)    return style_l

Total Loss

为了让合成的图样产生最佳的效果,势必在content loss和style loss间须取得平衡,因此分别引入α和β作为决定合成图像中content 和style的成分多寡,在求解total loss 的最佳解过程採用梯度下降法(Gradient descent)搭配Adam优化器实现。

代码中首先初始化loss后,去进一步原图分别和content/style image的差异,最初由于

def calculate_loss(gen_features, orig_feautes, style_featues):    style_loss=content_loss=0    for gen,cont,style in zip(gen_features,orig_feautes,style_featues):    content_loss+=calc_content_loss(gen,cont)    style_loss+=calc_style_loss(gen,style)    total_loss=alpha*content_loss + beta*style_loss     return total_loss

训练与结果展示

整体来说,为求加速训练,而非重头去随机产生我们要的合成图,所以这里採用origin_img.clone().requires_grad_(True)将原图直接複製一份作为最终预产生合成图的"範本",而optimizer决定了"图像"本身的训练,而非"模型",过程中不断修正调整图像的loss达到收敛,找到最佳的α和β的组合。

gen_img=origin_img.clone().requires_grad_(True)    optimizer=optim.Adam([gen_img],lr=opt.lr)    epoch=200    for e in range (epoch):        gen_features=model(gen_img)         orig_features=model(origin_img)        style_features=model(style_img)         total_loss=calculate_loss(gen_features, orig_features, style_features)        #optimize the pixel values of the generated image and back-propagate the loss        optimizer.zero_grad()        total_loss.backward()        optimizer.step() # update gen_img parameters

经过迭代更新200次后,其实合成出来的图象已经达到不错的效果,由于α和β的权重来决定原图偏向style的程度,若要使合成图更多style的部分,除了调大调β同时也必须增价epoch的训练回合才能达到预期的效果,可观察到训练7000回后,图像主体的颜色及纹路出现大幅度改变,确实效果也更像梵谷星空图风格了

image

另外,针对不同风格k效果在下图展示了epoch=7000设置α/β=0.01下产生的风格图

image

最后,完整代码可参考操考资料[5],欢迎互相交流,不吝指教

参考资料

Neural Networks IntuitionsDeep Residual Learning for Image RecognitionA Neural Algorithm of Artistic StyleDeep Learning & Art: Neural Style Transfermy_githubGram matrix格拉姆矩阵(Gram matrix)详细解读

关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章