计算机图形学【图像风格迁移】
图像风格迁移应用:从一张图像到另一张图像的肖像画风格迁移
Image Style Transfer
图像风格迁移是指将一张图像的内容与另一张图像的风格相融合,生成具有新风格的图像。风格(style)是指图像中不同空间尺度的纹理、颜色和视觉图案,内容(content)是指图像的高级宏观结构。
Representation of Content and Style
在深度学习实现上,我们需要确定我们的的目标是什么,就是保存原始图像的内容,同时采用参考图像的风格。那么就有一个适当的损失函数将对其进行最小化,如下:
loss = distance(style(reference_image)
−style(generated_image))
+distance(content(original_image)
−content(generated_image))
这里的 distance 是一个范数函数(用来衡量向量或矩阵的大小或长度),比如 L2 范数;content 是一个函数,输入一张图像,并计算出其内容的表示;style 是一个函数,输入一张图像,并计算出其风格的表示。将这个损失最小化,会使得 style(generated_image) 接近于 style(reference_image)、content(generated_image) 接近于 content(generated_image),从而实现我们定义的风格迁移。
神经风格迁移的一般流程:计算层激活 -> 计算损失函数 -> 梯度下降最优化损失函数
① 网络层激活:输出目标图像,原始图像,参考图像的层激活输出
② 计算损失函数: 通过所求的层激活计算对应的内容和风格损失函数
③ 梯度下降优化:通过梯度下降的方式减小损失函数,训练模型参数使得模型能学习到参考图像风格的纹理和保持内容的不变
在CNN卷积网络进行对象识别任务中,随着层次的加深对象的信息输出更加的明确,较前的层数特征图输出到一些更加通用的结构,比如猫狗分类中的基础边缘线条,而更深的层可以捕捉到更加全局和抽象的结构,如猫耳,猫眼睛。根据这个模式,我们可以通过不同深度的层特征图重建输入图像以可视化层所包含输入图像的信息。
Content Loss Function
以 VGG19 网络结构的 conv4_2 的输出作为图像的内容表示,定义内容损失函数。
Style Loss Function
使用 conv1_1, conv2_1, conv3_3, conv4_4, conv5_1 五个卷积层进行风格损失函数的计算,不同的权重会导致不同的迁移效果。
Total Loss Function
总体损失函数即内容损失函数和风格损失函数的加权,不同的权重会导致不同的迁移效果。 Algorithm 在风格表示上,由每一层输出的 feature_map 所得到的 Gram 矩阵可以作为图像风格的良好表示。具体计算方法为:将某一层输出的 N 个 feature_map 分别拉成向量,Gram 矩阵的元素为 feature_map 向量之间的内积。
实践过程
(0)环境准备
① 下载 imagenet-vgg-verydeep-19.mat 文件(可选)
② 下载安装包:(PyCharm下)
TensorFlow:用于构建和训练神经网络模型。
->其中在终端安装:
pip install tensorflow -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install tensorflow tensorflow-hub imageio pillow
NumPy:用于高效的数值计算。
SciPy:用于科学计算,特别是读取 VGG19 网络权重文件。
OpenCV:用于图像读取、处理和保存。
(1)需要转换的图像

(2)使用的图像绘画风格

(3)结果展示
运行程序:

等待很长时间……

成果如下图显示,深层特征图包含图像中对象的全局排列信息(高级、抽象),但像素值信息丢失了。浅层特征图几乎完整且精确地重建了图像的像素值。

(4)完整代码
import tensorflow as tf
import tensorflow_hub as hub
import numpy as np
import imageio.v2 as imageio
from PIL import Image
import os
# 图像路径
CONTENT_IMG = 'liuyifei.jpg' # 内容图像
STYLE_IMG = 'watercolor.jpg' # 风格图像
OUTPUT_DIR = 'output/' # 输出路径
# 检查并创建输出目录
if not os.path.exists(OUTPUT_DIR):
os.mkdir(OUTPUT_DIR)
def load_and_preprocess_image(image_path):
"""加载并预处理图像"""
img = imageio.imread(image_path)
img = np.array(Image.fromarray(img).resize((400, 500)))
img = img / 255.0 # 归一化到[0, 1]
return np.expand_dims(img.astype(np.float32), axis=0) # 添加批次维度
def save_image(image, path):
"""保存图像"""
image = np.squeeze(image) * 255 # 去掉批次维度并反归一化
image = np.clip(image, 0, 255).astype(np.uint8)
imageio.imwrite(path, image)
# 加载内容和风格图像
content_image = load_and_preprocess_image(CONTENT_IMG)
style_image = load_and_preprocess_image(STYLE_IMG)
# 使用 TensorFlow Hub 加载风格迁移模型
model = hub.load('https://tfhub.dev/google/magenta/arbitrary-image-stylization-v1-256/2')
# 进行风格迁移
stylized_image = model(tf.constant(content_image), tf.constant(style_image))[0]
# 保存结果图像
save_image(stylized_image.numpy(), os.path.join(OUTPUT_DIR, 'stylized_image2.jpg'))
print("风格迁移完成,输出图像已保存。")
另一个使用 tensorflow 但未完成调试的版本(供参考):
import tensorflow as tf
import numpy as np
import imageio.v2 as imageio
import os
import time
from PIL import Image
CONTENT_IMG = 'liuyifei.jpg' # 内容图像
STYLE_IMG = 'vangogh.jpg' # 风格图像
OUTPUT_DIR = 'output/' # 输出路径
IMAGE_W = 400 # 图像宽度,减小图像尺寸
IMAGE_H = 500 # 图像高度,减小图像尺寸
COLOR_C = 3 # 3 通道
NOISE_RATIO = 0.7 # 生成噪声图片
BETA = 5 # 内容损失的权重
ALPHA = 100 # 风格损失的权重
MEAN_VALUES = np.array([123.68, 116.779, 103.939]).reshape((1, 1, 1, 3)) # 通道颜色均值
STYLE_LAYERS = [('block1_conv1', 0.5), ('block2_conv1', 1.0)] # 减少风格层数量
CONTENT_LAYER = 'block4_conv2' # 更新层名称
if not os.path.exists(OUTPUT_DIR):
os.mkdir(OUTPUT_DIR)
def the_current_time():
"""打印当前时间"""
print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time()))))
def load_image(path):
"""加载一张图片并调整大小"""
image = imageio.imread(path)
image = np.array(Image.fromarray(image).resize((IMAGE_W, IMAGE_H)))
image = np.reshape(image, ((1,) + image.shape)) # 扩充了 1 维
image = image - MEAN_VALUES # 减去通道颜色均值
return image
def save_image(path, image):
"""存储一张图像"""
image = image + MEAN_VALUES # 加上通道颜色均值
image = image[0]
image = np.clip(image, 0, 255).astype('uint8')
imageio.imwrite(path, image)
def generate_noise_image(content_image, noise_ratio=NOISE_RATIO):
"""基于内容图像产生一张随机图片"""
noise_image = np.random.uniform(-20, 20, (1, IMAGE_H, IMAGE_W, COLOR_C)).astype('float32')
input_image = noise_image * noise_ratio + content_image * (1 - noise_ratio)
return input_image
# 加载内容和风格图像
content_image = load_image(CONTENT_IMG)
style_image = load_image(STYLE_IMG)
# 创建输入图像变量
input_image = tf.Variable(generate_noise_image(content_image))
# 加载VGG模型
base_model = tf.keras.applications.VGG19(include_top=False, weights='imagenet')
model = tf.keras.Model(inputs=base_model.input, outputs=[base_model.get_layer(layer_name).output for layer_name, _ in STYLE_LAYERS + [(CONTENT_LAYER, 1)]])
def compute_loss():
"""计算内容和风格损失"""
model_outputs = model(input_image)
content_loss = tf.reduce_mean(tf.square(model_outputs[-1] - model(content_image)[-1]))
style_loss = 0
for output, (_, weight) in zip(model_outputs[:-1], STYLE_LAYERS):
style_loss += weight * tf.reduce_mean(tf.square(output - model(style_image)[-1]))
total_loss = BETA * content_loss + ALPHA * style_loss
return total_loss
optimizer = tf.keras.optimizers.Adam(learning_rate=2.0)
# 训练
ITERATIONS = 500 # 减少迭代次数
for i in range(ITERATIONS):
with tf.GradientTape() as tape:
loss = compute_loss()
grads = tape.gradient(loss, input_image)
optimizer.apply_gradients([(grads, input_image)])
if i % 50 == 0: # 每 50 次迭代输出一次
output_image = input_image.numpy()
the_current_time()
print('Iteration: {}, Cost: {}'.format(i, loss.numpy()))
save_image(os.path.join(OUTPUT_DIR, 'output_{}_{}.jpg'.format(STYLE_IMG.split('.')[0], i)), output_image)
# 保存最终图像
output_image = input_image.numpy()
save_image(os.path.join(OUTPUT_DIR, 'output_{}_final.jpg'.format(STYLE_IMG.split('.')[0])), output_image)
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)