CNN快速风格迁移实战:PyTorch与VGG16源码深度解析
2026/9/23 1:34:35 网站建设 项目流程

简介:基于CNN卷积神经网络的图像风格迁移毕设项目,包含可直接运行的Python源码、已训练好的模型权重及详细操作说明,面向计算机、人工智能等专业学生及开发者,可应用于毕设、课程设计或实际项目参考。压缩包共93个文件,覆盖.py源码、.pth权重、jpg/png测试图片、mp4示例视频以及md/html文档,整体约57.03MB,模块划分清晰。当前已有1143人学习/下载,程序经测试运行成功,提供PyCharm+Anaconda环境配置和训练/预测命令参考。资源内预置星夜、马赛克、素描等多种风格模型,支持图片与视频风格迁移,可通过命令自定义训练风格;配套Web交互页面与操作说明,启动后可上传媒体文件实时预览效果,便于深入理解CNN风格迁移原理并进行二次开发。

1. 从毕设源码看 CNN 风格迁移:为什么是"训练网络"而不是"优化图像"

拿到这份包含train.pyapp.pyneural_style.py和多个.pth权重文件的 Python 源码包时,先别急着跑通 Web 界面。它的目录结构已经说明了一个关键事实:这不是 2016 年 Gatys 那类"对每张图做上千步梯度下降"的慢速风格迁移,而是 Johnson 等人提出的 Fast Neural Style Transfer——用 CNN 卷积神经网络把风格"训练"进一个前馈生成模型,推理时一次前向传播就出结果。checkpoints里以starry_night_28000_vgg16.pthmosaic_10000.pth命名的文件,正好暴露了训练轮数和底层的 VGG16 损失网络结构。这套源码适合两类人:一是做毕设或课程设计,需要快速展示"图片+视频风格迁移"完整效果的在校生;二是想理解生成网络与感知损失如何协同工作的工程师。接下来直接拆解源码中可复现的部分。

2. 生成网络与 VGG16 损失网络:models.py 和 CaffeLoader.py 的分工

2.1 前馈生成网络的残差结构

models.py中定义的TransformerNet是典型的编码-解码架构。输入端是 3 通道 RGB 图像,首层用 3x3 卷积把通道数映射到 32 维,经过 InstanceNorm 和 ReLU 后下采样到 64、128 通道,中间串联 5 个残差块,再通过最近邻插值上采样回原始分辨率。关键在最后的 Tanh 激活函数:输出范围被限制在[-1, 1],配合乘系数 150 的操作,对应训练时把像素归一化到[0, 1]的预处理方式。残差块内部没有 BN 而是用 InstanceNorm,是因为风格迁移任务对单张图像的统计特性敏感,InstanceNorm 逐样本归一化能减少内容结构被风格抹平的风险。

class TransformerNet(nn.Module): def __init__(self): super(TransformerNet, self).__init__() # 下采样阶段:3 -> 32 -> 64 -> 128 self.conv1 = ConvLayer(3, 32, kernel_size=9, stride=1) self.in1 = nn.InstanceNorm2d(32, affine=True) self.conv2 = ConvLayer(32, 64, kernel_size=3, stride=2) self.in2 = nn.InstanceNorm2d(64, affine=True) self.conv3 = ConvLayer(64, 128, kernel_size=3, stride=2) self.in3 = nn.InstanceNorm2d(128, affine=True) # 5 个残差块,保持 128 通道 self.res1 = ResidualBlock(128) self.res2 = ResidualBlock(128) self.res3 = ResidualBlock(128) self.res4 = ResidualBlock(128) self.res5 = ResidualBlock(128) # 上采样:128 -> 64 -> 32 -> 3 self.deconv1 = UpsampleConvLayer(128, 64, kernel_size=3, stride=1, upsample=2) self.in4 = nn.InstanceNorm2d(64, affine=True) self.deconv2 = UpsampleConvLayer(64, 32, kernel_size=3, stride=1, upsample=2) self.in5 = nn.InstanceNorm2d(32, affine=True) self.deconv3 = ConvLayer(32, 3, kernel_size=9, stride=1) self.tanh = nn.Tanh()

ConvLayer里用了 ReflectionPad2d 做边界填充,而不是零填充,避免边缘出现棋盘格伪影。UpsampleConvLayer先做最近邻上采样再用卷积平滑,这种组合在计算量上比转置卷积更小,生成图像的块状感也更轻。这是源码包中直接可复用的模块,改写时如果只想保留 3 个残差块,把res4res5注释掉即可,但要留意感受野变小后,大尺寸内容图上的笔触连贯性会打折扣。

2.2 VGG16 损失网络与 Caffe 权重加载

neural_style.py中通过CaffeLoader.py加载 VGG16 权重,这是这套源码比较有年代感的部分。PyTorch 官方torchvision.models.vgg16的权重结构是featuresclassifier分离的,而 Caffe 版本的 VGG16 各层命名规则不同,所以CaffeLoader的核心工作就是把 Caffe 权重按层名逐层映射到 PyTorch 的nn.Sequential模块里。LossNetwork只截取了 VGG16 的卷积部分到relu4_1之前,后面的全连接层和池化层完全丢弃,因为风格迁移只需要中间层的特征图。

class LossNetwork(nn.Module): def __init__(self, vgg_model): super(LossNetwork, self).__init__() # 只保留到 relu4_1,包含 13 个卷积层和 4 个池化层 self.vgg_layers = vgg_model.features[:22] # 冻结全部参数,不参与梯度更新 for param in self.vgg_layers.parameters(): param.requires_grad = False def forward(self, x): # 返回四层特征,供内容损失和风格损失共用 results = [] for name, layer in self.vgg_layers._modules.items(): x = layer(x) if name in ['3', '8', '13', '21']: # relu1_1, relu2_1, relu3_1, relu4_1 results.append(x) return results

内容损失取的是relu3_3层的特征图,对应当前生成图与原始内容图在语义结构上的差异;风格损失则取relu1_1relu2_1relu3_1relu4_1四层的 Gram 矩阵差异。损失权重上,style_weight通常设为 1e10 量级,content_weight为 1,这是因为 Gram 矩阵经过 VGG 深层特征后数值极小,必须放大风格项才能平衡。如果训练出的结果内容保留过多而风格不明显,优先把style_weight提升到 5e10 或 1e11。

3. Flask Web 端与 checkpoints 实测:app.py 推理链路

3.1 启动网页服务

app.py是基于 Flask 的轻量 Web 应用,依赖templates/index.htmlstatic/style.css。启动方式就是操作说明里那行命令:

cd 项目根目录 python app.py

服务默认监听5000端口,浏览器访问http://127.0.0.1:5000会渲染上传表单。上传的图片先经过utils.py中的load_image函数:用 OpenCV 读取 BGR 格式,转成 RGB,缩放到 512 像素以内的长边,再归一化到[0, 1],最后转成[1, 3, H, W]的 PyTorch Tensor。推理时把 Tensor 送入TransformerNet,输出经过denormalize裁剪回[0, 255]并转成 BGR 写回磁盘。

app.py中风格化处理的核心逻辑如下:

def stylize_image(image_path, model_path, output_path): # 加载风格模型,map_location 保证 CPU 单机也能推理 style_model = TransformerNet() state_dict = torch.load(model_path, map_location=lambda storage, loc: storage) style_model.load_state_dict(state_dict) style_model.eval() # 读取并预处理输入图片,OpenCV 读入的是 BGR image = cv2.imread(image_path) image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) image = utils.resize_and_pad(image, 512) # 保持宽高比,短边补零 img_tensor = utils.numpy_to_tensor(image).unsqueeze(0) with torch.no_grad(): stylized = style_model(img_tensor) stylized = stylized.squeeze(0) out = utils.tensor_to_numpy(stylized) out = cv2.cvtColor(out, cv2.COLOR_RGB2BGR) cv2.imwrite(output_path, out)

注意map_location参数,源码包中的.pth若是 GPU 训练保存的,在纯 CPU 环境加载时必须指定映射到 CPU,否则会报 CUDA 不可用的错误。resize_and_pad函数把输入图像统一处理成 512 像素以内的尺寸,这直接影响推理显存占用——如果显卡只有 4GB,可以把 512 改成 384。

3.2 不同 checkpoints 的风格差异与推理耗时

源码包里预置了四个权重文件,对应四种风格,实测下来差异明显:

权重文件风格来源效果特征单张 512px 推理耗时(RTX 3060)
starry_night_28000_vgg16.pth梵高《星月夜》笔触漩涡感强,色彩饱和度偏高约 0.18s
mosaic_10000.pth马赛克镶嵌块状色斑明显,适合人像约 0.18s
cuphead_10000.pth游戏《茶杯头》卡通描边风格,暗部压得较重约 0.18s
sketch_2000.pth手绘素描边缘线条粗糙,灰度主导约 0.18s

耗时几乎一致,因为生成网络结构完全相同,差异只在权重参数上。测试时注意sketch_2000.pth的训练步数只有 2000,风格化结果中内容轮廓保留度明显低于其他三个,边缘会出现大量短线噪声,这属于训练不充分的正常表现,不是模型损坏。

3.3 命令行快速测试不依赖 Web

不想开网页时,直接用test_on_image.py更省事,它等价于 Web 端图片功能但少了 Flask 开销:

python test_on_image.py --input images/content/zurich.jpeg \ --model checkpoints/starry_night_28000_vgg16.pth \ --output output.jpg \ --image_size 512

参数--image_size控制推理分辨率,超过 512 时显存占用成平方增长。test_on_video.py则逐帧调用同一模型,输出到output_video.mp4。如果只测单帧效果,建议先跑图片脚本,因为视频脚本里av库的编码参数如果没配好,会先卡在写帧这一步。

4. train.py 训练闭环:从 COCO 数据集到自定义风格模型

4.1 训练命令与参数拆解

操作说明里给出了完整的训练命令,拆开看每个参数的职责:

python train.py \ --dataset_path data/coco/images/ \ --style_image images/styles/adriaen-van-ostade_landscape.jpg \ --epochs 1 \ --batch_size 4 \ --image_size 256

--dataset_path指向 COCO 数据集的图片目录,训练时随机裁剪image_size尺寸的 patch 作为内容图。--style_image是新风格的参考图,建议分辨率不低于 512x512,且风格纹理要足够密集——纯色抽象画训练出的模型容易把内容图整体染色。--batch_size 4在 8GB 显存下是安全值,VGG16 损失网络和生成网络同时驻留显存,batch_size 提到 8 大概率 OOM。--epochs 1只是验证流程能跑通的最小配置,真正得到可商用模型通常要 2 到 5 个 epoch,训练步数对应 checkpoints 文件名里的_10000_20000等数字。

4.2 数据加载与风格图像预处理

train.pyImageFolder搭配transforms做数据增强:

transform = transforms.Compose([ transforms.Resize(256), # 短边缩放到 256 transforms.RandomCrop(256), # 随机裁剪,增加内容多样性 transforms.ToTensor(), ])

内容图来自 COCO 数据集的任意场景照片,因为风格迁移不要求内容图与风格图有语义关联,只要内容多样性足够,网络就能学会"保留结构、替换纹理"。风格图则在每个 iteration 中单独加载并缩放:

style_img = load_image(args.style_image, size=args.image_size) style_img = style_img.repeat(args.batch_size, 1, 1, 1) # 复制成 batch 大小

style_weightcontent_weight的配比是整个训练的关键。源码neural_style.py中默认style_weight=1e10content_weight=1。如果跑出来的效果内容结构被破坏,把content_weight上调到 1.5 或 2;如果风格化不彻底,把style_weight提到 5e10。这两个值每调整一次都要重训,所以建议先用小数据集跑一个 epoch 观察损失曲线走势,再全量训练。

# neural_style.py 核心损失计算 style_loss = 0 for fm_s, fm_t in zip(feature_style, feature_target): gram_s = gram_matrix(fm_s) gram_t = gram_matrix(fm_t) style_loss += F.mse_loss(gram_s, gram_t) content_loss = F.mse_loss(feature_content, feature_target_content) total_loss = content_weight * content_loss + style_weight * style_loss

gram_matrix的实现是b, c, h, w的特征图 reshape 成b, c, h*w,然后x @ x.transpose(1, 2)除以c*h*w归一化。这一步必须用torch.bmm批量矩阵乘,逐样本 for 循环计算 Gram 矩阵会拖慢训练速度三倍以上。

4.3 训练完成的模型保存与复用

每个 epoch 结束后,train.py调用torch.save(model.state_dict(), checkpoint_path)保存生成网络权重。这里只保存生成网络,不保存 VGG16 损失网络,因为推理阶段完全不需要 VGG16。加载权重时注意load_state_dict默认要求键名严格匹配,如果自定义改了生成网络结构,加载旧权重会报缺失参数错误。

5. 视频风格迁移:帧闪烁问题与模型排错技巧

视频风格化的实现思路很直接:test_on_video.py用 OpenCV 读视频帧,逐帧送入生成模型得到风格化帧,再用av库按原视频的 fps 和分辨率重新编码输出。但逐帧独立推理会引入一个显著问题,就是帧与帧之间的风格特征不稳定——同一场景的相邻帧,星星的笔触方向、纹理疏密都会有跳变,实际播放时表现为闪烁。常见做法是牺牲一点风格强度换取时域稳定性:把输入帧叠加 3 到 5 帧的均值再推理,或者干脆接受逐帧处理的闪烁感,毕竟毕设演示场景下视觉冲击力比平滑度更重要。

排错方面,整条链路里最常见的三类报错集中在环境与权重匹配上。第一类是torch.loadNo module named 'CaffeLoader',说明脚本在 Python 环境变量里找不到同目录模块,直接pip install -e .或把项目根目录加入PYTHONPATH即可。第二类是加载.pthsize mismatch for conv1.weight,原因是模型定义与训练时的通道数不一致——检查models.py开头通道数是否有改动。第三类是 CUDA OOM,这不是代码问题,把app.pytest_on_image.py--image_size从 512 降到 256,或在推理前加torch.cuda.empty_cache()手动释放缓存。checkpoints里的sketch_2000.pth是训练样本,风格效果偏弱属预期,想改善需要按第 4 章的流程重训。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询