简介:一份基于Python的图像超分辨率重建源码包,面向想学习或复现SR算法的开发者,覆盖数据预处理、模型定义、训练与测试完整流程。包内共5个文件,全部为Python脚本,分别承担工具函数、数据扩展、主流程、模型构建和测试评估等角色,压缩包仅10KB,轻量便于快速下载分析。已有619人学习/下载。源码对应超分辨率技术中的核心流程,可结合SRCNN、VDSR等深度学习模型思路,查看数据加载、模型搭建与训练评估的具体实现。通过阅读和运行这些脚本,能直观理解低分辨率图像到高分辨率重建的工程细节,也可基于现有模块进行二次修改与优化。整体适合配合理论文章进行动手实践,是入门图像超分辨率的实用参考。
1. 图像超分辨率重建源码:先搞清这份 Python 工程到底解决什么问题
监控截图里看不清车牌、老照片放大后人脸发虚、电商图一放大边缘全是锯齿——图像超分辨率重建(SR)就是把低分辨率(LR)图恢复成高分辨率(HR)图的技术,不是简单插值填像素。这份 Python 源码包含 utils.py、expand_data.py、main.py、model.py、test.py 五个文件,从数据增强、模型定义到训练、测试,覆盖一条用深度学习做超分的完整链路。
适合课程设计、毕业设计,或已会用 OpenCV 插值放大但嫌效果不够好的从业者。它能帮你把"从零训练一个超分模型"完整跑通,也是你换数据集、换倍率时的改造起点。
2. 算法选型与源码阅读:SRCNN、VDSR、ESPCN、SRGAN 怎么分辨
超分辨率重建本质是一个病态问题(ill-posed problem):同一个低分辨率像素块,可以对应无数种高分辨率纹理。算法干的不是"查表还原",而是在约束条件下猜出"最可能"的高频细节。"重建质量高"说的就是这个猜测足够接近真实。这也是为什么插值方法一旦放大超过 2 倍就露馅——它假设图像局部平滑,而真实世界的纹理恰恰充满高频跳变。
看源码之前,我习惯先把几条主路线在脑子里过一遍,不然拿到 model.py 根本不知道自己在看什么。超分方法分两大类:基于插值的方法和基于学习的方法。插值方法不依赖数据,拿到一张图就能算,速度快但补不出新细节;学习方法依赖大量 LR-HR 配对数据,训练阶段费时费力,但推理时能把训练中学到的纹理先验用上,效果上限高得多。这份源码里出现了 model.py 和 main.py,显然是学习方法的工程形态,具体是哪一条路线,打开 model.py 三分钟就能判断。
2.1 插值方法与学习式方法的本质区别
插值方法里最常碰见的是最近邻、双线性和双三次三种。最近邻直接复制邻近像素,放大后锯齿感明显,基本只用来做速度优先的预览;双线性对周围 2x2 像素做加权平均,图像被抹得发糊;双三次对周围 4x4 像素做多项式拟合,是 OpenCV 里cv2.INTER_CUBIC的实现,也是超分领域生成 LR 图最常用的降采样方式。插值的本质是平滑假设,它只能把已有信息抹匀,无法凭空生成新的高频纹理,所以放大倍数一高,边缘和纹理都撑不住。
学习式方法走的是另一条路。传统机器学习做法先手工提取特征,再用稀疏编码或近邻检索把 LR 块映射到 HR 块,特征工程和字典学习是两套独立流程,工程上很绕。深度学习方法把特征提取、非线性映射、图像重建揉进一个端到端网络里,输入 LR、输出 HR,中间过程全部自动学,这也是 2014 年 SRCNN 之后的主流形态。
| 方法 | 核心思路 | 优点 | 局限 |
|---|---|---|---|
| 最近邻插值 | 复制最近的像素 | 计算开销最低 | 锯齿严重 |
| 双线性插值 | 2x2 邻域加权 | 平滑过渡 | 细节全部抹平 |
| 双三次插值 | 4x4 邻域多项式拟合 | 边缘比双线性好 | 无法恢复真实纹理 |
| 深度学习 | 端到端学习 LR→HR 映射 | 能生成新细节 | 依赖数据与算力 |
这张表是选型的第一层判断:如果你的需求是"放大后别太糊就行、不能等 GPU",插值就够了;如果追求"恢复出纹理细节",才需要往下看学习式方法。这份源码既然训练、测试脚本齐全,目标显然在后者。
2.2 四个经典网络架构:SRCNN、VDSR、ESPCN、SRGAN 的选型逻辑
SRCNN 是 2014 年第一篇把 CNN 引入超分的文章。结构上只有三层卷积:特征提取、非线性映射、重建。注意它的输入不是原始 LR,而是先双三次插值放大到目标尺寸再进网络,所以卷积计算全部发生在高分辨率空间,算力开销大,感受野也小。作为教学基线它非常合格——结构简单、调试容易、CPU 上也能跑通,很多课程源码都以它为底。
VDSR 解决的问题是"深浅"。SRCNN 只有三层,学到的高频映射能力有限,VDSR 把卷积层数加到 20 层,并引入残差学习:网络只学 LR 和 HR 之间的差值,而不是直接输出整幅图像。输入尺寸做不到太大,所以 VDSR 一般要求输入 LR 图有一定宽度的边界上下文,训练也得用小学习率慢慢磨。它的意义在于示范了"深网络 + 残差"在超分里的价值,边缘恢复比 SRCNN 明显更利落。
ESPCN 是冲着效率去的。它提出亚像素卷积(sub-pixel convolution),对应 PyTorch 里的nn.PixelShuffle:网络先在低分辨率特征图上做卷积,最后一层把多个通道重新排列成高分辨率图像。这样计算量集中在分辨率最低的阶段,推理速度很快,适合实时视频超分这类场景。代价是训练时对通道数设置比较敏感,写错排列逻辑会出现周期性伪影。
SRGAN 关注的不再是 PSNR,而是人眼感受。前几个方法都用像素级损失(L2 或 L1)训练,这种损失天然偏向平滑结果——PSNR 高,但放大后皮肤、毛发像被磨过。SRGAN 引入生成对抗结构:生成器输出 HR 图,判别器判断它是真图还是生成图,再叠加感知损失,逼迫网络生成纹理逼真的结果。视觉效果确实更强,但训练不稳定、调参成本高,而且 PSNR 常常不如前面几个。
| 架构 | 核心机制 | 优点 | 适合场景 |
|---|---|---|---|
| SRCNN | 三层卷积,先放大后重建 | 结构简单,教学友好 | 入门、CPU 训练 |
| VDSR | 20 层卷积 + 残差学习 | 边缘恢复好 | 有 GPU 的常规训练 |
| ESPCN | 亚像素卷积重排 | 推理快、省显存 | 实时/低功耗场景 |
| SRGAN | 生成对抗 + 感知损失 | 视觉纹理真实 | 追求观感而非指标 |
拿到 model.py 的时候,先对着这张表找特征:看到三层左右的卷积没有残差,大概率是 SRCNN 系;看到多个相同结构的卷积块和残差连接,往 VDSR/残差网络方向想;看到PixelShuffle就是 ESPCN 系;看到两个网络(生成器和判别器)和两个损失,那训练脚本里必然有 GAN 的训练循环。这套源码的文件列表里只有一个 model.py 和一个 main.py,最可能的是 SRCNN 或轻量残差变体——这类教学源码用 SRCNN 的概率最高,因为显存友好、容易跑通。
2.3 拿到源码先别急着跑:五分钟反推 model.py 的路线
我拿到任何一份超分源码,第一件事不是配环境,而是花五分钟读 model.py,判断项目水平、路线和潜在坑位。在终端执行:
grep -n "class\|def forward\|Conv2d\|PixelShuffle\|Upsample\|ConvTranspose2d\|ReLU\|ResidualBlock" model.py这条命令把所有类定义、前向函数、卷积层、上采样操作带行号列出来。看到Conv2d的密集程度可以判断网络深浅;看到PixelShuffle说明走了子像素路线;看到Upsample(scale_factor=, mode='bilinear')说明上采样放在网络前段;看到带残差连接的 Block 类,说明借鉴了 VDSR 或更深结构。光看这个 grep 输出,模型的骨架基本就清楚了。
再看损失函数和优化器配置,这决定训练行为:
grep -n "Loss\|optimizer\|lr\|weight_decay\|scheduler" main.pyMSELoss对应 PSNR 导向的训练目标,L1Loss在近几年的超分实现里更常见,训练更稳;Adam配1e-4左右的学习率是常规组合,SGD的话一般要配更低的学习率和动量。这一轮看完,心里就有底了:这是教学型基线还是可扩展工程,训练大概要多少显存,跑通一个能看结果的实验需要多久。
提示:如果 grep 出来的结构非常简单,比如只有两三个卷积模块、没有残差也没有 PixelShuffle,不要失望。教学源码的价值在于链路完整,而不是指标刷新。先用它把流程跑通,再逐步替换模型文件,是性价比最高的用法。
从这份源码的文件组织来看——五个文件分工明确,expand_data.py独立负责数据增强,test.py独立负责推理评测——它是典型的教学工程:不追 SOTA,目标是把超分的完整物链路在两三百行代码里走通。这也是它值得下载的原因:你改的不是一个黑匣子,而是每一步都能看懂、能改动的白盒子。
3. 环境搭建与数据准备:从零把五个文件的工程跑通
拿到源码先别急着双击 main.py。超分训练依赖 PyTorch 环境和配对数据,这两步没准备好,跑起来之后排除错的成本会翻倍。下面按我自己的操作顺序来写。
3.1 Python 环境配置:torch 版本、CUDA 和 opencv 的搭配
如果你之前已经装过 python,跳过安装那一步直接建虚拟环境。我一般用 conda,因为方便切换 pytorch 和 cuda 的版本组合,翻车后也容易整个删掉重来。网上各种 python 安装教程都行,但环境隔离这一步别省,直接在 base 环境里装 torch,早晚会因为版本冲突后悔:
conda create -n superres python=3.8 conda activate superres pip install torch==1.13.1 torchvision==0.14.1 --index-url https://download.pytorch.org/whl/cu118 pip install opencv-python numpy tqdmPython 3.8 是兼容性最稳的选择,太新的版本偶尔会和旧代码里的语法或依赖冲突。torch 版本不用追求最新,按你机器上 CUDA 的版本来;如果没装 CUDA 或者只是想先跑通逻辑,把--index-url换成 CPU 版即可,SRCNN 这种小网络在 CPU 上也能完成一次小规模训练。opencv 负责读图和双三次缩放,numpy 负责矩阵运算,tqdm 用来显示训练进度。
装完验证一下 GPU 是否可用:
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"输出True说明 GPU 可用,False也不影响先把流程跑通,只是训练速度慢一个量级。如果这里报了 CUDA 相关的版本错误,多半是 torch 和驱动不匹配,去 PyTorch 官网按 CUDA 版本重新选 wheel 就行,不要自己在终端里乱设环境变量硬凑。
3.2 数据目录怎么组织:LR/HR 配对是超分训练的命根子
超分是监督学习,训练数据必须成对出现:一张 HR 原图配一张由它降采样得到的 LR 图。常见的目录结构是这样:
data/ train/ hr/ # 高清原图,png 或 jpg lr/ # 对应降采样图,文件名与 hr 一一对应 val/ hr/ lr/LR 图怎么来?最常见也是论文默认的做法,是用双三次插值把 HR 图缩小到目标倍率:
import cv2 import os def make_lr(hr_path, lr_path, scale=2): # 用双三次插值把 HR 缩小 scale 倍,生成配对的 LR img = cv2.imread(hr_path) h, w = img.shape[:2] lr = cv2.resize(img, (w // scale, h // scale), interpolation=cv2.INTER_CUBIC) cv2.imwrite(lr_path, lr) # 对 train/hr 下所有原图生成 2x 的 LR for name in os.listdir("data/train/hr"): make_lr(f"data/train/hr/{name}", f"data/train/lr/{name.replace('.png', '_x2.png')}")这段代码的关键在cv2.INTER_CUBIC:超分领域约定俗成用双三次降采样来生成 LR,因为大多数论文、预训练模型和评价基准(比如 Set5、Set14)都默认这个退化方式。你在训练和测试时必须用同一种退化方式,否则训练时模型学的是"双三次降采样后的恢复",测试时喂给它一张别的方式降采样的图,效果必然打折。val 集不用太大,十张覆盖不同场景的图就够用来观察训练趋势,真正的指标评测放到训练结束后的测试阶段。
注意:不要用 PIL 的
Image.resize默认插值或 OpenCV 的INTER_NEAREST生成 LR,得到的结果会让模型学到错误的映射。低倍率下差异不大,4x 时差异非常明显。
3.3 expand_data.py 在做什么:数据增强的常见写法
数据集通常不大,几百张 HR 图直接训练容易过拟合,所以这份源码专门把数据增强抽成了expand_data.py。超分里的增强和分类不一样,不能随便改变像素内容,只能做几何变换:随机裁剪、水平翻转、垂直翻转、90 度旋转。这些变换不改变图像的"退化关系"——HR 和 LR 必须做完全相同的变换,配对关系才成立。
一个典型的实现长这样:
import random def get_patch(hr, lr, patch_size=96, scale=2): # 从同一位置裁出 HR/LR 小方块,位置满足 scale 倍率关系 ih, iw = lr.shape[:2] iy = random.randint(0, ih - patch_size) ix = random.randint(0, iw - patch_size) lr_patch = lr[iy:iy + patch_size, ix:ix + patch_size] hr_patch = hr[iy * scale:(iy + patch_size) * scale, ix * scale:(ix + patch_size) * scale] return hr_patch, lr_patch def augment(hr, lr): # 水平和垂直翻转增加样本量,90 度旋转提高网络对方向的鲁棒性 if random.random() < 0.5: hr, lr = hr[:, ::-1], lr[:, ::-1] if random.random() < 0.5: hr, lr = hr[::-1], lr[::-1] if random.random() < 0.25: hr = hr.transpose(1, 0, 2) lr = lr.transpose(1, 0, 2) return hr, lr这两段函数是超分训练里最标准的增强写法。注意get_patch里的坐标换算:LR 图的坐标乘上 scale 才是 HR 图里对应的坐标,这一步算错,网络对不齐输入输出,损失函数永远降不下去。patch_size 一般取 64 到 96,太大显存压力大,太小模型学不到足够的上下文。expand_data.py在你的源码里大概率就是这套逻辑,你可以直接改 patch_size 观察训练速度和效果的变化。角度旋转一次就能让样本量乘 4,配合翻转,几百张原图就能撑起一个像样的训练集。
3.4 utils.py 里的 PSNR 与 SSIM:先搞懂评测口径再谈效果
utils.py里通常放着评测指标,最常见的是 PSNR 和 SSIM。PSNR 的公式看着简单,实现里全是细节:超分评测一般只算 Y 通道(亮度),因为人眼对亮度最敏感,RGB 三通道全算反而会稀释指标。
import cv2 import numpy as np def psnr_y(img1, img2, shave=4): # 输入是 0-255 的 RGB 图,先转 YCbCr 再取 Y 通道 y1 = cv2.cvtColor(img1, cv2.COLOR_BGR2YCrCb)[..., 0].astype(np.float64) y2 = cv2.cvtColor(img2, cv2.COLOR_BGR2YCrCb)[..., 0].astype(np.float64) # 裁掉边缘 shave 像素,避免边界效应对指标的影响 y1 = y1[shave:-shave, shave:-shave] y2 = y2[shave:-shave, shave:-shave] mse = np.mean((y1 - y2) ** 2) if mse == 0: return float("inf") return 10.0 * np.log10(255.0 * 255.0 / mse)三个细节值得记住:第一,必须裁边,shave 通常取 scale 或者 scale 的整数倍,因为网络边缘的插值/反卷积结果天然不可靠;第二,范围是 255 而不是 1,说明输入图是 0-255 的 uint8,如果你代码里归一化到了 [0,1],公式里的 255 要相应改成 1;第三,channel 顺序是 BGR 是 OpenCV 的默认,换成 PIL 读图就是 RGB,转换函数要对应。SSIM 直接调skimage.metrics.structural_similarity就行,注意设对data_range=255和channel_axis参数,同样只算 Y 通道、同样裁边,参数保持一致才有可比性。
这套评测口径直接决定了你在 test.py 里看到的数字是否有意义。如果一篇博客说 PSNR 33dB,另一篇说 29dB,先别急着下结论,先确认它们的评测口径是否一致——shave 大小、是否只算 Y 通道、LR 生成方式,任何一个不同,指标就差出两三个 dB,这是超分领域最大的"数字陷阱"之一。
4. 训练主循环与参数解读:main.py 里最值得改的五个超参数
数据准备好之后,训练就是 main.py 的事。很多初学者拿到代码直接python main.py然后盯着 loss 发呆,跑完一轮发现效果不对也不知道该动哪儿。这一章把训练主循环拆开,再给你一张参数调整的对照表。
4.1 训练主循环在做什么:一次 forward-backward 的完整链条
无论哪份超分源码,main.py 的训练骨架都长一个样:
# 训练主循环的通用骨架,对应 main.py 的核心逻辑 for epoch in range(args.epochs): model.train() for lr_img, hr_img in train_loader: lr_img = lr_img.to(device) # 输入搬到 GPU hr_img = hr_img.to(device) # 标签搬到 GPU out = model(lr_img) # 前向:LR 经过网络得到重建 HR loss = criterion(out, hr_img) # 与真实 HR 算差距 optimizer.zero_grad() # 清空上一步的梯度 loss.backward() # 反向传播计算梯度 optimizer.step() # 更新网络权重 torch.save(model.state_dict(), f"checkpoints/epoch_{epoch}.pth")逐行看:model.train()把网络切到训练模式,影响 BatchNorm 和 Dropout 的行为;lr_img, hr_img来自 DataLoader 的 batch,形状是(B, C, H, W);out是网络重建的高分辨率图,和hr_img尺寸必须严格一致,不一致说明模型输出层或数据配对出错了;loss.backward()之后的optimizer.step()才是真正更新权重的地方。每轮结束存一个 checkpoint,是防止训练中断白烧 GPU 的最简单手段,也方便你回溯哪个 epoch 的效果最好。
如果你的 main.py 里还有验证环节,一般会在每个 epoch 结束或每 N 个 epoch 跑一次 val_loader,记录验证集 PSNR,只在验证指标更好时覆盖保存模型。这份源码里 test.py 单独存在,意味着评测默认在训练完成后做,你需要自己注意别把最后一次保存的 checkpoint 当成最优——训练后期 loss 可能在震荡,最后一步未必是效果最好的一步。
4.2 五个关键超参数:选错一个等于白烧几小时 GPU
训练超参数决定了收敛速度和最终效果,五个参数优先关注:
| 参数 | 推荐起始值 | 怎么调 | 典型翻车点 |
|---|---|---|---|
| scale | 与 LR 生成倍率一致 | 2x 跑通后再试 4x | 数据是 2x、模型输出是 4x,尺寸对不上直接报错 |
| patch_size | 64~96 | 显存不足先降这个 | 调太小(<32)模型学不到上下文 |
| batch_size | 16 | 配合 patch_size 一起降 | 单独把 batch 降到 1,BN 层会失效 |
| learning_rate | 1e-4(Adam) | 不收敛降到 3e-5 | 5e-3 起手,loss 直接震荡 |
| epochs | 50~100 | 看验证指标是否 plateau | 只看训练 loss 不看验证,容易过拟合 |
scale是全局一致性最强的参数:数据生成用它、数据加载裁剪用它、模型输出尺寸校验也用它。改 scale 不是改一个数字那么简单,牵一发动全身,具体怎么联动我在最后一章细说。patch_size和batch_size是显存的两个旋钮,优先降 patch_size 而不是 batch_size,因为 patch_size 影响一次看到的内容范围,batch_size 只影响梯度估计的稳定性。学习率用 Adam 配1e-4起步,这是超分领域被验证过无数次的组合;换成 SGD 的话一般还要降一个量级,并配 momentum。epochs 先定 50 跑一轮看趋势,验证 PSNR 还在涨就继续,涨不动了就收。
4.3 数据加载和训练命令:从 Dataset 到跑起来
训练入口的 DataLoader 靠 Dataset 类支撑,超分 Dataset 的核心工作是"把 LR-HR 配对、裁剪、增强、归一化"串起来:
# 对应 main.py 里 Dataset 的核心逻辑 class SRDataset(Dataset): def __init__(self, hr_dir, lr_dir, scale, patch_size): self.hr_paths = sorted(glob.glob(hr_dir + "/*.png")) self.lr_paths = sorted(glob.glob(lr_dir + "/*.png")) # HR 和 LR 文件数量、排序必须一一对应 assert len(self.hr_paths) == len(self.lr_paths), "HR/LR 数量不一致" def __getitem__(self, idx): hr = cv2.imread(self.hr_paths[idx]) lr = cv2.imread(self.lr_paths[idx]) hr, lr = get_patch(hr, lr, self.patch_size, self.scale) hr, lr = augment(hr, lr) # HWC -> CHW,BGR -> RGB,并归一化到 [0, 1] hr = torch.from_numpy(hr.transpose(2, 0, 1)[::-1].copy()).float() / 255.0 lr = torch.from_numpy(lr.transpose(2, 0, 1)[::-1].copy()).float() / 255.0 return lr, hr注意两个隐藏细节:一是路径排序,sorted保证 HR 和 LR 按文件名顺序对齐,如果文件名规则不一致(比如 HR 叫001.png、LR 叫001_x2.png),sorted 之后可能错位,训练时 loss 不降,先检查这一步;二是[::-1]的 BGR 转 RGB 操作,OpenCV 读进来是 BGR,PyTorch 的图像模型普遍按 RGB 约定,转不转直接影响最终效果。虽然超分训练一般不用预训练权重,但评测时和别人的结果对比,通道顺序不一致会差出明显数字。
数据链路准备好之后,训练命令长这样:
python main.py --scale 2 --patch-size 96 --batch-size 16 \ --lr 1e-4 --epochs 50 --gpu 0 --checkpoint-dir ./checkpoints每个参数的落点:--scale 2告诉数据加载和模型输出你要做 2x 超分;--patch-size 96对应上面 Dataset 里的裁剪尺寸;--batch-size和--lr直接传给 DataLoader 和优化器;--checkpoint-dir是权重保存目录,建议每次实验换一个目录,别覆盖掉上一轮的对比结果。如果你机器上没有 GPU,把--gpu 0去掉或者写--gpu -1,代码里一般会退回 CPU,小 patch 小规模训练也能在可接受的时间内完成。
训练完成后,用 test.py 验证:python test.py --checkpoint ./checkpoints/epoch_50.pth --input data/val/lr/demo.png --scale 2。它会加载权重、重建图像并把结果写到指定目录。第一次跑通之前,建议先用一张小图验证输出尺寸是不是正确的 scale 倍数,再做正式的指标评测。
注意:训练前先确认代码里
torch.manual_seed(seed)是否固定了随机种子。不固定的话,同样的命令每次跑出来的曲线会有差异,排错时很难判断改动是否有效。固定 seed 后复现性会好很多,对比实验才有意义。
5. 避坑与排查:五个高频翻车现场及对应处理办法
超分训练里翻车是常态,我自己第一次跑 SRCNN 就交了整晚 GPU 的学费。下面五个问题是我在课程源码和实际项目里见过最多的,基本覆盖了从训练到测试的主要坑位。每一条都按"现象 → 原因 → 解决"来写,方便你对照排查。
5.1 训练 loss 不降反升,曲线像心电图
**现象:**日志里 loss 前几个 epoch 还在 0.1 左右,后面开始上下乱跳,甚至越来越大,验证 PSNR 原地不动。
**原因:**最常见的是学习率太大,超分网络的损失面很陡,Adam 配 5e-3 以上的学习率很容易在最优解附近弹跳;其次是输入数据没有归一化,raw 的 0-255 像素值喂进网络,梯度过大导致训练发散;还有一种隐蔽的情况是 batch 内图像尺寸不一致,DataLoader 把不同 H/W 的图硬拼,要么报错,要么产生异常张量。
**解决:**先把学习率降到 1e-4 甚至 3e-5;确认预处理里图像除以 255 归一化到 [0,1];在 Dataset 的__getitem__里打印一下返回张量的 shape,保证每张图都是(3, patch, patch)。这三步做完,绝大多数震荡问题会消失。如果还震荡,检查 loss 是否把 HR 和 LR 的输入输出弄反了,这种低级错误在改代码时很容易混进来。
5.2 显存 OOM,batch 从 32 降到 4 还是炸
**现象:**训练一开始就报CUDA out of memory,把 batch_size 一路降到 4 甚至 2,仍然撑不住。
**原因:**你一直在降 batch,但 patch_size 没动。显存占用主要由输入张量、中间特征图和梯度三部分组成,patch 尺寸的平方增长直接放大特征图占用;另外,如果网络把上采样放在前面,卷积全部在高分辨率空间计算,显存消耗会翻好几倍,这是 SRCNN 这类"先放大后卷积"结构的通病。
**解决:**先把 patch_size 从 96 降到 48,用nvidia-smi观察显存占用再来考虑 batch。如果还不行,检查模型输出尺寸——有的实现会在网络末端用nn.Upsample把图放大到 4x,而你以为自己在跑 2x,白白浪费显存。还有一个现代解法是自动混合精度:训练循环里加torch.cuda.amp.autocast(),FP16 计算能把显存占用压到一半左右,代价是少数算子可能溢出,需要GradScaler配合。
5.3 训练 PSNR 很高,测试图却糊成一团
**现象:**验证集 PSNR 能到 32dB 以上,但拿一张新的测试图跑 test.py,输出明显发糊,细节像是被平均过。
**原因:**训练和测试的数据分布不一致,这是超分项目里最典型的隐性翻车。常见的有三类:一是测试图本身分辨率远大于训练 patch,网络没见过这么大尺寸的输入;二是测试图的降采样方式和训练时不一致,你训练用双三次生成的 LR,测试却拿一张本来就小的图直接放大,退化方式根本不匹配;三是训练和测试的归一化方式不一致,训练时 [0,1],测试时忘了除 255,网络输出的分布完全不对。
**解决:**先把测试流程调成和训练严格一致——同一套退化函数生成 LR、同一个通道顺序、同一个归一化范围;如果测试图比训练图大太多,测试时把图切成多块重叠 patch 分别重建再拼回去,避免一次性整图进网络。改完这三处再看效果,多数"训练好测试差"的问题出在这,而不是模型本身。
5.4 重建结果出现棋盘格状伪影
**现象:**放大后的图像网格状纹理明显,尤其在边缘和文字周围,像蒙了一层棋盘格纱窗。
**原因:**棋盘伪影几乎都是反卷积(转置卷积ConvTranspose2d)或 PixelShuffle 使用不当产生的。转置卷积在某些配置下输出在中心位置权重高、边缘权重低,叠加起来形成不均匀的重叠花纹;PixelShuffle 如果通道排列顺序写错,重排后也会产生周期性错位。
**解决:**先查 model.py 里用的是什么上采样方式。如果是ConvTranspose2d,优先换成nn.PixelShuffle或先nn.Upsample(scale_factor, mode='bilinear')再接一层卷积;如果是 PixelShuffle,确认它的输入通道数等于输出通道数乘 scale 的平方,并直接用torch.nn.functional.pixel_shuffle,不要自己手写 reshape。改完重训一版,棋盘纹通常在几个 epoch 内就能看到明显改善。
5.5 有 GPU 但训练速度和 CPU 差不多
现象:torch.cuda.is_available()输出 True,但一个 epoch 的时间没有明显加速,GPU 利用率在 nvidia-smi 里一直在个位数徘徊。
**原因:**数据加载成了瓶颈。最常见的是num_workers=0,DataLoader 在 GPU 训练时用单线程在主进程里读图和处理,GPU 大部分时间在等数据;另一个是每次迭代都在 Dataset 里重新调用cv2.imread,没有做缓存,几百张图反复读盘。
**解决:**DataLoader 里把num_workers设为 CPU 核数的一半(比如 4 或 8),pin_memory=True打开;如果数据量不大,在 epoch 开始前把全部训练集提前读进内存做缓存,能省掉反复读盘的开销。改完之后 GPU 利用率通常会从个位数涨到 80% 以上,训练速度的提升比换显卡还明显。
6. 进阶验证:把源码从 2x 改成 4x 的三个联动动作
2x 跑通之后,最常见的下一步需求是 4x——监控画面放大、老照片修复、卫星图切片增强基本都是 4x。直接把 main.py 里的 scale 改成 4 并不会工作,因为超分项目里 scale 是一个贯穿全局的约束,至少三处必须一起动。
6.1 三处必须联动修改
第一处是数据生成。训练用的 LR 必须改成双三次降采样 4 倍,测试时生成 LR 的函数也要同步。最好把降采样封装成一个函数,训练和 test.py 共用同一个入口:
def degrade(hr, scale): # 训练和测试共用的退化函数,保证 LR 生成方式完全一致 h, w = hr.shape[:2] lr = cv2.resize(hr, (w // scale, h // scale), interpolation=cv2.INTER_CUBIC) return lr第二处是网络输出。如果你的模型末端原本是 2x 的上采样,4x 时要么把上采样倍数改成 4,要么在 ESPCN 系模型里把最后一个卷积的输出通道数从out_ch * 4改成out_ch * 16——PixelShuffle 的通道数要求是输出通道数乘 scale 的平方,这个数字算错,运行到最后一层必然报 shape 错误。第三处是评测裁边,PSNR 计算里的 shave 参数一般要跟着 scale 放大,2x 时裁 4 像素,4x 时可以裁 8 像素左右,否则边界效应会污染指标。
6.2 改完怎么验证:固定三件套
我自己的验证流程是固定的三件事:一张自然图像、一条退化函数、一份对比记录。先拿一张包含文字和边缘的清晰照片,用degrade生成 4x LR,再分别用插值放大和训练好的模型重建,并排对比;然后记录重建图的 PSNR、SSIM 和推理耗时;最后把同样的流程用在不同风格图片上跑一遍,确认不是只在某一张图上效果好。文字边缘的锐利程度和是否有伪影,比 PSNR 数字更能反映模型真实水平。
改 scale 最容易忽略的其实是评估习惯。从那以后我每次改倍率,都强制自己先把退化函数、网络输出、评测裁边三处同时改掉,再拿一张固定测试图跑通首轮,确认指标和视觉都正常之后,才开始大规模训练——这个习惯帮我避免了好几次"训了一夜、起来发现 scale 没改全"的浪费。希望帮到你。
本文还有配套的精品资源,点击获取