基于Tensorflow 2.0实现的图片风格迁移

**

基于Tensorflow 2.0实现的图片风格迁移

**

  • 摘 要 神经风格迁移是一种优化技术,用于将两个图像(一个内容图像和一个风格参考图像)混合在一起,使输出的噪声图像看起来像内容图像,
    但是使用了风格参考图像的风格。 这是通过优化输出图像以匹配内容图像的内容统计数据和风格参考图像的风格统计数据来实现的。
    这些统计数据可以使用卷积网络从图像中提取。
    本次论文采用Tensorflow2.0实现简单的图片风格迁移,采用官方VGG19训练模型,对内容、风格图片进行训练,通过增加训练次数与噪声图片生成次数达到减小图片内容损失以及风格损失的目的;实验表明通过增加训练次数与噪声图片生成次数可以明显的减小图片的内容以及风格损失。

  • 关键词:神经网络;风格迁移;卷积网络;VGG19;Tensorflow2.0

**

1 绪论

**

  • 所谓图像风格迁移,即给定内容图片A,风格图片B,能够生成一张具有A图片内容和B图片风格的图片C。比如说,我们可以使用梵高先生的名画《星夜》
  • 作为风格图片,来与其他图片生成具有《星夜》风格新图片。举两个简单的例子:利用梵高的名画——《星夜》作为风格图片,一个常见的动漫图片作为内容图片。分别计算出内容图片与风格图片的内容特征,再基于风格图片生成的的噪声图片风格特征与内容图片融合生成一张新的噪声图片即风格迁移后的图片。

基于Tensorflow 2.0实现的图片风格迁移_第1张图片

图1-1 风格图片以及迁移前后对比图

  • 针对原Demo对其训练次数以及训练后融合次数进行修改,将训练次数改为150次(原Demo为100次),融合次数改为30次(原Demo为20次)。随着训练次数和融合次数增加图片的内容损失以及风格损失有着明显的降低。下图为修改后对比图:

基于Tensorflow 2.0实现的图片风格迁移_第2张图片

图1-2 训练次数与融合次数不同对比图
**

2 项目

**

2.1 项目器材

  • 联想拯救者Y7000笔记本Windows10系统,16GB内存,主硬盘128GB,CPU(Intel@Core i5-8300H),显卡(NVIDIA GTX 1050 Ti 4GB)

2.2 项目环境

  • Anaconda3-5.1.0+PyCharm+Python3.6

2.3 项目内容

2.3.1 项目包含文件

  • 本项目一共包含四个Python文件以及一个TXT文件,其中model.py文件为模型初始化文件,即调用官方VGG19模型文件;settings.py文件为整个项目设置相关参数的重要文件,主要设置内容特征层loss加权系数、风格特征层loss加权系数、内容图片和风格图片输入路径、生成图片输出路径、内容和特征总loss加权系数、输出图片大小以及训练次数等;utils.py文件主要作用为对图片进行解码、归一化处理;train.py文件主要作用为计算风格和内容特征、计算风格和内容loss、加速训练以及生成噪声图片(即风格迁移后的图片);requirements.txt文件可通过修改文件中涉及相关Python包的版本,达到在不同电脑上也能正常运行的目的。

2.3.2 实验过程

  • 首先查看运行设备的Python环境以及项目涉及的相关附件包版本,通过修改requirements.txt文件,将涉及包版本修改与运行设备中相关版本。如下图所示:

基于Tensorflow 2.0实现的图片风格迁移_第3张图片

图2.3.2—1 修改requirements.txt文件

  • 其次通过settings.py文件设置图片的输入与输出位置和输出图片的大小以及相关训练epoch个数以及epoch训练次数。如下图所示:

基于Tensorflow 2.0实现的图片风格迁移_第4张图片

图2.3.2—2 修改相关参数

  • 运行train.py文件,等待模型训练,训练结束后可在输出文件夹中查看输出风格迁移后的图片。如下图:

基于Tensorflow 2.0实现的图片风格迁移_第5张图片

图2.3.2—3 风格迁移后的输出图片
**

3 项目所遇问题

**

3.1 版本兼容性问题

  • 由于python以及相关附件包版本不同,部分相关函数的调用方法有所改变。以至于在初步运行Demo时总是报错,其中报错最多的为找不到某一个附件包内所涉及的函数调用。通过在CSDN网站内大量查询解决办法,最终确定为版本不兼容,更新项目所涉及相关附件包后得以解决。
    其中在正式确定本次项目Demo前,在运行其它类似项目时遇到以下问题:

  • Tensorflow与keras版本有相关对应关系[1]。

  • 例如keras2.3.1对应tensorflow2.1、2.0.1.15.

  • Tensorflow与python版本有对应关系。[2]

  • 例如windows环境下tensorflow1.6.0对应python3.5-3.6

  • Pytorch和Torchvision以及python版本有严格对应关系[3]。

  • 例如torch1.4.0对应torchvision0.5.0,python2.7或者python3.5—3.8。

3.2 图片输出问题

  • 在原项目源码中发现对于风格迁移后的噪声图片输出大小做出了严格的限制(即将图片压缩后输出),在查询相关知识点后,发现可以通过python的图像处理库——PIL修改输出图片的大小[4];PIL提供了功能丰富的方法,比如格式转换、旋转、裁剪、改变尺寸、像素处理、图片合并等。源码将输出图片大小定为450*300,通过PIL库中Image读取内容噪声图片,再通过相关方法将输出噪声图片大小改为与内容图片大小。

基于Tensorflow 2.0实现的图片风格迁移_第6张图片

图3.2—1 修改噪声图片输出大小

3.3 epoch训练问题

  • 原项目中epoch训练个数为20个,训练次数为100次,从输出的噪声图片看,输出的噪声图片对风格、内容特征的损失比较少,但再将epoch训练个数增加至30个、训练次数增加至150次后,输出的噪声图片的内容、风格特征损失达到一个理想值,当继续增加epoch训练个数和训练次数时提升并不明显,最终确定epoch训练个数为30个训练次数为150次。

结 论

  • 原项目中[5]重点提到了以梵高名画《星空》作为风格图片进行相关的图像风格融合,且风格与内容的损失比较少。但该项目对于其它风格的图像迁移也有着不错的表现,例如赛博朋克风格、素描风格等,其中赛博朋克风格在建筑上的表现更佳,对于其它类型的图片表现不够理想,素描风格的整体表现都不错。

  • 该项目所训练的模型不止能对于绘画内的风格迁移,摄影类也能很好的进行风格迁移,但对于摄影类(即真实类)的风格、内容损失较大,能够明显的感受到有很大的突兀感,不够真实。其绘画(素描)类的风格迁移表现不够理想,突兀感很强烈,风格损失过大。

赛博朋克凤格:
基于Tensorflow 2.0实现的图片风格迁移_第7张图片

素描风格:
基于Tensorflow 2.0实现的图片风格迁移_第8张图片

  • 该项目还有着很大的优化空间,当风格、内容图像特点比较多时,项目训练时间也会更长。后续可以通过提升电脑性能以及优化源码完成对训练时间的缩短。

参考文献

  • [1]tensorflow和keras版本对应关系(blog.csdn.net/weixin_40109345/article/details/106730050?spm=1001.2014.3001.5502)
  • [2]各操作系统与Tensorflow版本配套关系(blog.csdn.net/u014797226/article/details/80632440?spm=1001.2014.3001.5502)
  • [3]Pytorch和Torchvision版本对应(blog.csdn.net/qq_40263477/article/details/106577790?spm=1001.2014.3001.5502)
  • [4]Python「PIL」:调整图片大小(blog.csdn.net/qq_41297934/article/details/105302393?spm=1001.2014.3001.5502)
  • [5]有趣的深度学习——使用TensorFlow 2.0实现图片神经风格迁移
    (https://blog.csdn.net/aaronjny/article/details/104879258)

附录

  • model.py
import typing
import tensorflow as tf
import settings
def get_vgg19_model(layers):
    """
    创建并初始化vgg19模型
    :return:
    """
    # 加载imagenet上预训练的vgg19
    vgg = tf.keras.applications.VGG19(include_top=False, weights='imagenet')
    # 提取需要被用到的vgg的层的output
    outputs = [vgg.get_layer(layer).output for layer in layers]
    # 使用outputs创建新的模型
    model = tf.keras.Model([vgg.input, ], outputs)
    # 锁死参数,不进行训练
    model.trainable = False
    return model
class NeuralStyleTransferModel(tf.keras.Model):
    def __init__(self, content_layers: typing.Dict[str, float] = settings.CONTENT_LAYERS,
                 style_layers: typing.Dict[str, float] = settings.STYLE_LAYERS):
        super(NeuralStyleTransferModel, self).__init__()
        # 内容特征层字典 Dict[层名,加权系数]
        self.content_layers = content_layers
        # 风格特征层
        self.style_layers = style_layers
        # 提取需要用到的所有vgg层
        layers = list(self.content_layers.keys()) + list(self.style_layers.keys())
        # 创建layer_name到output索引的映射
        self.outputs_index_map = dict(zip(layers, range(len(layers))))
        # 创建并初始化vgg网络
        self.vgg = get_vgg19_model(layers)
    def call(self, inputs, training=None, mask=None):        
        outputs = self.vgg(inputs)
        # 分离内容特征层和风格特征层的输出,方便后续计算 typing.List[outputs,加权系数]
        content_outputs = []
        for layer, factor in self.content_layers.items():
            content_outputs.append((outputs[self.outputs_index_map[layer]][0], factor))
        style_outputs = []
        for layer, factor in self.style_layers.items():
            style_outputs.append((outputs[self.outputs_index_map[layer]][0], factor))
        # 以字典的形式返回输出
        return {'content': content_outputs, 'style': style_outputs}
  • settings.py
from PIL import Image
# 内容特征层及loss加权系数
CONTENT_LAYERS = {'block4_conv2': 0.5, 'block5_conv2': 0.5}
# 风格特征层及loss加权系数
STYLE_LAYERS = {'block1_conv1': 0.2, 'block2_conv1': 0.2, 'block3_conv1': 0.2, 'block4_conv1': 0.2,
                'block5_conv1': 0.2}
# 内容图片路径
CONTENT_IMAGE_PATH = './images/test_0.jpg'
# 风格图片路径
STYLE_IMAGE_PATH = './images/style_2.jpeg'
# 生成图片的保存目录
OUTPUT_DIR = './output/'

# 内容loss总加权系数
CONTENT_LOSS_FACTOR = 1
# 风格loss总加权系数
STYLE_LOSS_FACTOR = 100

# 加载内容图片
img_pillow = Image.open(CONTENT_IMAGE_PATH)

# 图片宽度
#WIDTH = 450
WIDTH = img_pillow.width
# 图片高度
#HEIGHT = 300
HEIGHT = img_pillow.height
# 训练epoch数
EPOCHS = 1
# 每个epoch训练多少次
STEPS_PER_EPOCH = 100
# 学习率
LEARNING_RATE = 0.03

  • utils.py
import tensorflow as tf
import settings
# 我们准备使用经典网络在imagenet数据集上的与训练权重,所以归一化时也要使用imagenet的平均值和标准差
image_mean = tf.constant([0.485, 0.456, 0.406])
image_std = tf.constant([0.299, 0.224, 0.225])
def normalization(x):
    """
    对输入图片x进行归一化,返回归一化的值
    """
    return (x - image_mean) / image_std
def load_images(image_path, width=settings.WIDTH, height=settings.HEIGHT):
    """
    加载并处理图片
    :param image_path: 图片路径
    :param width: 图片宽度
    :param height: 图片长度
    :return: 一个张量
    """
    # 加载文件
    x = tf.io.read_file(image_path)
    # 解码图片
    x = tf.image.decode_jpeg(x, channels=3)
    # 修改图片大小
    x = tf.image.resize(x, [height, width])
    x = x / 255.
    # 归一化
    x = normalization(x)
    x = tf.reshape(x, [1, height, width, 3])
    # 返回结果
    return x


def save_image(image, filename):
    x = tf.reshape(image, image.shape[1:])
    x = x * image_std + image_mean
    x = x * 255.
    x = tf.cast(x, tf.int32)
    x = tf.clip_by_value(x, 0, 255)
    x = tf.cast(x, tf.uint8)
    x = tf.image.encode_jpeg(x)
    tf.io.write_file(filename, x)

  • train.py
import os
import numpy as np
from tqdm import tqdm
import tensorflow as tf
from model import NeuralStyleTransferModel
import settings
import utils
# 创建模型
model = NeuralStyleTransferModel()
# 加载内容图片
content_image = utils.load_images(settings.CONTENT_IMAGE_PATH)
# 风格图片
style_image = utils.load_images(settings.STYLE_IMAGE_PATH)
# 计算出目标内容图片的内容特征备用
target_content_features = model([content_image, ])['content']
# 计算目标风格图片的风格特征
target_style_features = model([style_image, ])['style']
M = settings.WIDTH * settings.HEIGHT
N = 3
def _compute_content_loss(noise_features, target_features):
    """
    计算指定层上两个特征之间的内容loss
    :param noise_features: 噪声图片在指定层的特征
    :param target_features: 内容图片在指定层的特征
    """
    content_loss = tf.reduce_sum(tf.square(noise_features - target_features))
    # 计算系数
    x = 2. * M * N
    content_loss = content_loss / x
    return content_loss
def compute_content_loss(noise_content_features):
    """
    计算并当前图片的内容loss
    :param noise_content_features: 噪声图片的内容特征
    """
    # 初始化内容损失
    content_losses = []
    # 加权计算内容损失
    for (noise_feature, factor), (target_feature, _) in zip(noise_content_features, target_content_features):
        layer_content_loss = _compute_content_loss(noise_feature, target_feature)
        content_losses.append(layer_content_loss * factor)
    return tf.reduce_sum(content_losses)
def gram_matrix(feature):
    """
    计算给定特征的格拉姆矩阵
    """
    # 先交换维度,把channel维度提到最前面
    x = tf.transpose(feature, perm=[2, 0, 1])
    # reshape,压缩成2d
    x = tf.reshape(x, (x.shape[0], -1))
    # 计算x和x的逆的乘积
    return x @ tf.transpose(x)


def _compute_style_loss(noise_feature, target_feature):
    """
    计算指定层上两个特征之间的风格loss
    :param noise_feature: 噪声图片在指定层的特征
    :param target_feature: 风格图片在指定层的特征
    """
    noise_gram_matrix = gram_matrix(noise_feature)
    style_gram_matrix = gram_matrix(target_feature)
    style_loss = tf.reduce_sum(tf.square(noise_gram_matrix - style_gram_matrix))
    # 计算系数
    x = 4. * (M ** 2) * (N ** 2)
    return style_loss / x


def compute_style_loss(noise_style_features):
    """
    计算并返回图片的风格loss
    :param noise_style_features: 噪声图片的风格特征
    """
    style_losses = []
    for (noise_feature, factor), (target_feature, _) in zip(noise_style_features, target_style_features):
        layer_style_loss = _compute_style_loss(noise_feature, target_feature)
        style_losses.append(layer_style_loss * factor)
    return tf.reduce_sum(style_losses)


def total_loss(noise_features):
    """
    计算总损失
    :param noise_features: 噪声图片特征数据
    """
    content_loss = compute_content_loss(noise_features['content'])
    style_loss = compute_style_loss(noise_features['style'])
    return content_loss * settings.CONTENT_LOSS_FACTOR + style_loss * settings.STYLE_LOSS_FACTOR


# 使用Adma优化器
optimizer = tf.keras.optimizers.Adam(settings.LEARNING_RATE)

# 基于内容图片随机生成一张噪声图片
noise_image = tf.Variable((content_image + np.random.uniform(-0.2, 0.2, (1, settings.HEIGHT, settings.WIDTH, 3))) / 2)


# 使用tf.function加速训练
@tf.function
def train_one_step():
    """
    一次迭代过程
    """
    # 求loss
    with tf.GradientTape() as tape:
        noise_outputs = model(noise_image)
        loss = total_loss(noise_outputs)
    # 求梯度
    grad = tape.gradient(loss, noise_image)
    # 梯度下降,更新噪声图片
    optimizer.apply_gradients([(grad, noise_image)])
    return loss


# 创建保存生成图片的文件夹
if not os.path.exists(settings.OUTPUT_DIR):
    os.mkdir(settings.OUTPUT_DIR)

# 共训练settings.EPOCHS个epochs
for epoch in range(settings.EPOCHS):
    # 使用tqdm提示训练进度
    with tqdm(total=settings.STEPS_PER_EPOCH, desc='Epoch {}/{}'.format(epoch + 1, settings.EPOCHS)) as pbar:
        # 每个epoch训练settings.STEPS_PER_EPOCH次
        for step in range(settings.STEPS_PER_EPOCH):
            _loss = train_one_step()
            pbar.set_postfix({'loss': '%.4f' % float(_loss)})
            pbar.update(1)
        # 每个epoch保存一次图片
        utils.save_image(noise_image, '{}/{}.jpg'.format(settings.OUTPUT_DIR, epoch + 1))

  • requirements.txt
tensorflow==2.0.0
numpy==1.19.5
tqdm==4.62.3

转载请注明来源

你可能感兴趣的:(tensorflow,深度学习,计算机视觉)