ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

超分辨率算法复现:SRCNN训练、PSNR评估与避坑指南

超分辨率算法复现:SRCNN训练、PSNR评估与避坑指南 简介面向数字图像处理课程大作业的超分辨率算法复现资料包围绕算法实现与项目说明展开适合计算机、数学、电子信息等专业的学生用于课程设计、期末大作业或毕业设计参考。资源包含MATLAB源码26个m文件、41张bmp测试图像以及1份项目说明文档压缩包共68个文件整体约13.46MB内容中区分低分辨率输入LR、双线性插值BI、超分重建super等模块便于对照算法流程查看中间结果。目前已有263人学习下载。内容覆盖从低分辨率输入到超分辨率重建的完整实现既有可直接运行的源码也有说明文档辅助梳理原理读者可复现超分重建过程观察不同模块的产物与效果差异并在此基础上扩展或改进算法适合作为项目参考和算法入门素材。1. 超分辨率算法复现先搞清楚大作业要交付什么数字图像处理大作业里超分辨率算法复现常年是热门选题。你可能已经拿到或者准备动手做一个叫作“超分辨率算法复现源码项目说明.zip”的交付物——里面通常是一个经典网络比如 SRCNN、FSRCNN的源码、一组数据处理脚本以及一份能讲清楚“我做了什么、效果如何”的项目说明。这件事真正花时间的不是让训练跑起来而是让训练指标和论文对得上、让说明文档能支撑源码、让交上去的东西别人能一次跑通。这篇笔记把完整落地路径讲清楚适合正在选课、期末要交源码和报告的人也适合想快速复现一个超分基线并写出可靠说明的从业者。2. 选哪个算法复现SRCNN 与 FSRCNN 的取舍复现一门大作业第一步不是打开代码写模型而是先定复现目标。超分辨率算法从 2014 年到现在已经有大量变体但大作业场景下大部分人都选 SRCNN 或 FSRCNN。原因很简单结构透明、训练耗时可控、指标能够用经典论文数值做参照而且源码一旦跑通项目说明里能写的东西非常多。2.1 SRCNN 为什么是复现首选三层卷积就把事办了SRCNN 的核心思路是“先放大、再重建”。输入是已经用双三次插值bicubic放大到目标尺寸的低分辨率图然后过三层卷积第一层9×9 卷积64 个特征通道负责特征提取第二层1×1 卷积32 个通道做非线性映射第三层5×5 卷积输出 3 通道重建高分辨率结果。这里有个关键点常被忽略SRCNN 的网络本身没有上采样层全靠输入端的 bicubic 插值把尺寸对齐。所以你在写数据预处理时必须把低分辨率图先放大到和高分辨率图相同的尺寸否则训练时张量尺寸直接不匹配。损失函数用的是 MSE对应最大化 PSNR。这个选择对超分任务天然友好因为 MSE 越低PSNR 越高指标好看且训练曲线稳定。我一般会建议没有特殊要求的人优先选 SRCNN因为它的可解释性太适合写报告了每一层的作用、卷积核大小为什么这样选、感受野覆盖了多少像素区域全部能展开写。2.2 FSRCNN 与 ESPCN什么时候值得换网络如果你不想全篇只写一个结构或者老师明确要求“不要只做最简单的”FSRCNN 是第二优选。FSRCNN 的改动在于把“先放大再卷积”变成“先卷积再放大”网络输入就是原始低分辨率图末端用反卷积或亚像素卷积完成上采样。这样做的收益是速度快、计算量小但随之而来的是网络里多了收缩层和扩展层写说明时要把通道数变化交代清楚工作量比 SRCNN 大。ESPCN 则是用亚像素卷积实现实时超分思路更炫但训练稳定性对大作业来说略差如果只是想拿一个靠谱分数不建议首个尝试。对比维度SRCNNFSRCNNESPCN输入端尺寸已放大的低分辨率图原始低分辨率图原始低分辨率图结构复杂度低三层卷积中加收缩/扩展中高亚像素卷积训练速度慢一些输入像素多快快报告可写性高容易逐层分析中中踩坑风险低中较高如果你只有一周时间选 SRCNN如果你有十周以上的课程周期想做得深一点FSRCNN 更合适只有当选题明确要求“实时处理”时才去碰 ESPCN。2.3 源码目录怎么组织项目说明和代码对得上拿了源码和项目说明的框架第一件事是检查目录组织能不能支撑“按文档复现”这条标准。常见做法是把文件分成模型、数据、训练、评估、说明文档五块文件/目录职责train.py训练入口加载数据、定义模型、跑 epochmodel.py网络结构定义SRCNN/FSRCNN 类make_dataset.py从原始图片生成训练用的 h5 数据evaluate.py对测试集算 PSNR/SSIM输出结果图requirements.txt依赖清单README.md / 项目说明.md环境、数据、运行步骤、结果表格顺序特别重要。项目说明里的运行命令必须和源码入口一一对应不能出现“运行 train.py”但实际入口是 main.py 的情况。我习惯把 README 里的命令写得可以直接复制粘贴python make_dataset.py --data ./data/T91 --scale 2 python train.py --epochs 50 --lr 1e-4 --batch_size 16 python evaluate.py --checkpoint ./checkpoints/best.pt这里每个命令的输入输出都有依赖关系先准备数据再训练最后评估。说明文档里除了这三行还要把环境版本写清楚否则换一台机器很容易跑崩。3. 超分复现训练流程数据集预处理与训练脚本复现超分算法的训练部分关键是数据怎么准备。很多人栽在训练上其实问题出在前一步数据没做对。这一章把数据集选型、预处理脚本、训练脚本和参数设置一次讲完。3.1 数据集选型T91 训练、Set5 测试的最小组合超分辨率领域公开数据集很多T91、Set5、Set14、Urban100、DIV2K 是常见的几个。对大作业来说不要上来就下 DIV2K它体积大、预处理时间长跑一个完整实验可能占掉你大半时间。我一般推荐的组合是T91 用于训练Set5 或 Set14 用于测试。数据集用途规模与特点T91训练91 张自然图像超分论文经典训练集Set5测试5 张图像常用于报告 PSNRSet14测试14 张图像比 Set5 更全面Urban100测试100 张城市建筑图纹理结构难DIV2K训练/测试高分辨率量大适合完整复现T91 只有 91 张图直接训很快配合随机裁剪能生成足够多的 patch。数据增强方面我对每张图做水平翻转和 90/180/270 度旋转把样本量扩到 8 倍。对超分任务来说增强收益虽然不如分类任务那么夸张但在训练数据本来就少的作业场景里值得做。3.2 预处理脚本裁 patch、降采样、存 h5训练超分模型不能把整张图直接塞进网络显存有限是一方面更重要的是整图训练会让样本数量太少。常见做法是从高分辨率图上随机裁出固定大小的 patch再对每个 patch 降采样得到对应的低分辨率图。我一般把处理好的数据存成 h5 而不是一堆 png。原因只有一条h5 单文件读取快训练迭代时不用反复打开几百个小文件。# make_dataset.py import cv2 import numpy as np import h5py from pathlib import Path patch_size 96 scale 2 hr_dir Path(data/T91) with h5py.File(data/train.h5, w) as f: lr_group f.create_group(lr) hr_group f.create_group(hr) idx 0 for img_path in sorted(hr_dir.glob(*.png)): hr cv2.imread(str(img_path)) hr cv2.cvtColor(hr, cv2.COLOR_BGR2RGB) h, w, _ hr.shape # 保证宽高都能被 scale 整除否则降采样后尺寸对不上 hr hr[: h - h % scale, : w - w % scale] for i in range(0, hr.shape[0] - patch_size 1, patch_size): for j in range(0, hr.shape[1] - patch_size 1, patch_size): hr_patch hr[i:i patch_size, j:j patch_size] # 先缩小再放大回原尺寸SRCNN 需要同尺寸输入输出 lr_small cv2.resize(hr_patch, (patch_size // scale, patch_size // scale), interpolationcv2.INTER_CUBIC) lr_big cv2.resize(lr_small, (patch_size, patch_size), interpolationcv2.INTER_CUBIC) lr_group.create_dataset(str(idx), datalr_big) hr_group.create_dataset(str(idx), datahr_patch) idx 1 print(saved patches:, idx)这段代码里有两个容易理解偏的点。第一hr先裁剪到能被 scale 整除的尺寸是为了让每次下采样都严格对应整数像素位置避免出现半像素对齐问题。第二存进 h5 的lr_big已经是放大回原尺寸的图这和 SRCNN 的输入设计一致如果你后来换成了 FSRCNN这里就要同时保存lr_small和hr_patch训练时输入不放大。关于 stride我这里用的是固定步长等于 patch_size也就是不重叠裁剪。如果你想增加样本量可以把外层循环改成随机采样。但要注意随机采样要设固定随机种子否则每次生成的数据集不一样后面跑出来的实验结果没法复现。3.3 训练脚本MSE 损失与最优权重保存训练部分的核心就是模型定义、损失函数、优化器和 checkpoint 保存。SRCNN 模型定义如下# model.py import torch import torch.nn as nn class SRCNN(nn.Module): def __init__(self): super().__init__() # padding 按 (kernel - 1) // 2 设置保持特征图尺寸不变 self.conv1 nn.Conv2d(3, 64, kernel_size9, padding4) self.conv2 nn.Conv2d(64, 32, kernel_size1, padding0) self.conv3 nn.Conv2d(32, 3, kernel_size5, padding2) self.relu nn.ReLU() def forward(self, x): x self.relu(self.conv1(x)) x self.relu(self.conv2(x)) x self.conv3(x) # 最后一层不加 ReLU输出为像素值 return x注意最后一个卷积层后面不加激活函数因为输出是 RGB 像素值加 ReLU 会强制截断大于 0 的值反而导致 PSNR 上不去。padding 的计算方式是(kernel_size - 1) // 2目的是让每个卷积层输出尺寸和输入一致这也是超分网络能端到端训练的前提。训练循环里我按“每个 epoch 在验证集上算一次 PSNR保存最优权重”的思路写# train.py import torch from torch.utils.data import DataLoader, Dataset import h5py class SRDataset(Dataset): def __init__(self, h5_path): self.f h5py.File(h5_path, r) self.keys list(self.f[lr].keys()) def __len__(self): return len(self.keys) def __getitem__(self, i): lr torch.from_numpy(self.f[lr][self.keys[i]]).permute(2, 0, 1).float() / 255.0 hr torch.from_numpy(self.f[hr][self.keys[i]]).permute(2, 0, 1).float() / 255.0 return lr, hr model SRCNN() optimizer torch.optim.Adam(model.parameters(), lr1e-4) criterion nn.MSELoss() loader DataLoader(SRDataset(data/train.h5), batch_size16, shuffleTrue, num_workers2) for epoch in range(50): model.train() total_loss 0.0 for lr, hr in loader: optimizer.zero_grad() loss criterion(model(lr), hr) loss.backward() optimizer.step() total_loss loss.item() * lr.size(0) print(fepoch {epoch} loss: {total_loss / len(loader.dataset):.4f}) # 验证逻辑每个 epoch 后在 Set5 上算一次 PSNR保存最优权重 # val_psnr compute_psnr(model, val_loader) # if val_psnr best_psnr: # torch.save(model.state_dict(), checkpoints/best.pt)数据加载里做了归一化直接除以 255 把像素值映射到 0 到 1 之间。这里有个前后一致性要求训练时归一化评估时也要用同样的方式处理否则 PSNR 计算结果会乱套。optimizer 用 Adam初始学习率 1e-4是 SISR 复现里最常见可靠的一组设置。3.4 关键参数速查epoch、batch、lr 怎么调给初次复现的人一组可直接抄的参数跑通后再慢慢调参数推荐值调整方向与注意点patch_size96显存不足时降到 64图像细节丰富时可以加到 128scale2 或 3大作业优先 2训练快且效果明显4 倍难度大batch_size16显存不足时用梯度累积代替调小 batchlearning rate1e-4loss 不降就降到 1e-5不要直接上 1e-2epochs50SRCNN 收敛慢50 轮起步看曲线决定提前停optimizerAdam默认 beta 即可不需要额外调 momentum这些参数之间是联动的。patch_size 越大每张图能裁出的样本越少但单样本信息量越大batch_size 越大收敛越稳定但和 patch_size 一起变大会直接把显存吃满。我一般先把 patch_size 定死再调整 batch_size 去适配显存最后才碰学习率。4. 超分结果怎么评PSNR 与 SSIM 计算代码和调参训练跑完只是第一关评估才是真正拉开分数的地方。超分任务的评估有两个固定惯例第一只在 YCbCr 色彩空间的 Y 通道上计算 PSNR 和 SSIM第二计算前去掉图像边界若干像素。如果这两条没做你的指标和论文、和同学之间根本没有可比性。4.1 PSNR 与 SSIM 的 Python 实现只在 Y 通道算只算 Y 通道的原因很实际人眼对亮度最敏感超分论文也都按这个标准报告方便横向比较。RGB 三通道直接算 PSNR 会把色彩噪声也计入指标导致数字偏低且不稳定。# evaluate.py import numpy as np import cv2 from skimage.metrics import structural_similarity def rgb_to_y(img): # BT.601 亮度公式输入 RGB 范围 0~1 return 0.299 * img[..., 0] 0.587 * img[..., 1] 0.114 * img[..., 2] def psnr_y(pred, gt, borderscale): # 去掉边界像素避免卷积 padding 带来的干扰 pred_y rgb_to_y(pred[border:-border, border:-border]) gt_y rgb_to_y(gt[border:-border, border:-border]) mse np.mean((pred_y - gt_y) ** 2) if mse 0: return float(inf) return 10.0 * np.log10(1.0 / mse) def ssim_y(pred, gt, borderscale): pred_y rgb_to_y(pred[border:-border, border:-border]) gt_y rgb_to_y(gt[border:-border, border:-border]) return structural_similarity(pred_y, gt_y, data_range1.0)border 取 scale 的含义是下采样几倍就裁掉几圈像素。因为卷积层对边缘区域的感受野覆盖不完整这些位置的像素重建质量天然偏低如果不去掉每个图的 PSNR 都会被拉低且不稳定。SSIM 直接调 scikit-image 的 structural_similarity窗口默认 11×11这是论文通用设置。这里要注意data_range1.0因为输入已经归一化到 0 到 1 了如果输入是 0 到 255 的整数图像data_range 要改成 255否则 SSIM 偏低甚至失真。4.2 训练曲线怎么看loss 与 PSNR 的对应关系训练时 loss 用 MSE评估时看 PSNR两者之间有明确的数学关系。当像素值归一化到 0 到 1 时PSNR -10 * log10(MSE)所以 loss 到 PSNR 有个简单对照MSE loss对应 PSNR0.0120.0 dB0.00325.2 dB0.00130.0 dB0.000335.2 dB这张表价值在于判断训练状态。比如训练到 30 轮时 loss 停在 0.001 附近对应 PSNR 大约 30 dB这是 SRCNN 在 T91 训练、Set5 测试的合理区间。如果 loss 降到了 0.0001 但 PSNR 只有 32 dB那要检查预处理或者评估代码有没有问题而不是盲目继续训。另外要澄清一点训练集 loss 和验证集 PSNR 之间不是严格同步的。训练集 loss 还在下降验证集 PSNR 可能已经开始波动甚至下降这就是过拟合的信号。看到这个现象优先减小训练轮数或者在保存 checkpoint 时始终以验证集 PSNR 为准而不是以最后一个 epoch 的权重为准。4.3 调参的几个真实方向先定 patch 再动 lr超分模型的调参和分类模型不太一样。分类任务过拟合可以加正则、加数据增强超分任务里这些手段收益有限因为模型本来就在做低层重建增强过度反而让输入分布偏离真实场景。我习惯按这个顺序排查先确认数据生成一致再确认归一化一致然后才去动训练超参。数据不一致导致的指标偏差远比学习率设置错误更隐蔽。比如你用 OpenCV 生成低分辨率图你用 PIL 生成两个流程在一样的参数下会差出 0.3 到 0.5 dB。训练超参里最玄学的是学习率。SRCNN 用 Adam 时1e-4 起步是稳的loss 连续 10 个 epoch 不降我会把学习率降到 5e-5 再跑 20 轮。反过来如果你看到 loss 一开始就剧烈震荡那不是学习率的问题是数据没对齐或者归一化范围错了。先把数据和评估代码检查一遍再回头改学习率能省很多时间。5. 超分复现避坑五个最常翻车的细节这一章是血泪经验合集。下面五条都是我在实际复现和评审别人作业时见过最多的问题每一条都按“现象、原因、解决”的顺序写清方便你对号入座。5.1 插值算法不一致同一张图 PSNR 差 0.5dB现象代码完全按论文跑了训练稳定但测试 PSNR 比论文结果低 0.3 到 0.5 dB怎么调都追不上。原因SRCNN 的对比基线是 MATLABimresize(..., bicubic)生成的而 OpenCV 的INTER_CUBIC双三次插值核和 MATLAB 不完全相同PIL 的 BICUBIC 又是另一个实现。三者的插值系数有细微差异在 2 倍降采样时会直接影响低分辨率图的像素值进而传导到训练和评估全流程。解决统一全流程的插值实现。最简单的方式是全部使用 OpenCV 的INTER_CUBIC并在项目说明里写明“所有下采样和上采样均使用 OpenCV 双三次插值”。如果你的作业要求对齐某篇论文的数字就按那篇论文的实现选库别混用。还有一个更稳妥的做法直接用论文作者公开的 h5 或 MAT 数据绕开插值差异这个坑。5.2 归一化范围错位PSNR 算出上百或恒低现象训练 loss 很漂亮但 evaluate 时 PSNR 动不动 100 dB或者恒低在 15 dB 以下。原因训练代码里把像素除以 255 归一化到 0 到 1但评估代码直接读原始整数图像或者反过来了。PSNR 公式里 MAX 值只在 0 到 1 范围内才是 1.0如果像素值范围不匹配mse 会被无限放大或缩小。解决训练和评估各自只做一次图像读取读取后立刻统一转 float 并除以 255。不要在训练脚本里归一化一次在评估脚本里又用另一种方式处理。我建议把归一化写成一个公共函数训练和评估共用同一个函数避免两处逻辑将来不一致。5.3 数据泄漏验证集 PSNR 虚高现象训练时验证集 PSNR 一路冲到 35 dB 以上比论文还好但测试集结果只有 28 dB 左右明显不科学。原因T91 作为训练集Set5 作为测试集这个组合本来是合理的。但如果你的预处理脚本把 Set5 的图片也裁进了训练数据或者随机裁剪时种子设置不当导致测试图像块混入训练模型就提前见过“答案”了。解决训练集和测试集目录物理隔离训练脚本只读取训练目录下的文件评估脚本只读取测试目录。两个脚本使用不同的随机种子并且在项目说明里明确列出数据集来源和划分方式。如果你从网上下载的数据集压缩包本身就把图片混在一起先做一次目录清洗再开始预处理。5.4 GPU 显存不足用梯度累积撑住 batch size现象patch_size 设成 96、batch_size 设成 32一跑训练就 CUDA out of memory但调小 batch 后训练效果明显变差。原因超分训练的张量比较大。96×96×3 的输入图经过 64 通道卷积后中间特征图尺寸是 96×96×64batch 32 时显存占用是个不小的数字。强行调小 batch 会让梯度估计噪声变大收敛不稳定。解决保持名义 batch_size 不变改用梯度累积。每 4 个 step 更新一次参数等效于 batch_size 32但瞬时显存占用只相当于 batch 8。accum_steps 4 optimizer.zero_grad() for i, (lr, hr) in enumerate(loader): loss criterion(model(lr), hr) / accum_steps loss.backward() if (i 1) % accum_steps 0: optimizer.step() optimizer.zero_grad()注意将 loss 除以 accum_steps这样梯度累积后的均值与一次跑大 batch 的梯度量级一致。如果显存还是不够优先把 patch_size 从 96 降到 64这是对超分模型参数影响最小的一步。5.5 项目说明和代码脱节评审按文档跑不通现象说明文档写得像综述但里面没有一条能直接执行的命令。评审按文档步骤打开终端第一步就报错“找不到 module”。原因把“项目说明”写成了“课程论文”重点全放在算法背景和公式推导上没有覆盖复现所需的环境、数据路径、运行顺序和预期输出。解决项目说明里至少包含四块环境依赖清单、数据准备命令、训练与评估命令、最终结果的表格或截图。把命令按顺序编号保证别人复制粘贴能跑通。我自己的习惯是写完代码后按照 README 的命令从头到尾执行一遍用一个干净环境验证凡是执行不了的命令当场改掉。这一步比改十版算法结构都加分。6. 交作业前的进阶技巧命令行、对比图与实验记录最后一章讲三个能让你的源码和项目说明立刻上档次的小技巧都是实际评审时容易留下好印象的点。6.1 用 argparse 固定超参与随机种子直接写在代码里的超参数交到别人手里没法跑出你报告的效果。把关键参数全部提到命令行参数里是最低成本的改进。# train.py import argparse import torch parser argparse.ArgumentParser(descriptionSRCNN training) parser.add_argument(--scale, typeint, default2) parser.add_argument(--epochs, typeint, default50) parser.add_argument(--lr, typefloat, default1e-4) parser.add_argument(--batch_size, typeint, default16) parser.add_argument(--seed, typeint, default42) args parser.parse_args() torch.manual_seed(args.seed)固定随机种子这一点容易被忽略。超分训练里有数据加载顺序、权重初始化等多个随机源不固定种子的话同一个人同一套代码两次跑出来结果都会略有差异。写进参数表再在项目说明里给出完整的命令模板别人复现你的数字时就不再依赖运气。6.2 输出三列对比图LR、SR、HR 拼在一张图测试结果不要只给一组数字把低分辨率图、重建图、原始高分辨率图横向拼在一起是评估阶段最有说服力的交付物。# evaluate.py 内的拼接逻辑 import cv2 import numpy as np lr_np cv2.cvtColor(lr, cv2.COLOR_RGB2BGR) sr_np cv2.cvtColor(sr, cv2.COLOR_RGB2BGR) hr_np cv2.cvtColor(hr, cv2.COLOR_RGB2BGR) comparison np.hstack([lr_np, sr_np, hr_np]) cv2.putText(comparison, fPSNR {psnr_val:.2f} dB, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imwrite(result_scale2.png, comparison)注意图片缩放显示时保持三列等宽才能在视觉上体现超分效果。我也习惯在图上直接标注该图的 PSNR这样评审看到图时不用翻文字部分。6.3 用 CSV 记录实验每个实验留个后悔药调参过程中真正容易忘的不是代码而是“为什么这次结果比上次高”。我现在养成的习惯是每次实验跑完把关键参数和结果追加进一个 CSV 文件import csv with open(experiments.csv, a, newline) as f: writer csv.writer(f) writer.writerow([args.scale, args.epochs, args.lr, args.batch_size, val_psnr, val_ssim, T91 train, Set5 val])这张表不仅是你的实验记录还是项目说明里对比表的原始素材。老师说“把这个实验补一组 3 倍超分”你能直接从 CSV 里翻出之前跑过的参数组合而不是重新猜一遍。超分复现最怕的不是踩坑是踩完坑忘了坑在哪。希望这个习惯和上面这些方法能帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进