简介:图像超分辨率WDSR的PyTorch复现项目,面向深度学习与超分重建方向的开发者、研究人员,适用于算法复现、模型对比及论文实验场景。项目对WDSR-A和WDSR-B两种结构均做了完整实现,训练、验证、测试代码齐备,注释详细,并配有科研常用的Loss与PSNR曲线绘制脚本,方便直接嵌入自己的实验流程。资源共47个文件,以16个Python脚本为核心,涵盖数据预处理(DIV2K转h5)、模型定义、参数配置、训练评估等环节;另有12个tar格式模型权重文件,覆盖x2、x3、x4倍率下最优与最新检查点;其余为配置文件与说明文档,压缩包整体约95.87MB。数据划分、数据增强、SSIM计算等模块单独封装,结构清晰,便于按需修改和二次开发。已有24770人学习下载。对于想快速上手超分辨率任务、复现WDSR或基于预训练权重开展对比实验的读者,这份资源提供了从数据处理到结果可视化的完整闭环,值得收藏。
1. 为什么是WDSR:一个在PSNR之外把“宽度”玩明白的结构
如果你在超分方向调过EDSR或SRResNet,应该会注意到一个老问题:把网络做深做宽,PSNR是涨上去了,但参数量和显存也跟着失控。WDSR(Wide Activation for Super Resolution)论文里给了另一个思路——不需要一味加深度,而是在ReLU激活前用线性低秩卷积把特征通道撑宽,激活后再压缩回低维,从而在相同参数量下获得更高的PSNR和更清晰的纹理。这个仓库把WDSR-A和WDSR-B两种变体用PyTorch完整复现,带x2、x3、x4的best/latest权重文件,还包含训练、验证、benchmark测试、SSIM计算和科研绘图脚本。适合刚接触图像超分辨率重建的入门者,也适合需要快速拿现成权重跑对比实验的老手——你不需要从零训练,load权重就能在自己的图上复现超分效果。
2. 数据管线:从DIV2K到h5及数据增强的细节
超分实验的复现性很大程度取决于数据预处理。DIV2K是超分benchmark的默认训练集,但这个仓库没有直接用原始PNG流式读入,而是先用core/data/div2k.py把训练集和测试集打包成h5格式,再转成Tensor。这样做的直接好处是:训练时随机裁剪、翻转、旋转都在内存中的小patch上进行,不必反复IO大图,训练速度快很多,而且h5文件方便跨机器拷贝。
2.1 图像读取与Tensor转换:core/data/utils.py里的基础操作
utils.py里集中了读图、PIL转Numpy、数据增强等操作。下面这段代码是基于该项目常见写法整理出的核心逻辑,和仓库里的utils.py行为一致:
import numpy as np from PIL import Image def load_img(filepath): """读取图像并转为RGB float数组,值域[0,1]""" img = Image.open(filepath).convert('RGB') return np.array(img).astype(np.float32) / 255.0 def rgb2ycbcr(img): """RGB转YCbCr,超分常用只训练/评估Y通道""" if img.ndim == 3: img = img.astype(np.float64) y = 16.0 + (64.738 * img[..., 0] + 129.057 * img[..., 1] + 25.064 * img[..., 2]) / 256.0 return y def augment(lr, hr, hflip=True, rot=True): """数据增强:随机水平翻转和90度旋转""" if hflip and np.random.rand() > 0.5: lr = lr[:, ::-1, :] hr = hr[:, ::-1, :] if rot: k = np.random.randint(0, 4) lr = np.rot90(lr, k) hr = np.rot90(hr, k) return lr.copy(), hr.copy()这里load_img先做归一化到[0,1],避免了后续模型输出层再乘255带来的数值不稳定。rgb2ycbcr返回的Y通道是uint8转换后的float,范围在[16,235]之间,计算PSNR时通常只取Y通道且裁掉边界。augment里的np.rot90传入k=0时不会复制,所以要.copy()防止后面h5写入时共享内存出问题。值得注意的是这个仓库的DIV2K专属均值偏移放在core/model/common.py里,训练时先对输入减均值,输出再加回,这一步在测试图像时别忘了。
2.2 制作h5数据集:div2k.py为什么要把图片patch化
div2k.py负责把DIV2K的HR原图切patch,同时生成对应的LR。常见做法是先用bicubic下采样得到LR,然后随机或滑动取patch对。整个流程类似:
import h5py import numpy as np from PIL import Image def make_h5(hr_dir, lr_dir, h5_path, patch_size=192, stride=96, scale=4): hr_list = sorted(hr_dir.glob('*.png')) with h5py.File(h5_path, 'w') as f: for i, hr_path in enumerate(hr_list): hr = load_img(hr_path) lr_path = lr_dir / hr_path.name.replace('x4', f'x{scale}') # 假设LR已存在 lr = load_img(lr_path) # 滑动裁剪HR和对应LR for top in range(0, hr.shape[0] - patch_size + 1, stride): for left in range(0, hr.shape[1] - patch_size + 1, stride): hr_patch = hr[top:top + patch_size, left:left + patch_size] lr_top = top // scale lr_left = left // scale lr_patch = lr[lr_top:lr_top + patch_size // scale, lr_left:lr_left + patch_size // scale] # 增强后写入 lr_patch, hr_patch = augment(lr_patch, hr_patch) f.create_dataset(f'hr_{i}_{top}_{left}', data=hr_patch) f.create_dataset(f'lr_{i}_{top}_{left}', data=lr_patch)这个脚本的patch_size和stride直接决定训练样本数量。patch_size=192在DIV2K上能得到较大的感受野,但显存有限时建议降到128;stride可以小于patch_size让patch之间重叠,但对训练效果提升有限,反而增加训练集冗余。实际训练时如果能在线随机裁剪,就没有必要离线存h5。这个仓库选择h5是因为训练前已经固定好patch,复现论文指标时不需要再抖动随机性。
3. WDSR模型架构与实验配置:A/B变体如何选
WDSR的核心贡献是提出了一种“宽激活”残差块。传统的SRResNet先降维再卷积再升维,ReLU放在低维空间上,而WDSR反过来——先用1x1卷积把通道数线性放大(比如从64放大到256),再做ReLU,然后通过低秩卷积或1x1卷积把通道压缩回64。这个看似简单的顺序调整,让信息在激活前保持高维,非线性变换后不丢失边缘细节,理论上能加速训练收敛,也让相同参数量下的表示能力更强。
3.1 WDSR-A与WDSR-B的差异
WDSR-A采用最直接的宽激活残差单元:输入先经过一个1x1卷积把通道数从f扩展到f*r,接着ReLU,再用一个1x1卷积把通道压回f,最后加一个可选的3x3卷积。WDSR-B则在这个基础上拆成两个并行分支,其中一个分支只做恒等映射,另一个分支做低秩卷积,最后相加。用PyTorch描述的话,WDSR-B的残差块大概长这样:
import torch.nn as nn class WDSRBlockB(nn.Module): def __init__(self, n_feats=64, expansion=4, wn=False): super().__init__() self.expand = nn.Conv2d(n_feats, n_feats * expansion, 3, padding=1) self.contract = nn.Conv2d(n_feats * expansion, n_feats, 1, padding=0) self.relu = nn.ReLU(inplace=True) if wn: self.expand = nn.utils.weight_norm(self.expand) self.contract = nn.utils.weight_norm(self.contract) def forward(self, x): y = self.relu(self.expand(x)) y = self.contract(y) return x + yn_feats对应权重文件名里的f32,expansion对应文件名里的r4,也就是宽度倍率。WDSR-B用3x3卷积做通道扩展,感受野更大,但参数量略高于A。实际训练时,如果感觉A收敛慢,可以换成B试;如果显存不大,A的f32+r4默认配置是性价比最高的。
3.2 权重文件名的含义与option.py参数对应
仓库里的权重文件名如WDSR-A-f32-b16-r4-x3-best.pth.tar,拆解后是:模型类型A,基本特征数32,残差块数16,宽度倍率4,超分倍率3,后缀best表示验证集上PSNR最高的权重,latest表示最近保存的权重。对应的option.py里核心参数如下:
| 参数 | 含义 | 默认值 | 建议 |
|---|---|---|---|
model | WDSR-A或WDSR-B | WDSR-B | 小数据集用A,大数据集用B |
n_resblocks(b16) | 残差块数量 | 16 | 8可提速,32可提升但显存翻倍 |
n_feats(f32) | 基础通道数 | 32 | 64提升明显但显存暴涨 |
n_expand(r4) | 宽度倍率 | 4 | 4是论文最佳,6偏重纹理 |
scale | 超分倍率 | 2/3/4 | 按目标设 |
这些参数在weight文件和option.py里一致性校验,如果你自己训练时改了n_feats,不要直接加载仓库给的权重,否则Pytorch会报state_dict不匹配。调试阶段可以在option.py里把--seed固定,保证每次裁图和权重初始化一致。
4. 训练闭环:train.py、eval.py与benchmark验证
拿到这个仓库,第一步不是跑测试,而是把训练、验证、benchmark三条路径走通。这样你才知道权重文件是怎么来的,也方便后续在自己数据集上微调。训练入口是train.py,它负责数据加载、模型构建、损失计算、梯度反向和checkpoint保存。
4.1 训练脚本结构与执行流程
先看命令行怎么启动训练,以x3倍率、WDSR-B为例:
python train.py --model WDSR-B --scale 3 --n_feats 32 --n_resblocks 16 --n_expand 4 --batch_size 16 --lr 1e-4 --epochs 300 --save_dir epoch/训练循环里的关键部分(简化自train.py):
from core.model import WDSR_B from core.data import get_training_set from option import args from torch.utils.data import DataLoader from torch.optim import Adam import torch # 模型初始化,加载数据 model = WDSR_B(n_resblocks=args.n_resblocks, n_feats=args.n_feats, n_expand=args.n_expand, scale=args.scale) train_set = get_training_set(args.scale) train_loader = DataLoader(train_set, batch_size=args.batch_size, shuffle=True, num_workers=4, pin_memory=True) criterion = torch.nn.L1Loss() # 超分常用L1 optimizer = Adam(model.parameters(), lr=args.lr) # 训练一个epoch for lr, hr in train_loader: lr, hr = lr.cuda(), hr.cuda() sr = model(lr) loss = criterion(sr, hr) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 0.1) # 防止梯度爆炸 optimizer.step()这里用L1Loss而不是L2(MSE),因为L1在超分任务上通常能产出更锐利的边缘,PSNR略低但主观更好。clip_grad_norm_是训练早期稳定性的保险丝,当lr较大时尤其需要。每训练几个epoch,脚本会调用eval.py在DIV2K验证集上算一次PSNR,如果比历史最好就保存为*best.pth.tar,否则只保存*latest.pth.tar。
4.2 eval.py验证单个模型
验证脚本的用法非常直接:
python eval.py --model WDSR-B --scale 4 --n_resblocks 16 --n_feats 32 --n_expand 4 --resume epoch/WDSR-B-f32-b16-r6-x4-best.pth.tar验证时有一个容易忽略的细节:必须在模型输出后立即用clamp把像素值裁剪到[0,1],否则某些新结构会产生略微超出范围的输出,导致最终PSNR虚高。仓库里常见的验证逻辑是:
def eval_psnr(sr, hr): sr = sr.clamp(0, 1) # 先裁剪 mse = ((sr - hr) ** 2).mean() psnr = 10 * torch.log10(1.0 / mse) return psnr.item()注意这里的1.0是像素范围。如果归一化后hr在[0,1],就用1;如果你用了图像均值偏移,记得先把均值加回来再计算,否则PSNR会偏低。
4.3 test_benchmark.py批量测试5个benchmark
test_benchmark.py是给论文对比用的,它一次性跑Set5、Set14、BSD100、Urban100、Manga109五个数据集,输出每个数据集的平均PSNR/SSIM。运行方式:
python test_benchmark.py --model WDSR-B --scale 2 --resume epoch/WDSR-B-f32-b16-r6-x2-best.pth.tar --data benchmark_root/脚本内部会对每张图做多尺度自集成(self-ensemble)来提高分数:把输入翻转四个方向,再每个方向测一次,最后取平均。这会把推理时间放大8倍,但PSNR通常能提升0.1~0.2dB。如果你的项目是需要跑实时推理,就不要开self-ensemble,直接测原始方向即可。
4.4 draw_evaluation.py科研绘图
训练完成后,draw_evaluation.py负责把日志里的loss和PSNR曲线画出来。仓库在epoch/目录下会保留每个epoch的记录,绘图脚本读取这些json或txt后生成两张图。以下是我整理出的绘图核心:
import matplotlib.pyplot as plt def plot_metrics(history, save_path): plt.figure(figsize=(8, 5)) plt.plot(history['epoch'], history['loss'], label='Training Loss') plt.xlabel('Epoch') plt.ylabel('Loss') ax2 = plt.twinx() ax2.plot(history['epoch'], history['psnr'], color='orange', label='PSNR(dB)') ax2.set_ylabel('PSNR (dB)') plt.legend() plt.tight_layout() plt.savefig(save_path, dpi=300)绘图的亮点是双y轴,左边loss,右边PSNR,在论文里能直观看到loss下降的同时PSNR上升。如果要用于正式科研绘图,建议把线条加粗、去掉顶部右边框,字号改成小五号,这样和期刊字体统一。
5. 用官方权重跑自己的图像:test.py与SSIM计算
这个仓库最省事的地方在于,你已经有了不同倍率的best权重,不需要真正训练就能复现论文级结果。把权重放在epoch/下,随便找一张自然图片,执行:
python test.py --input data/mypic.png --output data/mypic_x4.png --model WDSR-B --scale 4 --resume epoch/WDSR-B-f32-b16-r6-x4-best.pth.tartest.py会读取图像,转换到YCbCr,只对Y通道做超分,CbCr直接用bicubic上采样,最后合并保存。这样做的原因是WDSR训练时只对Y通道监督,CbCr用插值不容易出现明显色偏,能省一半计算量。如果你需要RGB端到端超分,需要自己写一个把RGB三通道都过网络的脚本。
5.1 在自己数据上验证SSIM
仓库自带的pytorch_ssim库提供了SSIM计算接口,使用前先确认参考图像和超分图像尺寸一致:
import torch from pytorch_ssim import SSIM from PIL import Image import numpy as np def compute_ssim(hr_path, sr_path): hr = load_img(hr_path) # [0,1] sr = load_img(sr_path) hr_t = torch.from_numpy(hr).permute(2, 0, 1).unsqueeze(0) sr_t = torch.from_numpy(sr).permute(2, 0, 1).unsqueeze(0) ssim_module = SSIM(window_size=11, size_average=True) return ssim_module(hr_t, sr_t).item()window_size=11是DIV2K上的通用配置,如果图像尺寸很小(比如小于64像素),建议把window_size设成奇数且小于图像尺寸,否则会报padding错误。注意这个SSIM是基于高斯权重窗口的,和skimage的compare_ssim(win_size=11)不一定完全对齐,对比指标时最好统一用同一个库。
5.2 容易踩的坑:边界效应和Y通道指标
计算PSNR/SSIM时,超分图像边缘往往有meshgrid导致的伪影,因此benchmark评测时通常会先去掉scale像素的边界再计算。做法很简单:
crop = scale hr = hr[crop:-crop, crop:-crop] sr = sr[crop:-crop, crop:-crop]另外,如果发现加载仓库权重后PSNR和readme对不上,先检查输入图像是否对齐到/scale的整数倍。WDSR的反卷积层会要求输入尺寸对倍率取整,否则模型内部上采样后尺寸差1个像素,指标掉0.3dB以上。遇到这种情况,最常见的做法是用torch.nn.functional.interpolate将输入先放大到合适的尺寸,再做超分,最后再裁剪回目标尺寸。
如果你要在论文里用这些权重出图,建议额外保存超分前的低清图像和超分后的结果,并标注好哪个权重文件、哪个模型版本,方便审稿时复现。这个仓库的latest权重是训练末尾状态,best是验证集最优,论文里一般用best表格数据,latest只有在你需要继续预训练时才用。
本文还有配套的精品资源,点击获取