ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python图像超分辨率实战:从SRCNN到ESRGAN的工程化实现

Python图像超分辨率实战:从SRCNN到ESRGAN的工程化实现 简介这份资源面向计算机视觉初学者、课程设计学生及需要完成图像超分辨率实验的开发者提供一套可直接运行的Python项目源码与配套设计报告。项目基于TensorFlow构建超分辨率模型涉及NumPy、scipy.misc、PIL.image等图像处理库并需配置NVIDIA GPU、CUDA与cuDNN环境适合作为深度学习入门实践或毕业设计参考。压缩包共45个文件约2.84MB包含4个Python脚本分别负责模型定义、训练、工具函数与主流程1份docx设计报告以及24张png和10张jpg图像样本另有sln解决方案、pyproj工程配置与README说明目录结构清晰便于快速导入Visual Studio 2017运行。目前已有1331人学习下载。读者可从中获得完整的超分辨率训练与推理代码、实验报告撰写框架、数据集组织方式及环境配置思路适合对照复现并在此基础上调整网络结构或更换数据集进行二次开发。1. 图像超分辨率重建从一张模糊截图到可交付的 Python 工程手机截屏放大两倍就糊成马赛克监控画面里车牌永远差几个像素才能看清老照片扫描件放到 4K 屏幕上全是锯齿——这些场景背后是同一个问题低分辨率图像丢失了高频信息而人眼对边缘和纹理的缺失极其敏感。图像超分辨率Image Super Resolution要做的就是用一个模型把低清图映射成高清图补回那些丢失的细节。Python 生态里这条路已经相当成熟从 OpenCV 的传统插值到 PyTorch 的深度学习模型几行代码就能跑通一个最小闭环。这篇笔记面向想动手复现的开发者不管你是刚配好 vscode python 环境的新手还是想找一个能落地的超分方案的老手下面会从环境搭建、模型选型、训练推理到避坑把一条完整的工程路径讲清楚。适合谁有 Python 基础、能看懂张量维度、愿意花一个下午跑通第一个超分模型的人。2. 超分任务的技术底座退化模型、评价指标与数据准备2.1 为什么 bicubic 下采样是超分模型的默认退化方式超分任务有一个容易被忽略的前提低清图是怎么来的。真实场景里低分辨率图像的退化过程可能包含模糊、噪声、压缩伪影、下采样等多种因素叠加但学术研究和工程复现中最常见的做法是用双三次插值bicubic对高清图做下采样来构造配对数据。原因很直接bicubic 退化是可逆性较好的确定性操作模型学到的映射关系稳定训练不容易发散。具体来说一张 HR 图像 I_HR 经过退化函数 D 得到 LR 图像 I_LR D(I_HR)超分模型 F 要学的是 F(I_LR) ≈ I_HR。当 D 是 bicubic 下采样时这个逆问题相对良态当 D 包含真实噪声和未知模糊核时问题变成盲超分难度陡增。我一般建议刚上手时先用 bicubic 退化跑通全流程确认模型结构和训练循环没问题再考虑换更复杂的退化。常见的退化参数设置缩放因子 scale 取 2、3、4其中 ×4 是最常用的基准下采样前是否加高斯模糊取决于你想模拟的场景。如果目标场景是手机截图放大bicubic 就够了如果是监控视频建议加上 σ1.0 左右的高斯模糊再下采样。2.2 PSNR 和 SSIM 到底该怎么看超分模型训练完第一件事是看指标。PSNR峰值信噪比和 SSIM结构相似性是两个绕不开的数字但它们各有盲区。PSNR 计算的是像素级均方误差的对数变换公式是 PSNR 10·log10(MAX²/MSE)。它的优点是计算简单、数值直观缺点是和人眼感知相关性差——一张过度平滑但像素误差小的图PSNR 可能比一张纹理锐利但略有偏移的图更高。SSIM 从亮度、对比度、结构三个维度比较图像更接近人眼判断但在纹理密集区域仍然会给出偏高的分数。实操中的做法是训练时用 PSNR 做早停判断因为它的梯度信号稳定最终评估时 PSNR 和 SSIM 都报再补一组视觉对比图。如果 PSNR 高但肉眼看着糊大概率是模型过度平滑了需要检查损失函数里是否只有 L1/L2 损失。指标计算基础对什么敏感典型 ×4 超分数值PSNR像素 MSE整体亮度偏移28–32 dBSSIM局部统计量结构完整性0.85–0.92LPIPS深度特征距离感知质量越低越好2.3 用 Python 构建 DIV2K 格式的训练对数据准备这一步很多人卡在目录结构和格式上。DIV2K 是超分领域最常用的公开数据集它的组织方式值得直接借用HR 图像放在DIV2K_train_HR目录下LR 图像按缩放因子放在DIV2K_train_LR_bicubic/X4目录下文件名一一对应。如果你手头只有 HR 图像需要自己生成 LR。下面这段代码用 OpenCV 做 bicubic 下采样同时保留文件名映射关系import cv2 import os import numpy as np def generate_lr(hr_dir, lr_dir, scale4): 从 HR 目录生成 LR 图像使用 bicubic 下采样 hr_dir: 高清图目录 lr_dir: 低清图输出目录 scale: 缩放因子常用 2/3/4 os.makedirs(lr_dir, exist_okTrue) for fname in os.listdir(hr_dir): if not fname.lower().endswith((.png, .jpg, .jpeg)): continue hr_path os.path.join(hr_dir, fname) img cv2.imread(hr_path, cv2.IMREAD_COLOR) if img is None: print(f跳过无法读取的文件: {fname}) continue h, w img.shape[:2] # 先裁剪到 scale 的整数倍避免尺寸不整除 h_crop h - h % scale w_crop w - w % scale img img[:h_crop, :w_crop] lr cv2.resize(img, (w_crop // scale, h_crop // scale), interpolationcv2.INTER_CUBIC) cv2.imwrite(os.path.join(lr_dir, fname), lr) print(f完成LR 图像保存在 {lr_dir}) generate_lr(./data/DIV2K_train_HR, ./data/DIV2K_train_LR/X4, scale4)这段代码的关键点有三个一是裁剪到 scale 的整数倍否则 resize 后的尺寸会有舍入误差训练时对不齐二是用cv2.INTER_CUBIC而不是默认的INTER_LINEAR因为 bicubic 是超分任务的标准退化方式三是保留原始文件名后续 DataLoader 配对时直接按文件名索引即可。参数方面scale 决定了下采样倍数也决定了模型的上采样倍数。如果你要做 ×4 超分LR 图像的尺寸就是 HR 的 1/4。训练时通常还会做随机裁剪比如从 HR 上裁 192×192 的 patch对应 LR 就是 48×48这个尺寸在显存和感受野之间比较平衡。注意生成 LR 时不要用 JPEG 压缩后再读取压缩伪影会引入额外退化导致模型学到错误的映射。如果必须用 JPEG质量因子至少设 95。3. 从零搭一个可训练的超分模型SRCNN 到 ESRGAN 的选型与实现3.1 模型选型SRCNN、EDSR、ESRGAN 分别适合什么场景超分模型经过几年迭代已经形成了几个清晰的梯队。选型时不要只看论文里的 PSNR 数字要看你的场景更在意什么。SRCNN 是最早的深度学习超分模型三层卷积结构极简。它的优势是训练快、显存占用低适合用来验证数据管线和训练循环是否正确。缺点是感受野小×4 超分时细节恢复能力有限PSNR 通常在 28 dB 左右。EDSR 去掉了 SRCNN 里的 batch normalization用残差块堆叠在 PSNR 指标上表现很好。它的训练时间比 SRCNN 长但收敛稳定适合对保真度要求高的场景比如医学影像放大。EDSR 的 ×4 模型在 DIV2K 验证集上能到 32 dB 以上。ESRGAN 在 EDSR 的基础上引入了生成对抗网络判别器负责判断图像是真实的还是生成的生成器则努力骗过判别器。结果是 ESRGAN 的 PSNR 可能比 EDSR 低一点但视觉锐度和纹理自然度明显更好。如果你的场景是人眼观看——比如老照片修复、游戏贴图放大——ESRGAN 是更合适的选择。模型参数量级训练难度PSNR 倾向适用场景SRCNN~57K低偏低验证流程、教学EDSR~1.5M中高保真度优先ESRGAN~16M高中感知质量优先3.2 用 PyTorch 写一个最小可训练的 SRCNN下面这段代码实现了一个完整的 SRCNN 训练循环包括数据加载、模型定义、损失函数和优化器。你可以直接复制运行只需要把数据路径改成自己的。import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import Dataset, DataLoader import cv2 import os import numpy as np class SRDataset(Dataset): 超分数据集按文件名配对 HR 和 LR def __init__(self, hr_dir, lr_dir, patch_size192, scale4): self.hr_dir hr_dir self.lr_dir lr_dir self.patch_size patch_size self.scale scale self.fnames sorted(os.listdir(hr_dir)) def __len__(self): return len(self.fnames) def __getitem__(self, idx): fname self.fnames[idx] hr cv2.imread(os.path.join(self.hr_dir, fname), cv2.IMREAD_COLOR) lr cv2.imread(os.path.join(self.lr_dir, fname), cv2.IMREAD_COLOR) hr cv2.cvtColor(hr, cv2.COLOR_BGR2RGB) lr cv2.cvtColor(lr, cv2.COLOR_BGR2RGB) # 随机裁剪 h, w lr.shape[:2] if h self.patch_size // self.scale and w self.patch_size // self.scale: lr_patch self.patch_size // self.scale top np.random.randint(0, h - lr_patch 1) left np.random.randint(0, w - lr_patch 1) lr lr[top:toplr_patch, left:leftlr_patch] hr hr[top*self.scale:(toplr_patch)*self.scale, left*self.scale:(leftlr_patch)*self.scale] # 转 tensor归一化到 [0,1] lr torch.from_numpy(lr.transpose(2,0,1)).float() / 255.0 hr torch.from_numpy(hr.transpose(2,0,1)).float() / 255.0 return lr, hr class SRCNN(nn.Module): 三层卷积超分模型 def __init__(self): super().__init__() self.net nn.Sequential( nn.Conv2d(3, 64, kernel_size9, padding4), nn.ReLU(inplaceTrue), nn.Conv2d(64, 32, kernel_size1, padding0), nn.ReLU(inplaceTrue), nn.Conv2d(32, 3, kernel_size5, padding2) ) def forward(self, x): # 先双三次上采样到目标尺寸再卷积 x nn.functional.interpolate(x, scale_factor4, modebicubic, align_cornersFalse) return self.net(x) # 训练配置 device torch.device(cuda if torch.cuda.is_available() else cpu) model SRCNN().to(device) criterion nn.L1Loss() # L1 比 L2 更不容易过平滑 optimizer optim.Adam(model.parameters(), lr1e-4) dataset SRDataset(./data/DIV2K_train_HR, ./data/DIV2K_train_LR/X4) loader DataLoader(dataset, batch_size16, shuffleTrue, num_workers4) for epoch in range(50): model.train() total_loss 0 for lr, hr in loader: lr, hr lr.to(device), hr.to(device) optimizer.zero_grad() sr model(lr) loss criterion(sr, hr) loss.backward() optimizer.step() total_loss loss.item() print(fEpoch {epoch1}, Loss: {total_loss/len(loader):.4f})这段代码有几个设计决策需要说明。第一SRCNN 的前向传播里先做了 bicubic 上采样这是原论文的做法模型只负责精修上采样后的图像。第二损失函数用 L1 而不是 MSE因为 L1 对异常值更鲁棒生成的图像边缘更锐利。第三随机裁剪的 patch 尺寸是 192×192 的 HR 对应 48×48 的 LR这个尺寸在 16GB 显存下可以跑 batch size 16。参数调整建议学习率 1e-4 是 Adam 的常用起点如果 loss 震荡就降到 5e-5batch size 根据显存调整但不要小于 8否则 BN 统计量不稳定虽然 SRCNN 没有 BN但小 batch 梯度噪声大epoch 数 50 是 SRCNN 的收敛区间EDSR 需要 200 以上。3.3 训练过程中的显存与速度权衡超分模型的显存占用主要来自三个部分模型参数、中间特征图、梯度缓存。以 EDSR ×4 为例16 个残差块、64 通道输入 48×48 的 LR patch中间特征图尺寸是 48×48×64单个样本占约 0.6MBbatch size 16 就是 10MB 左右加上梯度翻倍模型参数本身约 6MB总体在 1GB 以内。但如果你把 patch 尺寸提到 96×96特征图占用翻四倍显存就会紧张。速度方面训练时的主要瓶颈是卷积计算。RTX 3060 上 SRCNN 一个 epoch 大约 2 分钟EDSR 大约 15 分钟ESRGAN 因为要训练判别器时间再翻倍。如果嫌慢可以先用小 patch 和少 epoch 跑通流程确认 loss 在下降后再放大。推理阶段的显存占用和输入尺寸直接相关。一张 1080p 的 LR 图像做 ×4 超分输出是 4K中间特征图在 EDSR 里可能占到 4GB 以上。解决办法是分块推理把大图切成有重叠的小块逐块超分后拼接重叠区域做加权融合消除接缝。4. 推理部署与效果验证把模型跑成可用的工具4.1 单张图像推理的完整脚本训练完模型下一步是把它变成能处理任意图像的推理脚本。下面这段代码加载训练好的权重对输入图像做超分并保存结果。import torch import cv2 import numpy as np from model import SRCNN # 假设模型定义在 model.py 里 def inference(model_path, input_path, output_path, scale4): device torch.device(cuda if torch.cuda.is_available() else cpu) model SRCNN().to(device) model.load_state_dict(torch.load(model_path, map_locationdevice)) model.eval() img cv2.imread(input_path, cv2.IMREAD_COLOR) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 裁剪到 scale 的整数倍 h, w img.shape[:2] h_crop h - h % scale w_crop w - w % scale img img[:h_crop, :w_crop] tensor torch.from_numpy(img.transpose(2,0,1)).float() / 255.0 tensor tensor.unsqueeze(0).to(device) with torch.no_grad(): output model(tensor) output output.squeeze(0).cpu().numpy().transpose(1,2,0) output np.clip(output * 255, 0, 255).astype(np.uint8) output cv2.cvtColor(output, cv2.COLOR_RGB2BGR) cv2.imwrite(output_path, output) print(f超分完成输出尺寸: {output.shape}) inference(./checkpoints/srcnn_x4.pth, ./test_lr.png, ./test_sr.png)推理脚本和训练脚本的关键区别在于model.eval()关闭 dropout 和 BN 的训练模式torch.no_grad()关闭梯度计算节省显存输出后要clip到 [0,255] 再转 uint8否则会出现溢出导致的噪点。参数说明scale必须和训练时一致用 ×4 训练的模型不能直接做 ×2 推理输入图像如果尺寸不是 scale 的整数倍裁剪掉边缘几个像素比补零更安全补零会在边缘产生明显伪影。4.2 用 PSNR/SSIM 脚本量化验证模型效果推理完不能只看图要有数字。下面这个脚本计算超分结果和 HR 参考图之间的 PSNR 和 SSIM。import cv2 import numpy as np from skimage.metrics import peak_signal_noise_ratio, structural_similarity def evaluate(sr_path, hr_path): sr cv2.imread(sr_path, cv2.IMREAD_COLOR) hr cv2.imread(hr_path, cv2.IMREAD_COLOR) # 确保尺寸一致 if sr.shape ! hr.shape: sr cv2.resize(sr, (hr.shape[1], hr.shape[0])) psnr peak_signal_noise_ratio(hr, sr, data_range255) ssim structural_similarity(hr, sr, channel_axis2, data_range255) print(fPSNR: {psnr:.2f} dB, SSIM: {ssim:.4f}) return psnr, ssim evaluate(./test_sr.png, ./test_hr.png)这段代码依赖scikit-image安装命令是pip install scikit-image。注意channel_axis2是新版 skimage 的参数旧版用multichannelTrue。如果 PSNR 低于 28 dB说明模型欠拟合或数据有问题如果 SSIM 低于 0.85检查一下 HR 和 SR 是否对齐有时候保存图像时的颜色空间转换会导致通道错位。4.3 分块推理解决大图显存溢出前面提到大图推理会爆显存分块推理是标准解法。核心思路是把 LR 图像切成有重叠的块逐块超分再把结果拼回去重叠区域做线性加权融合。def tiled_inference(model, img_tensor, scale4, tile_size64, overlap8): 分块推理tile_size 是 LR 块大小overlap 是重叠像素数 b, c, h, w img_tensor.shape output torch.zeros(b, c, h*scale, w*scale, deviceimg_tensor.device) weight torch.zeros_like(output) step tile_size - overlap for i in range(0, h, step): for j in range(0, w, step): i_end min(i tile_size, h) j_end min(j tile_size, w) i_start max(0, i_end - tile_size) j_start max(0, j_end - tile_size) tile img_tensor[:, :, i_start:i_end, j_start:j_end] with torch.no_grad(): sr_tile model(tile) # 计算融合权重边缘低中间高 h_tile (i_end - i_start) * scale w_tile (j_end - j_start) * scale w_i torch.ones(h_tile, deviceimg_tensor.device) w_j torch.ones(w_tile, deviceimg_tensor.device) if i_start 0: w_i[:overlap*scale] torch.linspace(0, 1, overlap*scale) if i_end h: w_i[-overlap*scale:] torch.linspace(1, 0, overlap*scale) if j_start 0: w_j[:overlap*scale] torch.linspace(0, 1, overlap*scale) if j_end w: w_j[-overlap*scale:] torch.linspace(1, 0, overlap*scale) w_map w_i.unsqueeze(1) * w_j.unsqueeze(0) output[:, :, i_start*scale:i_end*scale, j_start*scale:j_end*scale] sr_tile * w_map weight[:, :, i_start*scale:i_end*scale, j_start*scale:j_end*scale] w_map return output / weight.clamp(min1e-6)这段代码的关键是权重图的设计重叠区域用线性渐变权重越靠近块边缘权重越低这样拼接时不会出现明显的接缝。tile_size和overlap需要根据显存调整64 和 8 是一组保守值显存充裕可以加大 tile_size 减少块数。提示分块推理的结果和整图推理会有微小差异因为卷积在块边缘的感受野被截断了。如果对一致性要求高把 overlap 设大一些至少覆盖模型最大感受野的一半。5. 避坑与排查超分训练里最容易翻车的五个地方5.1 损失不下降输出全是灰色现象训练几个 epoch 后模型输出的图像接近纯灰色PSNR 停在 10 dB 左右。原因最常见的是数据归一化不一致。训练时 HR 和 LR 都除以 255 归一化到 [0,1]但推理时忘了做同样的归一化或者反过来。另一个可能是学习率太大模型直接发散。解决检查数据加载和推理脚本里的归一化是否一致。用print(lr.min(), lr.max(), hr.min(), hr.max())确认数值范围。学习率从 1e-4 开始如果 loss 在前几个 batch 就变成 NaN降到 1e-5。5.2 PSNR 很高但图像看起来糊现象验证集 PSNR 到 32 dB但超分结果肉眼看着像蒙了一层雾。原因损失函数只有 L1 或 MSE模型倾向于输出所有可能高分辨率图像的平均值导致高频细节被平滑掉。这是回归类损失的固有缺陷。解决在损失里加入感知损失VGG 特征距离或对抗损失。如果不想上 GAN至少把 L1 换成 Charbonnier 损失它对边缘的惩罚更温和。另一个办法是训练时加入边缘增强的后处理但这是治标不治本。5.3 训练 loss 震荡剧烈现象loss 曲线上下跳动没有稳定下降的趋势。原因batch size 太小导致梯度噪声大或者学习率相对于 batch size 偏高。另外如果数据里有损坏的图像文件也会造成偶发的 loss 尖峰。解决先把 batch size 提到 16 以上如果显存不够就减小 patch 尺寸。学习率用余弦退火调度从 1e-4 降到 1e-6。检查数据集中是否有全黑或全白的异常图像用脚本过滤掉。5.4 推理结果有棋盘格伪影现象超分图像上出现规律的网格状纹理尤其在平滑区域明显。原因转置卷积deconvolution的 stride 和 kernel size 不匹配时会产生棋盘格效应。很多超分模型用 PixelShuffle 替代转置卷积就是为了避免这个问题。解决如果模型用了转置卷积换成nn.PixelShuffle上采样。如果已经用了 PixelShuffle 还有伪影检查上采样前的特征图是否有 NaN 或极值必要时在卷积后加 clamp。5.5 显存溢出但 GPU 利用率不高现象训练时显存爆了但nvidia-smi显示 GPU 利用率只有 30%。原因数据加载是瓶颈。num_workers设得太小或者数据预处理在 CPU 上做 OpenCV resize 太慢GPU 一直在等数据。解决把num_workers提到 4 或 8用pin_memoryTrue加速数据传输。如果还是慢把 LR 图像预先生成好存到磁盘不要在训练时实时下采样。另外用torch.cuda.amp混合精度训练可以省一半显存速度也能提升。6. 进阶技巧用预训练 VGG 做感知损失提升视觉质量如果你已经跑通了 SRCNN 或 EDSR但觉得输出还是不够锐下一步值得试的是感知损失。它的思路是不直接比较像素而是把超分结果和 HR 分别送进一个预训练的 VGG 网络比较它们在中间层的特征差异。这样模型会倾向于生成纹理和结构更自然的图像而不是像素平均后的平滑结果。具体实现上取 VGG19 的conv4_4层输出作为特征计算 L1 距离。权重通常设 0.1 到 1.0 之间和像素损失加权求和。下面是一个最小实现import torch import torch.nn as nn import torchvision.models as models class PerceptualLoss(nn.Module): def __init__(self, layer_idx35): super().__init__() vgg models.vgg19(pretrainedTrue).features self.loss_net nn.Sequential(*list(vgg.children())[:layer_idx]).eval() for p in self.loss_net.parameters(): p.requires_grad False self.criterion nn.L1Loss() def forward(self, sr, hr): # VGG 输入需要归一化到 ImageNet 统计量 mean torch.tensor([0.485, 0.456, 0.406]).view(1,3,1,1).to(sr.device) std torch.tensor([0.229, 0.224, 0.225]).view(1,3,1,1).to(sr.device) sr_norm (sr - mean) / std hr_norm (hr - mean) / std return self.criterion(self.loss_net(sr_norm), self.loss_net(hr_norm))使用时把它和 L1 损失加权total_loss l1_loss 0.1 * perceptual_loss。注意 VGG 的输入需要 ImageNet 归一化而超分模型的输出是 [0,1] 范围所以要先做归一化再送进 VGG。layer_idx35对应conv4_4这个层的感受野和纹理特征比较平衡再深会偏向语义信息对超分帮助不大。感知损失的一个副作用是 PSNR 可能会降 0.5 到 1 dB但视觉锐度提升明显。如果你的场景是人眼观看这个交换是值得的。如果下游任务是目标检测或 OCRPSNR 更重要感知损失权重设小一点或者不用。我自己的习惯是先用纯 L1 训练到收敛保存一个 checkpoint然后加载这个 checkpoint 加上感知损失微调 20 个 epoch。这样既保证了训练稳定又拿到了视觉质量的提升。微调时学习率降到 1e-5避免破坏已经学好的底层特征。最后说一个验证技巧不要只看验证集的平均 PSNR挑几张有代表性的图——包含文字、纹理、边缘的——单独看超分前后的对比。平均指标会掩盖个例的失败而实际部署时用户记住的往往就是那几张翻车的图。希望帮到你。本文还有配套的精品资源点击获取
返回列表