简介:基于深度卷积网络的图像超分辨率译文资料,聚焦董超等人提出的SRCNN模型,面向图像处理与深度学习入门者及计算机视觉方向学生。文档完整梳理了低/高分辨率图像端到端映射、网络结构设计、训练策略及三种颜色通道扩展等关键内容,并结合稀疏编码方法对比,解释传统方法与CNN的内在联系。资源包共1个docx文件,大小约127KB,以Word文档形式呈现,便于检索、标注与碎片化阅读,适合作为论文精读、组会分享或项目复现前的参考底稿。目前已有92人学习下载,内容兼顾原理推导和实验结论,可帮助读者快速把握超分辨率重建问题的主流思路和SRCNN的改进脉络。
1. 为什么图像超分辨率重建绕不开深度卷积网络
图像超分辨率重建这个任务,听起来只是把图放大,但传统做法的瓶颈不在放大本身,而在放大之后细节怎么补。双三次插值只能给出平滑的轮廓,字典学习类方法能补出一些纹理,但要逐块匹配、调参数,工程化路径又长又脆。深度卷积网络把这件事变成了一次端到端的映射学习:低分辨率图先放大到目标尺寸,再由卷积层学习从模糊到清晰的映射,重建出的高频细节由网络参数决定,而不是插值公式。董超团队的SRCNN是这条技术路线公认的起点,三层卷积、不到一万个参数,就把当时基于稀疏编码的经典方法比了下去。这里有个反直觉的结论:超分重建的竞争力并不靠堆深度,而在于用监督信号逼着卷积核学会“该恢复哪些高频信息”。下面按任务建模、数据与训练、评估、细节调优的顺序,把这套方案拆成可以照着复现的一整套流程。
2. 把重建问题拆成卷积网络能解的形态
2.1 前置上采样:为什么先把LR放大到目标尺寸再送进网络
深度卷积网络做超分,第一步要决定低分辨率输入(LR)以什么形态进入网络。常见做法是“前置上采样”:先用双三次插值把LR放大到高分辨率(HR)的尺寸,再让卷积网络在放大后的图上做重建。这样输入和输出的空间尺寸完全一致,损失函数可以直接在像素级计算,数据处理时也不需要处理坐标映射。
前置上采样最大的好处是省事。LR和HR的像素位置一一对应,训练时从HR裁剪一块patch,从放大后的LR上裁剪同一位置的patch,这对训练数据的生成非常友好。SRCNN原文就是这么做的,三层卷积不改变特征图尺寸,全程使用same padding,输入输出尺寸保持一致。
这个方案的缺点也明显:图像被提前放大,卷积层实际处理的像素数变多,训练速度和显存开销都会上升。后来FSRCNN、ESPCN转向“后置上采样”,在网络的最后一层才放大特征图,节省计算量,但引入的亚像素卷积和反卷积也带来新的伪影问题。对于首次跑通训练闭环的场景,前置上采样依然是最稳的起点。
2.2 三层卷积:特征提取、非线性映射与重建的分工
SRCNN把超分重建分成三个阶段,正好对应三层卷积。标准配置是这样的:
| 层 | 卷积核 | 步长 | 输出通道 | 作用 |
|---|---|---|---|---|
| 特征提取 | 9×9 | 1 | 64 | 从输入图中提取局部结构特征 |
| 非线性映射 | 1×1 | 1 | 32 | 在通道维度做信息融合与压缩 |
| 重建 | 5×5 | 1 | 1 | 将特征图映射回像素空间 |
第一层用大核9×9,是因为图像超分需要一定的空间上下文,小卷积核的局部感受野不够,难以区分纹理和噪声。第二层改用1×1,思路来自Network in Network,在相同尺寸下做跨通道的信息混合,同时把64通道压缩到32通道,降低第三层的计算压力。第三层用5×5把特征图变回单通道的亮度图,完成重建。
值得注意的细节是第二层为什么不继续用大卷积核。超分任务里的特征提取和重建是两个不同性质的步骤:提取阶段需要空间信息,所以核要大;映射阶段只需要重新组合各通道特征,1×1在数学上是足够的。如果第二层也用5×5,感受野会进一步扩大,但参数和过拟合风险同时上升,在数据量不充裕的场景不划算。实践中可以保留这个结构,只在训练数据明显不足时调整中间层通道数。
2.3 感受野决定能“看”多大的上下文
三层卷积的等效感受野是可以直接算出来的。给定一组卷积核尺寸和步长,感受野从最后一层向前递推:
def receptive_field(kernel_sizes, strides): rf = 1 for k, s in zip(kernel_sizes[::-1], strides[::-1]): rf = (rf - 1) * s + k return rf # SRCNN 的三层卷积:9x9、1x1、5x5,步长均为 1 rf = receptive_field(kernel_sizes=[9, 1, 5], strides=[1, 1, 1]) print(rf) # 输出 13这个计算结果意味着网络输出中心像素的预测,实际上依赖输入图上周围13×13像素的信息。如果感受野太小,网络看不见足够多的结构上下文,就会把边缘恢复成模糊的过渡带;如果盲目加大卷积核,参数数量和训练难度同步上升。
实际训练中,从HR图像裁剪的patch通常取33×33,对应放大后的LR patch也是33×33。这个尺寸比13×13的感受野大一圈,目的是给边缘像素留出充足的上下文信息。评估时则会反过来裁剪掉边界区域,因为边缘像素的感受野不完整,重建结果天然偏弱,这部分在算PSNR时要把边界去掉,后面第4章会单独说明。
3. 从Patch到收敛:用PyTorch复现一个最小训练闭环
3.1 数据准备:用双三次下采样制造LR/HR对
训练超分网络需要成对的HR原图和对应的LR退化图。公开数据集可以直接下载,但自己制造LR/HR对会更容易控制退化方式。常见做法是用双三次插值把HR缩小到目标倍率,再把缩小后的图放大回原尺寸,作为网络的输入。
建议先裁patch再放进Dataset,避免每张完整图重复做下采样。下面是数据准备的一种可靠写法:
import cv2 import numpy as np from torch.utils.data import Dataset class SRCNNDataset(Dataset): def __init__(self, image_paths, scale=3, patch_size=33, stride=14): self.patches = [] for path in image_paths: hr = cv2.imread(path) # 读入 BGR 图 h, w = hr.shape[:2] # 双三次下采样得到 LR,再放大回 HR 尺寸 lr = cv2.resize(hr, (w // scale, h // scale), interpolation=cv2.INTER_CUBIC) lr_up = cv2.resize(lr, (w, h), interpolation=cv2.INTER_CUBIC) # 只取 Y 通道训练,CbCr 用插值放大即可 hr_y = cv2.cvtColor(hr, cv2.COLOR_BGR2YCrCb)[..., 0] lr_y = cv2.cvtColor(lr_up, cv2.COLOR_BGR2YCrCb)[..., 0] # 滑窗裁剪,stride 小于 patch_size 时样本有重叠 for y in range(0, h - patch_size + 1, stride): for x in range(0, w - patch_size + 1, stride): lr_patch = lr_y[y:y + patch_size, x:x + patch_size] hr_patch = hr_y[y:y + patch_size, x:x + patch_size] self.patches.append((lr_patch, hr_patch)) def __len__(self): return len(self.patches) def __getitem__(self, idx): lr, hr = self.patches[idx] # 归一化到 [0, 1],训练更稳定 lr = lr.astype(np.float32) / 255.0 hr = hr.astype(np.float32) / 255.0 return lr[np.newaxis, ...], hr[np.newaxis, ...]这段代码里有两个关键选择。
第一,只对Y通道做重建。YCrCb颜色空间中,Y通道代表亮度信息,人眼对亮度变化最敏感,而CbCr两个色度通道对分辨率的要求低得多。实际应用时通常只把Y通道交给卷积网络,两个色度通道直接用双三次插值放大,主观质量没有明显损失,训练和推理速度却快了不少。
第二,stride取14而不是patch_size。stride等于patch_size时,相邻patch没有重叠,数据量少,且patch之间信息被割裂;stride取patch_size的一半左右,会让同一图像区域多次出现在不同patch里,等效做了一次位置层面的数据增强。如果担心样本量太大,可以增大stride到如21或28,代价是训练集多样性下降。
3.2 模型定义与参数初始化
网络结构直接按9×9、1×1、5×5三层卷积实现。这里初始化方式值得说明:ReLU激活配合较深的卷积层,Kaiming初始化是合理选择,但SRCNN参数量少,默认的PyTorch初始化(均匀分布)在实践中也能收敛,不必追求特定的初始化方式。
import torch import torch.nn as nn import torch.nn.functional as F class SRCNN(nn.Module): def __init__(self, num_channels=1): super().__init__() self.conv1 = nn.Conv2d(num_channels, 64, kernel_size=9, padding=4) self.conv2 = nn.Conv2d(64, 32, kernel_size=1, padding=0) self.conv3 = nn.Conv2d(32, num_channels, kernel_size=5, padding=2) def forward(self, x): x = F.relu(self.conv1(x)) x = F.relu(self.conv2(x)) x = self.conv3(x) # 最后一层不加激活 return xpadding参数值得逐层核对:9×9卷积的padding=4保证输出尺寸不变,5×5卷积的padding=2同理。1×1卷积不需要padding。第三层不加ReLU是因为输出要回归到像素值域,范围包括0到1,如果加激活函数会把输出截断到非负区间,造成重建结果整体偏亮。
3.3 训练循环:损失、优化器与学习率配合
训练配置沿用一套相对稳定的组合:MSE损失、Adam优化器、初始学习率1e-4、配合学习率衰减和梯度裁剪。MSE对应PSNR最大化,因为PSNR是从MSE推导出来的,降低MSE就是在提高PSNR。
model = SRCNN() criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-4) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.5) for epoch in range(epochs): model.train() total_loss = 0.0 for lr_patch, hr_patch in dataloader: optimizer.zero_grad() output = model(lr_patch) loss = criterion(output, hr_patch) loss.backward() # 梯度裁剪防止异常样本造成 loss 突跳 torch.nn.utils.clip_grad_norm_(model.parameters(), 0.4) optimizer.step() total_loss += loss.item() scheduler.step() print(f"epoch {epoch}: loss = {total_loss / len(dataloader):.4f}")两个参数需要根据数据规模调整。
学习率:1e-4是Adam在图像回归任务里最常见的起点。如果loss下降太慢,可以提高到3e-4,但不要把初值设到1e-3,SRCNN的MSE loss梯度量级不大,学习率过大会导致loss在训练初期震荡。
梯度裁剪阈值:0.4是针对MSE loss的一个经验值。MSE对大误差的惩罚是平方级的,个别异常patch可能产生很大的梯度,一次更新就把权重推到坏的区域。clip_grad_norm_把梯度的全局范数限制在0.4以内,防止这种情况。阈值设太小(如0.05)会导致训练长期不收敛,因为正常梯度也被截断了。
3.4 训练中要盯的指标与常见异常
训练过程中真正要盯的不只是loss下降,还有验证集上的PSNR变化。常见的异常情况有三种。
第一种是loss下降但验证PSNR纹丝不动。这通常意味着数据预处理不一致。比如验证时输入图像没有做和训练相同的归一化,或者LR的退化方式不同(训练用双三次缩小,验证却用了直接抽像素),都会导致模型在训练和测试时面对不同的输入分布。先检查数据路径,再怀疑网络结构。
第二种是loss稳定在某个值附近不降。这种现象在patch_size偏小时比较常见,33×33的patch对高层次的结构信息感知有限,模型只能学到局部纹理的统计规律。可以尝试把patch_size提升到41或49,同时调大stride保持训练样本总数不变。
第三种是训练初期loss爆炸。多半是输入数据里出现了异常值,比如图像没有正确除以255归一化,或者图像本身是全黑的(数值全为0)。检查数据集中是否存在纯色或损坏的图像样本,比调整学习率更有效。
4. 用PSNR和SSIM给重建结果打分前,先处理四个边界问题
4.1 只算Y通道:颜色空间转换的顺序别搞错
评估超分模型时,一个常见的错误是把模型输出的Y通道图像直接转成RGB再和HR比较。正确做法是先让SR和HR处于同一颜色空间,再对Y通道单独打分。用cv2实现时要特别注意读入图像是BGR顺序,先做颜色转换再取通道:
import cv2 import numpy as np from skimage.metrics import structural_similarity as ssim def evaluate_sr(sr_bgr, hr_bgr, border=14): # 输入均为 cv2.imread 读出的 BGR 图像 sr_y = cv2.cvtColor(sr_bgr, cv2.COLOR_BGR2YCrCb)[..., 0] hr_y = cv2.cvtColor(hr_bgr, cv2.COLOR_BGR2YCrCb)[..., 0] # 去掉边界,消除 padding 伪影的影响 sr_y = sr_y[border:-border, border:-border] hr_y = hr_y[border:-border, border:-border] psnr_val = cv2.PSNR(hr_y, sr_y) ssim_val = ssim(hr_y, sr_y, data_range=255, win_size=11) return psnr_val, ssim_val这段代码的评估逻辑和训练数据生成是对应的。训练时模型只见过Y通道,评估时也应该只算Y通道的PSNR。如果直接在RGB三个通道上统一算PSNR,色度通道的插值误差会拉低分数,而模型本身并没有优化这部分信息,得到的分值并不能反映模型的真实重建能力。
4.2 边界裁剪:14像素的由来
评估时去掉边界,是超分领域默认的预处理步骤。原因有两层:第一,卷积padding在图像边缘产生伪影;第二,不同论文对边界处理方式不一致,不裁剪会导致分数横向对比不公平。
14这个数字不是拍脑袋定的,它对应三层卷积总感受野的一半。第2章已经计算出总感受野是13,从中心像素向边缘扩展6.5个像素。裁剪14像素是行业里更保守的默认值,覆盖了卷积伪影的主要影响范围。如果是自己训练的模型,把border设成7或14差距不大,但对外对比结果时,建议统一使用14,这是多数基准评测沿用的规矩。
4.3 双三次退化的一致性:评估数据要和生产环境用同一把尺子
图像超分的退化过程不是一个数学上唯一确定的操作,同样的图像用不同库的“双三次插值”,结果会有细微差异。常见的退化实现包括MATLAB的imresize、OpenCV的INTER_CUBIC、PIL的BICUBIC,它们的核函数和边界处理不完全一致。
这里有一个容易被忽略的坑:训练时用OpenCV做双三次下采样,评估时如果加载了别人预处理的LR数据(比如用其他库生成的),相当于模型面对了一种和训练分布不一致的输入。行业里的标准动作是:训练和评估统一使用同一种退化实现,在论文里明确注明。自己在本地测试时,坚持用同一段cv2.resize代码处理训练和测试数据即可,不必纠结哪种实现更好。
4.4 SSIM存在“看起来高分、其实糊”的情况
SSIM比PSNR更接近主观感知,但它也有明显的盲区。SSIM的局部统计基于一个固定窗口(默认win_size=11),在纹理密集区域,窗口内方差大,SSIM对细节损失的敏感度反而不如在平滑区域高。一个重建后纹理被磨平的图像,在墙面、天空等平滑区域拿到很高的局部SSIM,整体分数会被拉高,掩盖了细节丢失。
验证方法是把图像分成8×8的小块,对每个块单独计算PSNR和SSIM,观察是否存在“低分块”集中在纹理区域的情况。如果高频区域的PSNR明显低于整体均值,说明模型的细节重建能力不足,这时候整体分数已经没有参考意义。我一般会在评估时同时输出这两个分数,并额外打印边缘区域的平均PSNR,用于判断模型到底是全局变清晰了,还是只在平滑区域刷分。
5. 让重建更稳的四个细节:padding、多倍率、梯度裁剪与数据增强
5.1 把padding换成replicate的收益
nn.Conv2d默认的padding_mode是zeros,在图像边缘补一圈黑边。对于超分任务,零填充会让边缘像素的颜色值被拉向黑色,导致重建结果在边界发暗。改用replicate模式,让边缘像素向外复制,可以在不引入额外参数的情况下消除这一伪影。
注意padding_mode改成replicate后,卷积对应的有效感受野会向外延伸,评估时的边界裁剪宽度要相应增加2个像素,否则边界伪影仍然会影响分数。
5.2 一个模型同时兼容x2/x3/x4的采样策略
想让一个模型支持多个缩放倍率,做法是在训练时随机选择缩放倍数。每个epoch开始时,为每个batch单独采样一个scale值,比如先从[2, 3, 4]里随机选一个,再用这个scale生成该batch的LR图像。关键约束是同一batch内的scale必须一致,因为前置上采样后所有样本尺寸相同,但不同scale的退化特征差异较大,如果混在一个batch里,梯度方向会互相干扰。我用这个方式训过x4模型,再让它跑x2,PSNR会比专用模型低0.2~0.4dB,但部署时只需要一个权重文件,省下来的存储和切换成本值得。
5.3 梯度裁剪与学习率退火配合的区间
梯度裁剪的阈值要和学习率联动调整。学习率1e-4时,clip_grad_norm_的max_norm取0.1到0.5之间比较稳妥;如果学习率降到1e-5,梯度裁剪可以放宽到1.0甚至关闭,因为此时梯度本身已经很小。配合StepLR按30个epoch乘0.5的衰减速度,训练后期loss曲线会更平缓。这套组合对MSE损失有效,换成感知损失或GAN loss时要重新调参,因为感知损失的梯度量级通常比MSE大一个数量级。
5.4 数据增强必须保持LR与HR同步
超分任务的数据增强有一项硬约束:LR和HR必须使用完全相同的几何变换,一旦旋转或翻转不同步,训练数据就废了。更稳妥的做法是把随机变换应用到HR上,再对变换后的HR做双三次下采样生成对应的LR,避免两次随机操作带来的不可控性。
下面是这个环节常见的配置与易错点:
| 细节 | 检查入口 | 常见设定 | 容易踩的坑 |
|---|---|---|---|
| 边界处理 | padding_mode | replicate | 没同步增大评估裁剪宽度 |
| 多倍率训练 | 同batch内scale | batch内固定,batch间随机 | x2/x3/x4样本混在一个batch |
| 梯度裁剪 | clip_grad_norm_ | max_norm=0.1~0.5 | 阈值小于0.05导致不收敛 |
| 数据增强 | LR与HR共用随机数 | 90度旋转4种加水平翻转2种 | 分别生成随机变换导致LR/HR错位 |
把padding换成replicate并同步把评估裁剪宽度加2个像素,是比调学习率更直接的稳定性收益。验证方法也简单:固定同一张测试图,只改这一项,对比边缘区域的PSNR变化量。
本文还有配套的精品资源,点击获取