简介:面向图像处理与深度学习初学者的Python图像超分辨率重建源码包,围绕低分辨率到高分辨率的重建任务,系统介绍了图像分辨率与超分辨率重建的基本概念,涵盖基于插值的最近邻、双线性、双三次方法,以及基于学习的传统样例、稀疏表示和SRCNN、VDSR、ESPCN、SRGAN等主流算法原理,并提供了从数据预处理、数据增强、模型构建、训练到测试评估的完整代码流程。压缩包采用rar格式,共5个文件,均为py脚本,utils.py负责工具函数与图像处理,expand_data.py实现数据扩充,model.py定义网络结构,main.py串联训练主流程,test.py用于模型验证与超分结果测试,包体仅10KB,结构清晰紧凑,适合快速对照学习与二次开发。目前已有619人浏览学习,借助该源码可理解超分辨率重建从数据处理、模型设计到推理验证的完整工程实现,是入门深度学习图像重建任务、开展自定义SR实验的实用参考资料。
1. 图像超分辨率重建到底在解决什么:不是放大图片,是补出细节
把一张 96×96 的低分辨率图放大成 384×384,普通双三次插值只做几何扩展,得到的是边缘发虚、细节全丢的“大图”。图像超分辨率重建(Super-Resolution, SR)的目标是从低分辨率(LR)图中恢复高分辨率(HR)纹理,本质是学习 LR 到 HR 的映射,属于经典的低层次视觉任务。很多人把它和“图像增强”“锐化”混在一起,实际区别在于 SR 有明确的退化模型和重建基准——给定退化后的 LR,要尽可能逼近原来未被退化的 HR。做这个方向适合两类人:一类是刚接触 python 教程和图像处理、想找真实落地项目的初学者,一类是做监控、遥感、医学影像需要提升小图可用度的从业者。本文围绕一套可直接复现的 Python 重建源码,从模型选型、数据准备到训练推理和踩坑记录,按项目落地顺序展开。
2. 选对模型和指标再动手:EDSR 与 PSNR/SSIM 的基本盘
2.1 重建质量怎么看:PSNR/SSIM 不是摆设
评估 SR 模型最常见的两个指标是 PSNR 和 SSIM。PSNR 基于 MSE,单位是 dB,数值越高代表像素级的逼近越好;SSIM 比较局部亮度、对比度和结构相似性,范围 0 到 1,越接近 1 结构保留越完整。两者并不总是一致——有的模型 PSNR 高但图像看起来“塑料感”重,SSIM 反而能反映这种结构失真,所以论文里通常两者一起报。
在实际源码里这两个指标的计算位置要提前设计好,不能在验证阶段临时抱佛脚。PyTorch 中 PSNR 计算核心如下:
import torch
import torch.nn.functional as F
def compute_psnr(img1, img2, max_val=1.0):
"""
img1, img2: 形状 [N, C, H, W],Tensor,取值需在相同量纲下比较
max_val: 图像最大值,float 类型为 1.0,uint8 为 255
"""
mse = F.mse_loss(img1, img2)
if mse == 0:
return float('inf')
psnr = 10 * torch.log10(max_val ** 2 / mse)
return psnr.item()
这段逻辑非常简单,但有一个容易被忽略的参数 max_val 。如果数据预处理时把图像归一化到 [0,1],这里就要传 1.0;如果没归一化就喂入 uint8 范围的 tensor,则要传 255,否则 PSNR 数值整体偏移,和论文里对比时就失去意义。另一点是 mse 为 0 时直接返回 inf,这个分支在测试验证时一旦触发,通常意味着输入和输出完全相同,若模型恒等映射了输入,说明网络学习坍塌,需要在训练循环里捕获这种异常。
SSIM 的计算最好直接用 pytorch_msssim 这类现成包,自己实现涉及高斯窗口、标准差、C1/C2 常数,手动写容易在边界和通道维度上出偏差。安装后调用同样需要指定 data_range :
from pytorch_msssim import SSIM
ssim_module = SSIM(data_range=1.0, size_average=True, channel=3)
ssim_val = ssim_module(img1, img2).item()
2.2 源码选型:EDSR、RCAN、SRGAN 各适合什么场景
面向 python 实现的开源超分源码库很多,但不同模型适用场景差别很大。
EDSR(Enhanced Deep Super-Resolution)是基线中的基线:去掉 BatchNorm,只保留残差块堆叠,结构干净、训练稳定、显存友好,最适合第一篇真正跑通和读懂源码的入门选择。RCAN 引入了通道注意力,参数多一倍、训练更长,换取约 0.2-0.5dB 的提升,适合追求指标上限且机器有余量的场景。SRGAN 走感知损失路线,生成图像锐利、视觉效果讨喜,但 PSNR 不如 EDSR,且对抗训练的不稳定性会让新手翻车。此外还有轻量化模型 ESPCN 适合 CPU 实时推理,但重建能力弱不少。
选择上没有绝对最优。服务端离线处理数据,首选 EDSR;产品里做主观体验优化,再考虑 SRGAN。不要一上来就加载几百 MB 的大型预训练模型,先把 EDSR 或 ESPCN 跑通一条推理链路,理解输入输出张量形状,后续换模型架构只是替换网络主体代码的事。
2.3 缩小范围:一套能跑通的最小训练链路
完整项目源码通常包含 data、models、utils、train.py 和 test.py 几个模块。对应到目录结构是:
sr_project/
├── data/
│ └── dataset.py # 加载 HR,生成 LR,Patch 采样
├── models/
│ ├── edsr.py # EDSR 网络结构
│ └── common.py # 卷积块、残差块、上采样模块
├── utils/
│ ├── metrics.py # PSNR / SSIM
│ └── utils.py # checkpoint 保存、日志
├── train.py # 训练入口
└── test.py # 测试入口
这套结构几乎在所有开源的 pytorch SR 仓库里都能见到,命名略有差异但模块划分基本一致。先搭目录再写代码,比在单个 notebook 里塞全部逻辑更容易排查错误。
3. 准备数据与预处理:从 DIV2K 到 LR-HR 训练对
3.1 生成 LR 图像:bicubic 降采样是默认基准
SR 训练需要 LR-HR 对,但公开数据集提供的是 HR。通行的做法是对 HR 做双三次(bicubic)降采样生成 LR,采样因子通常为 2、3、4。DIV2K 是超分领域默认的训练验证集,包含 800 张 2K 分辨率的训练图和 100 张验证图。如果网络环境或磁盘限制不方便下载完整 DIV2K,也可以用公开的 SR-RAW、Set5、Set14 替代,但作为算法评测基准还是以 DIV2K 为准。
实际操作时使用 PIL 或 OpenCV 的 resize 接口即可,注意插值常数必须和模型假设一致:
from PIL import Image
hr = Image.open('0801.png').convert('RGB')
lru = hr.resize((hr.width // 4, hr.height // 4), Image.BICUBIC)
lr = lru.resize((hr.width, hr.height), Image.BICUBIC)
这段示意里充分体现了一个重要约定:先大图缩小,再放大回去,用来模拟真实退化。直接把 lru 当训练输入会降低分辨率,无法和原图对齐。源码里常用的退化流程大多遵循这种 two-step bicubic 操作,有些严格复现论文的实现会加噪声、模糊核等,但训练初期不做这些花样,先保证 LR 和 HR 尺寸一致。
3.2 Patch 采样与数据加载器
高分辨率图 2K 尺寸全图进 GPU 不现实,训练时从 HR 图上随机裁剪固定尺寸 patch,再对应裁剪 LR patch。EDSR 的常见设定是 HR patch 大小 192×192,x4 模型对应 LR patch 48×48。这个设计决定了模型的感受野和训练内存,patch 越大模型越容易学到全局低频信息,但显存压力直线上升。
PyTorch 数据加载器实现的要点在于预处理:
import random
import torch
from torch.utils.data import Dataset
from PIL import Image
class SRDataset(Dataset):
def __init__(self, hr_paths, scale=4, hr_patch_size=192, augment=True):
self.hr_paths = hr_paths
self.scale = scale
self.hr_patch_size = hr_patch_size
self.lr_patch_size = hr_patch_size // scale
self.augment = augment
def __getitem__(self, idx):
hr = Image.open(self.hr_paths[idx]).convert('RGB')
# 随机裁剪 HR patch
x = random.randint(0, hr.width - self.hr_patch_size)
y = random.randint(0, hr.height - self.hr_patch_size)
hr_patch = hr.crop((x, y, x + self.hr_patch_size, y + self.hr_patch_size))
# 在 HR patch 上直接生成 LR patch
lr_patch = hr_patch.resize((self.lr_patch_size, self.lr_patch_size), Image.BICUBIC)
# 转 Tensor,归一化到 [0, 1]
hr_t = torch.from_numpy(np.array(hr_patch)).permute(2, 0, 1).float() / 255.0
lr_t = torch.from_numpy(np.array(lr_patch)).permute(2, 0, 1).float() / 255.0
# 数据增强:随机翻转、旋转90度
if self.augment and random.random() > 0.5:
lr_t = torch.flip(lr_t, dims=[2])
hr_t = torch.flip(hr_t, dims=[2])
return lr_t, hr_t
关键点在于:LR patch 直接从 HR patch 上降采样而来,保证空间对齐;增强操作要同步施加在 LR 和 HR 上。如果先对整张大图裁 LR 再裁 HR,边界对不齐会让模型学到错误映射,训练时 loss 波动异常,这是新手经常踩的坑。
3.3 训练/验证/测试集的划分逻辑
DIV2K 的官方划分是 800 训练 + 100 验证,很多源码实现直接用其中一部分验证、一部分测试。测试集一般取零样本的 Set5、Set14、BSD100、Urban100,这些数据集图像数目少、类型差异大,用于评估泛化性。划分时要避免训练集在内容上污染测试结果,尤其 Urbans100 包含大量建筑线条,对边缘重建能力非常敏感,适合检验模型有没有过度拟合特定纹理。
4. 训练、验证与调参:把 EDSR 跑起来的完整脚本
4.1 网络结构实现:残差块堆叠
EDSR 的核心是残差块的堆叠和最后的上采样模块。下面是精简但能训练的结构:
import torch.nn as nn
def conv3x3(in_channels, out_channels, bias=True):
return nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1, bias=bias)
class ResBlock(nn.Module):
def __init__(self, channels, res_scale=1.0):
super().__init__()
self.body = nn.Sequential(
conv3x3(channels, channels),
nn.ReLU(True),
conv3x3(channels, channels)
)
self.res_scale = res_scale
def forward(self, x):
res = self.body(x)
return x + res * self.res_scale
class EDSR(nn.Module):
def __init__(self, num_channels=3, feature_channels=64, num_blocks=16, scale=4):
super().__init__()
self.head = conv3x3(num_channels, feature_channels)
self.body = nn.Sequential(*[ResBlock(feature_channels) for _ in range(num_blocks)])
self.tail = nn.Sequential(conv3x3(feature_channels, feature_channels))
self.upsample = nn.Sequential(
nn.Conv2d(feature_channels, feature_channels * scale * scale, kernel_size=3, padding=1),
nn.PixelShuffle(scale),
conv3x3(feature_channels, num_channels)
)
def forward(self, x):
head = self.head(x)
body_out = self.body(head)
tail_out = self.tail(body_out)
return self.upsample(head + tail_out)
ResBlock 里的 res_scale 参数是 EDSR 的一个关键设计,默认取 0.1。残差缩放缓解了深层网络叠加后梯度爆炸的问题,相比直接在 forward 里返回 body_out ,这种方式让训练能走更多残差块而不发散。上采样用 PixelShuffle ,把通道数扩充 scale×scale 倍再重排像素,对应 ESPCN 时期就定型的一种亚像素卷积思路。换 scale=2 或 3 时,只需要把最后一层的倍数改成 4 或 9。
4.2 训练循环与损失函数选择
EDSR 原论文使用的是 L1 损失,而不是 L2(MSE)。在 SR 任务中 L1 对异常像素更鲁棒,训练收敛到相同的 PSNR 水平时视觉效果更干净,同时 L2 容易让图像偏平滑。以下是训练循环骨架:
import torch.optim as optim
def train_one_epoch(model, dataloader, lr_optimizer, criterion, device):
model.train()
total_loss = 0.0
for lr, hr in dataloader:
lr = lr.to(device)
hr = hr.to(device)
sr = model(lr)
loss = criterion(sr, hr)
lr_optimizer.zero_grad()
loss.backward()
lr_optimizer.step()
total_loss += loss.item() * lr.size(0)
return total_loss / len(dataloader.dataset)
loss 用的是 nn.L1Loss() ,等价于 MAE,梯度量级比 MSE 小一些,所以学习率取值会比 L2 模型稍大。EDSR 常用学习率是 1e-4,训练 30 到 100 epoch 之后再衰减到 1e-5 继续微调。优化器选择 Adam,beta 保持默认,weight_decay 一般设 0 或非常小的 1e-5,SR 里正则化加多了容易把高频细节一起抹掉。
4.3 验证与模型保存策略
训练过程里每若干个 epoch 跑一次验证,算出验证集 PSNR 再决定是否保存。保存要同时存模型权重和优化器状态,避免训练中断后无可恢复:
best_psnr = 0.0
for epoch in range(start_epoch, total_epochs + 1):
avg_loss = train_one_epoch(...)
if epoch % 5 == 0:
psnr_val = evaluate(model, val_loader, device)
# 保存最优模型与最近模型
is_best = psnr_val > best_psnr
if is_best:
best_psnr = psnr_val
torch.save({'epoch': epoch, 'model': model.state_dict(),
'optimizer': optimizer.state_dict(), 'psnr': psnr_val},
'checkpoint_best.pth')
if epoch % 10 == 0:
torch.save(...) # checkpoint_latest.pth
一个值得说明的细节是验证函数里要加上 torch.no_grad() 并保证模型处于 eval() 。如果验证集图片太大,一次性全图过模型会爆显存,常见做法是滑窗裁剪再拼回结果,但这个操作要和训练时的 pad 方式保持一致,否则拼接处出现接缝。
4.4 关键超参数参考表
| 参数 | 建议取值 | 说明 |
|---|---|---|
| feature_channels | 64 | 增大到 128 能提升上限,显存翻倍代价 |
| num_blocks | 16 | 32 块是 EDSR-B 方案,深不一定是好 |
| learning_rate | 1e-4 | Adam 初始,后期衰减 0.5 倍率乘 epoch 步进 |
| hr_patch_size | 192 | x4 时 LR patch 为 48,视野太小收敛慢 |
| batch_size | 16 | 单个 2080Ti 级别可用,累计梯度或混合精度扩展 |
| loss | L1 | L2 会带来模糊,不建议替换 |
| 训练 epoch | 60~100 | 更久只能换回边缘收益,超分数据集迭代较快 |
5. 超分辨率重建避坑:5 个常见翻车现场
5.1 图像边界像素全糊:padding 策略没对
现象:重建的测试图四周明显比中心模糊,甚至出现黑边或颜色异常。
原因:卷积层默认的 padding=0 会让特征图每次卷积后变小,网络深度增加后有效区域逐层收缩。源码里虽然多数卷积用了 padding=1,但上采样模块和最后输出层如果没有对应的边界处理,输出和输入尺寸不一致。
解决:确保每个卷积层维持输入输出同尺寸;PyTorch 的 nn.Conv2d 里设 padding=1 配 kernel_size=3,是让分辨率不变的标准组合。测试滑窗推理时,建议对输入图像先做一次 torch.nn.functional.pad ,padding 量等于模型感受野的一半,重建后再裁掉,能从根部消除边缘效应。不要靠随机裁 patch 掩盖这个问题。
5.2 颜色通道偏色:YCbCr 与 RGB 转换不统一
现象:训练好的模型对部分图输出颜色怪异,比如肤色偏青,或者通道间出现错位。
原因:很多公开评测把图像转换到 YCbCr,只在亮度通道 Y 上做重建,把 Cb/Cr 直接用双三次插值放大。如果你的源码训练时在 RGB 三通道上都做 L1 损失,而测试时只取 Y 通道,或在数据加载里做了不同的色彩空间转换,重建结果会有系统性偏差。
解决:把训练和测试的预处理抽成同一个函数,统一从 RGB 读入、统一在 RGB 上训练、统一用 RGB 输出。要复现论文指标再用 YCbCr 方案,注意 Cb/Cr 通道只插值不重建,保存前转回 RGB 时要对齐精度位数。顺带一提,很多源码里 transforms.ToTensor() 之后数据范围是 [0,1],和 max_val=255 的测评混在一起时,指标会低到离谱。
5.3 batch size 一大就爆显存:梯度累积和混合精度
现象:batch size 设 32 时 CUDA out of memory,只降到 8 才能跑。
原因:EDSR 输入输出的张量尺寸和通道数不小,batch size 翻倍,中间激活值占用几乎翻倍,显存吃紧是常态。
解决:不降 batch size 的替代方案是梯度累积。每步先分离 loss 并回传梯度,隔几步 accumulate 后再更新一次权重。
accumulation_steps = 4
optimizer.zero_grad()
for i, (lr, hr) in enumerate(dataloader):
loss = criterion(model(lr), hr) / accumulation_steps
loss.backward()
if (i + 1) % accumulation_steps == 0:
optimizer.step()
optimizer.zero_grad()
5.4 验证指标高但视觉差:PSNR 的局限
现象:模型验证集 PSNR 比论文还高 0.3dB,放大看结果却过度平滑,眼睛和毛发糊成一片。
原因:PSNR 只反映逐像素误差,对微小的空间错位不敏感,纹理整体偏移时 MSE 反而小。超分领域有时只看 PSNR 会导致模型输出偏向均值回归,即“安全”的模糊解。
解决:验证时除 PSNR 外固定保存几张视觉对比图,观察边缘锯齿和高频纹理。在测试阶段用 Set14 或 Urban100 的特定序列,做一次用户主观打分,比单个数值更能说明问题。这也是很多论文最终选择用 LPIPS(感知相似度)作为补充指标的原因,如果你的源码版本里没实现,也可以引入它做辅助判断。
5.5 训练 loss 不下降:学习率与初始化
现象:loss 在前 10 个 epoch 几乎不动,或者先快速下降到某个平台后直接变平。
原因:SR 模型对 LR 输入初始的 head 和 tail 权重随机,L1 loss 在小学习率下更新缓慢是常态;而平台期通常是因为学习率过大导致在局部最优附近震荡,而非真正收敛。
解决:先跑一个真实 batch 过一遍看 loss 是否从 0.4-0.6 量级起步;如果一开始就 NaN 或小于 0.01,说明归一化或梯度出问题。平台期使用余弦退火或 StepLR 将学习率降一个量级,再继续 20 个 epoch。恢复训练时不要只加载模型权重,把 optimizer 状态一起加载,并且同步记得设置 model.train() ,否则 BN 的 running stats 还是训练前的缓冲值。
6. 把关重建效果的落地技巧:测试集推理与视觉对比
6.1 测试集推理脚本
写一个干净的 test.py,输入一张任意尺寸的图,输出放大后的 SR 图并保存,和双三次插值结果并排对比。核心过程如下:
def load_model(checkpoint_path, device):
model = EDSR(num_blocks=16).to(device)
ckpt = torch.load(checkpoint_path, map_location=device)
model.load_state_dict(ckpt['model'])
model.eval()
return model
def predict(model, lr_tensor, device):
with torch.no_grad():
sr_tensor = model(lr_tensor.unsqueeze(0).to(device))
return sr_tensor.squeeze(0)
这个脚本的重点是测试时不使用任何训练时的增广手段,也不做梯度计算。输出时再用 torch.clamp 限制到 [0,1] 的边界,转成 numpy 后乘以 255 保存为 uint8 图像,避免因微小越界导致保存时出现灰点。
6.2 视觉对比图
落地的验收不能只信数值,需要打印并排对比图。把 LR 双三次插值结果和模型 SR 结果水平拼接,PSNR 标注在图下方。注意 LR 的双三次结果也应该用同样的 resize 流程生成,保证只有“插值”和“网络重建”两部分差异。检验边缘时,对同一图像区域裁剪放大 2 倍展示,查看线条是否连续、建筑窗格是否有重影。
6.3 关于上采样与迭代优化
这套链路跑通后,再考虑引入退化核估计或盲超分,不要一开始就上复杂方案。我自己的血泪经验是第一次用 SRGAN 跑单图重建,直接拿论文里的预训练权重去推理效果不错,但做真实低清监控图时完全崩掉,因为真实退化和 bicubic 假设不一致。之后的习惯是每次训练完必做三件事:在 Urban100 上看线条边缘、在低光照图上检查噪声放大、对比 batch 里最差样本的 PSNR 分布。这么做之后,从源码跑通到实际项目交付,心理预期会稳很多。
超分问题里数据退化和模型训练手拉手,不能只盯一个数值。希望这篇笔记能帮你在源码之上建立起自己的判断框架,少走几步弯路。
403




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



