简介面向 Python 图像处理学习者的图像超分辨率重建源码包聚焦低分辨率图像到高分辨率图像的恢复过程适合正在研究 SRCNN、VDSR、SRGAN 等深度学习方法的开发者阅读也可用于理解传统插值与深度学习的差异。压缩包内共 5 个 py 文件包体约 10KB源码模块覆盖数据扩展与预处理、模型定义、训练流程和测试环节结构紧凑适合从零梳理超分辨率重建的整体链路。已有 619 人学习下载。通过阅读这份源码可以理解低分辨率图像载入、训练样本组织、模型训练与结果测试的完整实现思路并可根据自身数据调整缩放因子、损失函数和训练参数代码中的模块划分也为二次开发提供了便利可作为课程设计、算法实验或小型项目的参考框架。图像超分辨率重建也常用于监控、医学影像等场景这份源码可作为一个不错的入门实践起点。1. 图像超分辨率重建究竟是什么从一张看不清的截图说起监控画面里想辨认人脸老照片上想恢复纹理医学影像中想放大局部病灶——这些需求都指向同一个问题用算法从低分辨率图像中重建出高分辨率图像。图像超分辨率重建Image Super-ResolutionSR做的事和普通缩放完全不同缩放只做插值超分做的是“重建”也就是根据先验知识补出原图没有的像素细节。这篇内容围绕Python生态里的超分源码来写从数据准备、模型训练到推理部署给出能直接照着做的完整路径适合已经会一点PyTorch、想把图像超分辨率重建落地到自己项目里的开发者。2. 超分模型凭什么补出细节三类重建思路与选型依据2.1 插值为什么不够超分要解决的其实是一个“反问题”先明确一个概念你直接用OpenCV把一张小图用双三次插值放大四倍得到的只是原始信息的平滑外推不会出现任何真正的新纹理。放大后的人脸轮廓变软了但眼睛的瞳仁细节依然不存在。原因并不复杂——插值算法只依赖目标像素周围一个有限窗口做加权平均它不携带“一张脸应该长什么样”这类全局先验。图像超分辨率重建处理的恰恰是另一个方向的问题。低分辨率图像可以看作高分辨率图像经过退化过程之后的观测值退化一般包括下采样、模糊和加噪写成公式就是Y D(x) nY 是观测到的低分辨率图x 是想要恢复的高分辨率图D 是退化算子数据准备中最常用的就是双三次下采样n 是噪声。给定 Y 反推 x这个映射是不唯一的——同一张模糊小图可以对应无数个细节不同的清晰大图。超分模型要做的不是找到那个唯一的解而是从这些可能解里挑一个符合自然图像统计规律、概率最高的结果。这个“反问题”视角直接影响了工程实现。分类任务关心顶层语义超分任务却要求在逐像素的空间细节上做精确回归。所以超分网络在结构上更看重感受野和放大倍数的匹配训练时也更依赖成对的监督数据。很多把图像分类项目里的代码改一改就拿来跑超分的人第一步就会碰壁——因为两套任务对网络设计、损失函数和数据管线的要求差异太大了。2.2 三条技术路线从字典学习到卷积网络再到对抗生成有代表性的超分重建路线大致有三代。最早是基于重建的优化方法比如稀疏编码超分。核心思路是把图像小块在离线训练好的过完备字典上进行稀疏编码假设低分辨率小块的稀疏系数与高分辨率小块共享同一组基从而用系数反推出高分辨率图像。这套方法在数学上很优雅但字典是事先学好的遇到复杂自然场景泛化能力明显不足现在基本被数据驱动的方法取代。第二代是基于CNN的回归方法代表模型包括2014年的SRCNN、2016年的VDSR、2017年的EDSR和2018年的RCAN。这些模型直接在LR到HR之间拟合映射关系训练目标是最小化重建图与真实图之间的像素误差。输出稳定、PSNR指标高但也有一个普遍弱点纹理过度平滑尤其在4倍以上放大时细节看起来很“面”缺乏真实感。第三代是基于GAN的生成方法代表是SRGAN和后来的Real-ESRGAN。生成器负责重建判别器负责区分真实高清图和生成图两者对抗博弈迫使生成图靠近真实照片的纹理分布。主观视觉更锐利但训练难度更大结果也可能生成不存在的伪纹理PSNR反而可能低于纯CNN模型。三代路线可以放在一起对比方法代表模型核心机制适用场景传统重建稀疏编码超分字典学习加稀疏约束灰度图、退化过程明确CNN回归SRCNN / EDSR / RCAN深层卷积直接回归HR常规放大、指标优先GAN生成SRGAN / Real-ESRGAN对抗学习加感知损失4倍以上放大、主观清晰优先别被“最新最强”四个字裹挟先搞清楚业务到底衡量的是PSNR数值还是最终的人眼体验。机器可以给一张平滑图打高分但放到大屏幕上大家看的全是边缘和纹理。2.3 选型判断标准放大倍数、数据品类和算力约束落到具体项目上我习惯先回答三个问题每个问题都会直接指向一组选型方案。第一放大倍数是多少。2倍放大对大多数场景来说CNN类的EDSR已经足够。2倍退化温和高频信息基本还在GAN引入的随机扰动反而可能带来伪影。到了4倍以上尤其是人脸、文字这类纹理密度高的内容CNN模型容易把边缘糊成一团GAN类模型的价值才体现出来。第二数据品类是否统一。通用自然图像可以直接用通用场景预训练好的ESPCN或Real-ESRGAN权重如果是人脸特写、车牌识别这种垂直域强烈建议找专门的超分模型或者把通用权重拿到领域数据上微调——这条路径比从零训练容易收敛得多效果也稳定。第三部署算力强不强。服务器GPU可以上RCAN这种大体积模型移动端和Web端就要考虑ESPCN这类轻量结构或者把模型导出成ONNX做量化。综合判断下来多数项目第一步不是选最新最贵的模型而是先用SRCNN或ESPCN把数据、训练、评估流程全部跑通再根据效果决定要不要升级到GAN方案。超分项目的瓶颈往往不在模型精度而在数据退化方式和评估口径这两个问题在最小模型上暴露得最快。3. 用Python跑通超分重建源码环境、数据与最小推理3.1 环境准备Python、PyTorch和图像库的最小配置这一节把运行的底座先搭起来。Python的虚拟环境隔离是第一步避免把系统的Python搞乱。PyTorch装CPU版还是GPU版取决于你手头有没有显卡——没有显卡也能跑通整个推理链路只是训练会很慢。# 创建虚拟环境Windows下激活命令略有不同 python -m venv venv_sr source venv_sr/bin/activate # 安装PyTorch无GPU时可安装CPU版体积小很多 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install opencv-python numpy tqdm--index-url是指定PyTorch官方whl包源的参数cpu对应CPU版本想要CUDA加速就替换成对应的cu121。图像读写用OpenCV最省事但它读进来默认是BGR通道顺序很多教程默认RGB——这个差异在超分项目里造成过大量错位后面的代码会显式处理。3.2 构造HR-LR训练对下采样脚本与退化参数超分任务里最常见的数据准备方式是“自己造监督对”找一批高清大图用下采样生成低分辨率版本形成HR和LR的配对。没有现成数据集时写个脚本就能完成。import cv2 import os import numpy as np def make_pairs(img_dir, out_dir, scale4, noise_sigma0): os.makedirs(os.path.join(out_dir, HR), exist_okTrue) os.makedirs(os.path.join(out_dir, LR), exist_okTrue) for idx, name in enumerate(os.listdir(img_dir)): img cv2.imread(os.path.join(img_dir, name)) if img is None: continue h, w img.shape[:2] # 裁成可被scale整除的尺寸避免后续resize尺寸对不上 h_crop, w_crop h // scale * scale, w // scale * scale img img[:h_crop, :w_crop] # 下采样得到低分辨率图 lr cv2.resize(img, (w_crop // scale, h_crop // scale), interpolationcv2.INTER_CUBIC) if noise_sigma 0: noise np.random.normal(0, noise_sigma, lr.shape).astype(np.float32) lr np.clip(lr.astype(np.float32) noise, 0, 255).astype(np.uint8) # 再把LR放大回原尺寸供输入输出等分辨率的网络使用 lr_up cv2.resize(lr, (w_crop, h_crop), interpolationcv2.INTER_CUBIC) cv2.imwrite(os.path.join(out_dir, HR, f{idx:05d}.png), img) cv2.imwrite(os.path.join(out_dir, LR, f{idx:05d}.png), lr_up)这段脚本的核心是退化模拟。INTER_CUBIC是双三次插值比INTER_LINEAR保留更多边缘信息是超分数据准备里最常用的下采样方式。noise_sigma参数用来叠加高斯噪声实际场景中的低分辨率图往往不止丢了高频还带了传感器噪声但有噪声的数据会让模型学到的退化函数更接近真实在真实低清图上效果更好。这里先留了参数训练下一步再调。3.3 模型定义用SRCNN理解超分的最小网络结构SRCNN是最容易上手的超分网络结构只有三个卷积层没有残差、没有注意力但它作为理解超分源码的起点非常称职。import torch import torch.nn as nn class SRCNN(nn.Module): def __init__(self, in_channels3): super().__init__() # 第一层9x9卷积提取特征输出64张特征图 self.conv1 nn.Conv2d(in_channels, 64, kernel_size9, padding4) # 第二层1x1卷积压缩特征输出32张特征图 self.conv2 nn.Conv2d(64, 32, kernel_size1, padding0) # 第三层5x5卷积重建图像输出回到原始通道数 self.conv3 nn.Conv2d(32, in_channels, kernel_size5, padding2) def forward(self, x): x torch.relu(self.conv1(x)) x torch.relu(self.conv2(x)) x self.conv3(x) return x第一层9×9卷积感受野大能覆盖周围较大区域来估计中心像素第二层1×1卷积不增加感受野但能把64维特征压缩到32维大幅减少参数第三层5×5卷积输出重建结果。padding的设置是为了让卷积不改变特征图尺寸输入输出保持同分辨率。如果你不想从随机初始化开始训练可以直接加载公开的SRCNN权重但要注意权重里的state_dict的键名必须和这个模型类完全一致否则load_state_dict会报key不匹配。3.4 一段完整的单图推理程序从磁盘到高清输出把模型接到真实图片上完整流程包括读图、颜色空间转换、亮度通道推理、通道合并和保存这里有一段可以直接抄走的代码。import cv2 import numpy as np import torch def sr_inference(model, img_path, scale4, devicecpu): bgr cv2.imread(img_path) # 转YCrCb之后只在亮度通道做超分重建 ycbcr cv2.cvtColor(bgr, cv2.COLOR_BGR2YCrCb) y, cr, cb cv2.split(ycbcr) h, w y.shape # 亮度通道先放大到目标尺寸 y_up cv2.resize(y, (w * scale, h * scale), interpolationcv2.INTER_CUBIC) y_tensor torch.from_numpy(y_up.astype(np.float32) / 255.0) y_tensor y_tensor.unsqueeze(0).unsqueeze(0).to(device) model.to(device) model.eval() with torch.no_grad(): sr_tensor model(y_tensor) sr_y sr_tensor.clamp(0, 1).squeeze().cpu().numpy() * 255.0 sr_y sr_y.astype(np.uint8) # 色度通道双三次放大即可与重建后的亮度通道合并 cr_up cv2.resize(cr, (w * scale, h * scale), interpolationcv2.INTER_CUBIC) cb_up cv2.resize(cb, (w * scale, h * scale), interpolationcv2.INTER_CUBIC) out cv2.merge([sr_y, cr_up, cb_up]) out cv2.cvtColor(out, cv2.COLOR_YCrCb2BGR) cv2.imwrite(sr_output.png, out)这段代码有三个关键点。第一只在亮度通道重建因为人眼对亮度细节最敏感且单通道训练能减少颜色噪声干扰第二输入模型前必须把LR先放大到目标尺寸SRCNN这类网络要求输入输出同分辨率第三torch.no_grad()关闭梯度计算推理阶段的显存和耗时都会明显下降。clamp(0, 1)这步容易被忽略网络输出可能略超出0到1范围不截断的话保存出来的图会有过曝或纯黑区域。4. 超分训练三板斧损失函数、学习率和评价指标怎么调4.1 损失函数选择L1为什么比L2更适合图像重建训练超分模型第一个要决策的是损失函数。从分类任务转过来的开发者第一反应往往是MSEL2损失因为它数学上有唯一最优解还直接对应PSNR指标。但实际训练时L2损失有两个明显问题对异常像素惩罚过重一个高亮噪点就拉偏整个梯度而且L2倾向于把结果“平均化”输出的图像边缘偏平滑这是像素级损失的天然缺陷。现在训练SR模型的主流做法是L1损失或者L1加感知损失的组合。L1在误差较小时梯度恒定不容易被极端像素带偏训练更稳定。感知损失则是把重建图和真实HR图分别送进预训练分类网络比较中间特征层的差异用来约束纹理质感。如果把感知损失权重加大图像边缘会更锐利但也会引入高频噪声需要拿捏权重。import torch.nn as nn class CharbonnierLoss(nn.Module): L1的平滑近似训练时比原版L1更稳定 def __init__(self, eps1e-6): super().__init__() self.eps eps def forward(self, pred, target): diff pred - target return torch.mean(torch.sqrt(diff * diff self.eps ** 2))这个损失函数的本质是给L1加了一个极小常量避免误差接近0时梯度出现奇点。eps控制平滑半径取值越大小误差区域的梯度越平缓训练越稳但过大也会导致细节学不进去。我一般从1e-4开始试观察前面500个迭代的loss曲线再决定。注意这个eps不是Adam里的epsilon两者完全不相关。4.2 Adam优化器的学习率、权重衰减和余弦退火策略超分训练里Adam是默认优化器但参数初始化直接影响收敛质量。学习率太高会震荡太低则几百个epoch都推不动细节。import torch.optim as optim def build_optimizer(model, lr1e-4, wd1e-5): # 偏置项不做权重衰减正则化只作用于卷积核权重 decay_params [p for n, p in model.named_parameters() if bias not in n] no_decay_params [p for n, p in model.named_parameters() if bias in n] optimizer optim.Adam([ {params: decay_params, weight_decay: wd}, {params: no_decay_params, weight_decay: 0.0}, ], lrlr) return optimizer把偏置项和权重分开处理是因为偏置不需要正则化加weight_decay反而会干扰参数更新。lr1e-4是CNN型超分模型从零训练的常见起点用预训练权重微调时学习率可以提到2e-4但训练epoch数可以缩短一半。wd1e-5是很小的权重衰减但如果训练数据只有几百张图建议加大到1e-4否则模型容易背下训练集纹理验证集PSNR却上不去。学习率衰减策略上我用得最多的是余弦退火。它的曲线前半程保持较大学习率快速下降loss后半程把学习率压到极小值精修细节比固定学习率效果明显。PyTorch里一行就能配置# 每个epoch结束后自动更新学习率 scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max100, eta_min1e-6)T_max是退火周期这里设成100个epoch。如果训练计划只跑30个epochT_max也要改成30否则退火没走完训练就停了模型等于用了半个学习率曲线。4.3 PSNR、SSIM与肉眼指标和主观感受不一致怎么办训练完先别急着用眼睛判断工程上一般用PSNR和SSIM两个指标量化效果。PSNR是峰值信噪比本质看全局像素误差数值越大越好SSIM衡量两幅图的结构相似性范围0到1越接近1越好。PSNR的计算实现非常直观import cv2 import numpy as np def calc_psnr(sr, hr): sr sr.astype(np.float64) hr hr.astype(np.float64) mse np.mean((sr - hr) ** 2) if mse 0: return float(inf) return 10 * np.log10(255.0 * 255.0 / mse)这段代码只用了全局MSE是PSNR公式的标准写法。真正要小心的是指标的可信区间——两个模型的PSNR只差0.2 dB以内肉眼基本分辨不出来。而且PSNR更偏向平滑结果的模型因为模型宁肯把边缘糊掉也不愿在像素层面冒风险这恰恰是GAN模型的PSNR常低于CNN模型、但主观清晰度更高的原因。主观质感介于可量化和玄学之间能稳定的只有一套固定测试图和固定评估逻辑。提示以每50个epoch为一个节点固定一组测试图保存输出配合PSNR和SSIM一起观察。单看指标容易误判单看肉眼又难以横向比较。5. 超分源码避坑指南四个高频翻车场景的现象、原因与解法5.1 训练loss在下降输出图却一直是模糊的现象训练日志里loss曲线稳定下降跑了20个epoch后把验证集图片放进模型输出结果和双三次插值几乎看不出差别。原因这种情况绝大多数出在数据预处理上。最常见的翻车点是输入与标签的尺寸或通道数不一致模型实际上在“学习输出输入本身”。比如输入是放大到目标尺寸的LR标签是原始HR但下采样时用了不同插值方式或者归一化逻辑不一致模型看到的分布完全变了。还有一种情况是退化强度太弱原图本身就很清晰模型轻松学会“原样输出”在真实低清图上一测就露馅。解决先冻结模型用固定随机权重在验证集上跑一遍确认输入输出形状完全一致。再打印LR和HR的均值和方差肉眼确认两张图差距合理。如果数据差距没有问题但loss收敛后输出依旧模糊就要考虑扩大模型感受野或者增加网络深度。最小模型跑不动的时候评估一下你的patch尺寸是不是太小——patch只有32×32时4倍模型能看到的有效信息非常有限。5.2 输出图边缘出现黑边或伪影现象模型在画面中心区域重建效果不错但靠近边界处会出现深浅不一的黑边严重时呈锯齿状。原因卷积层的padding设置不规范是主因。padding不够时卷积核在边界只能扫到部分有效像素其余区域被零值填充模型被迫学会用零处理边界生成结果的边缘自然就崩了。另一个常见诱因是推理输入的长宽不是scale的整数倍卷积和resize叠加后边界错位。解决推理前把输入图的长宽对齐到scale的整数倍并确认padding覆盖卷积核半径。SRCNN第一层padding4对应9×9核半径第三层padding2对应5×5核半径。换用其他模型时先算每层的有效感受野再定padding。如果输出边缘还有1-2像素的黑线最终方案是裁掉边缘输出几乎所有超分项目都会在边界处做一些舍弃。5.3 显存不足patch裁剪与batch size的取舍现象训练一启动就报CUDA out of memory或者勉强启动了跑几个epoch又崩掉。原因超分模型的显存占用和图像尺寸平方相关不裁patch直接把原图喂进去12GB显存秒满。注意瓶颈在特征图而不是batch维度所以很多项目把batch size调到1也扛不住。解决把训练输入裁成固定patch再做随机增强既控制显存又起到数据增强的作用。import numpy as np def random_crop_pair(lr, hr, patch128, scale4): # LR和HR按scale对应裁剪成配对区域 lr_h, lr_w lr.shape[:2] lr_patch patch // scale x np.random.randint(0, lr_w - lr_patch 1) y np.random.randint(0, lr_h - lr_patch 1) lr_crop lr[y:y lr_patch, x:x lr_patch] hr_crop hr[y * scale:(y lr_patch) * scale, x * scale:(x lr_patch) * scale] return lr_crop, hr_croppatch128时4倍模型下LR裁剪尺寸是32×32显存占用很小batch size可以开得比较松。显存还是不够的时候优先调低patch而不是batch_size——但patch太小时训练不稳定128是我实践里常用的平衡点。随机裁剪本身也是重要的正则化手段配合随机翻转和旋转能极大提升数据多样性。5.4 重建结果偏色或整体变灰通道顺序和归一化的坑现象模型输出的亮度结构都正常但颜色饱和度变低有的图偏绿或偏蓝。原因最常见的有两类。一是训练在RGB空间进行但推理时忘了OpenCV读进来的默认是BGR通道错位后颜色自然不对。二是模型在Y通道上训练推理时漏了把重建后的Y和色度通道合并直接保存单通道结果出来的必然是一张灰度图。解决通道问题靠统一颜色空间转换函数解决排查时先打印输出张量的通道数是1就说明漏了合并。还有一个隐蔽问题数据归一化不一致——训练做了(x / 255 - 0.5) / 0.5推理只做了x / 255模型看到的输入分布完全变了。我的习惯是把预处理封装成一个函数训练和推理共用同一份代码而不是各写各的。def preprocess_y(y_channel): # 训练和推理都必须走这同一个函数 y y_channel.astype(np.float32) / 255.0 y (y - 0.5) / 0.5 return torch.from_numpy(y).unsqueeze(0).unsqueeze(0)这样改动的影响面最小改归一化只改一个函数训练和推理同时生效。6. 把超分模型部署到实际应用ONNX导出、CPU推理与验证闭环6.1 从PyTorch到ONNX动态尺寸是必选项训练好的模型要落地通常不会直接拖着PyTorch环境走而是先导出成ONNX。ONNX的好处是跨框架、跨语言C、Java甚至前端都能调用还能享受CPU和NPU上针对性的推理优化。import torch model SRCNN() model.load_state_dict(torch.load(ckpt.pth, map_locationcpu)) model.eval() dummy torch.randn(1, 1, 256, 256) torch.onnx.export( model, dummy, srcnn_x4.onnx, input_names[lr_y], output_names[sr_y], dynamic_axes{lr_y: {0: batch, 2: height, 3: width}, sr_y: {0: batch, 2: height, 3: width}}, opset_version11 )dynamic_axes让导出的模型支持任意输入尺寸——漏了这一步模型被固定成256×256换一张720×480的图就报形状不匹配。opset_version11兼容性好多数部署框架都支持我通常优先用11。6.2 用ONNX Runtime在CPU上推理不装PyTorch也能跑部署端最省事的运行时是ONNX Runtime它不需要PyTorch环境常见CPU都有深度优化量化后甚至能在工控机上实时跑小图。import onnxruntime as ort sess ort.InferenceSession(srcnn_x4.onnx, providers[CPUExecutionProvider]) def infer_on_cpu(y_channel): # y_channel是uint8二维数组已经放大到目标尺寸 inp y_channel.astype(np.float32)[None, None, :, :] / 255.0 result sess.run([sr_y], {lr_y: inp})[0] return result[0, 0].clip(0, 1) * 255.0providers参数可以按优先级写多个执行后端比如[CUDAExecutionProvider, CPUExecutionProvider]ONNX Runtime会自动选取当前环境可用的那个。sess.run的输入字典键名必须和导出时的input_names完全一致。6.3 用一张图验证整个链路下采样、超分、对比指标部署完成后我习惯做一次端到端的验证拿一张高清图先下采样成模糊小图再喂给部署好的ONNX模型保存输出后与原始高清图计算PSNR。如果ONNX推理的结果和PyTorch下差超过0.3 dB多半是预处理或通道转换在部署端没对齐。CPU上推理一张1080p图SRCNN大约几十到几百毫秒压不到预期延迟时就先降输入分辨率打通链路再慢慢提回去。这个从最小闭环出发、每步都留验证锚点的习惯是我在超分项目里最想让你带走的经验。希望帮到你。本文还有配套的精品资源点击获取