图像风格迁移技术详解与实践
1. 快速风格迁移
1.1 加载预训练模块
要将新风格应用到目标图像上,需要从
tfhub
加载预训练模块。使用以下语句:
import tensorflow_hub as hub
hub_module = hub.load('https://tfhub.dev/google/magenta/arbitrary-image-stylization-v1-256/2')
这里的模块名为
arbitrary-image-stylization-v1-256/2
。
1.2 执行风格迁移
模块加载完成后,将两个张量作为输入传递给模块进行转换:
import tensorflow as tf
outputs = hub_module(tf.constant(target_image), tf.constant(style_image))
stylized_image = outputs[0]
outputs
是一个形状为
(1, 300, 400, 3)
的张量,这是转换后图像的数据。注意,
300x400
是原始
1600x1200
图像的缩小版本。
1.3 显示输出图像
要显示图像,需要将张量转换为图像格式:
import numpy as np
import PIL.Image
tensor = stylized_image*256
tensor = np.array(tensor, dtype=np.uint8)
tensor = tensor[0]
PIL.Image.fromarray(tensor)
因为
stylized_image
中的数据范围是
0
到
1
,所以需要将图像数据乘以
256
。如果要显示缩小后的图像,可以使用以下代码:
import matplotlib.pyplot as plt
plt.imshow(tensor)
1.4 完整代码示例
import tensorflow as tf
import re
import urllib
import numpy as np
import matplotlib.pyplot as plt
import PIL.Image
import tensorflow_hub as hub
from tensorflow.keras.preprocessing.image import load_img, img_to_array
from matplotlib import gridspec
from IPython import display
from PIL import Image
def download_image_from_URL(imageURL):
imageName = re.search('[a-z0-9\-]+\.(jpe?g|png|gif|bmp|JPG)', imageURL, re.IGNORECASE)
imageName = imageName.group(0)
urllib.request.urlretrieve(imageURL, imageName)
imagePath = "./" + imageName
return imagePath
# 目标图像的 URL
target_url = "https://raw.githubusercontent.com/Apress/artificial-neural-networks-with-tensorflow-2/main/ch12/ferns.jpg"
target_path = download_image_from_URL(target_url)
# 风格图像的 URL
style_url = "https://raw.githubusercontent.com/Apress/artificial-neural-networks-with-tensorflow-2/main/ch12/on-the-road.jpg"
style_path = download_image_from_URL(style_url)
content = Image.open(target_path)
style = Image.open(style_path)
plt.figure(figsize=(10, 10))
plt.subplot(1, 2, 1)
plt.imshow(content)
plt.title('Content Image')
plt.subplot(1, 2, 2)
plt.imshow(style)
plt.title('Style Image')
plt.tight_layout()
plt.show()
def image_to_tensor_style(path_to_img):
img = tf.io.read_file(path_to_img)
img = tf.image.decode_image(img, channels=3, dtype=tf.float32)
img = tf.image.resize(img, [256,256])
img = img[tf.newaxis, :]
return img
def image_to_tensor_target(path_to_img, image_size):
img = tf.io.read_file(path_to_img)
img = tf.image.decode_image(img, channels=3, dtype=tf.float32)
img = tf.image.resize(img, [image_size,image_size], preserve_aspect_ratio=True)
img = img[tf.newaxis, :]
return img
output_image_size = 400
target_image = image_to_tensor_target(target_path,output_image_size)
style_image = image_to_tensor_style(style_path)
hub_module = hub.load('https://tfhub.dev/google/magenta/arbitrary-image-stylization-v1-256/2')
outputs = hub_module(tf.constant(target_image), tf.constant(style_image))
stylized_image = outputs[0]
tensor = stylized_image*256
tensor = np.array(tensor, dtype=np.uint8)
tensor = tensor[0]
PIL.Image.fromarray(tensor)
plt.imshow(tensor)
2. 深入理解风格迁移原理
2.1 风格迁移原理
风格迁移的原理是提取图像(通常是著名画作)的风格,并将其应用到你选择的图像内容上。因此,有两个输入图像:内容图像和风格图像,新生成的图像通常称为风格化图像。生成的图像包含与内容图像相同的内容,但具有与风格图像相似的风格。这显然不是通过简单地叠加图像来实现的,所以我们的程序必须能够分别区分给定图像的内容和风格。
2.2 VGG16 架构
Gatys 等人(2015)提出了风格迁移的核心思想,即预训练用于图像分类的卷积神经网络(CNN)知道如何编码图像的感知和语义信息。我们将使用 VGG16 来提取图像的特征,然后独立处理其内容和风格。
由于我们只对特征提取感兴趣,而不是图像分类,因此不需要 VGG 网络的全连接层或最终的 softmax 分类器。Keras 提供了预训练的 VGG16 模型,可以分离出各层。要移除最顶层的全连接层,在提取模型层时需要将
include_top
变量的值设置为
False
。
2.3 创建项目
创建一个新的 Colab 项目,并将其重命名为
CustomStyleTransfer
。安装以下两个包:
!pip install keras==2.3.1
!pip install tensorflow==2.1.0
注意,在编写本文时,该项目中使用的预训练 VGG16 模型只能在上述指定的 Keras 和 TensorFlow 版本下运行,尚不支持更新的版本。
导入所需的库:
import tensorflow as tf
import re
import urllib
from tensorflow.keras.preprocessing.image import load_img, img_to_array
from matplotlib import pyplot as plt
from IPython import display
from PIL import Image
import numpy as np
from tensorflow.keras.applications import vgg16
from tensorflow.keras import backend as K
from keras import backend as K
from scipy.optimize import fmin_l_bfgs_b
2.4 下载图像
编写一个下载函数并调用它来下载项目所需的两个图像:
def download_image_from_URL(imageURL):
imageName = re.search('[a-z0-9\-]+\.(jpe?g|png|gif|bmp|JPG)', imageURL, re.IGNORECASE)
imageName = imageName.group(0)
urllib.request.urlretrieve(imageURL, imageName)
imagePath = "./" + imageName
return imagePath
# 目标图像的 URL
target_url = "https://raw.githubusercontent.com/Apress/artificial-neural-networks-with-tensorflow-2/main/ch12/blank-sign.jpg"
target_path = download_image_from_URL(target_url)
# 风格图像的 URL
style_url = "https://raw.githubusercontent.com/Apress/artificial-neural-networks-with-tensorflow-2/main/ch12/road.jpg"
style_path = download_image_from_URL(style_url)
# 调整目标图像的大小
from tensorflow.keras.preprocessing.image import load_img
width, height = load_img(target_path).size
img_height = 400
img_width = int(width * img_height / height)
2.5 显示图像
使用以下代码显示两个图像:
content = Image.open(target_path)
style = Image.open(style_path)
plt.figure(figsize=(10, 10))
plt.subplot(1, 2, 1)
plt.imshow(content)
plt.title('Content Image')
plt.subplot(1, 2, 2)
plt.imshow(style)
plt.title('Style Image')
plt.tight_layout()
plt.show()
2.6 图像预处理
由于要使用 VGG16 模型提取图像特征,需要按照 VGG 训练过程处理图像数据。Keras 提供了
preprocess_input
函数,该函数接受编码一批图像的张量或 numpy 数组作为输入,并返回预处理后的 numpy 数组或类型为
float32
的
tf.tensor
。该方法将图像从 RGB 转换为 BGR,并对每个通道进行零中心化。
def preprocess_image(image_path):
img = load_img(image_path, target_size=(img_height, img_width))
img = img_to_array(img)
img = np.expand_dims(img, axis=0)
img = tf.keras.applications.vgg16.preprocess_input(img)
return img
def deprocess_image(x):
# Remove zero-center by mean pixel
x[:, :, 0] += 103.939
x[:, :, 1] += 116.779
x[:, :, 2] += 123.68
# 'BGR'->'RGB'
x = x[:, :, ::-1]
x = np.clip(x, 0, 255).astype('uint8')
return x
2.7 模型构建
将图像张量数据输入 VGG16 模型,提取特征图、内容和风格表示。模型将加载预训练的 ImageNet 权重。
target = K.constant(preprocess_image(target_path))
style = K.constant(preprocess_image(style_path))
# 用于存储生成图像的占位符
combination_image = K.placeholder((1, img_height, img_width, 3))
# 将三个图像组合成一个批次
input_tensor = K.concatenate([target, style, combination_image], axis=0)
# 构建 VGG16 网络
model = vgg16.VGG16(input_tensor=input_tensor, weights='imagenet', include_top=False)
# 查看模型摘要
model.summary()
2.8 损失计算
2.8.1 内容损失
内容损失表示随机生成的噪声图像(G)与内容图像(C)的相似程度。计算公式如下:
[L_{content}(P, F) = \frac{1}{2}\sum_{i,j}(F_{ij}^l - P_{ij}^l)^2]
代码实现:
def content_loss(base, combination):
return K.sum(K.square(combination - base))
2.8.2 风格损失
计算风格损失需要先计算 Gram 矩阵,Gram 矩阵用于找到不同通道之间的相关性,后续用于衡量风格。
def gram_matrix(x):
features = K.batch_flatten(K.permute_dimensions(x, (2, 0, 1)))
gram = K.dot(features, K.transpose(features))
return gram
def style_loss(style, combination):
S = gram_matrix(style)
C = gram_matrix(combination)
channels = 3
size = img_height * img_width
return K.sum(K.square(S - C)) / (4. * (channels ** 2) * (size ** 2))
2.8.3 总变差损失
为了使输出图像更平滑,定义总变差损失:
def total_variation_loss(x):
a = K.square(x[:, :img_height - 1, :img_width - 1, :] - x[:, 1:, :img_width - 1, :])
b = K.square(x[:, :img_height - 1, :img_width - 1, :] - x[:, :img_height - 1, 1:, :])
return K.sum(K.pow(a + b, 1.25))
2.8.4 计算总损失
首先选择 VGG16 的内容和风格层,然后定义一些权重变量,用于计算损失分量的加权平均值。
# Dict mapping layer names to activation tensors
outputs_dict = dict([(layer.name, layer.output) for layer in model.layers])
# 用于内容损失的层
content_layer = 'block5_conv2'
# 用于风格损失的层
style_layers = ['block1_conv1', 'block2_conv1', 'block3_conv1', 'block4_conv1', 'block5_conv1']
# 损失分量的权重
total_variation_weight = 1e-4
style_weight = 10.
content_weight = 0.025
# 定义损失
loss = K.variable(0.)
layer_features = outputs_dict[content_layer]
target_features = layer_features[0, :, :, :]
combination_features = layer_features[2, :, :, :]
loss = loss + content_weight * content_loss(target_features, combination_features)
for layer_name in style_layers:
layer_features = outputs_dict[layer_name]
style_reference_features = layer_features[1, :, :, :]
combination_features = layer_features[2, :, :, :]
sl = style_loss(style_reference_features, combination_features)
loss += (style_weight / len(style_layers)) * sl
loss += total_variation_weight * total_variation_loss(combination_image)
2.9 评估器类
定义一个
Evaluator
类,用于一次性计算损失和梯度:
grads = K.gradients(loss, combination_image)[0]
# Function to fetch the values of the current loss and the current gradients
fetch_loss_and_grads = K.function([combination_image], [loss, grads])
class Evaluator(object):
def __init__(self):
self.loss_value = None
self.grads_values = None
def loss(self, x):
assert self.loss_value is None
x = x.reshape((1, img_height, img_width, 3))
outs = fetch_loss_and_grads([x])
loss_value = outs[0]
grad_values = outs[1].flatten().astype('float64')
self.loss_value = loss_value
self.grad_values = grad_values
return self.loss_value
def grads(self, x):
assert self.loss_value is not None
grad_values = np.copy(self.grad_values)
self.loss_value = None
self.grad_values = None
return grad_values
evaluator = Evaluator()
2.10 生成输出图像
使用 L-BFGS 算法进行优化,从随机像素开始生成风格化图像:
iterations = 50
x = preprocess_image(target_path)
x = x.flatten()
for i in range(1, iterations):
x, min_val, info = fmin_l_bfgs_b(evaluator.loss, x, fprime=evaluator.grads, maxfun=10)
print('Iteration %0d, loss: %0.02f' % (i, min_val))
img = x.copy().reshape((img_height, img_width, 3))
img = deprocess_image(img)
2.11 显示图像
plt.figure(figsize=(50, 50))
plt.subplot(3,3,1)
plt.imshow(load_img(target_path, target_size=(img_height, img_width)))
plt.subplot(3,3,2)
plt.imshow(load_img(style_path, target_size=(img_height, img_width)))
plt.subplot(3,3,3)
plt.imshow(img)
plt.show()
2.12 完整代码
!pip install keras==2.3.1
!pip install tensorflow==2.1.0
import tensorflow as tf
import re
import urllib
from tensorflow.keras.preprocessing.image import load_img, img_to_array
from matplotlib import pyplot as plt
from IPython import display
from PIL import Image
import numpy as np
from tensorflow.keras.applications import vgg16
from tensorflow.keras import backend as K
from keras import backend as K
from scipy.optimize import fmin_l_bfgs_b
def download_image_from_URL(imageURL):
imageName = re.search('[a-z0-9\-]+\.(jpe?g|png|gif|bmp|JPG)', imageURL, re.IGNORECASE)
imageName = imageName.group(0)
urllib.request.urlretrieve(imageURL, imageName)
imagePath = "./" + imageName
return imagePath
# 目标图像的 URL
target_url = "https://raw.githubusercontent.com/Apress/artificial-neural-networks-with-tensorflow-2/main/ch12/blank-sign.jpg"
target_path = download_image_from_URL(target_url)
# 风格图像的 URL
style_url = "https://raw.githubusercontent.com/Apress/artificial-neural-networks-with-tensorflow-2/main/ch12/road.jpg"
style_path = download_image_from_URL(style_url)
# Dimensions for the generated picture.
width, height = load_img(target_path).size
img_height = 400
img_width = int(width * img_height / height)
content = Image.open(target_path)
style = Image.open(style_path)
plt.figure(figsize=(10, 10))
plt.subplot(1, 2, 1)
plt.imshow(content)
plt.title('Content Image')
plt.subplot(1, 2, 2)
plt.imshow(style)
plt.title('Style Image')
plt.tight_layout()
plt.show()
# Preprocess the data as per VGG16 requirements
def preprocess_image(image_path):
img = load_img(image_path, target_size=(img_height, img_width))
img = img_to_array(img)
img = np.expand_dims(img, axis=0)
img = tf.keras.applications.vgg16.preprocess_input(img)
return img
def deprocess_image(x):
# Remove zero-center by mean pixel
x[:, :, 0] += 103.939
x[:, :, 1] += 116.779
x[:, :, 2] += 123.68
# 'BGR'->'RGB'
x = x[:, :, ::-1]
x = np.clip(x, 0, 255).astype('uint8')
return x
target = K.constant(preprocess_image(target_path))
style = K.constant(preprocess_image(style_path))
# This placeholder will contain our generated image
combination_image = K.placeholder((1, img_height, img_width, 3))
# We combine the 3 images into a single batch
input_tensor = K.concatenate([target, style, combination_image], axis=0)
# Build the VGG16 network with our batch of 3 images as input.
model = vgg16.VGG16(input_tensor=input_tensor, weights='imagenet', include_top=False)
model.summary()
# compute content loss for the generated image
def content_loss(base, combination):
return K.sum(K.square(combination - base))
def gram_matrix(x):
features = K.batch_flatten(K.permute_dimensions(x, (2, 0, 1)))
gram = K.dot(features, K.transpose(features))
return gram
def style_loss(style, combination):
S = gram_matrix(style)
C = gram_matrix(combination)
channels = 3
size = img_height * img_width
return K.sum(K.square(S - C)) / (4. * (channels ** 2) * (size ** 2))
def total_variation_loss(x):
a = K.square(x[:, :img_height - 1, :img_width - 1, :] - x[:, 1:, :img_width - 1, :])
b = K.square(x[:, :img_height - 1, :img_width - 1, :] - x[:, :img_height - 1, 1:, :])
return K.sum(K.pow(a + b, 1.25))
# Dict mapping layer names to activation tensors
outputs_dict = dict([(layer.name, layer.output) for layer in model.layers])
# Name of layer used for content loss
content_layer = 'block5_conv2'
# Name of layers used for style loss;
style_layers = ['block1_conv1', 'block2_conv1', 'block3_conv1', 'block4_conv1', 'block5_conv1']
# Weights in the weighted average of the loss components
total_variation_weight = 1e-4
style_weight = 10.
content_weight = 0.025
# Define the loss by adding all components to a `loss` variable
loss = K.variable(0.)
layer_features = outputs_dict[content_layer]
target_features = layer_features[0, :, :, :]
combination_features = layer_features[2, :, :, :]
loss = loss + content_weight * content_loss(target_features, combination_features)
for layer_name in style_layers:
layer_features = outputs_dict[layer_name]
style_reference_features = layer_features[1, :, :, :]
combination_features = layer_features[2, :, :, :]
sl = style_loss(style_reference_features, combination_features)
loss += (style_weight / len(style_layers)) * sl
loss += total_variation_weight * total_variation_loss(combination_image)
grads = K.gradients(loss, combination_image)[0]
# Function to fetch the values of the current loss and the current gradients
fetch_loss_and_grads = K.function([combination_image], [loss, grads])
class Evaluator(object):
def __init__(self):
self.loss_value = None
self.grads_values = None
def loss(self, x):
assert self.loss_value is None
x = x.reshape((1, img_height, img_width, 3))
outs = fetch_loss_and_grads([x])
loss_value = outs[0]
grad_values = outs[1].flatten().astype('float64')
self.loss_value = loss_value
self.grad_values = grad_values
return self.loss_value
def grads(self, x):
assert self.loss_value is not None
grad_values = np.copy(self.grad_values)
self.loss_value = None
self.grad_values = None
return grad_values
evaluator = Evaluator()
iterations = 50
x = preprocess_image(target_path)
x = x.flatten()
for i in range(1, iterations):
x, min_val, info = fmin_l_bfgs_b(evaluator.loss, x, fprime=evaluator.grads, maxfun=10)
print('Iteration %0d, loss: %0.02f' % (i, min_val))
img = x.copy().reshape((img_height, img_width, 3))
img = deprocess_image(img)
plt.figure(figsize=(50, 50))
plt.subplot(3,3,1)
plt.imshow(load_img(target_path, target_size=(img_height, img_width)))
plt.subplot(3,3,2)
plt.imshow(load_img(style_path, target_size=(img_height, img_width)))
plt.subplot(3,3,3)
plt.imshow(img)
plt.show()
总结
本文介绍了两种图像风格迁移的方法:一种是使用预训练的
tfhub
模块进行快速风格迁移;另一种是深入理解风格迁移原理,使用 VGG16 模型手动实现风格迁移。通过详细的代码示例和步骤说明,帮助读者掌握图像风格迁移的技术。
3. 两种风格迁移方法对比
3.1 快速风格迁移(使用 tfhub 模块)
-
优点
- 简单易用 :只需加载预训练模块,将目标图像和风格图像作为输入传递给模块,即可快速得到风格化图像,代码实现简单,无需复杂的模型构建和训练过程。
- 速度快 :由于使用了预训练的模型,避免了从头开始训练模型的时间消耗,能够在较短时间内完成风格迁移。
-
缺点
- 灵活性低 :只能使用预训练模块提供的固定风格迁移方式,无法对风格迁移的过程进行深入调整和优化。
- 定制性差 :对于一些特殊的风格需求或个性化的风格迁移任务,难以满足要求。
3.2 手动实现风格迁移(使用 VGG16 模型)
-
优点
- 高度定制化 :可以根据自己的需求选择不同的层来计算内容损失和风格损失,调整损失函数的权重,从而实现个性化的风格迁移效果。
- 深入理解原理 :通过手动实现风格迁移的过程,能够深入理解风格迁移的原理和背后的数学知识,有助于进一步优化和改进风格迁移算法。
-
缺点
- 实现复杂 :需要进行图像预处理、模型构建、损失计算、梯度计算等多个步骤,代码实现较为复杂,对开发者的技术要求较高。
- 训练时间长 :使用 L - BFGS 算法进行优化时,需要多次迭代计算损失和梯度,训练时间较长。
| 方法 | 优点 | 缺点 |
|---|---|---|
| 快速风格迁移(tfhub 模块) | 简单易用、速度快 | 灵活性低、定制性差 |
| 手动实现风格迁移(VGG16 模型) | 高度定制化、深入理解原理 | 实现复杂、训练时间长 |
4. 风格迁移的应用场景
4.1 艺术创作
风格迁移可以将著名画作的风格应用到普通照片上,创造出具有艺术感的图像,为艺术家和设计师提供新的创作灵感。例如,将梵高的《星月夜》风格应用到风景照片上,使照片具有梵高画作的独特笔触和色彩风格。
4.2 图像处理
在图像处理领域,风格迁移可以用于图像的美化和修复。通过将高质量图像的风格迁移到低质量图像上,提高图像的视觉效果和质量。
4.3 娱乐应用
许多手机应用程序利用风格迁移技术,为用户提供有趣的拍照和图像处理功能。用户可以选择不同的风格模板,将自己的照片转换为各种风格的艺术作品,增加娱乐性。
5. 未来发展趋势
5.1 模型轻量化
随着移动设备和嵌入式设备的普及,对风格迁移模型的轻量化需求越来越高。未来的研究将致力于开发更轻量级的模型,减少模型的计算量和存储空间,以便在资源有限的设备上运行。
5.2 实时风格迁移
目前的风格迁移方法在处理大规模图像或视频时,速度较慢,难以实现实时处理。未来的研究将聚焦于提高风格迁移的速度,实现实时风格迁移,应用于视频直播、游戏等领域。
5.3 多风格融合
现有的风格迁移方法主要是将一种风格应用到图像上,未来的研究可能会探索多风格融合的技术,将多种不同的风格同时应用到图像上,创造出更加丰富和独特的风格效果。
6. 总结与展望
本文详细介绍了图像风格迁移的两种方法,包括快速风格迁移和手动实现风格迁移,并对两种方法的优缺点进行了对比分析。同时,探讨了风格迁移的应用场景和未来发展趋势。
风格迁移作为计算机视觉领域的一个重要研究方向,具有广泛的应用前景和发展潜力。随着技术的不断进步和创新,相信风格迁移技术将在更多领域得到应用,为人们带来更加丰富和精彩的视觉体验。
希望本文能够帮助读者深入理解图像风格迁移的原理和实现方法,激发读者在该领域的研究和探索热情。在实际应用中,读者可以根据自己的需求和场景选择合适的风格迁移方法,实现个性化的风格迁移效果。
附录:风格迁移流程 mermaid 流程图
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px
A([开始]):::startend --> B{选择方法}:::decision
B -->|快速风格迁移| C(加载预训练模块):::process
B -->|手动实现风格迁移| D(创建项目并安装依赖):::process
C --> E(准备目标图像和风格图像):::process
E --> F(执行风格迁移):::process
F --> G(显示风格化图像):::process
D --> H(下载图像):::process
H --> I(显示图像):::process
I --> J(图像预处理):::process
J --> K(构建 VGG16 模型):::process
K --> L(计算损失):::process
L --> M(定义评估器类):::process
M --> N(生成风格化图像):::process
N --> G
G --> O([结束]):::startend
以上流程图展示了两种风格迁移方法的整体流程,帮助读者更好地理解风格迁移的步骤和过程。
超级会员免费看
646

被折叠的 条评论
为什么被折叠?



