简介:数字图像处理超分辨率算法复现项目,面向计算机、数学、电子信息等专业课程设计、期末大作业与毕业设计参考,也适合希望深入经典图像复原算法的爱好者学习。资源包含完整源码与项目说明,从低分辨率图像读取、插值放大到超分重建均有覆盖,并附有对照实验结果,可帮助理解算法内部流程及不同阶段的效果差异。压缩包共68个文件,以26个m脚本源码为核心,辅以41个bmp实验图像与1个md说明文档,整体仅13.46MB,轻量便捷;其中m脚本对应算法主程序与功能函数,bmp图像为低分辨率输入、双三次插值及超分输出示例,md文档简要说明项目结构与运行要点。目录按code、src、pict、LR、BI、super等模块组织,便于按处理环节检索和二次调试。目前已有263人学习使用,适合具备基础图像处理知识、希望快速复现算法并完成课程任务或毕业设计的读者。
1. 超分辨率算法复现到底在复现什么:先搞清楚这 30 分的作业要你交什么
每年到了数字图像处理期末,超分辨率(Super-Resolution, SR)几乎都是大作业里的“钉子户”。这个标题里最关键的两个字不是“超分辨率”,而是“复现”。复现意味着你不需要提出新算法,也不需要把数学推导写到天荒地老,你需要做的是:把一篇论文里提出的超分模型,在本地用代码跑通,用公开数据集训练或推理出结果,然后把源码、说明文档和实验报告按规范打包成一个 zip 交上去。换句话说,这是“照着论文复现 + 工程化落地”的综合题,不是“研究型课题”。
现阶段主流课程里能选、能复现、又好写报告的超分算法主要是三类:传统插值法(双三次插值)、经典深度学习模型(SRCNN、FSRCNN、ESPCN、VDSR)以及生成对抗类模型(SRGAN、ESRGAN)。对大多数大作业来说,SRCNN 是首选,ESPCN 是次选,SRGAN 系列难度偏高,一周内从零复现风险大。这篇文章就围绕“怎么把一个超分算法完整复现出来”这件事展开,覆盖选型理由、数据集处理、训练代码、评估指标和踩坑记录,最后给一套可以直接写进报告里的验证与可视化方案。
如果你是第一次接触这类大作业,建议先想清楚:你手里有多少时间、机器有没有 GPU、导师对报告篇幅有没有要求。这三个问题直接决定你选哪个算法。
2. 选哪个算法复现:SRCNN、ESPCN、FSRCNN 的取舍与理由
2.1 三个主流算法的核心差异:网络结构、参数量和复现难度
先看一张思路对比,再谈选型。
| 算法 | 核心思路 | 参数量级 | 训练难度 | 报告可写深度 |
|---|---|---|---|---|
| SRCNN | 先用双三次插值放大,再用三层卷积网络做映射 | 约 5.7 万 | 低 | 中等 |
| FSRCNN | 不先放大,直接在低分辨率空间提取特征,最后用反卷积放大 | 约 1.2 万 | 低 | 中等 |
| ESPCN | 不先放大,在低分辨率空间提取特征,最后一层用亚像素卷积(PixelShuffle)重排 | 约 3 万 | 低 | 较高 |
| VDSR | 加深网络到 20 层,引入残差学习 | 约 66 万 | 中 | 较高 |
| SRGAN | 引入生成对抗网络,用感知损失替代像素损失 | 约 100 万+ | 高 | 高 |
对大作业来说,SRCNN 最稳妥的原因有两条。第一,它只有三个卷积层,网络结构简单,训练时间短,CPU 也能在半小时内完成一个初步模型在小数据集上的训练。第二,它是超分领域深度学习的开山之作,论文中讲清楚了“为什么用插值放大作为前置步骤”“为什么选择 MSE 作为损失函数”这两个关键点,报告里能引用的理论内容多。
ESPCN 也值得考虑,因为“亚像素卷积”这一个点就能在报告里单独开一节展开讲,显得你理解了“从低分辨率到高分辨率不是简单反卷积,而是特征通道重排”的本质。但 ESPCN 的 PixelShuffle 在 PyTorch 里实现起来比 SRCNN 的 Conv2d 要绕一点,部分同学会在维度变换上卡住。如果你平时没怎么写过 PyTorch,老老实实选 SRCNN。
2.2 模型实现的两种路径:从零手写还是用现成架构改
复现这件事,存在“从零手写”和“基于官方开源代码修改”两条路。大作业评分时,老师通常更看重你对代码的理解程度,而不是代码是不是完全由你一个字母一个字母敲出来的。
常见做法是:参考论文作者给出的官方实现,或 GitHub 上认可的复现版本,理解每一行代码的作用后,按自己的结构重写一遍。这种做法下的代码虽然和原版有相似痕迹,但你已经掌握了其中逻辑,能在答辩时回答“为什么这个 Conv2d 的 kernel_size 是 9”这类追问。
一个 SRCNN 的完整模型定义,用 PyTorch 写出来大概是这样的:
import torch.nn as nn class SRCNN(nn.Module): def __init__(self, num_channels=1, upscale_factor=3): super(SRCNN, self).__init__() # 第一层:图像块提取,对应论文中的 Patch extraction # 输入是经过双三次插值放大后的低分辨率图像,输出 64 个特征图 self.conv1 = nn.Conv2d( num_channels, 64, kernel_size=9, padding=9//2 ) # 第二层:非线性映射,对应论文中的 Non-linear mapping self.conv2 = nn.Conv2d( 64, 32, kernel_size=1, padding=0 ) # 第三层:重建,对应论文中的 Reconstruction self.conv3 = nn.Conv2d( 32, num_channels, kernel_size=5, padding=5//2 ) self.relu = nn.ReLU(inplace=True) def forward(self, x): x = self.relu(self.conv1(x)) x = self.relu(self.conv2(x)) x = self.conv3(x) return x这里的三个参数需要说明:第一层 kernel_size 取 9,是因为论文作者实验发现 9×9 的感受野能较好提取局部结构信息;第二层用 1×1 卷积做跨通道的信息融合,相当于一个非线性映射;第三层用 5×5 卷积输出最终的高分辨率图像。激活函数只有前两层用 ReLU,最后一层不加,因为输出是像素值,不需要把负值截断成 0。
2.3 预训练权重和“从零训练”的关系:什么时候你不用自己训
很多大作业的评分标准里,“模型是训练出来的”和“模型是加载预训练权重跑通的”是两种评价档次。想让报告有说服力,最好自己训,哪怕只训几个 epoch。但为了保险起见,也可以先加载一份预训练权重跑通推理流程,确认整个数据通路没有错误,再决定要不要从头训练。
这里有个很实用的操作链路:先用官方或第三方预训练权重跑通测试集,记录一份 PSNR/SSIM 数值作为“基准线”。然后你再从头训练,如果训出来的模型效果接近或超过这条基准线,说明复现正确;如果差得远,说明训练超参有问题,需要调。这种做法在报告里写出来,老师会认为你有工程验证意识。
3. 数据集与预处理:把公开数据转成模型能吃的格式,以及裁剪和归一化的四个坑
3.1 常见超分数据集选型:DIV2K、Set5、Set14、Urban100 怎么搭配
超分复现最常用的数据集搭配是“训练集 + 测试集”模式。训练集用来拟合模型参数,测试集用来评估模型效果。课程大作业中,通常不需要追求大规模数据集。
常见做法是:如果机器配置够用,训练集选 DIV2K 的 800 张高清图;如果机器配置一般,或者时间紧张,可以选用 T91(91 张图)和 General100(100 张图)这类小型数据集。测试集选 Set5 或 Set14,因为这两套数据在超分论文中是“标准答案”,你训练出来的 PSNR 可以直接和论文报告值对比。
选型建议如下表:
| 数据集 | 图片数量 | 用途 | 建议 |
|---|---|---|---|
| DIV2K | 800 训练 + 100 验证 | 训练 | GPU 环境下首选 |
| T91 | 91 张 | 训练 | CPU 环境或时间紧张时选 |
| General100 | 100 张 | 训练 | 和 T91 搭配使用 |
| Set5 | 5 张 | 测试 | 必选,论文对比标准 |
| Set14 | 14 张 | 测试 | 可选,增加说服力 |
| Urban100 | 100 张 | 测试 | 可选,含大量纹理细节,更能体现模型差异 |
3.2 数据对的生成逻辑:先下采样再上采样,还是先上采样再下采样
超分任务需要的是“低分辨率图-高分辨率图”对。常见做法是:把原始高清图先做高斯模糊,再按缩放因子下采样得到低分辨率图,低分辨率图作为模型输入;原始高清图作为监督信号。但实际复现中,训练输入往往不是直接拿低分辨率图,而是先对它做双三次插值上采样到目标尺寸,再送入模型。这是必要的,因为 SRCNN 内部不包含放大模块,它只做“从插值放大的模糊图到清晰图”的映射。
生成数据对的代码可以写成一个工具函数:
import cv2 import numpy as np def generate_lr_hr_pair(img, upscale_factor=3): # 原始高清图必须是 H x W x 3 或者 H x W 的数组 h, w = img.shape[:2] # 保证边长能被放大因子整除,否则下采样后再上采样尺寸会对不上 h_new = h - (h % upscale_factor) w_new = w - (w % upscale_factor) img = img[:h_new, :w_new] # 先缩小到低分辨率尺寸 lr_h, lr_w = h_new // upscale_factor, w_new // upscale_factor lr_img = cv2.resize(img, (lr_w, lr_h), interpolation=cv2.INTER_CUBIC) # 再把低分辨率图放大回原始尺寸,作为 SRCNN 的输入 lr_up = cv2.resize(lr_img, (w_new, h_new), interpolation=cv2.INTER_CUBIC) # 原始高清图是训练标签 hr_img = img return lr_up, hr_img这段代码里的尺寸对齐逻辑值得多说一句。如果不做h - (h % upscale_factor)这一步,图片尺寸不能被缩放因子整除时,低分辨率图放大回来会和高分辨率图尺寸不一致,训练时张量拼接会直接报错。这是新手最常见的翻车点之一。
3.3 训练时的 Patch 裁剪策略:为什么不能整图直接塞进网络
SRCNN 论文中,训练时是从训练图像上随机裁剪出一批固定大小的 patch 来训练的,而不是把整张大图直接送入网络。两个原因:第一,GPU 显存有限,整张大图(比如 2K 分辨率)塞不进小显存显卡;第二,随机裁剪天然起到数据增强的作用,能让模型看到更多不同的局部结构。
常见参数是:patch 大小为 33×33,stride 为 14。这里要注意,这个 33×33 是裁剪的 HR 图尺寸,对应的 LR 输入尺寸也是 33×33(因为已经把 LR 图放大到和 HR 一样尺寸了)。如果用的是 ESPCN 这类不预先放大的模型,裁剪时就要按“LR patch 尺寸 = HR patch 尺寸 / 放大因子”来设计。
PyTorch 中裁剪逻辑的一个标准实现片段:
def random_crop(img, size=33): h, w = img.shape[:2] if h < size or w < size: raise ValueError(f"图片尺寸 {h}x{w} 小于裁剪尺寸 {size},请更换训练集或调大缩放因子") top = np.random.randint(0, h - size + 1) left = np.random.randint(0, w - size + 1) return img[top:top + size, left:left + size]注意前提条件:如果裁剪尺寸大于原图,直接抛异常终止程序,这样可以避免后续训练中断以后你还一头雾水,不知道是哪张图出了问题。
3.4 归一化范围不统一导致的“伪高分”:0-255 还是 0-1 必须一以贯之
这是超分复现中涉及分数高低的一个隐藏细节。如果训练时把图片归一化到 [0, 1],评估时却用 [0, 255] 区间的像素值计算 PSNR,PSNR 数值会虚高 20 多个 dB。很多同学第一次跑完训练看到 PSNR 高达 45dB 以为模型效果爆表,后来才发现是计算时单位没统一,这类“虚假繁荣”在答辩时很容易被老师拆穿。
正确逻辑是:训练阶段把像素值除以 255 变到 [0, 1] 区间,评估阶段要么保持 [0, 1] 区间统一计算,要么把输出乘回 255 再计算。不能一半一半。建议在整个项目里固定下来,比如在项目说明文档里写清楚“所有图像均以 0-1 浮点数格式送入模型,PSNR 计算基于 0-1 区间”。这样也能避免代码在不同模块间来回传递时出现单位错乱的问题。
4. 训练与评估:用 PyTorch 从零跑通 SRCNN 的完整流程
4.1 Dataset 类与数据加载器:把上一章的预处理逻辑封装成可迭代对象
PyTorch 的数据流水线基于torch.utils.data.Dataset类。你需要实现一个自定义 Dataset,在__getitem__方法里完成读图、生成 LR-HR 对、裁剪、转 Tensor 这一整套操作。
import torch from torch.utils.data import Dataset from PIL import Image class SRCNNDataset(Dataset): def __init__(self, image_paths, upscale_factor=3, patch_size=33): self.image_paths = image_paths self.upscale_factor = upscale_factor self.patch_size = patch_size def __len__(self): return len(self.image_paths) def __getitem__(self, idx): # 读取高清图并转成 RGB 数组 img = np.array(Image.open(self.image_paths[idx]).convert('RGB')) # 生成 LR-HR 对 lr_up, hr = generate_lr_hr_pair(img, self.upscale_factor) # 在 HR 图上随机裁剪,LR 图用同一位置裁剪 h, w = hr.shape[:2] top = np.random.randint(0, h - self.patch_size + 1) left = np.random.randint(0, w - self.patch_size + 1) hr_patch = hr[top:top + self.patch_size, left:left + self.patch_size] lr_patch = lr_up[top:top + self.patch_size, left:left + self.patch_size] # 转成 float 张量并归一化到 [0, 1] lr_tensor = torch.from_numpy(lr_patch.transpose(2, 0, 1)).float() / 255.0 hr_tensor = torch.from_numpy(hr_patch.transpose(2, 0, 1)).float() / 255.0 return lr_tensor, hr_tensor这段代码的关键在于lr_patch和hr_patch使用了同一组(top, left)坐标。因为 LR 图已经通过双三次插值被放大到了与 HR 图相同的尺寸,所以两幅图的空间位置是一一对应的,可以直接用同一组裁剪参数。如果你不是先用插值放大而是直接送原始小图,那裁剪坐标就必须按放大因子换算,这又是一个容易出错的细节。
4.2 训练主循环:学习率、 BatchSize 和 Epoch 该怎么定
SRCNN 训练时常见的配置是:Adam 优化器,初始学习率1e-4,BatchSize 16,Epoch 50 到 100。如果你的训练集不大(比如 T91),50 个 epoch 已经足够;如果用的是 DIV2K,100 个 epoch 会更稳妥。学习率衰减策略可以用StepLR每 30 个 epoch 乘以 0.1,或者训练到中期手动降低学习率。
以下是一段可以直接跑的训练主循环代码:
import torch.optim as optim from torch.utils.data import DataLoader from torchvision.transforms import Compose, ToTensor # 假设 train_paths 是所有训练图片路径的列表 dataset = SRCNNDataset(train_paths, upscale_factor=3, patch_size=33) dataloader = DataLoader(dataset, batch_size=16, shuffle=True, num_workers=2) model = SRCNN(num_channels=3, upscale_factor=3) criterion = nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=1e-4) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1) num_epochs = 50 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model.to(device) for epoch in range(num_epochs): model.train() total_loss = 0.0 for lr_tensor, hr_tensor in dataloader: lr_tensor = lr_tensor.to(device) hr_tensor = hr_tensor.to(device) optimizer.zero_grad() sr_tensor = model(lr_tensor) loss = criterion(sr_tensor, hr_tensor) loss.backward() optimizer.step() total_loss += loss.item() * lr_tensor.size(0) avg_loss = total_loss / len(dataset) current_lr = optimizer.param_groups[0]['lr'] print(f"Epoch {epoch+1:03d}, Loss: {avg_loss:.6f}, LR: {current_lr:.2e}") if (epoch + 1) % 10 == 0: torch.save(model.state_dict(), f"srcnn_epoch_{epoch+1}.pth")num_workers=2表示用两个子进程读取数据,能加快数据加载速度,但如果你在 Windows 环境下运行发现报错,可以把它改成 0,问题会立即消失。损失函数固定用MSELoss,因为 SRCNN 论文的核心假设就是“像素均方误差最小化等价于最大化 PSNR”,这一点要写进报告的损失函数设计说明里。
4.3 评估指标: PSNR 和 SSIM 的计算细节,以及彩色图该算什么通道
超分任务最常用的两个评估指标是 PSNR(峰值信噪比)和 SSIM(结构相似性)。计算 PSNR 时,如果输入是彩色图,常见做法是只计算 Y 通道(亮度通道)的 PSNR,因为人眼对亮度变化最敏感。
import math import cv2 import numpy as np def calculate_psnr(img1, img2): # 输入是 [0, 255] 范围的 uint8 或 float 图像 mse = np.mean((img1.astype(np.float64) - img2.astype(np.float64)) ** 2) if mse == 0: return float('inf') return 20 * math.log10(255.0 / math.sqrt(mse)) def calculate_psnr_y_channel(sr_img, hr_img): # 转 YCbCr 色彩空间,取 Y 通道 sr_y = cv2.cvtColor(sr_img, cv2.COLOR_RGB2YCrCb)[:, :, 0] hr_y = cv2.cvtColor(hr_img, cv2.COLOR_RGB2YCrCb)[:, :, 0] return calculate_psnr(sr_y, hr_y)SSIM 计算可以直接用skimage.metrics.structural_similarity。但要注意边界处理:SRCNN 的输出边缘会有黑边或伪影,评估前通常会用shave参数去掉边缘像素,一般去掉 6 个像素的边界。这里有一个论文汇报的规律:不去边界算出的 PSNR 会比去边界低 0.3 到 0.8dB,因为边缘存在卷积边界效应。报告里要说明你用的是哪种方式,否则别人复现你结果时会得到不同的数值。
5. 复现避坑指南:从数据到代码的 5 条血泪经验
5.1 坑一:验证集 PSNR 很高,测试集上效果却一塌糊涂
现象:训练时每 10 个 epoch 在验证集上算一次 PSNR,数值一直在涨且表现得很好,但最后在 Set5 上测试,生成的高分辨率图边缘模糊,PSNR 比论文低很多。
原因:训练过程中验证集和训练集来自同一数据分布,且验证集图片可能参加过训练集的裁剪。模型过拟合到了训练集的统计特征上,没有泛化能力。
解决:训练集和测试集必须严格分开,测试集只用 Set5、Set14 这类公开标准数据集,这些数据不参与任何形式的训练。同时在报告里说明,测试时使用的模型是训练完成后保存的最优权重,而不是最后一个 epoch 的权重。保存最优权重的方法:每个 epoch 结束算一次验证集 PSNR,超过历史最大值就覆盖保存best_model.pth。
5.2 坑二:使用不同库的双三次插值,得到的数据对不一致
现象:数据预处理时用 OpenCV 的cv2.resize做双三次插值,测试阶段加载预处理好的图片时用了 PyTorch 或者 scikit-image 的 resize,最终 PSNR 结果比预期低 0.5dB 甚至更多。
原因:不同图像处理库对双三次插值的实现细节不同,虽然名字一样,但插值核的近似方式和边界处理策略有差异。你在预处理阶段生成的 LR 图和测试阶段重新缩放的图不是同一套数据,导致模型输入分布漂移。
解决:整个项目统一使用同一个库完成所有 resize 操作。我一般全用cv2.resize,因为 OpenCV 的插值实现是标准参考。如果要用torch.nn.functional.interpolate做在线缩放,就要保证训练和测试都用它。这条坑在写报告时也值得提一嘴,因为你项目说明文档里写的预处理代码和评估代码如果用了不同的库,老师一眼就能看出来。
5.3 坑三:训练 Loss 不降反升,或出现 NaN
现象:训练刚开始几个 epoch,Loss 从 0.03 降到 0.01 后突然跳到 nan,或者全程在振荡不下降。
原因:最常见的有三类——学习率过大、BatchSize 过小导致梯度方向抖动、输入图像中存在全零或全黑的异常图片,这类图片反向传播时容易让权重更新异常。另一种可能是Conv2d参数初始化不当。
解决:先看数据。加一段检查代码,在 Dataset 的__getitem__返回前检查张量方差,如果方差为 0 就打日志跳过该样本。再调小学习率到1e-5尝试,如果 Loss 能稳定下降说明原学习率大了。最后确保输入张量是float32而不是float64或uint8,否则 PyTorch 某些算子上会类型不匹配报错。
5.4 坑四:显存不足,但减小 BatchSize 后效果变差响应变慢
现象:8GB 显存的显卡,BatchSize 设为 16 时直接 Out of Memory;改到 4 后能跑,但训练速度极慢,而且最终效果明显变差。
原因:BatchSize 过小时,Batch Normalization 统计量不准确,梯度噪声变大,模型收敛不稳定。
解决:不要只降 BatchSize,同时要降输入 patch 尺寸。比如把 patch 从 33×33 降到 21×21,显存占用会显著降低,模型照样能收敛。效果变差的补偿手段是加大训练 epoch 数量。另外可以把torch.backends.cudnn.benchmark = True加上,让 cuDNN 自动优化卷积算法,这在输入尺寸固定的情况下提升明显。
5.5 坑五:保存模型后加载推理,结果和训练时不一致
现象:训练结束后在测试集上验证并记录了 PSNR,把模型保存为srcnn.pth,期末验收现场加载模型再跑一遍测试集,结果 PSNR 低了 1dB 多。
原因:主要是训练和推理阶段的数据预处理路径不一致。训练时 LR 图经过随机裁剪、归一化后直接送进模型,推理时却走了另一套预处理代码,忘了归一化这一步,或者归一化后再astype(np.uint8)丢了精度。
解决:推理阶段和训练阶段共用同一个预处理函数,从写代码的第一天就封装一个preprocess()函数供两边调用。同时推理代码里加载模型之后要调用model.eval(),并用with torch.no_grad():包住前向推理,否则权重中的 Dropout 或 BatchNorm 行为会变成训练模式,输出结果不稳定。
6. 进阶与验收:把结果做进报告里的技巧、消融实验思路和一次复现的完整验证
如果你想在及格的基础上冲高分,这里有一个很实用的小技巧:让模型“多尺度复现”。现在的 SRCNN 代码里写死了upscale_factor=3,你可以把模型改成支持任意缩放因子的版本——训练一个 3 倍模型,在推理时通过插值降尺度到 2 倍输入来近似测试。具体做法是:推理时先对高分辨率图做 2/3 比例的双三次缩放,再送进 3 倍模型,输出的图相当于原始图的 2 倍放大。这种操作虽然不是严格意义上的多尺度模型,但能够在小作业的篇幅下展示你对尺寸关系的理解。
消融实验是报告里最容易拉开差距的部分。你可以做三组对比:A 组用论文里的 9-1-5 卷积核配置;B 组把第二层卷积核从 1×1 改成 3×3;C 组去掉 ReLU 激活函数。把三组实验的 PSNR/SSIM 和训练时间做成一张对比表,在报告里分析“更大的卷积核带来更高的重建精度但参数量和计算时间也成倍增加”“去掉 ReLU 后模型退化为线性变换,效果明显下降”。这是最安全的加分项,因为所有数据都来自你自己真实的实验记录。
完整验证流程可以按以下三步走:第一步,用预训练权重跑通推理流程,确认结果能够复现论文中预设的 PSNR 数值范围;第二步,从头训练 50 个 epoch,保存 best 模型;第三步,在 Set5 上做最终测试,输出重建结果图和原图对比,并计算 PSNR/SSIM。不要只在命令行里打印数值,要保存一张对比图——把 LR 放大图、SR 重建图、HR 原图从左到右拼在一张图里,标注各自的 PSNR 值。这张图是报告的封面图材料,也是答辩时最直观的展示形式。
最后说一个我的个人习惯:每次复现项目,我都会在项目根目录下放一个README.md,里面写清楚“用 Python 3.8 和 PyTorch 1.9 以上版本,先运行python train.py训练模型,再运行python test.py --checkpoint best_model.pth测试”。这份文件不用长,但目录结构要清晰——data/放数据集,models/放模型定义,checkpoints/放训练权重,results/放测试输出。老师打开 zip 包的第一印象,基本决定你这门课大作业的分数区间,而这个印象 50% 由项目说明文档决定。希望这套流程能帮你把这条复现路走得稳一点,省下熬夜踩坑的时间。
本文还有配套的精品资源,点击获取