简介:以超分辨率算法复现为核心的课程大作业项目包,面向计算机、数学、电子信息等专业读者,适合作为数字图像处理课程的期末大作业、课程设计或毕业设计参考。项目基于MATLAB编写,包含26个源代码文件(.m),并配有41张BMP格式测试图像,覆盖低分辨率(LR)、双三次插值(BI)与超分辨率重建(super)等多组实验场景;说明文档采用Markdown格式,清晰梳理了算法原理、代码结构与复现流程,可帮助读者快速理解从低分辨率输入到高分辨率重建的核心步骤。压缩包共68个文件,总大小仅13.46MB,代码、位图图像与说明文档分离存放,目录按实验模块划分,结构简洁,下载后即可直接运行调试。对于希望在已有代码基础上二次开发、扩展算法功能的读者,项目结构也提供了较为清晰的起点。目前已有264人学习浏览,适合具备一定图像处理与MATLAB基础的同学参考借鉴。
1. 数字图像处理大作业:超分辨率算法复现到底在复现什么
超分辨率算法复现,表面上是把一张低分辨率图放大成高分辨率图,实际上是在补回降采样时丢掉的细节。数字图像处理大作业里,这个题目的含金量不在「能放大」,而在「放大后不糊、不出现伪影、指标可复现」。拿到一份「源码+项目说明.zip」,第一反应是解压后直接训练;做过的都清楚,这份源码里真正决定成败的是数据退化方式、评估口径和训练参数,模型结构反而只是其中一环。这篇笔记按做完一整套复现的路径展开:先讲清 SRCNN、FSRCNN、ESPCN 怎么选,再落到数据管线、训练循环和 PSNR/SSIM 口径,最后把踩过的坑一条条摆出来。适合正在做课程大作业,或打算把超分当入门方向的同学。
2. 超分算法选型与原理:SRCNN、FSRCNN 与 ESPCN 的复杂度边界
如果大作业只允许交一份模型,我建议复现 FSRCNN;如果老师要求讲原理,从 SRCNN 讲起。三个模型本质是同一套逻辑——用卷积从低分辨率图像块里学出高分辨率细节的映射规则——但工程取舍差别很大。SRCNN 是第一个把深度学习完整带进超分的工作,三组卷积就能讲完「特征提取-非线性映射-重建」;FSRCNN 把上采样挪到网络尾部,速度提升一个数量级还不明显掉点;ESPCN 用亚像素卷积,是后来工程部署里最常见的上采样方案。
2.1 为什么先复现 SRCNN:三块卷积搭出超分的完整逻辑
SRCNN 的思路是先拿 bicubic 插值把 LR 放大到目标尺寸,再让网络在这个「放大但模糊」的图上做重建。输入是三通道的大图,第一层用 64 个 9x9 卷积核在图像块级别提特征,第二层用 32 个 1x1 卷积核做非线性映射,最后一层用 3 个 5x5 卷积核重建出 HR,全程 ReLU 激活,损失就是输出与真值的 MSE。这个结构当时把传统稀疏编码方法远远甩开,而且代码量极小,适合作为大作业的 baseline。
答辩时用 SRCNN 能讲清楚一个核心问题:超分为什么是「学」出来的而不是「插」出来的。bicubic 插值只假设相邻像素的平滑性,遇到边缘和纹理就直接糊掉;SRCNN 的卷积核在训练中逐渐学会了从「模糊块」匹配到「清晰块」的规则,等效于一个数据驱动的字典映射。MSE loss 直接优化像素均方误差,所以 PSNR 天然对齐这个目标。它的问题也明显:输入先被 bicubic 放大,特征计算都发生在高分辨率空间,计算量大;整个网络浅,感受野小,重建复杂纹理的能力有限。
2.2 FSRCNN 与 ESPCN 的取舍:速度、反卷积棋盘格与整数倍放大
FSRCNN 的关键改动是去掉了 pre-upsampling。网络先在 LR 空间把头部的 5x5 卷积(56 通道)做特征提取,再用 1x1 卷积把通道压到 12,接着过 4 层 3x3 卷积做映射,尾部用 1x1 升回 56 通道,最后用 9x9 的转置卷积一步放大到目标尺寸。因为大部分计算都发生在低分辨率空间,FSRCNN 的训练和推理速度比 SRCNN 快一个数量级,效果还能略好一点,这也是它在课程项目里受欢迎的原因。
复现 FSRCNN 时最常遇到的视觉问题是棋盘格伪影:转置卷积的 stride 大于 1 时,相邻卷积核的输出在重叠区域权重不一致,周期性高低亮形成方格纹理,高分辨率重建任务里这个现象非常扎眼。想绕开它,就换成 ESPCN 的亚像素卷积:尾部输出 r² 个通道,用 PixelShuffle 按位置重排成一张放大 r 倍的图,比如 4 倍放大就是 16 个通道拼成一张大图,没有重叠计算,也就没有棋盘格。对大作业来说,我更推荐把 FSRCNN 复现出来,再把尾部换成 PixelShuffle 当「改进点」写进报告——这个改动比换一个大模型更能体现你对上采样原理的理解。
2.3 参数对照表与选型判断:网络深度、patch 大小与工况匹配
| 对比项 | SRCNN | FSRCNN | ESPCN |
|---|---|---|---|
| 主计算空间 | HR(先 bicubic 放大) | LR | LR |
| 上采样方式 | 前端插值 | 尾部转置卷积 | 尾部亚像素卷积 |
| 典型结构 | 9x9-1x1-5x5,64-32-3 | 5x5 提特征 + 4 层 3x3 映射 | 3x3 特征 + PixelShuffle |
| 参数量级 | 约 5-8 万 | 约 8-12 万 | 接近 FSRCNN |
| 棋盘格风险 | 无 | 有 | 低 |
| 适合的角色 | 原理基线 | 课程复现主力 | 工程部署/改进方向 |
选型判断上我的经验是:如果作业要求对比实验,用 SRCNN 做 baseline、FSRCNN 做改进模型,再加一组 bicubic 插值做传统方法对照,三组结果就能撑起完整的分析;如果只求最终效果和速度,直接 FSRCNN;如果训练数据只有几百张图,别碰大模型,SRCNN 和 FSRCNN 这种小网络几千步内就能收敛到可用的效果,大模型在这个数据量下反而过拟合。
3. 用 PyTorch 跑通最小超分复现:数据管线、训练循环与模型保存
拿到「源码+项目说明.zip」之后,第一步不是读模型结构,而是确认数据是怎么进网络的。超分训练集是成对的:HR 原图和由 HR 退化生成的 LR。退化方式直接决定模型学什么,最常见的约定是用 bicubic 插值做 4 倍降采样。这一章把链路完整过一遍:退化函数的正确写法、三个必调参数、训练循环与 checkpoint 策略。
3.1 数据集与退化管线:bicubic 4x 降采样在 PIL 和 PyTorch 里如何对齐
数据集方面,DIV2K 是超分论文里最常见的训练集,800 张高清图做训练、100 张做验证;课程环境跑不动全量的话,取其中前几十张再配合随机裁剪完全够演示训练。真正容易出问题的是退化一致性的坑:训练时用 PIL 生成 LR、测试时却用 OpenCV 生成 LR,两边 bicubic 内核有细微差异,PSNR 会因此浮动 0.2-0.5 dB。更麻烦的是有些论文的数字是用 MATLAB imresize 出来的,PyTorch 里想对齐它,得把 F.interpolate 的参数设对。我一般把退化统一放进一个函数里。
import torch import torch.nn.functional as F def make_lr(hr_tensor, scale=4): # hr_tensor: [B, C, H, W], 值域 [0, 1] lr = F.interpolate( hr_tensor, scale_factor=1.0 / scale, mode="bicubic", align_corners=False, antialias=True, ) return lr逻辑说明:这个函数在输入 HR 上用 bicubic 插值做降采样,输出就是和 HR 配对的 LR。align_corners=False 表示像素中心对齐,这更接近 MATLAB 和 PIL 的坐标系约定,换 True 的话结果会整体平移半个像素,训练和测试混用两种设置,模型会在对齐上反复横跳;antialias=True 会在降采样前应用低通滤波,避免摩尔纹和混叠,这是做超分数据时很少有人注意但影响很大的开关。
参数说明:scale_factor 建议写成分数 1/scale,不要用整数,部分环境里整数 scale_factor 会按「向上取整到最近整数倍」处理,4 倍场景还好,3 倍时尺寸就错了;测试时如果不想依赖 scale,可以直接传 size=(h // scale, w // scale),两种写法结果等价,但整套代码里只能统一用一种,混用会让维度对不上。
3.2 三个必调参数:patch size、归一化范围与学习率
超分训练通常不直接整图进网络。显存是限制,但更重要的是随机裁剪本身是最强的数据增强。标准做法是从 HR 图里随机裁 96x96 或 128x128 的 patch,再降采样成 24x24 或 32x32 的 LR。patch 大小直接影响感受野覆盖:SRCNN 输出只依赖输入局部,patch 小也能收敛;FSRCNN 有 4 层 3x3 映射加尾部大卷积核,patch 太小的话边缘效应占比变高,验证指标会慢半拍。我用 96x96 HR patch 配 4 倍降采样、batch size 16,单张普通显卡就可以稳定跑。
| 参数 | 推荐值 | 出错信号 |
|---|---|---|
| HR patch size | 96x96 或 128x128 | 训练 PSNR 涨但验证不稳,多半是 patch 偏小 |
| 归一化范围 | 统一到 [0,1] | 输出 min/max 与输入不一致就是归一化没对齐 |
| 学习率 | Adam,1e-4 起步,20 轮后减半 | loss 震荡就降到 1e-5,loss 不动就检查 LR 是否正确降采样 |
归一化的坑最隐蔽:训练时把 HR 和 LR 都除以 255 归一到 [0,1],测试时也要对输入做同样处理,否则模型输出会整体偏到接近 0 的黑色图像,PSNR 直接崩到十几 dB。我习惯在 dataset 的__getitem__里完成归一化,这样训练和验证走同一条数据通路,谁都不会漏掉这一步。学习率方面,SRCNN/FSRCNN 这种小网络不需要 warmup,但建议开梯度裁剪,数值设 0.5 左右,能有效压住尾部转置卷积的梯度震荡。
3.3 训练主循环与 checkpoint:按 PSNR 保存 best 模型
给出一个能直接改用的训练骨架。模型以 FSRCNN 为例,因为它结构紧凑、训练快,适合课程环境反复调参。模型定义和训练循环拆成两块,方便你单独替换网络结构。
import torch import torch.nn as nn class FSRCNN(nn.Module): def __init__(self, scale=4, d=56, s=12, m=4): super().__init__() self.head = nn.Conv2d(3, d, 5, padding=2) # 特征提取,LR 空间 self.shrink = nn.Conv2d(d, s, 1) # 通道降维,省计算 self.map = nn.Sequential( *[nn.Conv2d(s, s, 3, padding=1) for _ in range(m)] ) # 非线性映射 self.expand = nn.Conv2d(s, d, 1) # 通道升维 self.tail = nn.ConvTranspose2d( d, 3, 9, stride=scale, padding=4, output_padding=scale - 1 ) # 尾部放大 self.relu = nn.ReLU(inplace=True) def forward(self, x): x = self.relu(self.head(x)) x = self.relu(self.shrink(x)) x = self.relu(self.map(x)) x = self.relu(self.expand(x)) x = self.tail(x) return x逻辑说明:head 卷积只在 LR 图上提特征,shrink 把通道从 56 压到 12,让 4 层 3x3 映射的运算量大幅下降;expand 恢复通道后交给转置卷积放大。d=56、s=12、m=4 是论文里的常用配置,参数量约 8 万,小数据集上几十个 epoch 就能看到趋势。转置卷积的 output_padding 是为了配合 stride 把输出尺寸精确对齐到输入尺寸的整数倍,少写这一项,4 倍放大时输出会差一行像素。
model = FSRCNN(scale=4).cuda() opt = torch.optim.Adam(model.parameters(), lr=1e-4) crit = nn.MSELoss() best_psnr = 0.0 for epoch in range(30): model.train() for hr in loader: # hr: [B, 3, 96, 96] lr = make_lr(hr, 4) # [B, 3, 24, 24] sr = model(lr) loss = crit(sr, hr) opt.zero_grad() loss.backward() nn.utils.clip_grad_norm_(model.parameters(), 0.5) opt.step() psnr = validate(model, val_loader) # 口径见第 4 章 if psnr > best_psnr: best_psnr = psnr torch.save( {"state_dict": model.state_dict(), "psnr": psnr}, f"checkpoints/fsrcnn_x4_best.pth", )逻辑说明:每个 batch 里 HR patch 先降采样成 LR,网络输入 24x24、输出目标 96x96,loss 直接在 HR 分辨率下计算,这正是超分「从 LR 重建 HR」的定义。clip_grad_norm_ 放在 backward 之后,数值 0.5 对这类小网络足够温和。checkpoint 按验证 PSNR 最优保存,同时记录 PSNR 数值,方便事后回溯哪个 epoch 的权重最好,也方便断点续训前确认上一次跑到什么水平。
参数说明:epoch=30 只是演示门槛,论文级训练要跑几百轮;更省心的做法是早停——验证 PSNR 连续 10 轮不涨就停,比固定轮数更稳,也省电。checkpoint 文件名里带上 scale 和当前指标,避免后面做 2x/3x 对比实验时把权重搞混。
4. 评估超分结果:PSNR 与 SSIM 的计算口径与可复现基线
超分论文里最常见的一句话是「我们的方法在 Set5 上 PSNR 达到 XXX dB」。但同一个模型,不同人复现出来能差 0.5 dB 甚至更多,问题几乎都出在评估口径。这一章把计算口径定死,再给一个能直接替换的验证脚本。
4.1 PSNR 的 Y 通道计算口径:边界裁剪、MAX 值与 float 精度
PSNR 的定义很简单:10 乘以 log10(最大像素值平方除以均方误差)。但落地时有三个细节必须统一。第一,只在 Y 通道算。RGB 转 YCbCr 后取亮度分量 Y 计算 MSE,这是超分论文的默认约定,因为人眼对亮度变化最敏感;直接在 RGB 三个通道上算,颜色噪声也会被计入,数字会偏低且不稳定。第二,边界裁剪。网络输出在图像边缘的像素不完全可信,计算前先把四周各裁掉若干像素,常见做法是裁 8 像素或 scale 像素。第三,MAX 值跟归一化范围走:数据在 [0,1] 则 MAX=1,在 [0,255] 则 MAX=255。还有一个精度坑:MSE 很小时 float32 的累加误差会影响小数点后两位,计算时用 float64 更稳。
提示:报告里写 PSNR 一定要带上四个条件——Y 通道、边界裁剪值、MAX 值、测试集名称。少一个,别人就没法复现你的数字,这在课程答辩里等于送分题变送命题。
4.2 SSIM 与视觉对比:为什么 PSNR 高 0.3 dB 看不出差别
SSIM 在局部窗口内比较两幅图像的亮度、对比度和结构,三个分量相乘得到 0 到 1 之间的相似度,窗口默认 11x11、高斯加权。它的意义在于弥补 PSNR 的盲区:MSE loss 训练出来的模型倾向于输出「安全」的平滑结果,因为把边缘附近的值平均化能让整体误差更小;这样的图 PSNR 往往不低,但放大看纹理是糊的。相反,一个模型如果敢于重建锐利边缘,哪怕位置偏了半个像素,PSNR 会被边缘误差拖低,肉眼却觉得更清楚。
所以课程报告里,PSNR/SSIM 表只能算一半证据,另一半是视觉对比图。我习惯固定三个视角:整体图、一个含密集纹理的区域(比如树叶或砖墙)、一个人物脸部边缘区域,每行放 HR、bicubic、模型输出三张,裁剪相同位置放大到同样尺寸。答辩时这张图比任何指标都直观。
4.3 用 Set5 做批量验证的脚本骨架
Set5 是超分论文里出镜率最高的测试集,五张图、单张尺寸不大,CPU 都能在几分钟内跑完,适合课程作业反复验证。下面这段把 PSNR 和 SSIM 的实现直接写出来,不依赖 skimage 的版本差异,也方便你贴进报告附录。
import numpy as np import torch import torch.nn.functional as F def y_channel(img): # img: [1, C, H, W], float, [0,1] r, g, b = img[:, 0], img[:, 1], img[:, 2] return 0.257 * r + 0.504 * g + 0.098 * b + 16.0 / 255.0 def calc_psnr(sr, hr, border=8, max_val=1.0): sr_y = y_channel(sr.detach().cpu())[:, :, border:-border, border:-border] hr_y = y_channel(hr.detach().cpu())[:, :, border:-border, border:-border] mse = ((sr_y - hr_y) ** 2).mean(dtype=torch.float64) if mse.item() < 1e-12: return float("inf") return 10.0 * np.log10(max_val**2 / mse.item()) def calc_ssim(sr, hr, window_size=11): L = 1.0 c1, c2 = (0.01 * L) ** 2, (0.03 * L) ** 2 kernel = torch.ones(1, 1, window_size, window_size, device=sr.device) kernel /= kernel.sum() x = sr.mean(dim=1, keepdim=True) y = hr.mean(dim=1, keepdim=True) mu_x = F.conv2d(x, kernel, padding=window_size // 2) mu_y = F.conv2d(y, kernel, padding=window_size // 2) var_x = F.conv2d(x * x, kernel, padding=window_size // 2) - mu_x * mu_x var_y = F.conv2d(y * y, kernel, padding=window_size // 2) - mu_y * mu_y cov = F.conv2d(x * y, kernel, padding=window_size // 2) - mu_x * mu_y up = (2 * mu_x * mu_y + c1) * (2 * cov + c2) down = (mu_x**2 + mu_y**2 + c1) * (var_x + var_y + c2) return (up / down).mean(dtype=torch.float64).item()逻辑说明:calc_psnr 先转到 Y 通道,再按 border=8 裁边,MSE 用 float64 累加;calc_ssim 用均值卷积替代窗口统计,实现的是简化版但口径与 skimage 一致,足够课程报告用。验证时先对模型输出做 clamp(0,1),避免个别像素越界把 MSE 拉高,再与 HR 对齐计算。算完五张图,取平均 PSNR 和平均 SSIM。
5. 超分复现避坑:解压源码后最常踩的五个翻车现场
「源码+项目说明.zip」解压只是一个开始。真正耗时间的是训练和评估过程中的隐性坑。这一章把最容易翻车的五类问题列出来,每条按现象、原因、解决展开,都是我实际遇到过或者帮人排查过的。
5.1 现象:zip 解压到一半报「文件名过长」或出现乱码目录
Windows 自带解压工具处理 Linux/macOS 打包的 zip 时,经常报「无法解压,文件名过长」或者解压出乱码目录。原因是压缩包里是 UTF-8 编码的路径,Windows 资源管理器按本地代码页解析,两边对不上;另外项目路径嵌套太深也会触发 MAX_PATH 限制。解决方式是装 7-Zip,解压时选「解压到当前文件夹」,它对编码的处理更稳;如果路径确实太长,把解压目标直接放 D:\sr_project 这种短目录。另一个相关教训:解压后先看包里的文件清单。如果包含 .pth 或 .ckpt 权重文件,优先用 torch.load 验证能否加载;旧版本权重在新版 PyTorch 里偶尔加载报错,这不是你的代码问题,看清报错是缺 key 还是序列化格式不兼容。
5.2 现象:训练 loss 正常下降,但验证 PSNR 卡在 20 dB 附近不动
loss 在降、学习率在调,验证集 PSNR 却一直 20 dB 出头,这种情况我见过太多次。先别怀疑模型结构,大概率是评估口径错了:模型输出还在 [-1,1] 或反归一化前的状态,直接和 [0,1] 的 HR 算 MSE,数字自然上不去;另一种常见低级错误是把 LR 当成 SR 去和 HR 比,降采样图本来就糊,PSNR 当然低。排查手段是打印 model 输出和 HR 的 min/max,确认值域同源;再把 LR 也丢进 PSNR 脚本算一遍当基线。Set5 上 4 倍 bicubic 基线大概在 26 dB 上下,SRCNN 能到 30 dB 左右。如果模型结果低于 bicubic 基线,要么训练没收敛,要么评估代码有 bug,二选一。
5.3 现象:测试输出图出现明显的方格或棋盘格纹理
棋盘格是转置卷积放大最典型的特征。原因是 stride 大于 1 的转置卷积,相邻卷积核的输出在空间上重叠,重叠区域的权重叠加不均匀,形成周期性高亮。FSRCNN 尾部用 9x9 stride=4 的转置卷积,这个现象在纹理密集区域尤其明显。解决方案有三条,按优先级排:把尾部换成 PixelShuffle,也就是 ESPCN 的做法,从根上消除叠加不均;或者放弃尾部放大,回到 SRCNN 那样先 bicubic 放大再做卷积;如果坚持转置卷积,就加大卷积核并用奇数,配合 output_padding 把尺寸对齐,能减轻但很难完全消除。这个坑在报告里反而是加分项——把「棋盘格成因与 PixelShuffle 改进」写清楚,比堆更多训练技巧更能体现功底。
5.4 现象:PSNR 比论文低 0.5 dB 以上,反复检查也找不到原因
完全按论文结构复现,Set5 上还是比论文低零点几个 dB,这是复现党的常态,不是 bug。主要来源有两个:一是退化差异,论文的 bicubic 大多是 MATLAB imresize 生成,PyTorch 的 F.interpolate 和 PIL 的 resize 都有细微差异,叠加几张图就能差 0.2-0.4 dB;二是测试协议差异,部分论文会做 8 种姿态(翻转+旋转)推理取平均,这种自集成能提 0.1-0.3 dB,但很多复现者不知道。应对方式是把这些差异写进实验条件:标明「我们使用 PyTorch bicubic 退化、无自集成」,并给出一组 bicubic 基线的本地参考值。老师看的是分析和对比是否严谨,不是你和论文数字差多少。
5.5 现象:训练集指标很漂亮,放到真实照片上一塌糊涂
模型在 Set5 上 PSNR 很高,拿手机拍的照片一放大,效果反而不如 bicubic,原因只有一个:退化分布不匹配。真实照片的退化包含失焦模糊、传感器噪声、压缩伪影等,而你训练时只用了干净的 bicubic 降采样,模型根本没有见过这种输入。如果作业允许,在数据管线里随机加高斯模糊和噪声,或者直接用真实 LR/HR 对做微调,能明显改善;如果只是固定课题,报告里一定写一节「局限性」,说明当前模型假设退化是理想 bicubic,真实场景需要重新估计退化核。这一句话能避免答辩时被问到哑口无言。
6. 先跑通再调优:用一张 512x512 图验证整条超分链路
拿到任何超分源码,我都不会直接开训练,而是先做单图端到端验证。取一张 512x512 的清晰图,用训练同款退化方式降到 128x128,加载模型推一版,然后按顺序检查四件事:输入的 shape 是否是 (1,3,128,128),输出是否是 (1,3,512,512);输出的 min/max 是否落在 [0,1];保存成 PNG 后和 bicubic 放大结果对比,看是更清晰还是有棋盘格;最后算一次 PSNR,确认高于 bicubic 基线。这套流程跑完,数据退化、上采样配置、归一化约定、保存逻辑这些最容易出错的部分基本都暴露了。
我把这个逻辑固定成独立脚本 quick_test.py,每次改完参数先跑它,确认链路没断再开训练,几乎成了整个项目的冒烟测试。提交作业前,也用它生成报告需要的对比图。
| 检查项 | 期望值 | 出错时看哪里 |
|---|---|---|
| 输入 shape | (1, 3, H/scale, W/scale) | 数据管线退化因子是否统一 |
| 输出 shape | (1, 3, H, W) | 转置卷积 stride/output_padding、PixelShuffle 的 r |
| 输出值域 | [0,1] 与 HR 同范围 | 训练/测试归一化是否走同一段代码 |
| PSNR 对比 | 高于 bicubic 基线 | 模型是否没加载权重、评估口径是否一致 |
| 保存图 | 无棋盘格、无黑边 | 上采样方式、边界 padding 方式 |
这个习惯救过我很多次,因为超分项目里最耗时的从来不是训练,而是「模型文件没拷全、依赖版本不对、退化方式不一致」这类不费脑但极磨人的问题。数据、模型、评估各用一个独立脚本隔开,哪个环节出问题就只动哪个文件,其他一律不碰。这套做法让我少熬了太多夜。希望帮到你。
本文还有配套的精品资源,点击获取