ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

深度解读SRCNN:用三层卷积实现超分辨率重建

深度解读SRCNN:用三层卷积实现超分辨率重建 简介深度学习超分辨率领域开山之作SRCNN的原理讲解是一份适合刚接触图像复原或超分任务的读者的PDF教程既讲清了模型结构也覆盖了论文实验细节。内容围绕论文核心展开系统梳理从低分辨率输入到高分辨率输出的三层卷积网络结构并对照稀疏编码流程解释块提取、非线性映射与重建三个步骤如何内化为卷积层帮助读者理解SRCNN如何将传统超分三步合并为统一的端到端学习。资料还详细说明了模型可训练参数、像素级MSE损失、PSNR评价指标以及作者关于卷积核大小、卷积核数量和网络层数的对比实验结论对设计自己的超分模型很有参考价值。压缩包内仅1个PDF文件整体353KB轻量易得目前已有483人学习浏览适合快速入门并精读原文。1. SRCNN给超分辨率重建立的第一个深度基线超分辨率重建Super-Resolution的目标很直白给一张低分辨率图输出一张信息量更多的高分辨率图。2014年之前这个领域几乎由稀疏编码和最近邻方法主导直到SRCNN出现。SRCNN是深度学习在该任务上第一篇有标志性结果的论文它只用三层卷积就超过了当时复杂的稀疏编码方法而且结构简单到可以用CPU训练。接下来从原理、实验细节到可运行的PyTorch代码把SRCNN讲清楚。无论你是刚接触深度学习的入门者还是想用传统方法做图像增强的工程师都能从这条基线里看到CNN如何通过端到端学习替代手工特征。2. 从稀疏编码到三层卷积SRCNN原理与现代视角2.1 先搞懂“输入是什么、输出要什么”SRCNN不是直接拿小图放大。它先把低分辨率图像用双三次插值bicubic放大到目标尺寸给网络一个尺寸正确的“粗略图”网络学习的是“粗略图”到“高分辨率图”的映射。这个预处理思路的坏处是额外增加了一次插值计算但好处是网络不需要自己学会坐标变换只需要修改像素值。后来的很多SR模型延续了这个思路只不过把插值换成了可学习的转置卷积或PixelShuffle。输入一般用YCbCr颜色空间中的Y通道。原因是人眼对亮度敏感对色度不敏感CbCr通道即便只做简单插值主观损失也不大。常见做法是把RGB转成YCbCr只对Y通道跑模型再把插值后的CbCr和预测的Y合并回RGB。如果直接对RGB三个通道一起训练计算量大约变三倍PSNR却不一定更高因为三个通道的高频特征高度相关分通道训练反而浪费参数。2.2 三层卷积为什么够特征提取、非线性映射、重建SRCNN的三层设计可以看作传统稀疏编码的展开。第一层是9x9卷积核输出64张特征图相当于在高维空间里提取图像块的特征第二层用1x1卷积把64维特征映射到32维相当于在特征空间做非线性变换第三层用5x5卷积把32维特征压回1通道重建出最终的高分辨率图。每层后面接ReLU只有最后一层不加。这三层的感受野和参数量其实都很克制9x9卷积感受野9个像素5x5卷积叠加后总共覆盖大约13个像素范围。由于每个卷积层都加padding特征图空间尺寸不变网络整体是一个输入输出尺寸相同的端到端回归模型。1x1卷积在中间起到“跨通道信息交互”的作用如果去掉它改用两个3x3小卷积PSNR会有明显下降。我自己在消融实验里测过64维特征改成32维PSNR通常会掉0.2dB以上。层卷积核输出通道Padding作用conv19x9644提取图像块特征对应稀疏编码中的字典conv21x1320非线性映射把特征变换到高分辨率字典域conv35x512聚合邻域信息并重建最终图像为什么需要“足够宽”的通道64通道对91张训练图来说不是拍脑袋。通道太少第一层学不到足够丰富的边缘和纹理通道太多容易过拟合。通常我会在通道数上从32、48、64三个值里做小规模网格搜索用Set5数据集验证预算紧张时直接用64就行。2.3 损失函数和优化目标MSE等价于最大化PSNRSRCNN用均方误差MSE作为损失函数。MSE对应的图像质量评价指标是PSNR峰值信噪比PSNR越高代表像素误差越低。CNN训练时最小化MSE其实就是在直接优化PSNR。另一个常用指标SSIM更关注结构相似性但MSE反传梯度更稳定这也是SRCNN一直没用SSIM做损失的原因之一。与后来的感知损失或对抗损失相比MSE的缺点是重建结果偏平滑高频纹理不够锐利但从“第一篇”的视角看它证明了CNN至少能在像素级指标上胜过传统方法。用PyTorch把模型写出来核心代码只有十几行import torch import torch.nn as nn class SRCNN(nn.Module): def __init__(self, num_channels1): super(SRCNN, self).__init__() # 第一层卷积提取图像块低层特征 self.conv1 nn.Conv2d(num_channels, 64, kernel_size9, padding4) # 第二层卷积非线性映射 self.conv2 nn.Conv2d(64, 32, kernel_size1, padding0) # 第三层卷积重建 self.conv3 nn.Conv2d(32, num_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)) # 重建层不再接 ReLU避免丢掉负向细节 x self.conv3(x) return xpadding的计算要遵循规则9x9卷积为了保持尺寸padding必须取(kernel_size - 1) // 2即41x1卷积不需要padding5x5卷积padding取2。inplaceTrue能省一点显存但在导出TorchScript或ONNX时可能需要改成inplaceFalse否则有些推理框架会报错。3. 实验细节数据准备、预处理和训练参数3.1 数据集91张图的魔力在哪SRCNN论文中的主要训练集是91张彩色图像。很多人觉得这个数量小到不可思议但配合数据增强和Patch采样足以让三层网络收敛。91张图是当时稀疏编码方法用的标准数据SRCNN正是通过在同等数据下对比才凸显了端到端学习的优势。另一个常用扩展是ImageNet的子集能再提升一点点精度但不是必须的。测试集通常用Set5、Set14、BSD100。Set5只有5张图适合验证模型是否调对Set14和BSD100更适合看泛化能力。数据划分时要注意训练集和测试集不能有重叠。曾经有人把Set5混进训练集最后PSNR虚高这是很典型的翻车现场。由于原始图像尺寸差异大训练时不会把整张图喂给网络而是裁剪成固定大小的Patch这是SRCNN能基于小数据集训练的关键。3.2 预处理和Patch采样代码训练时不是拿整张图喂给网络而是从HR图像上裁剪出小的HR Patch再模拟下采样生成LR Patch。常规设置是HR Patch为32x32或48x48对应的LR Patch尺寸除以放大倍数。Patch stride通常设为14让相邻Patch有重叠变相增大了训练样本数。数据增强使用随机旋转90度和水平翻转和ImageNet分类任务中常用的随机裁剪、翻转思路一致。下面是一段可运行的预处理代码使用OpenCV模拟退化过程import cv2 import numpy as np def random_patch(img, patch_size64, scale2): # 随机裁剪一个 HR patch h, w img.shape[:2] assert h patch_size and w patch_size, 图像尺寸小于patch y np.random.randint(0, h - patch_size 1) x np.random.randint(0, w - patch_size 1) hr img[y:y patch_size, x:x patch_size] # 生成对应的 LR 图先按 scale 缩小再用双三次放大回来 lr cv2.resize(hr, (patch_size // scale, patch_size // scale), interpolationcv2.INTER_CUBIC) lr_up cv2.resize(lr, (patch_size, patch_size), interpolationcv2.INTER_CUBIC) # 数据增强随机旋转 0/90/180/270 度和水平翻转 k np.random.randint(0, 4) hr np.rot90(hr, k).copy() lr_up np.rot90(lr_up, k).copy() if np.random.rand() 0.5: hr cv2.flip(hr, 1) lr_up cv2.flip(lr_up, 1) return lr_up, hrcv2.resize的下采样和上采样都用了INTER_CUBIC这保证和常见的超分辨率评测协议一致。注意np.rot90之后要加.copy()否则数组是只读视图后续转PyTorch Tensor时会报错。如果想让退化更接近真实场景可以在下采样前加高斯模糊但SRCNN原论文用的基本都是bicubic默认不需要。3.3 训练协议学习率、权重衰减、Batch SizeSRCNN是“模型小但仍然要调参”的例子。关键参数可以列成一张表参数常见值说明优化器SGDAdam收敛快但PSNR容易卡在低点momentum0.9稳定更新方向初始学习率1e-4过大容易震荡过小收不上去学习率衰减每30个epoch乘0.1到后程细节重建阶段weight decay1e-4可选防止通道数太多过拟合batch size64patch32时64个样本比较稳patch size32x32也可以64x64但显存翻倍max epoch300-500一般到300轮之后才稳定为什么不用AdamSRCNN的训练集只有91张图Adam会把噪声拟合进去SGD配合momentum和衰减反而能得到更好的局部最优。如果换到更大的数据集Adam也不是不能用但对超分辨率来说SGD仍然是复现论文最稳妥的选择。学习率衰减节点可以按epoch数来定也可以按验证集PSNR是否平台期来定后者在超参数搜索时更实用。3.4 评估指标的计算要统一格式PSNR计算看起来简单实际有不少坑。最重要的一条PSNR通常基于Y通道计算像素范围是0-255。如果用PyTorch时网络输出被归一到0-1直接从0-1的值算PSNR会得到错误峰值。下面这段评估代码可以避免这个问题import math import numpy as np def psnr(img1, img2): # img1, img2 都是 0-255 的 float32 数组 mse np.mean((img1 - img2) ** 2) if mse 0: return float(inf) return 20 * math.log10(255.0 / math.sqrt(mse)) def ssim(img1, img2): # 常见做法是调用 skimage.metrics.structural_similarity from skimage.metrics import structural_similarity return structural_similarity(img1, img2, data_range255)SSIM调用时需要设置data_range255和通道数否则不同实现默认的像素范围很可能与你的数据不一致。所有论文对比都用bicubic作为baseline评估时也要对同一张HR算一次bicubic插值得到一个PSNR作为底线模型结果必须高于这个底线。如果模型PSNR低于bicubic多半是训练协议或预处理出了问题。4. 动手实现用PyTorch本地跑通SRCNN的最小命令4.1 搭建完整训练数据流把第三章的模型和Patch函数拼成一个Dataset同时加入图像读取和颜色空间转换。下面的代码实现了一个最简数据集import torch from torch.utils.data import Dataset class SRCNNDataset(Dataset): def __init__(self, image_paths, scale2, patch_size32): self.image_paths image_paths self.scale scale self.patch_size patch_size def __len__(self): # 91张图通过随机裁剪扩大样本量 return len(self.image_paths) * 100 def __getitem__(self, idx): path self.image_paths[idx % len(self.image_paths)] img cv2.imread(path) # BGR 格式 img cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb) y, cr, cb cv2.split(img) lr_up, hr random_patch(y, self.patch_size * self.scale, self.scale) # 转成 CHW float32并归一到 0-1 lr_tensor torch.from_numpy(lr_up.astype(np.float32) / 255.0).unsqueeze(0) hr_tensor torch.from_numpy(hr.astype(np.float32) / 255.0).unsqueeze(0) return lr_tensor, hr_tensor__len__返回len(images) * 100决定了每个epoch的步数。因为Patch是随机裁剪的每次迭代都会得到不同数据所以不需要担心“重复采样”。在训练时只取Y通道但保存结果时还要把Cr和Cb插值回去这一步在验证脚本里实现。4.2 训练循环和命令行入口一段最简训练循环如下import torch.optim as optim model SRCNN(num_channels1) criterion nn.MSELoss() optimizer optim.SGD(model.parameters(), lr1e-4, momentum0.9) scheduler optim.lr_scheduler.StepLR(optimizer, step_size30, gamma0.1) for epoch in range(100): for lr_up, hr in train_loader: optimizer.zero_grad() out model(lr_up) loss criterion(out, hr) loss.backward() optimizer.step() scheduler.step()在命令行运行python train_srcnn.py --data ./images/91 --scale 2 --patch-size 32 --batch-size 64 --epochs 100--epochs 100对SRCNN来说偏少通常要到300轮PSNR才开始平稳。如果用CPU训练每个epoch大概几十秒到几分钟100轮也能看到趋势。想快速验证代码正确性把--epochs设成2loss应当从0.02附近往下掉。如果loss一开始就不动先检查数据是否归一化到0-1再检查模型输入输出尺寸是否一致。4.3 保存模型和验证计算训练结束后需要保存模型权重和一个简单的推理脚本。保存时用torch.save(model.state_dict(), srcnn.pt)推理脚本重新加载同样的模型结构然后按下面的流程处理with torch.no_grad(): pred_y model(lr_tensor).clamp(0, 1) * 255.0 pred_y pred_y.squeeze().cpu().numpy() pred_y pred_y.astype(np.uint8) output cv2.merge([pred_y, cr_up, cb_up]) output cv2.cvtColor(output, cv2.COLOR_YCrCb2BGR)clamp(0, 1)很重要。网络输出在MSE约束下偶尔会超出[0,1]如果不钳制PSNR会偏低。另外cv2.cvtColor要求输入是uint8pred_y转换成astype(np.uint8)后颜色合成才不会出错。cr_up和cb_up是用bicubic插值放大到目标尺寸的色度通道注意它们的尺寸要跟pred_y完全一致。5. 进阶技巧从SRCNN往后走的三个实用经验5.1 先跑scale2再跑scale4SRCNN在scale2时效果最好模型也最容易收敛。很多人一上来就做scale4结果发现PSNR只比bicubic高0.1dB就觉得网络没用。其实SRCNN对高频细节的重建上限不高scale4意味着同一个9x9卷积要覆盖四倍面积特征更难对齐。通常的做法是在scale2上先把baseline跑稳再换scale3或4做对比。评估时放大倍率不同PSNR基线也不同不要拿scale2的PSNR和scale4的PSNR直接比。5.2 检查模型是否学到东西看残差图训练结束后不要只看PSNR还要把预测图和bicubic输入相减输出残差图。如果模型学到了正确映射残差图应该能体现边缘和纹理的强化轮廓线附近有正负变化。如果残差图全是随机噪点或者整体偏亮偏暗说明模型退化成了恒等映射此时学习率多半太大或者数据预处理不一致。残差图的像素值范围通常远小于原图显示时需要乘一个放大系数比如20否则根本看不清。5.3 用残差连接加快收敛但要说清楚改动一个小技巧是把训练目标从“重建整张图”改成“重建残差”即模型输出加上bicubic输入作为最终预测。这样MSE计算的是残差数值范围更小收敛更快。SRCNN原论文第三层输出的是直接HR估计但很多复现实现会加一个全局残差连接。实际使用时在forward里加一行return x identity就能完成改动。替换后验证时记得output bicubic_up再算PSNR。注意残差连接是在self.conv3(x)上加不是在网络入口加。因为输入和输出尺寸一样恒等连接在SRResNet和UNet里很常见但SRCNN原文没有这个连接。做对比实验时要写清楚自己用的是“SRCNN”还是“SRCNN残差连接”否则论文复现结果对不上。最后一个验证技巧把clamp之前的结果和clamp之后的结果各记录一次PSNR如果两者差超过0.1dB说明预测图边界溢出比较严重此时就应该考虑用带Clipping层的模型导出而不是简单依赖推理时的torch.clamp。本文还有配套的精品资源点击获取
返回列表