图像风格迁移核心程序框架

图像预处理

利用 torchvision.transforms 进行图像的预处理和后期处理。预处理的过程接收一个 PIL 图片,改变图片大小,转换为张量,进行标准化,最后乘以 255。对 RGB 三个通道进行标准化,这是 VGG 模型的要求。后期处理则为这一过程的逆过程。



class ImageCoder:
    def __init__(self, image_size, device):
        self.device = device

        # 预处理流程
        self.preproc = transforms.Compose([
            transforms.Resize(image_size),  # 改变图像大小
            transforms.ToTensor(),
            transforms.Normalize(mean=[0.485, 0.456, 0.406],  # 标准化
                                std=[1, 1, 1]),
            transforms.Lambda(lambda x: x.mul_(255))  # 将像素值缩放到 [0, 255]
        ])

        # 后处理流程
        self.postproc = transforms.Compose([
            transforms.Lambda(lambda x: x.mul_(1./255)),  # 将像素值缩放到 [0, 1]
            transforms.Normalize(mean=[-0.485, -0.456, -0.406], std=[1, 1, 1])  # 反标准化
        ])

        # 将张量转换为 PIL 图像
        self.to_image = transforms.ToPILImage()

    def encode(self, image_path):
        """将图像路径加载为张量并进行预处理"""
        image = Image.open(image_path)  # 加载图像
        image = self.preproc(image)  # 预处理
        image = image.unsqueeze(0)  # 增加批次维度
        return image.to(self.device, torch.float)  # 移动到指定设备

    def decode(self, image):
        """将张量解码为 PIL 图像"""
        image = image.cpu().clone()  # 复制到 CPU
        image = image.squeeze()  # 移除批次维度
        image = self.postproc(image)  # 后处理
        image = image.clamp(0, 1)  # 将像素值限制在 [0, 1] 范围内
        return self.to_image(image)  # 转换为 PIL 图像

参数定义 

这一部分对参数进行定义,确定内容损失函数使用的卷积层、风格损失函数使用的卷积层、各卷积层的权重以及最优化的步数。

content_layers = ['conv_4_2']  # 内容损失函数使用的卷积层
style_layers = ['conv_1_1', 'conv_2_1', 'conv_3_1', 'conv_4_1', 'conv_5_1']  # 风格损失函数使用的卷积层
content_weights = [1]  # 内容损失函数的权重
style_weights = [1e3, 1e3, 1e3, 1e3, 1e3]  # 风格损失函数的权重
num_steps = 200  # 最优化的步数

模型初始化 

这一部分中,调用 torchvision.models 提供的预先训练好的 VGG 模型。


class Model:
    def __init__(self, device, image_size):
        # 加载预训练的 VGG19 模型的特征提取部分
        cnn = torchvision.models.vgg19(pretrained=True).features.to(device).eval()
        self.cnn = deepcopy(cnn)  # 深拷贝模型以避免修改原始模型
        self.device = device

        # 初始化损失列表
        self.content_losses = []
        self.style_losses = []

        # 初始化图像处理器
        self.image_proc = ImageCoder(image_size, device)

运行风格迁移的主函数

主函数读取图片并进行预处理,随后依据 VGG 提取的特征图建立内容损失函数和风格损失函数(self._build()方法),再进行最优化得到迁移后的图片(self._transfer()方法)。这两个方法的实现在后面给出。

def run(self, content_image_path, style_image_path):
    content_image = self.image_proc.encode(content_image_path)
    style_image = self.image_proc.encode(style_image_path)

    self._build(content_image, style_image)  # 建立损失函数
    output_image = self._transfer(content_image)  # 进行最优化
    return self.image_proc.decode(output_image)

 利用VGG网络建立损失函数

这一部分中,程序遍历 VGG19 中的各层并进行编号,取定义好的特征图层建立内容损失函数和风格损失函数,并添加到模型中。

def _build(self, content_image, style_image):
    self.model = nn.Sequential()
    block_idx = 1 # 用于标识当前是第几个卷积块(通常一个卷积块包含多个卷积层、ReLU 激活层和池化层)。
    conv_idx = 1 # 用于标识当前卷积块中的第几个卷积层。
    # 逐层遍历 VGG19,取用需要的卷积层
    for layer in self.cnn.children(): # children() 方法返回模型的所有子模块(即各层),代码逐层遍历这些子模块。


        # 识别该层类型并进行编号命名
        if isinstance(layer, nn.Conv2d):
            name = 'conv_{}_{}'.format(block_idx, conv_idx)
            conv_idx += 1
        elif isinstance(layer, nn.ReLU):
            name = 'relu_{}_{}'.format(block_idx, conv_idx)
            layer = nn.ReLU(inplace=False) # 将 inplace 参数设置为 False,避免覆盖输入数据。
        elif isinstance(layer, nn.MaxPool2d):
            name = 'pool_{}'.format(block_idx)
            block_idx += 1 # block_idx 递增,表示进入下一个卷积块。
            conv_idx = 1 # conv_idx 重置为 1,因为下一个卷积块从第一个卷积层开始。
        elif isinstance(layer, nn.BatchNorm2d):
            name = 'bn_{}'.format(block_idx)
        else:
            raise Exception("invalid layer")
        
        self.model.add_module(name, layer) # 将当前层添加到 self.model 中,并使用之前生成的名称作为标识。
        
        if name in content_layers:
            # 添加内容损失函数
            target = self.model(content_image).detach() # 将 content_image 输入到当前模型中,提取目标特征,并通过 detach() 分离计算图。
            content_loss = ContentLoss(target)
            self.model.add_module("content_loss_{}_{}".format(block_idx, conv_idx),
                                  content_loss) # 将内容损失函数添加到模型中
            self.content_losses.append(content_loss) # 将内容损失函数添加到 self.content_losses 列表中,方便后续计算总损失。
        
        if name in style_layers:
            # 添加风格损失函数
            target_feature = self.model(style_image).detach()
            style_loss = StyleLoss(target_feature)
            self.model.add_module("style_loss_{}_{}".format(block_idx, conv_idx),
                                  style_loss)
            self.style_losses.append(style_loss)
    
    # 取卷积特征部分
    i = 0
    for i in range(len(self.model) - 1, -1, -1):
        if isinstance(self.model[i], ContentLoss) or isinstance(self.model[i], StyleLoss):
            break    # 从后向前遍历模型,找到最后一个 ContentLoss 或 StyleLoss 层。
    
    self.features = self.model[:(i + 1)] # 截取模型的前半部分(即特征提取部分),并将其赋值给 self.features。

风格迁移的优化过程 

这一部分中,程序用 LBFGS 算法对定义好的损失进行反向传播最优化,逐步改变图片内容,得到迁移后的图片。可以看到,该部分循环调用了 closure() 函数 num_steps 次。closure() 函数计算当前的风格损失和内容损失,将它们进行加权和,通过 loss.backward()计算梯度,并更新合成的图片。

def _transfer(self, content_image):
    output_image = content_image.clone()
    random_image = torch.randn(content_image.data.size(), device=self.device)
    output_image = 0.4 * output_image + 0.6 * random_image
    optimizer = torch.optim.LBFGS([output_image.requires_grad_()])
    print('Optimizing..')
    run = [0]
    while run[0] <= num_steps:

        def closure():
            optimizer.zero_grad()
            self.features(output_image)
            style_score = 0
            content_score = 0
            for sl, sw in zip(self.style_losses, style_weights):
                style_score += sl.loss * sw

            for cl, cw in zip(self.content_losses, content_weights):
                content_score += cl.loss * cw

            loss = style_score + content_score
            loss.backward()
            run[0] += 1
            if run[0] % 50 == 0:
                print("iteration {}: Loss: {:.4f} Style Loss: {:.4f} Content Loss: {:.4f}".format(
                    run[0], loss.item(), style_score.item(), content_score.item()))

            return loss

        optimizer.step(closure)

    return output_image

运行风格迁移 

这一部分中,首先获取计算硬件的类型(CPU 或 GPU),然后将用于运行风格迁移的
Model 类实例化,将风格图片和内容图片的路径传入,并通过运行 model.run 进行风格迁移。

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
image_size = 256
model = Model(device, image_size)
style_image_path = './images/van_gogh.jpg'
content_image_path = './images/street.jpg'
out_image = model.run(content_image_path, style_image_path)
plt.imshow(out_image)
plt.show()

 PS:本账号致力于持续创作优质技术内容,感谢各位同行朋友们的支持!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值