简介SRResCycGAN是欧洲计算机视觉研讨会2020年真实图像超分辨率挑战赛赛道三的官方PyTorch实现面向从事真实图像超分辨率研究的算法工程师与计算机视觉研究者。该方法借鉴循环生成对抗思想构造深度循环网络让低分辨率与高分辨率图像保持域一致性以四倍放大完成真实场景高质量重建并可快速部署到移动或嵌入式设备。压缩包共28个文件约45.21MB以Python代码与PNG实验图像为主6个脚本覆盖模型定义、推理与测试流程14张图片展示定量对比与视觉效果另有容器运行配置、说明文档与许可协议等辅助内容。资源内含测试结果目录、预训练权重、样例图片及完整演示代码凭此可复现论文实验或在新数据集上继续微调。目前已有512人浏览学习适合掌握PyTorch基础、希望快速上手超分赛题的开发者。1. 真实图像超分不好做SRResCycGAN 是 AIM2020 交出的答案用一张真实照片做 4 倍超分稍不注意就会翻车真实图像超高分辨率这个任务难点不在放大而在低分辨率图里的退化完全不可控。AIM2020 挑战赛上出现的 SRResCycGAN核心思路是用循环一致性生成对抗网络加深度残差卷积块让模型在没有严格配对数据的前提下学习真实退化。这次拆的是它的一份完整代码仓库从网络定义到训练、推理脚本都在里面。适合已经跑过 SRGAN、想在真实照片上落地的开发者也适合需要一套可改造超分基线的算法工程师。2. 循环一致性与残差卷积SRResCycGAN 凭什么不依赖配对数据2.1 为什么真实退化不能简单用下采样模拟经典超分模型的训练方式是把高清图做双三次下采样得到低分辨率图再让网络去学“反推”。这套思路在合成数据上效果很好但放到真实照片上就会露馅真实照片的低分辨率图是传感器噪声、镜头模糊、JPEG 压缩、去马赛克误差和缩放算法混在一起的结果退化过程完全未知。用固定下采样规则生成的训练对相当于让模型去学一个根本不存在的逆过程。我拆这份代码时最先注意到的就是它对“退化”的处理方式。SRResCycGAN 没有先把数据集强行配对而是把超分辨率当作一个跨域映射问题低分辨率域和高分辨率域之间用循环结构互相约束。这个思路的收益是明显的——测试时遇到没见过的退化组合模型不会完全懵掉因为它训练时看到的是“低分辨率域整体长什么样”而不是某一种具体退化。2.2 SRResCycGAN 的循环结构前向、反向与 Cycle Loss代码仓库里最核心的部分是一对生成器。前向生成器负责把低分辨率图变成高分辨率图反向生成器负责把高分辨率图退回低分辨率图。前向做完一轮再用反向生成器把结果变回低分辨率此时应该和输入的低分辨率图非常接近这就是前向循环一致性。反过来也一样高分辨率图先退化再恢复也应该回到原来的样子。这个设计把“配对”要求拆掉了。真实场景里你很难拿到同一场景的成对高低分辨率照片但很容易拿到大量低分辨率图和大量高清图。循环一致性让这两批数据能够在训练中互相约束不需要知道每张低分辨率图对应哪张高清图。仓库里的循环损失实现类似下面这样class CycleLoss(nn.Module): def __init__(self, weight10.0): super().__init__() self.weight weight def forward(self, rec_lr, real_lr, rec_hr, real_hr): fwd_loss F.l1_loss(rec_lr, real_lr) bwd_loss F.l1_loss(rec_hr, real_hr) return self.weight * (fwd_loss bwd_loss)这段代码里的rec_lr是超分结果再经过反向生成器退回来的低分辨率图而不是超分结果本身rec_hr是高清图先退化再超分回去的结果。只用 L1 距离做约束是因为 L1 对异常像素更鲁棒不容易被少数过亮或过暗的点带偏。weight参数是循环一致性的总权重仓库默认给 10。这个值很敏感设太小循环约束形同虚设两个生成器会各自放飞设太大模型为了满足循环会牺牲细节输出变得过度平滑。我一般会在 5 到 20 之间先跑两个短实验再决定具体值。2.3 生成器与判别器选型以及和经典超分 GAN 的差异生成器骨架是深度残差卷积网络。这里的“残差”不是简单的跳连而是堆叠多个残差块每个残差块内部是卷积、BatchNorm、ReLU 的组合。上采样部分仓库用的是 PixelShuffle 这类像素重排方式而不是转置卷积。转置卷积容易在输出里留下棋盘状伪影PixelShuffle 在超分场景下更稳。判别器走的是 PatchGAN 路线。普通判别器输出一个标量判断整张图真还是假PatchGAN 输出一张特征图把图像切成若干个小块分别判真伪这样对局部纹理更敏感也更适合超分任务。经典超分 GAN 通常是一对一对抗SRResCycGAN 则多了一层循环约束训练时生成器要同时骗过判别器、满足循环一致性生成的高频纹理更接近真实分布但训练难度也随之上升。后面第 4 章的调参主要就是在平衡这两个损失。3. 跑通代码仓库环境、仓库结构与推理命令3.1 先看仓库结构再装环境这份代码仓库不是那种动辄几十个文件的大工程结构比较清晰。拿到手先别急着跑打开目录看一眼数据加载和退化参数在data相关模块里网络定义在models相关模块里训练和测试入口分别是带train和test的脚本。Options 相关模块集中管理命令行参数绝大多数调参不用改源码传命令行参数就行。环境方面仓库是 Python 写的老代码回购常用的是 Python 3.8 加 PyTorch 1.10 的组合。我按这个组合重建了一个环境兼容性最好直接用新版本 PyTorch 也不是不行但加载预训练权重时偶尔会遇到键名不匹配的问题。安装过程如下conda create -n srrescycgan python3.8 -y conda activate srrescycgan pip install torch1.10.0 torchvision0.11.0 pip install -r requirements.txt先建干净环境再装深度学习框架最后装依赖。如果你只有新版本 GPU 驱动PyTorch 会自动拉对应 CUDA 轮子不需要单独装 CUDA 工具包。requirements.txt里一般是 numpy、pillow、opencv-python、tqdm、scipy 这些常用库作用分别是处理数组、读写图像、图像变换、显示进度条和数值计算。OpenCV 这个依赖有时容易被忽略缺了它会在数据增强阶段报错。3.2 推理命令与参数含义跑通预训练模型是验证这份代码仓库是否值得继续投入最快的方式。假设仓库自带预训练权重推理命令一般长这样python test.py \ --name pretrained_srrescycgan \ --checkpoints ./checkpoints \ --input_dir ./test_images \ --output_dir ./output_up4 \ --scale 4 \ --gpu_ids 0name指定实验名代码会去checkpoints目录下找同名权重文件夹input_dir是输入的低分辨率图片目录支持文件夹批量处理output_dir是输出目录不存在会自动创建scale是放大倍数gpu_ids是 GPU 编号。如果你的机器只有一张卡填 0 就行。跑的时候留意一件事scale必须和预训练权重匹配。用 8 倍权重去跑 4 倍任务代码不一定会报错但输出的内容会非常奇怪因为网络结构和目标输出尺寸对不上。我建议第一次先按仓库默认的 4 倍跑确认流程通了再手动改 scale 测试。3.3 第一次跑通后先别急着训练很多新手第一次跑通就直扑训练我建议先做三件事。第一把输出图和输入图叠在一起看边缘确认没有明显重影第二把放大结果缩回原尺寸看看纹理是不是自然有没有水彩感第三换一批自己拍的照片专门挑夜间、强压缩、运动模糊的图看模型在这些退化下的表现。这一步能帮你快速判断这份代码的“退化假设”和你的业务场景是否匹配。如果它在你自己的图上表现很差问题多半不在代码而在数据和退化模型后面训练章节会专门讲怎么改。4. 训练与调参让 SRResCycGAN 真正适应你的退化数据4.1 数据怎么准备退化池是核心训练 SRResCycGAN数据准备和普通超分不太一样。普通超分需要成对的 LR-HR 图片这份代码仓库默认支持非配对训练一个目录放高清图另一个目录放低分辨率图模型自己学两个域的映射。但更常见的做法是“半配对”收集高清图作为 HR 域然后对每张 HR 随机生成多种退化得到一个退化后的低分辨率图集合模型在这个集合上训练。这样做的好处是既不需要真实配对图片又能控制退化分布。退化池是这里的关键。我一般会在数据加载阶段随机组合多种退化方式而不是固定一种def random_degrade(hr_batch): 为 HR 随机生成一种退化返回 LR 与本次退化参数 kernel random_kernel(sizenp.random.choice([7, 9, 11, 13])) lr gaussian_blur(hr_batch, kernel) lr downsample(lr, scale4) lr add_noise(lr, sigmanp.random.uniform(0, 10)) return lr, (kernel, sigma)退化池的核心思路是不要让模型只见过一种模糊核和一种噪声水平。真实照片的退化是高斯模糊、运动模糊、噪声、压缩伪影的叠加因此每次迭代随机选择模糊核尺寸、噪声强度能让模型见过更宽的退化分布。代码里的random.choice控制模糊核大小np.random.uniform控制噪声强度这两个随机源可以按你的场景调整。如果你的数据多是老照片可以把噪声上限调高如果多是截图JPEG 伪影权重就要加重。4.2 训练参数配置与显存预算训练命令核心参数如下python train.py \ --dataroot ./data \ --name real_sr_v1 \ --scale 4 \ --batch_size 1 \ --patch_size 128 \ --lr 5e-5 \ --lambda_cycle 10 \ --gan_weight 1 \ --n_epochs 100 \ --decay_epoch 50 \ --gpu_ids 0patch_size是训练时从大图上随机裁剪的块大小128 表示 128 乘 128 像素越大上下文信息越丰富但显存占用随面积平方增长batch_size设为 1 是因为循环 GAN 要同时跑两个方向显存需求比普通超分高不少lr是生成器学习率我习惯从 5e-5 起步比普通超分的 1e-4 更保守lambda_cycle对应第 2 章的循环损失权重gan_weight是对抗损失权重默认 1如果发现纹理过强可以降到 0.5 甚至 0.1。decay_epoch表示从第 50 个 epoch 开始学习率线性衰减到 0。如果训练脚本没有decay_epoch参数可以简单粗暴地手动分段前 50 个 epoch 用 5e-5后 50 个 epoch 降到 1e-5。循环 GAN 训练到后期特别容易震荡学习率不降下来损失曲线会像心跳一样上下跳。显存不够时优先把patch_size从 128 降到 96而不是把batch_size强行降到 1——PatchGAN 判别器需要足够的感受野来评估纹理patch 太小会让判别器只能看到局部色块失去纹理判断能力。4.3 训练过程怎么看、怎么停训练时不要只盯着一行行 loss 数字。仓库一般会在checkpoints目录下定时保存生成的样本图这些图才是判断模型是否在变好的第一依据。我习惯每个 epoch 保存一次生成样本然后拿最新样本和前一个 epoch 的样本对比细节是不是越来越多边缘是不是越来越干净颜色有没有发生偏移。loss 下降那只是数值上的反馈生成图在视觉上连续变好才是真的有效训练。停止时机也有讲究。常见做法是保存前 5 个 epoch 的权重作为候选训练结束后统一跑一遍无参考指标再挑视觉最好的。仓库如果没有这个功能可以手动把每个 epoch 的G.pth或等价文件复制出来命名。事后选模型后悔药都没得吃不如训练时多留几份。5. 常见问题与排查五个高频坑5.1 输出颜色发灰或整体偏移现象模型输出的图片整体发灰像加了一层白色滤镜或者色调偏向了某个方向和原图完全不对。原因生成器输出层用 tanh 激活函数输出范围是负一到正一而图像数据被归一化到零到一。如果保存图片时直接将负数值截断到 0暗部细节就会丢失整体显得发灰。解决检查输出处理逻辑确认生成器输出是否为[-1,1]范围。如果是保存前需要先做(output 1) / 2的归一化再映射到 0 到 255 的整数范围。这段逻辑通常在推理脚本的图像后处理函数里不要修改网络前向代码。5.2 训练 Loss 变 NaN现象训练跑到第几百个迭代loss 突然变成nan生成图全黑或全白重启训练也恢复不了。原因学习率过高加上判别器训练得太强生成器梯度爆炸。循环 GAN 里两个生成器相互影响一旦一个发散另一个也逃不掉。解决先把学习率从 1e-4 降到 5e-5再限制判别器的更新频率让生成器每更新两到三次判别器才更新一次。同时在优化器步骤前加梯度裁剪常见阈值是 1.0。如果代码里没有梯度裁剪参数可以用 PyTorch 的clip_grad_norm_手动加一行。5.3 显卡 OOMpatch 和 batch 怎么取舍现象训练刚开始就报CUDA out of memory显存直接爆掉。原因循环 GAN 一次前向要跑两个生成器显存占用是普通超分模型的两倍左右。再加上判别器整张图占用的显存很容易超出常规预期。解决先用batch_size1、patch_size96跑通整个训练流程确认不爆显存后再逐步往上加。如果 batch size 已经是 1优先缩小 patch size。实在想保留 patch 尺寸可以用梯度累积每累积 4 个 batch 再执行一次优化器 step等效扩大 batch size但不会增加单次前向的显存峰值。5.4 分块推理出现接缝现象高清大图无法一次性送入网络需要切成小块推理拼接后块与块之间有明显的亮暗接缝或模糊带。原因切块时不重叠边界像素缺少周围上下文模型对边界的预测和块内不一致另外 padding 方式也会加剧边界信息丢失。解决推理时使用重叠切块相邻块之间留一部分重叠区域拼接时对重叠区做线性加权融合。重叠宽度我一般取 patch 大小的四分之一到八分之一太窄接缝压不住太宽推理时间成倍增加。代码里再贴一段的话这部分逻辑通常在test.py的推理循环里直接改切块参数即可。5.5 PSNR 高但视觉变假现象模型在测试集上 PSNR 数值不错但人眼看着很假皮肤纹理像塑料边缘出现不自然的锯齿。原因PSNR 计算的是像素级误差真实图像超分场景下模型把噪声锐化成“假细节”反而能降低像素误差但视觉上并不真实。解决不要只盯 PSNR 和 SSIM额外用 NIQE、BRISQUE 这类无参考指标再结合人眼观察。判模型是否可用的硬标准是缩小到原尺寸后细节是否自然、边缘是否过度增强。这一点在第 6 章会单独展开。6. 验证与进阶无参考指标与分块推理的落地技巧验证真实超分模型是否真的可用我建议把复用落到一套固定流程。第一步收集 10 到 20 张覆盖你业务场景的照片夜间、白天、强压缩、弱光各来几张第二步把这些图分别跑一次超分第三步用无参考指标和原图一起做横向对比。无参考指标计算可以直接用现成的工具库import torch from pyiqa import create_metric metric create_metric(niqe, devicecuda:0) score metric(output_tensor) print(fNIQE {score.item():.3f})NIQE 分数越低表示图像质量越好通常聚焦在 3 到 6 之间。真实场景下它比 PSNR 更有参考价值因为不需要真实高清图作为对比基准。用同样的命令把输入的低分辨率图和输出图分别算一遍如果超分后 NIQE 没有明显下降说明模型只是在放大没有真正恢复细节。进阶使用时关注两个技巧。第一个是重叠切块的融合宽度推理高清大图时重叠宽度取 patch 尺寸的八分之一能有效抑制接缝又不显著增加耗时。第二个是退化池的持续更新模型上线后定期收集新场景的低分辨率图补充到退化池里重新训练避免模型对某类退化产生“偏好”。真实超分的退化分布会随设备更新而变化代码仓库给出的是初始版本后续维护靠的是自己的数据。从那以后我每次拿到新的超分代码仓库第一件事不是跑默认命令而是先确认它的退化假设和我的数据是否匹配再决定是直接推理还是重新训练。这个习惯帮我避开了不少坑。希望帮到你。本文还有配套的精品资源点击获取