图像超分辨率这几年被Transformer、扩散模型刷出了不少新纪录但我的建议始终没变过真想搞懂超分这一摊子事先把SRCNN老老实实复现一遍。SRCNNSuper-Resolution Convolutional Neural Network是深度学习做超分真正意义上的开山之作2014年出现的时候用极其朴素的3层卷积在效果上直接碾压了当时主流的稀疏编码类方法。这篇文章会带你用Pytorch从零搭建SRCNN把模型代码、训练流程、数据处理、评估指标一条龙讲清楚同时把论文里那些容易被忽略的细节翻译成实操层面的理解。适合刚学完Pytorch基础、想找一个正经CV项目练手的人也适合准备读超分方向论文但觉得一上来就啃Transformer有压力的同学——先把这条技术线走通后面看FSRCNN、EDSR、SRGAN、SwinIR都会顺很多。1. 从论文说起SRCNN到底在学什么1.1 超分问题的本质与SRCNN的解题思路超分辨率重建要做的事情概括起来就一句话给一张低分辨率图像恢复出对应的高分辨率版本。设低分辨率图像为ILR高分辨率图像为IHR目标是学一个映射F让F(ILR)无限接近IHR。这个问题天生是病态的。一张小图可以对应无数张大图每个低分辨率像素点都丢掉了太多高频信息你没法从缺失像素里凭空猜出唯一正确的答案。传统方法大多靠加先验来约束解空间比如稀疏编码假设图像块能用过完备字典线性稀疏表示再通过字典学习去估计系数。这类方法效果尚可但整个pipeline是割裂的训练时先学字典、再解稀疏系数、最后做重建每个环节都涉及迭代优化速度慢且调参痛苦。SRCNN的思路则完全是另一条路让卷积网络端到端地把低分辨率图像→高分辨率图像这个映射直接学出来。网络内部不需要任何手写先验训练时输入成对的LR-HR图像通过反复迭代参数让输出和真实HR之间的像素误差不断降低。推理时就是一次前向传播干脆利落。这里有一个非常关键的预处理步骤很多人复现时容易忽略输入网络的LR图像并不是原始的低分辨率小图而是先经过双三次插值放大到目标尺寸的版本。换句话说SRCNN只负责学插值放大之后到真实细节之间的残差映射像素放大本身交给了传统插值算法。这个设计极大降低了学习难度网络不用自己学会如何画大图只需要在已有大图基础上补细节就行。这种把放大操作前置的做法在后来很长一段时间里都是超分网络的主流范式被称为pre-upsampling结构。后面读FSRCNN论文时会看到作者们想尽办法去掉前置放大来提速理解SRCNN是理解这一切演进的基础。1.2 三层卷积的设计逻辑稀疏编码的深度版SRCNN论文最值得品味的地方是把三层网络与稀疏编码的求解过程做了严格对应。这算是学术工业界常说的讲故事但这个故事是真的有道理它从理论上解释了为什么网络要这么设计。第一层是特征提取用9×9大小的卷积核把输入图像切成一堆重叠的图像块再从图像块中提取高维特征向量。这一层对应稀疏编码里的图像块采样和字典编码过程。9×9的卷积核覆盖范围比较大能看到足够的局部上下文相当于传统方法里分块处理的操作。第二层是非线性映射把第一层输出的高维特征映射到另一个特征空间。你可以把它理解成对稀疏系数的非线性变换和逼近。论文原版在这个位置用的是1×1卷积因为作者想强调映射发生在特征维度上不依赖空间邻域信息。但实际复现时社区普遍发现改成5×5效果更好因为1×1映射完全忽略像素之间的关系而真实图像的高频细节不可能在单点特征上凭空产生。论文在实验部分也提到过用5×5的非线性映射确实优于1×1。第三层是重建把映射后的特征聚合起来生成最终的高分辨率图像。这一层对应稀疏编码里的字典重建负责把特征空间里的表示转换回像素空间的图像。三层网络各有明确分工每一层都能在传统方法里找到对应物。这是SRCNN论文的高明之处它不是拍脑袋叠卷积层而是从已有方法的求解流程里提炼出结构再用深度学习把每个环节变成可学习的参数。读这篇论文的时候你会发现作者始终在强调深度的网络结构不是越多越好结构要有道理可讲。1.3 复现前必须记住的三个论文结论第一输入输出是同一尺寸。SRCNN不是从LR小图直接预测HR大图它输出图像的尺寸和输入完全一样因为输入已经被插值放大过了。有些没细读论文的人把LR小图直接丢进网络发现尺寸对不上这就是没理解这个关键点。第二训练和评估都在Y通道进行。论文把RGB彩色图像转到YCbCr色彩空间网络的输入输出都只取Y通道亮度分量。原因很简单人眼对亮度变化最敏感图像的高频边缘细节几乎都集中在亮度通道而色度通道用双三次插值就不会有肉眼可察觉的损失。这样做还大幅降低了计算量网络从3通道输入变成1通道输入省了差不多三分之二的乘法运算。几乎所有经典超分论文评估PSNR时都只算Y通道这也是你能在不同论文之间对比数字的前提。第三训练数据量不需要很大。SRCNN在公开实验里用的是91张自然图像作为训练集配合数据增强就能训练出非常好的效果。放在今天动辄几十万张图的大数据集时代这显得有些不可思议但原因也简单网络只有三层参数量极小需要拟合的映射复杂度有限小规模高质量数据足以支撑。复现时完全可以用这91张图来跑不需要满世界找大规模数据集。2. 复现准备环境与数据2.1 环境清单与依赖安装复现SRCNN对硬件要求非常低一块普通游戏显卡或者干脆只用CPU都能跑通。我的建议是先用CPU跑通整个pipeline再去考虑GPU加速因为模型本身就小CPU上跑几个epoch也不会慢到让人崩溃。环境方面我推荐以下组合Python 3.8 以上版本Pytorch 1.10 以上2.x完全兼容torchvision用来做基础图像变换OpenCVcv2用来做双三次插值这是训练数据处理的核心工具scikit-image用来计算SSIM指标numpy 和 matplotlib不用多说pip install torch torchvision opencv-python scikit-image numpy matplotlib有个细节说一下双三次插值这一操作Pytorch本身也有实现但用起来不如OpenCV直接。而且训练和测试阶段如果用不同的插值库会产生细微的退化差异导致评测结果不稳定。我习惯统一用cv2.INTER_CUBIC。2.2 用双三次插值构造训练对SRCNN的训练数据构造思路非常清晰一句话把一张高分辨率图降低质量再让模型学着把质量恢复回去。具体流程是这样的。首先从训练集图像中随机裁剪出一块固定大小的HR patch比如64×64。然后用cv2的resize函数把这块patch缩小scale倍例如scale3时缩小为原来的三分之一得到约21×21的低分辨率版本。接着再用同样的resize函数、同样的插值方式把这个小图放大回64×64。这个放大后的64×64图像就是模型的输入原始的64×64 patch就是训练标签。为什么要先缩小再放大因为SRCNN学的是插值放大之后的样子到真实高分辨率样子之间的映射。如果不做缩小直接输入原始图网络学到的就是恒等映射没有任何意义。先缩小是为了创造一个带缺陷的输入再放大是为了让输入输出尺寸一致缺陷就是插值过程丢掉的高频细节。def make_lr_image(hr_patch, scale3): h, w hr_patch.shape[:2] lr_h, lr_w h // scale, w // scale lr cv2.resize(hr_patch, (lr_w, lr_h), interpolationcv2.INTER_CUBIC) lr_up cv2.resize(lr, (w, h), interpolationcv2.INTER_CUBIC) return lr_up这里要注意缩小后的尺寸要做整数除法保证是scale的整数倍否则放大回原尺寸时会有1-2像素的偏移训练时loss会莫名其妙地偏高。我在第一次复现时就踩过这个坑当时不是用h // scale而是直接cv2.resize到固定尺寸结果训练集和测试集的退化不一致PSNR卡住上不去。2.3 数据增强小数据集也能练得稳SRCNN训练集只有91张图如果不做数据增强网络很容易见过所有训练样本导致泛化性差。数据增强不需要花哨经典三件套就够了随机水平翻转、随机旋转90/180/270度、随机缩放。随机缩放的意思是每次取patch时以一定概率改变实际使用的scale。比如训练时设定scale3但读图后先按随机比例把整张图缩小一定倍数1到2倍之间随机再进入裁剪和退化流程。这样做的好处是让网络见过不同分辨率和不同纹理尺度下的图像提升鲁棒性。另外每个epoch开始的时候重新随机裁剪patch位置也很重要。因为91张图的尺寸不算大如果固定裁剪区域一个epoch内每个位置只被看一次提升有限。我在实现里是每次调用getitem时都重新随机裁剪让同一张图在不同epoch里以不同的局部块出现等效于把训练集扩大了几十倍。class SRCNNDataset(Dataset): def __init__(self, hr_paths, scale3, patch_size64, augmentTrue): self.hr_paths hr_paths self.scale scale self.patch_size patch_size self.augment augment def __len__(self): return len(self.hr_paths) def __getitem__(self, idx): hr cv2.imread(self.hr_paths[idx]) # BGR hr cv2.cvtColor(hr, cv2.COLOR_BGR2YCrCb) y, _, _ cv2.split(hr) h, w y.shape ph pw self.patch_size top random.randint(0, max(0, h - ph)) left random.randint(0, max(0, w - pw)) y_patch y[top:topph, left:leftpw] lr_up make_lr_image(y_patch, self.scale) if self.augment: if random.random() 0.5: y_patch y_patch[:, ::-1] lr_up lr_up[:, ::-1] k random.choice([0, 1, 2, 3]) if k: y_patch np.rot90(y_patch, k) lr_up np.rot90(lr_up, k) lr_t torch.from_numpy(lr_up.copy()).float().unsqueeze(0) / 255.0 hr_t torch.from_numpy(y_patch.copy()).float().unsqueeze(0) / 255.0 return lr_t, hr_t读图时先把RGB转成YCrCb然后只取Y通道参与训练。Cb和Cr通道不参与训练测试时用双三次插值直接放大就行。3. 动手实现模型、训练与评估3.1 模型搭建Pytorch版SRCNNSRCNN模型本身简单到有些朴素就是三个卷积层加两个ReLU激活。第一层9×9把1通道映射到64通道第二层5×564通道映射到32通道第三层5×532通道映射回1通道就是最终的重建结果。我前面提到过论文原版第二层是1×1卷积但社区普遍用5×5替代效果更好。这里我在代码里直接采用5×5版本你如果想严格复现论文原版把kernel_size改成1、padding改成0即可。import torch import torch.nn as nn class SRCNN(nn.Module): def __init__(self, in_channels1): super().__init__() self.conv1 nn.Conv2d(in_channels, 64, kernel_size9, padding4) self.conv2 nn.Conv2d(64, 32, kernel_size5, padding2) self.conv3 nn.Conv2d(32, in_channels, kernel_size5, padding2) self.relu nn.ReLU(inplaceTrue) def forward(self, x): x self.relu(self.conv1(x)) x self.relu(self.conv2(x)) x self.conv3(x) return x三个卷积层的padding都经过计算保证输入输出尺寸不变9×9卷积padding45×5卷积padding2这样任意尺寸的输入图进入网络输出尺寸都严格一致。这也是SRCNN能处理任意分辨率测试图的原因不需要把图片强行resize到固定大小。3.2 训练主循环与关键参数模型写好之后训练代码就是标准的Pytorch套路。损失函数用MSE这是论文的核心loss设置可以说超分领域至今仍在沿用MSE或者以它为基础的变体作为监督信号。优化器我推荐Adam学习率1e-4虽然论文用的SGDmomentum但Adam收敛更快尤其在这种小模型上几十个epoch就能看到明显效果。model SRCNN().to(device) criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr1e-4) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size30, gamma0.5) for epoch in range(60): model.train() total_loss 0 for lr, hr in train_loader: lr, hr lr.to(device), hr.to(device) optimizer.zero_grad() out model(lr) loss criterion(out, hr) loss.backward() optimizer.step() total_loss loss.item() print(fEpoch {epoch1}, Loss: {total_loss/len(train_loader):.6f})训练时把模型设置为train模式dropout和BN的行为才会正确。SRCNN没有BN层所以这个问题不明显但养成写model.train()和model.eval()的习惯很重要后面做复杂模型时不会吃亏。batch size的设置看显存决定我用的是64输入是64×64单通道显存占用很小。如果怕显存不够把batch size降到32patch降到48也能训练出不错的结果。3.3 评估指标PSNR和SSIM怎么算训练完成后需要量化评估效果。超分领域最核心的两个指标是PSNR和SSIM。PSNR是峰值信噪比定义基于MSEimport numpy as np import math def psnr(img1, img2, data_range1.0): mse np.mean((img1 - img2) ** 2) if mse 0: return float(inf) return 10 * math.log10(data_range ** 2 / mse)注意data_range参数。如果图像归一化到[0,1]data_range1.0如果像素值在[0,255]data_range255。很多新手算PSNR时忘了这个参数导致算出来的数字要么小得离谱要么大得离谱。业内通常把归一化后的图像套用data_range1.0进行计算。SSIM是结构相似性衡量两幅图像在亮度、对比度、结构三个维度的相似度。用scikit-image直接算from skimage.metrics import structural_similarity as ssim def ssim_score(img1, img2, data_range1.0): return ssim(img1, img2, data_rangedata_range)评估时的标准做法是把测试集的高分辨率图像做退化处理bicubic下采样再上采样把得到的LR图输入网络输出重建结果再和原始HR图计算PSNR和SSIM。同时也要计算bicubic插值结果的PSNR作为baseline这样才能看出网络到底提升了多少。def evaluate(model, test_hr_paths, scale3, devicecuda): model.eval() total_psnr_srcnn, total_psnr_bicubic 0, 0 for path in test_hr_paths: hr cv2.imread(path) hr cv2.cvtColor(hr, cv2.COLOR_BGR2YCrCb) y, cb, cr cv2.split(hr) lr cv2.resize(y, (y.shape[1]//scale, y.shape[0]//scale), interpolationcv2.INTER_CUBIC) lr_up cv2.resize(lr, (y.shape[1], y.shape[0]), interpolationcv2.INTER_CUBIC) with torch.no_grad(): input_t torch.from_numpy(lr_up / 255.0).float().unsqueeze(0).unsqueeze(0).to(device) out model(input_t).squeeze().cpu().numpy() * 255.0 total_psnr_srcnn psnr(out, y, data_range255.0) total_psnr_bicubic psnr(lr_up, y, data_range255.0) print(fSRCNN PSNR: {total_psnr_srcnn/len(test_hr_paths):.2f} dB) print(fBicubic PSNR: {total_psnr_bicubic/len(test_hr_paths):.2f} dB)这里只比较了Y通道符合论文的标准评估方式。Cb和Cr通道直接用bicubic放大后拼接回去得到最终的彩色结果。4. 实验记录参数配置与结果分析4.1 一份可以直接照抄的训练配置写到这里我把一套经过多次验证、效果稳定的完整训练配置列出来代码不需要改任何东西就可以直接跑。这套配置是我在复现SRCNN时沉淀下来的比论文原版参数做了一些针对性调整收敛更快且结果稳定。配置项参数说明训练集T9191张自然图像原论文设置无需更多数据测试集Set55张图像超分领域经典测试集倍率scale3也可以换成2或4代码通用patch_size64×64裁剪的HR patch大小batch_size64显存小可降为32优化器Adam论文原版SGDAdam收敛更快初始学习率1e-4大了容易震荡小了收敛太慢学习率衰减每30个epoch乘以0.5后期loss平台期需要小lr精细调整epoch60CPU也能在合理时间内跑完数据增强翻转旋转缩放小数据集必备颜色空间Y通道单通道CbCr交给bicubic这套参数下训练到第10个epoch左右就已经能看出明显效果视觉上重建图像比bicubic锐利不少。到40个epoch之后PSNR提升幅度放缓进入平台期学习率衰减会让损失继续小步下降。4.2 训练过程观察与结果解读训练过程中有几个现象值得关注。最开始几个epochloss会快速下降这是网络在学习基本的高频边缘恢复能力输出的图像从模糊变得稍微锐利。但到了中期loss下降速度明显变慢这是正常现象不代表模型不行了。此时如果看重建图细节纹理还不够干净放大会看到一些振铃和过冲。这时候需要让学习率衰减触发用更小的步长在参数空间里精细搜索。我惯用的观察方式是验证集PSNR和训练集loss并行监控。训练集loss只管下降但PSNR能反映真实重建质量。有一个很常见的现象训练loss还在降但验证集PSNR不涨了。这说明模型开始过拟合训练集的噪声分布。解决办法是加强数据增强或者提前停止。另一个观察点是bicubic baseline。Set5上3倍超分bicubic的PSNR大约是30.4dB左右。SRCNN训练到位后能做到32.5dB以上提升2dB以上视觉上边缘更加清晰文字和纹理的锯齿明显减少。如果复现后PSNR只比bicubic高0.5dB说明训练没有到位或者数据处理有问题这就要对照下一章的排查清单看是哪里出了偏差。最终效果的评价不能只看PSNR。PSNR对整体像素误差敏感但它不完美有时候PSNR更高但图像看起来更平滑、更油。SSIM从结构相似性维度补充了这一点通常SRCNN的SSIM也会比bicubic高0.02到0.05。观察重建结果时重点看边缘过冲是否自然、平坦区域有没有噪声放大、细纹理是否被过度平滑。5. 踩坑记录与规避技巧5.1 高频问题速查表复现SRCNN的过程中几乎所有问题都出在数据处理和评估环节模型本身反而不太容易出错。下面这张表是我整理的高频问题排查清单每一条都是实测踩过的坑。问题现象可能原因解决方案loss非常大且不下降输入像素值没有归一化到[0,1]检查数据加载代码确保除以255.0重建图像过暗或过亮训练和推理时归一化方式不一致推理时也要除以255.0输出再乘回255PSNR比论文低2dB以上训练和测试退化方式不一致统一用cv2.INTER_CUBIC不要混用PIL和cv2PSNR比bicubic只高一点点第二层用了1×1卷积换成5×5卷积提升非线性映射能力图像边界有明显的伪影裁剪和训练的padding处理不一致测试时直接用全图推理不要分块训练后期验证集PSNR震荡学习率过高、没有衰减添加StepLR或ReduceLROnPlateauGPU显存不足patch太大或batch太大patch降到48batch降到16或32结果边缘有明显的振铃网络容量不够或训练不充分增加epoch或者把第一层通道数翻倍到1285.2 效果提升的三条实战建议第一把patch size从64换成96甚至128。SRCNN的卷积核感受野有限三层卷积加起来覆盖范围其实不算大。patch太小会让每个训练样本里包含的上下文不足尤其对边缘纹理这种依赖大范围上下文的信息学习不利。我在实际测试中patch从64提高到96后PSNR大约能提升0.2dB左右代价是训练时间变长。显存不足的话可以降低batch size来换patch size。第二多尺度训练。训练时以一定概率随机改变下采样倍率比如一半时间用scale2一半时间用scale3这样模型面对不同退化程度时都能表现得稳定。这个技巧在测试时特别管用因为实际应用里你往往不知道输入图像的退化倍率。不过程度要适度把2、3、4三个倍率混着训练即可不用刻意追求更多。第三注意CbCr通道的处理不能太随便。虽然训练只在Y通道进行但最终展示彩色图像时CbCr通道如果直接用bicubic放大在颜色变化剧烈的边缘会出现色渗。我的做法是CbCr通道先bicubic放大再用一个轻量的中值滤波3×3去除边缘色斑。这个操作不会影响Y通道的PSNR但视觉体验会明显提升。我踩过最隐蔽的坑是训练集和测试集用了不同的图像读取方式。训练代码里用cv2.imread读图默认是BGR格式如果忘记转换直接split通道拿到的Y通道其实是B通道的亮度信息效果会非常差且难以察觉。一定记住cv2默认读进来是BGR必须显式cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb)之后再取通道。根据我个人复现这类经典论文的经验SRCNN做得久了会越来越觉得它的价值不在网络结构本身而在于它树立了超分任务的标准范式数据退化流程、评估范围、baseline对比方式。后面几乎所有超分模型都沿用了这套规矩。先把这个项目的每一个细节都吃透后续你去看FSRCNN的反卷积上采样、EDSR的残差结构、SRGAN的感知损失时都能一眼看出它们是在哪个环节做了改进、解决了什么问题。这就是复现经典项目最大的收获。