简介:一套基于PyTorch框架实现经典超分辨率卷积神经网络(SRCNN)的完整工程,面向零基础的小白用户,覆盖图像超分辨率的模型定义、数据预处理、训练与测试全链路。包体为七十四点六兆的压缩包,共二十八个文件:六个Python脚本承担模型构建、数据加载与训练测试逻辑;七个模型权重文件提供包括第三百九十九轮在内的多阶段训练结果;两个H5数据文件直接用于训练与评估;九张BMP图像方便进行超分效果对比展示。已有两千九百七十二人学习下载,项目自带README运行指引,并内置运行结果样例,可帮助读者快速验证超分模型对低分辨率图像的复原效果。针对小白常见卡点,代码中特别说明工作进程数需按电脑配置调整,并建议采用绝对路径,减少运行报错概率。整体结构清晰,适合作为图像超分方向的入门练习与课设参考。
1. 从经典入手:为什么还要学8年前的SRCNN
最近收到不少读者私信,问入门图像超分(Super-Resolution, SR)到底该从哪个模型开始。我的答案一直没变过:先老老实实把SRCNN吃透。虽然从今天的视角看,SRCNN的结构简单得甚至有点“朴素”,但它是第一个把深度学习引入超分任务的模型,后续的FSRCNN、ESPCN、SRGAN、EDSR,乃至现在热门的SwinIR和基于扩散模型的超分方法,核心思路都能在SRCNN身上找到影子。
SRCNN全称Super-Resolution Convolutional Neural Network,由汤晓鸥团队的Chao Dong等人于2014年提出,论文名是《Image Super-Resolution Using Deep Convolutional Networks》。它的核心贡献在于证明了纯卷积神经网络就能端到端地完成“低分辨率图到高分辨率图”的映射,不需要复杂的图像先验建模和人工特征工程。
这篇博文我会用PyTorch从零开始实现SRCNN,把数据准备、网络搭建、训练调参、测试评估整个链路走一遍。不管你是刚入门深度学习的小白,还是已经在CV其他方向有基础、想切入超分领域的同学,这套实操流程都能直接照着跑。整篇文章我尽量把“为什么这么做”的细节也讲清楚,而不只是贴一段能跑的代码。
有一点先说明:我用的环境是Ubuntu 20.04 + PyTorch 2.0.1 + CUDA 11.8,单张RTX 3090。代码上我会尽可能保证兼容性,如果你用的是CPU环境或者Windows,改一两处设备设置也能跑通,只是训练速度会慢不少。
2. 整体设计思路拆解
2.1 SRCNN的核心原理精讲
SRCNN的处理流程可以概括为三步:先把低分辨率图像通过双三次插值(Bicubic)放大到目标尺寸,然后把这个放大的图像送入三层卷积网络,最后输出重建的高分辨率结果。
我用生活化的方式解释一下这个过程。想象你有一张模糊的照片,传统方法是用插值算法“猜”出缺失的像素细节,结果往往边缘发虚,像隔着一层毛玻璃。SRCNN的思路是先做一次插值把尺寸放大,然后让卷积网络学会“去模糊”——它通过大量成对的低分辨率/高分辨率图像训练,自动学到一套规则:哪些纹理该锐化、哪些边缘该加强、哪些区域该平滑。本质上是让网络充当了一个“智能滤镜”。
具体到网络结构,三层卷积各有分工:
- 第一层:特征提取。用64个大小为9x9的卷积核,从插值后的图像中提取图像块特征,类似传统方法里的“稀疏编码”。
- 第二层:非线性映射。用32个1x1的卷积核(论文原版是5x5,但1x1更高效),将高维特征映射到另一个高维空间,相当于把提取到的特征进行组合和筛选。
- 第三层:重建。用3个大小为5x5的卷积核,把特征图映射回RGB三通道图像,输出最终的高分辨率结果。
这里有个关键设计细节值得说:整个网络没有池化层,没有全连接层,是完全的卷积结构。这意味着网络可以接受任意尺寸的输入图像,这也是为什么超分模型天然适合做全卷积网络的原因。
2.2 为什么用PyTorch而不是其他框架
超分方向的科研和工程实践中,PyTorch目前是绝对的主流选择。原因很直接:动态图机制让网络结构修改和调试变得非常方便,你可以在forward函数里随意打印中间张量的shape,随时断点查看特征图的变化,这对理解SRCNN这种逐步处理图像的过程特别友好。
另外PyTorch的数据加载接口Dataset和DataLoader设计得很清晰,配合torchvision.transforms做图像预处理几乎是超分项目的标准方案。再加上社区生态成熟,遇到问题搜索一下基本都是PyTorch版本的解决方案。
当然TensorFlow也不是不能做,但说实话,我在实际对比后感觉在超分这种需要精细控制图像张量操作的领域,PyTorch的tensor操作直观得多——permute、unsqueeze、view这些方法命名清楚,不像TF那样需要频繁考虑通道维度的排列方式。
2.3 项目文件结构规划
动手写代码前先把工程结构规划好,省得后面乱。我采用的目录结构如下:
srcnn-pytorch/ ├── data/ # 数据集存放目录 │ ├── train/ # 训练集HR图像 │ └── val/ # 验证集HR图像 ├── models/ │ ├── __init__.py │ └── srcnn.py # 网络结构定义 ├── utils/ │ ├── __init__.py │ ├── dataset.py # 数据集加载与预处理 │ └── metrics.py # PSNR/SSIM评估指标 ├── train.py # 训练脚本 ├── test.py # 测试脚本 └── checkpoints/ # 模型权重保存这个结构看起来简单,但实际用起来很顺手。模型定义、数据加载、训练逻辑、评估逻辑各司其职,后续想换成FSRCNN或者加GAN损失,只需要扩展对应模块就行。
3. PyTorch环境搭建与部署要点
3.1 环境配置的实操流程
这里插一段环境配置的实操经验,因为很多读者卡在第一步就走不下去了。我建议用Anaconda创建独立环境,避免不同项目的依赖互相冲突。
# 创建虚拟环境,Python版本选择3.9或3.10均可 conda create -n srcnn python=3.9 conda activate srcnn # 安装PyTorch,这里以CUDA 11.8为例 # 注意:安装命令一定要去PyTorch官网根据你的CUDA版本选择 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 # 安装其他依赖 pip install numpy matplotlib tqdm opencv-python pillow安装完成后务必验证一下GPU是否可用:
import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))如果输出cuda.is_available()为False,大概率是CUDA驱动版本和PyTorch版本不匹配。用nvidia-smi查看驱动支持的CUDA版本,然后对照PyTorch官网选择合适的安装命令。
3.2 数据集的准备与增强
SRCNN原论文使用的是91张图像的经典训练集,但91张图对于现代深度学习来说实在太少了,容易过拟合。我更推荐使用T91 + BSDS200的组合,或者直接用DIV2K数据集,效果会好很多。DIV2K的下载地址在官方数据集页面,包含800张2K分辨率的训练图像,下载后放入data/train目录即可。
真实超分场景中还有一个常见需求:自己准备数据集。做法是把手头的高清图片统一用双三次插值缩小到指定倍数(比如x2、x3、x4),作为训练时的LR输入。这里有个细节很多人忽略:缩小前最好先对图像做一次轻微的高斯模糊,模拟真实世界中的成像退化过程,这样训练出来的模型在实际图片上效果更好。
数据预处理我这里强调整几个关键点:
# 训练阶段的数据增强代码片段 class TrainDataset(Dataset): def __init__(self, hr_dir, lr_dir, patch_size=48, scale=2): self.hr_paths = sorted(glob.glob(os.path.join(hr_dir, '*.png'))) self.lr_paths = sorted(glob.glob(os.path.join(lr_dir, '*.png'))) self.patch_size = patch_size self.scale = scale def __getitem__(self, idx): hr = cv2.imread(self.hr_paths[idx]) lr = cv2.imread(self.lr_paths[idx]) # 随机裁剪HR图像块,并对应裁剪LR块 hr_patch = random_crop(hr, self.patch_size) lr_patch = random_crop(lr, self.patch_size // self.scale) # 随机水平翻转和90度旋转增强 if random.random() > 0.5: hr_patch = hr_patch[:, ::-1] lr_patch = lr_patch[:, ::-1] if random.random() > 0.5: hr_patch = np.rot90(hr_patch) lr_patch = np.rot90(lr_patch) # 转为Tensor并归一化到[0, 1] hr_tensor = torch.from_numpy(hr_patch.transpose((2, 0, 1))).float() / 255.0 lr_tensor = torch.from_numpy(lr_patch.transpose((2, 0, 1))).float() / 255.0 return lr_tensor, hr_tensor这里的关键思路是:HR图像裁剪48x48的图像块,LR图像对应裁剪24x24的图像块。训练时网络输入的是先插值放大后的LR块,所以实际上网络输入尺寸是48x48,输出也是48x48,标签是原始的HR块。
3.3 关于离线安装和GPU环境的补充说明
评论区有读者问过离线环境怎么装PyTorch。如果你所在的机器不能联网,可以在能联网的机器上先通过pip download命令把所有依赖包下载到本地,然后拷贝到离线机器上安装:
# 在联网机器上执行 pip download torch torchvision torchaudio -d /pytorch_packages/ # 拷贝到离线机器后执行 pip install --no-index --find-links=/pytorch_packages/ torch torchvision torchaudioGPU环境的坑主要在CUDA和cuDNN的版本匹配上。我的建议是优先选择PyTorch官方预编译的wheel包,因为它自带了CUDA运行库,不需要额外安装完整的CUDA Toolkit,省去大量环境兼容性的麻烦。
4. 网络结构实现与核心细节
4.1 模型定义源码逐行解读
SRCNN的网络定义非常简洁,完整代码如下:
import torch.nn as nn class SRCNN(nn.Module): def __init__(self, num_channels=3): super(SRCNN, self).__init__() self.conv1 = nn.Conv2d(num_channels, 64, kernel_size=9, padding=9//2) self.conv2 = nn.Conv2d(64, 32, kernel_size=5, padding=5//2) self.conv3 = nn.Conv2d(32, num_channels, kernel_size=5, padding=5//2) self.relu1 = nn.ReLU(inplace=True) self.relu2 = nn.ReLU(inplace=True) def forward(self, x): out = self.relu1(self.conv1(x)) out = self.relu2(self.conv2(out)) out = self.conv3(out) return out有几个细节展开说说:
第一,padding的计算。为了保证卷积前后特征图尺寸不变,padding应该设置为kernel_size // 2。9x9的卷积核padding=4,5x5的卷积核padding=2。这样网络在任意尺寸的输入上都能工作,输出和输入尺寸一致,这是超分网络的基本要求。
第二,第三层没有ReLU激活。因为输出要回归到RGB像素值,需要输出范围覆盖[0,1]甚至可能略微超出,如果加ReLU会把负值全部截断,影响重建精度。这是很多初学者容易犯的错误。
第三,inplace=True的作用是让ReLU运算直接修改输入张量,节省显存。在深层网络中显存紧张时,每个inplace操作都能省一份张量内存。
4.2 损失函数的选择逻辑
SRCNN原论文用的是MSE(均方误差)损失,这也是超分任务最经典的损失函数。MSE直接优化像素级别的差异,和PSNR指标高度相关,训练时通常能获得更高的PSNR值。
criterion = nn.MSELoss()不过MSE有个众所周知的缺陷:它假设像素误差服从高斯分布,但真实图像的纹理分布远不是这么简单,所以MSE损失训练出的模型倾向于产生过度平滑的结果,细节不够锐利。如果你追求更好的视觉质量,可以尝试L1损失或者感知损失(Perceptual Loss)。L1损失对离群点的惩罚更小,训练更稳定,很多现代超分模型都选择L1作为基础损失。我的经验是先用MSE训一个baseline,后续要调到视觉效果更好的时候再换L1或者感知损失。
4.3 优化器与学习率策略
这里的经验直接给结论:Adam优化器配上合适的初始学习率,效果稳定且收敛快。
optimizer = optim.Adam(model.parameters(), lr=1e-4) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=20, gamma=0.5)学习率设置在1e-4到1e-5之间效果都不错。我习惯用StepLR每20个epoch降一半学习率,总计训练100个epoch左右。如果你用SGD的话,初始学习率建议1e-2并配合Momentum=0.9,但需要更多epoch才能收敛,Adam用起来更省心。
还有一个训练策略值得提:交替优化。先固定学习率训练前10个epoch,让网络快速收敛到一个reasonable的区域,然后换成StepLR做精细调整。这个技巧在超分任务上实测稳定有效。
4.4 训练主循环完整代码
下面是训练脚本的核心部分,支持GPU加速、日志输出和模型保存:
import os import torch import torch.optim as optim from torch.utils.data import DataLoader from tqdm import tqdm from models.srcnn import SRCNN from utils.dataset import TrainDataset from torchvision.transforms import Compose, ToTensor, Resize def train(): device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = SRCNN().to(device) # 数据集加载:注意这里LR图像已经在datasets里生成好 train_dataset = TrainDataset(hr_dir='data/train_hr', lr_dir='data/train_lr') train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True, num_workers=4, pin_memory=True) criterion = nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=1e-4) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=20, gamma=0.5) os.makedirs('checkpoints', exist_ok=True) best_psnr = 0 for epoch in range(100): model.train() epoch_loss = 0 pbar = tqdm(train_loader, desc=f'Epoch {epoch+1}/{100}') for lr_img, hr_img in pbar: lr_img = lr_img.to(device) hr_img = hr_img.to(device) # 将LR图双三次插值放大到HR尺寸 lr_up = torch.nn.functional.interpolate( lr_img, size=(hr_img.shape[2], hr_img.shape[3]), mode='bicubic', align_corners=False ) optimizer.zero_grad() output = model(lr_up) loss = criterion(output, hr_img) loss.backward() optimizer.step() epoch_loss += loss.item() * lr_img.size(0) pbar.set_postfix(loss=loss.item()) scheduler.step() avg_loss = epoch_loss / len(train_dataset) print(f'Epoch {epoch+1} | Loss: {avg_loss:.6f}') # 每5个epoch保存一次checkpoint if (epoch + 1) % 5 == 0 or epoch == 0: torch.save({ 'epoch': epoch, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict(), 'loss': avg_loss, }, f'checkpoints/srcnn_epoch{epoch+1}.pth') torch.save(model.state_dict(), 'checkpoints/srcnn_final.pth') if __name__ == '__main__': train()这段代码里有一个很关键的细节:bicubic插值操作放到了训练循环内部。为什么不在数据加载时就把LR放大好?原因在于batch内所有图像的放大尺寸是一致的,放在循环里可以充分利用GPU加速。但有个坑是interpolate的align_corners参数,PyTorch的bicubic插值和OpenCV的双三次插值在边界处理上略有差异,为了保持训练和测试行为一致,这个参数固定设置为False即可。
另外,归一化范围要统一。常见操作是把像素值除以255,得到[0,1]区间的浮点数。有些实现用ImageNet的mean和std做标准化,但SRCNN像素级别的重建任务完全没必要这么做,反而会因为标准化引入额外的计算复杂度。
5. 训练数据准备与预处理细节
5.1 从HR图像生成LR图像
这一步是超分训练的基础,代码实现很简单,但不同退化方式会对模型效果产生明显影响。
import cv2 import glob import os from tqdm import tqdm def prepare_data(hr_dir, lr_dir, scale=2, blur_sigma=0.5): os.makedirs(lr_dir, exist_ok=True) hr_paths = sorted(glob.glob(os.path.join(hr_dir, '*.png'))) for i, hr_path in enumerate(tqdm(hr_paths)): img = cv2.imread(hr_path) h, w = img.shape[:2] # 先做轻度高斯模糊模拟成像退化 if blur_sigma > 0: img_blur = cv2.GaussianBlur(img, (0, 0), blur_sigma) else: img_blur = img # 缩小到LR尺寸 lr_img = cv2.resize(img_blur, (w // scale, h // scale), interpolation=cv2.INTER_CUBIC) # 保存LR图像 lr_path = os.path.join(lr_dir, os.path.basename(hr_path)) cv2.imwrite(lr_path, lr_img) # 统一转为PNG格式(如果是JPEG就先转换) hr_path_png = hr_path.replace('.jpg', '.png') cv2.imwrite(hr_path_png, img)退化模型的选取值得展开讲一下。SRCNN原论文的退化模型是bicubic下采样,即用双三次插值缩小图像。但真实场景中图像退化往往包含模糊、噪声、压缩伪影等因素。如果你想训练能处理真实图像的模型,建议在生成LR图像时引入随机的高斯模糊核和噪声。比如可以随机选择blur_sigma在0.2到1.0之间变化,再叠加sigma在0~10之间的高斯噪声,这样训练出来的模型泛化性能好很多。
5.2 数据划分与batch_size选择
训练/验证集的比例建议8:2。DIV2K数据集有800张训练图,可以取640张训练、160张验证。如果数据总量较小,比如只有T91的91张图,建议全体训练,然后用Set5、Set14这些公开测试集做验证,这样更有说服力。
batch_size的选择取决于显存大小。SRCNN参数量很小(57万左右),输入48x48的图像patch,batch_size=16在8GB显存上毫无压力,即使是4GB的GTX 1650也能跑。如果你的显存偏小,可以适当把patch_size降到32,效果损失不大。
关于num_workers的设置,这个参数常被忽略但影响训练速度。Windows系统只能设为0,Linux可以设成4或者8,配合pin_memory=True可以明显减少数据加载的耗时。我测试过在机械硬盘上num_workers=4相比num_workers=0能提升30%左右的训练速度。
6. 测试评估与可视化结果分析
6.1 PSNR和SSIM的计算实现
超分任务最常用的评估指标是PSNR(峰值信噪比)和SSIM(结构相似性)。PSNR衡量像素级别的重建误差,SSIM衡量结构信息的保持程度。计算代码可以直接用skimage库,也可以自己实现,这里给出简洁的PyTorch实现:
import torch import math def psnr(img1, img2): """计算PSNR,输入为[0,1]范围的tensor""" mse = torch.mean((img1 - img2) ** 2) if mse == 0: return float('inf') return 20 * math.log10(1.0 / math.sqrt(mse.item())) def compute_ssim(img1, img2, window_size=11, sigma=1.5): """SSIM简化实现,实际使用建议直接用skimage.metrics.structural_similarity""" from skimage.metrics import structural_similarity as ssim_sk img1_np = img1.squeeze().permute(1, 2, 0).cpu().numpy() img2_np = img2.squeeze().permute(1, 2, 0).cpu().numpy() return ssim_sk(img1_np, img2_np, channel_axis=-1)PSNR计算的注意事项有两点:一是必须确保两个图像在相同的数值范围(通常是[0,1]),二是测试时的HR图像需要和模型输出的尺寸完全一致。很多人在测试时直接用原始HR作为标签,但SRCNN的输入是先插值放大后的LR,输出应该和插值放大后的尺寸一样,而插值放大后的图像和原始HR虽然在尺寸上相同,但内容上存在插值误差,网络需要学习的就是消除这个误差。测试脚本的正确流程是:
def test(model, lr_path, hr_path, scale=2, device='cuda'): lr = cv2.imread(lr_path).astype(np.float32) / 255.0 hr = cv2.imread(hr_path).astype(np.float32) / 255.0 # LR放大到HR尺寸 lr_up = cv2.resize(lr, (hr.shape[1], hr.shape[0]), interpolation=cv2.INTER_CUBIC) # 转Tensor并推理 lr_tensor = torch.from_numpy(lr_up.transpose((2, 0, 1))).unsqueeze(0).float().to(device) with torch.no_grad(): sr = model(lr_tensor).cpu().squeeze(0).permute(1, 2, 0).numpy() # 评估指标 p = psnr(torch.from_numpy(sr.clip(0, 1)), torch.from_numpy(hr)) s = compute_ssim(torch.from_numpy(sr.clip(0, 1)), torch.from_numpy(hr)) return sr, p, s这里有个大坑务必注意:测试时计算PSNR必须使用y通道(YCbCr色彩空间的亮度通道),而不是直接对RGB三个通道求平均。这是因为人眼对亮度变化最敏感,超分领域的论文也都是统一用y通道报告PSNR。转换用skimage.color.rgb2ycbcr即可。
6.2 超分效果可视化
PSNR数值高不代表视觉效果好,所以我每次评估完指标都会把结果图贴出来对比LR、Bicubic、SRCNN输出和HR原图。常见的可视化做法是拼接对比图:
import matplotlib.pyplot as plt def visualize(lr, bicubic, sr, hr): plt.figure(figsize=(12, 4)) images = [lr, bicubic, sr, hr] titles = ['LR', 'Bicubic', 'SRCNN', 'HR'] for i, (img, title) in enumerate(zip(images, titles)): plt.subplot(1, 4, i + 1) plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) plt.title(title) plt.axis('off') plt.tight_layout() plt.savefig('result_comparison.png', dpi=150, bbox_inches='tight')选可视化图片时有个实用技巧:优先选择纹理复杂、边缘明显的区域,比如建筑物窗户、树叶、人物头发等。这些区域最容易看出超分算法的差距。我通常还会把某个关键区域的局部放大图单独裁出来放在右上角,这样观感更直观。
7. 训练过程中的常见问题排查实录
7.1 损失不下降或者直接变成NaN
这个问题我在群里被问了不下二十次。现象是loss一开始就打印出nan,或者训练到某个epoch后突然跳变到nan。排查路径依次是:
第一,检查数据集中是否有全黑或者全白的图片。这类图片的像素值恒定,在计算MSE时梯度可能出现异常。处理方法是清洗数据集,或者把这种图片从训练集剔除。
第二,检查学习率是否过大。Adam的默认学习率1e-3在SRCNN上实测会不稳定,我自己用1e-4最稳妥。如果已经出现NaN,先把学习率降到1e-5,重新加载最近的checkpoint继续训练。
第三,检查是否有超过16843008这类很大的像素值。通常在图像转Tensor时忘记除以255会导致输入范围异常,网络输出的值会爆炸。用torch.max(input_tensor)打印一下确认。
7.2 训练正常但PSNR上不去
如果你的loss已经收敛但测试PSNR不理想(比如x2超分PSNR低于30dB),最可能的原因是训练集太小或者数据增强不够。91张图训练出的模型在Set5上的PSNR大概是31dB左右,而DIV2K训练的模型可以达到33-34dB。先换大数据集,再考虑调参。
还有一个常见问题是没有做randcrop。有些人可能图省事用整张图训练,而没有随机裁剪patch。整张图训练有两个隐患:一是参数更新方向会被大尺寸图像的全局统计主导,对细节纹理的学习不充分;二是显存限制导致batch_size必须很小,训练效率低。用48x48的patch训练,不仅每个step能看更多样化的纹理特征,还能开更大的batch_size加快收敛。
7.3 输出图像有棋盘格或者条纹伪影
这通常是因为deconv操作导致的,但SRCNN里面全是普通卷积,出现棋盘格一般是因为bicubic插值后再输入网络,插值放大过程本身会产生轻微的振铃效应,尤其是在边缘附近。如果伪影比较明显,可以尝试把输入LR先用cubic再加一次中值滤波去除异常点。
另外,如果输出图像整体偏灰,检查是否忘记反归一化——就是把网络输出乘回255再保存图像。浮点数像素值在0到1之间直接保存会被imwrite当作接近全黑处理。
7.4 常见问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| Loss为NaN | 学习率过大/数据含异常值 | 降低学习率至1e-5,检查数据范围 |
| PSNR偏低 | 训练数据太少/退化模型不匹配 | 换DIV2K数据集,调整退化参数 |
| 输出图像偏灰 | 忘记反归一化 | 输出乘255后clip到[0,255]再保存 |
| GPU显存不足 | patch_size太大/batch太大 | 减小patch到32或batch到8 |
| 训练发散 | 未做数据归一化 | 图像除以255,统一到[0,1] |
| 测试PSNR比训练高 | 验证集太小或过拟合 | 增加验证集规模,添加数据增强 |
8. 后续扩展方向与实践建议
模型训练完成后,想要进一步提升效果和加深理解,我建议按下面的路线图做扩展。
第一步是把SRCNN和FSRCNN做对比实验。FSRCNN的核心改进是去掉了预插值步骤,直接对LR图像做特征提取,在网络的最后一层用deconvolution放大尺寸。这能直观地让你理解预插值对计算量和重建质量的影响,以及转置卷积作为上采样手段的特点。
第二步尝试把MSE换成感知损失或GAN损失。感知损失利用VGG网络中间层的特征做约束,能有效提升纹理细节的还原度;GAN损失则能让输出更接近真实图像的自然分布。注意这时候PSNR会下降,但视觉观感明显提升,这也是超分领域“指标与观感不一致”的典型例子。
第三步探索自监督超分。用单张图片做内部统计学习,不需要成对数据就能完成超分重建。这在老照片修复、医学影像超分等没有HR参考图的场景非常实用,SRCNN的网络骨架作为重建模块集成进去也很自然。
关于超分在实际业务中的应用,我做过一些项目验证:视频超分需要额外考虑时间一致性,直接逐帧跑SRCNN会出现闪烁,需要加入光流对齐或者3D卷积;遥感影像超分由于是16bit的TIFF格式,注意读取时要用rasterio而不是cv2,否则像素值被截断到8bit会丢失大量信息;共聚焦显微镜图像超分要注意噪声等级,这类图像噪声成泊松分布,训练时退化模型里加入泊松噪声更贴近真实。
我的个人体会是,SRCNN虽然年代久远,但把它从头到尾实现一遍的价值远超跑通一个SOTA模型。因为这个过程中你接触到的每一个细节——bicubic插值的边界处理、像素归一化的数值稳定性、PSNR计算时YCbCr通道的选择、patch提取与数据增强的配合——都是超分领域所有更复杂模型共同的基础。希望这篇博文能帮你走通这条路。
本文还有配套的精品资源,点击获取