论文: Real-World Super-Resolution via Kernel Estimation and Noise Injection
来源: CVPR 2020 Workshops(NTIRE 2020 Challenge)
机构: 腾讯优图实验室(Tencent Youtu Lab)
成就: NTIRE 2020 真实世界超分辨率挑战赛双赛道冠军(团队名:Impressionism)
代码: Real-SR-master
标签: 超分辨率、SRGAN、RRDBNet、图像退化建模、KernelGAN、噪声注入、深度学习
前言
图像超分辨率(Super-Resolution, SR)是将低分辨率(LR)图像重建为高分辨率(HR)图像的经典计算机视觉任务。现有方法在理想数据集上表现优异,但在真实世界图像上往往失效——根本原因是:训练时采用简单的双三次(bicubic)下采样构造 LR/HR 对,与真实相机拍摄时经过光学模糊、噪声污染、JPEG 压缩等多种退化的图像存在域差异(Domain Gap)。
RealSR 正是为解决这一根本问题而设计的,通过精准的退化框架建模,让训练数据与真实图像共享同一域分布,从而实现更真实的感知质量超分。
本文将从问题定义、整体架构、数据预处理、网络设计、损失函数、训练策略等维度全面解析 RealSR 项目的每一个技术细节。
目录
- 真实世界超分辨率的挑战
- RealSR 整体框架
- 项目代码结构
- 核心创新:退化建模框架
- 4.1 Track1:DF2K 赛道 —— 双三次下采样 + 噪声注入
- 4.2 Track2:DPED 赛道 —— KernelGAN 估计下采样核
- 数据预处理模块详解
- 5.1 噪声收集(collect_noise.py)
- 5.2 双三次数据集生成(create_bicubic_dataset.py)
- 5.3 核卷积数据集生成(create_kernel_dataset.py)
- 骨干网络:RRDBNet 深度解析
- 6.1 ResidualDenseBlock(RDB)
- 6.2 RRDB:残差中残差致密块
- 6.3 RRDBNet 完整结构
- 生成对抗训练:SRGANModel
- 7.1 生成器(Generator)
- 7.2 判别器架构
- 7.3 损失函数体系
- Dataset 数据管道
- 8.1 LQGTDataset 实现
- 8.2 在线噪声注入增强
- 训练策略与配置参数
- 9.1 Track1 训练配置
- 9.2 Track2 训练配置
- 9.3 学习率调度
- 推理测试模块
- 10.1 多种推理模式
- 10.2 分块推理(Chop)
- 10.3 Back Projection 后处理
- 分布式训练支持
- 量化评估指标
- 快速使用指南
- 总结与思考
1. 真实世界超分辨率的挑战
1.1 理想设置 vs 真实场景
传统超分辨率方法(如 SRCNN、EDSR、RCAN、ESRGAN)的训练范式:
HR 图像 ──[bicubic 下采样 ×4]──→ LR 图像
这种方式的问题:真实世界的 LR 图像并非由 bicubic 简单降采样得到,而是经过了:
- 光学模糊(Optical Blur): 镜头失焦、运动模糊
- 真实下采样核(Real Degradation Kernel): 不同相机、不同场景对应不同的 PSF(点扩散函数)
- 传感器噪声(Sensor Noise): 高斯噪声、泊松噪声、斑点噪声
- JPEG 压缩伪影(JPEG Artifacts): 图像存储压缩引入的块效应
1.2 域差异问题
训练数据域:bicubic LR ──→ HR
真实数据域:真实相机 LR(含噪声/模糊/压缩)──→ HR
域差异 → 模型在真实图像上失效(放大噪声、产生虚假细节)
RealSR 的核心贡献:设计一个精准的退化框架,使合成的训练数据 LR 图像与真实图像共享相同的退化分布,从而消除域差异。
2. RealSR 整体框架
┌─────────────────────────────────────────────────────────┐
│ RealSR 整体流程 │
│ │
│ HR 图像 │
│ │ │
│ ├──[真实退化建模]──→ 合成 LR 图像 │
│ │ ├─ Track1: bicubic + 噪声注入 │
│ │ └─ Track2: KernelGAN估计核 + 噪声注入 │
│ │ │
│ └──[LR/HR 配对训练集] │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ RRDBNet 生成器 │ ← ESRGAN backbone (23个RRDB) │
│ └─────────────────┘ │
│ │ 感知损失 + GAN损失(RaGAN) + 像素损失 │
│ ▼ │
│ SR 超分结果 │
│ │ │
│ ▼ (可选) │
│ Back Projection 后处理 │
└─────────────────────────────────────────────────────────┘
关键创新点总结
| 创新点 | 说明 |
|---|---|
| 真实退化框架 | 用 KernelGAN 估计真实下采样核,而非固定 bicubic |
| 噪声注入策略 | 从真实图像低频区提取高频噪声块,在线随机注入到 LR |
| RRDB 骨干 | 使用 ESRGAN 中经验证的 RRDBNet(23 个 RRDB 块) |
| RaGAN 对抗损失 | 相对平均 GAN,让生成图像"比真实图更真实" |
| VGG 感知损失 | 使用 VGG19 第 34 层特征(ReLU 前)计算感知相似度 |
| 自集成测试 | x8 旋转/翻转测试时间增强(TTA),提升鲁棒性 |
| 分块推理 | 支持超大图像分块处理,避免 GPU 内存溢出 |
3. 项目代码结构
Real-SR-master/
├── README.md # 项目说明文档
├── figures/ # 论文效果图、架构图
│ ├── arch.png # 网络架构图
│ ├── 0913.png / 0935.png # 超分效果对比图
│ ├── track1.png / track2.png # 赛道量化结果
│ └── df2k.png / dped.png # 定性结果对比图
└── codes/
├── train.py # 训练入口(支持分布式训练)
├── test.py # 测试/推理入口
├── data/ # 数据集模块
│ ├── LQGT_dataset.py # LR+GT 配对数据集(含噪声增强)
│ ├── LR_dataset.py # 单独 LR 数据集(仅测试)
│ ├── GT_dataset.py # 单独 GT 数据集
│ ├── data_loader.py # 噪声数据集加载器
│ ├── data_sampler.py # 分布式采样器
│ └── util.py # 数据工具函数
├── models/ # 模型模块
│ ├── SR_model.py # 纯 SR 模型(无 GAN,仅像素/感知损失)
│ ├── SRGAN_model.py # SRGAN 模型(含 GAN 对抗训练)
│ ├── base_model.py # 模型基类(保存/加载/打印等)
│ ├── lr_scheduler.py # 自定义学习率调度器
│ ├── networks.py # 网络工厂函数(define_G/D/F)
│ └── modules/
│ ├── RRDBNet_arch.py # ★ 核心生成器:RRDB 网络
│ ├── SRResNet_arch.py # MSRResNet(备选生成器)
│ ├── discriminator_vgg_arch.py # 判别器 + VGG感知特征提取
│ ├── loss.py # 损失函数(GAN/Charbonnier)
│ └── module_util.py # 网络工具函数
├── options/ # 配置文件
│ ├── df2k/
│ │ ├── train_bicubic_noise.yml # Track1 训练配置
│ │ └── test_df2k.yml # Track1 测试配置
│ └── dped/
│ ├── train_kernel_noise.yml # Track2 训练配置
│ └── test_dped.yml # Track2 测试配置
└── preprocess/ # 数据预处理脚本
├── collect_noise.py # ★ 从真实图像提取噪声块
├── create_bicubic_dataset.py # 生成 bicubic LR/HR 对
├── create_kernel_dataset.py # ★ 用 KernelGAN 核生成 LR/HR 对
├── paths.yml # 数据集路径配置
└── utils.py # 预处理工具函数
4. 核心创新:退化建模框架
RealSR 针对两个赛道提出了不同的退化建模策略:
4.1 Track1:DF2K 赛道 —— 双三次下采样 + 噪声注入
DF2K 数据集(DIV2K + Flickr2K)包含人工处理过的带噪声和伪影图像(称为 Corrupted)。退化流程:
HR(DF2K Source)
│
├── Step1: bicubic 下采样 ×4 → 合成 LR
│
├── Step2: 收集噪声块
│ ├── 从 Source 图像中提取低方差区域(平坦区域)
│ ├── 筛选条件:var < 20, mean > 0
│ └── 保存为 256×256 噪声块集合
│
└── Step3: 训练时在线噪声注入
└── 随机选取噪声块,叠加到 LR 图像上
关键洞察: 对于处理噪声图像(Corrupted),噪声分布隐含在图像低频区域中。通过从相同图像中收集这些噪声特征,可以准确复现原始退化模式。
4.2 Track2:DPED 赛道 —— KernelGAN 估计下采样核
DPED(DPED for iPhone)包含手机拍摄的真实照片。真实相机镜头下采样并非 bicubic,需要估计真实模糊核:
HR(DPED iPhone Source)
│
├── Step1: 使用 KernelGAN 估计每张图像的下采样核
│ ├── KernelGAN 是一种 GAN,专门学习单张图像的下采样核
│ ├── 输出:kernel_x4.mat(每张图像对应一个核)
│ └── 命令:python train.py --X4 --input-dir SOURCE_PATH
│
├── Step2: 用估计的核对 HR 进行下采样 → 合成 LR
│ ├── 随机选取一个核文件
│ └── imresize(HR, 1/4, kernel=k)
│
└── Step3: 收集 DPED 真实噪声块(筛选条件:var<20, mean>50)
└── 训练时在线叠加到 LR
KernelGAN 原理简介: KernelGAN 将"图像的 patch 跨尺度分布一致性"作为约束,用 GAN 框架学习一个 CNN 核,该核能让该核降采样后的图像与原图保持 patch 分布一致——这个核即近似于真实相机的下采样核(PSF)。
5. 数据预处理模块详解
5.1 噪声收集(collect_noise.py)
这是 RealSR 最独特的贡献之一——从真实图像中提取真实噪声:
def noise_patch(rgb_img, sp, max_var, min_mean):
"""
从图像中提取噪声纹理块
策略:寻找"低方差、高亮度"的平坦区域
- 低方差:该区域内容单一(如纯色墙面、天空),
图像中的亮度变化几乎全来自噪声,而非真实内容
- 高亮度(min_mean):暗区域的噪声不明显,避免误采
Args:
sp: patch size,固定 256×256
max_var: 方差上限(df2k=20, dped=20)
min_mean: 亮度下限(df2k=0, dped=50)
"""
img = rgb_img.convert('L') # 转灰度图计算统计量
rgb_img = np.array(rgb_img)
img = np.array(img)
collect_patchs = []
for i in range(0, w - sp, sp):
for j in range(0, h - sp, sp):
patch = img[i:i+sp, j:j+sp]
var_global = np.var(patch)
mean_global = np.mean(patch)
# 筛选:低方差(内容简单) + 足够亮度
if var_global < max_var and mean_global > min_mean:
rgb_patch = rgb_img[i:i+sp, j:j+sp, :]
collect_patchs.append(rgb_patch)
return collect_patchs
核心思想: 图像中内容简单(方差小)的区域,其像素变化主要由传感器噪声贡献,而非图像内容本身。这些块捕获了真实噪声的空间相关性和统计分布,比高斯噪声建模更真实。
5.2 核卷积数据集生成(create_kernel_dataset.py)
# 为每张 HR 图像随机选取一个 KernelGAN 估计的核进行下采样
kernel_paths = glob.glob(os.path.join(opt.kernel_path, '*/*_kernel_x4.mat'))
for file in tqdm(source_files):
input_img = Image.open(file)
input_img = TF.to_tensor(input_img)
# 先 ×2 下采样清洗图像(去除高频噪声,确保 HR 干净)
resize2_img = utils.imresize(input_img, 1.0 / opt.cleanup_factor, True) # cleanup_factor=2
# 保存为 HR(1/2 尺寸)
resize2_cut_img.save(tdsr_hr_dir + basename)
# 随机选取一个核,对 HR 进行 ×4 下采样 → LR
kernel_path = kernel_paths[np.random.randint(0, kernel_num)]
mat = loadmat(kernel_path)
k = np.array([mat['Kernel']]).squeeze()
resize3_cut_img = imresize(np.array(resize2_cut_img),
scale_factor=1.0/opt.upscale_factor,
kernel=k) # 使用真实估计核
resize3_cut_img.save(tdsr_lr_dir + basename)
两步下采样设计:
- 第一步(清洗): bicubic ×2 下采样,目的是消除原始 HR 图像中已有的噪声和伪影,获得"干净"的 HR
- 第二步(退化): 使用 KernelGAN 核 ×4 下采样,模拟真实相机拍摄时的退化
6. 骨干网络:RRDBNet 深度解析
RealSR 的生成器采用 RRDBNet(来自 ESRGAN),这是目前超分辨率领域综合性能最佳的骨干网络之一。
6.1 ResidualDenseBlock(RDB):5路卷积的密集连接块
class ResidualDenseBlock_5C(nn.Module):
"""5路密集连接残差块(5C = 5 Convolutions)"""
def __init__(self, nf=64, gc=32, bias=True):
super().__init__()
# gc: growth channel = 每次密集连接增长的通道数
self.conv1 = nn.Conv2d(nf, gc, 3, 1, 1, bias=bias)
self.conv2 = nn.Conv2d(nf + gc, gc, 3, 1, 1, bias=bias)
self.conv3 = nn.Conv2d(nf + 2*gc, gc, 3, 1, 1, bias=bias)
self.conv4 = nn.Conv2d(nf + 3*gc, gc, 3, 1, 1, bias=bias)
self.conv5 = nn.Conv2d(nf + 4*gc, nf, 3, 1, 1, bias=bias)
self.lrelu = nn.LeakyReLU(negative_slope=0.2, inplace=True)
# 权重初始化缩小 0.1 倍:防止训练初期梯度爆炸
mutil.initialize_weights([self.conv1,...,self.conv5], 0.1)
def forward(self, x):
# 密集连接:每层输入都包含之前所有层的特征
x1 = self.lrelu(self.conv1(x))
x2 = self.lrelu(self.conv2(torch.cat((x, x1), 1)))
x3 = self.lrelu(self.conv3(torch.cat((x, x1, x2), 1)))
x4 = self.lrelu(self.conv4(torch.cat((x, x1, x2, x3), 1)))
x5 = self.conv5(torch.cat((x, x1, x2, x3, x4), 1))
# 残差缩放:β=0.2,防止特征尺度过大
return x5 * 0.2 + x
密集连接路径示意:
x ──────────────────────────────────────────────→ + → 输出
│ ↑
├─[conv1]→ x1 ──────────────────────────────── │
│ │ │
├──────────[cat,conv2]→ x2 ─────────────────── │
│ │ │ │
├──────────[cat,cat,conv3]→ x3 ────────────── │ ×0.2
│ │ │ │ │
├──────────[cat,cat,cat,conv4]→ x4 ─────────── │
│ │ │ │ │ │
└──────────[cat,cat,cat,cat,conv5]→ x5 ─────────┘
密集连接的优势:
- 每一层都能访问之前所有层的特征,大幅提升特征复用率
- 梯度可以通过多条路径反向传播,缓解梯度消失
- 残差缩放(×0.2)保证训练稳定性
6.2 RRDB:残差中残差致密块
class RRDB(nn.Module):
"""Residual in Residual Dense Block(残差中的残差)"""
def __init__(self, nf, gc=32):
super().__init__()
# 3 个 RDB 串联
self.RDB1 = ResidualDenseBlock_5C(nf, gc)
self.RDB2 = ResidualDenseBlock_5C(nf, gc)
self.RDB3 = ResidualDenseBlock_5C(nf, gc)
def forward(self, x):
out = self.RDB1(x)
out = self.RDB2(out)
out = self.RDB3(out)
# 外层残差连接,同样缩放 0.2
return out * 0.2 + x
两级残差设计:
- 内层残差(RDB 内部):
x5 * 0.2 + x - 外层残差(RRDB 层面):
(RDB1→RDB2→RDB3)(x) * 0.2 + x
两级残差确保了即使在训练初期,信号也能稳定传播,不会因为多层叠加导致特征幅度爆炸。
6.3 RRDBNet 完整结构
class RRDBNet(nn.Module):
def __init__(self, in_nc, out_nc, nf, nb, gc=32):
"""
Args:
in_nc: 输入通道数(RGB=3)
out_nc: 输出通道数(RGB=3)
nf: 特征通道数(64)
nb: RRDB 数量(23,来自 ESRGAN)
gc: 密集连接 growth channel(32)
"""
super().__init__()
RRDB_block_f = functools.partial(RRDB, nf=nf, gc=gc)
# 输入特征提取
self.conv_first = nn.Conv2d(in_nc, nf, 3, 1, 1, bias=True)
# 23 个 RRDB 块串联(主干网络)
self.RRDB_trunk = mutil.make_layer(RRDB_block_f, nb) # nb=23
# 主干后的卷积(特征整合)
self.trunk_conv = nn.Conv2d(nf, nf, 3, 1, 1, bias=True)
# 上采样(2次 ×2 = 总体 ×4)
self.upconv1 = nn.Conv2d(nf, nf, 3, 1, 1, bias=True) # ×2 上采样后的卷积
self.upconv2 = nn.Conv2d(nf, nf, 3, 1, 1, bias=True) # ×4 上采样后的卷积
# 最终高分辨率卷积
self.HRconv = nn.Conv2d(nf, nf, 3, 1, 1, bias=True)
self.conv_last = nn.Conv2d(nf, out_nc, 3, 1, 1, bias=True)
self.lrelu = nn.LeakyReLU(negative_slope=0.2, inplace=True)
def forward(self, x):
# 特征提取
fea = self.conv_first(x)
# 长残差连接(跨越所有 RRDB):
# 类似 ResNet 的 skip connection,直接连接输入和 23 个 RRDB 之后
trunk = self.trunk_conv(self.RRDB_trunk(fea))
fea = fea + trunk
# 上采样:用最近邻插值 + 卷积(比反卷积更稳定,无棋盘格伪影)
fea = self.lrelu(self.upconv1(F.interpolate(fea, scale_factor=2, mode='nearest')))
fea = self.lrelu(self.upconv2(F.interpolate(fea, scale_factor=2, mode='nearest')))
# 最终输出
out = self.conv_last(self.lrelu(self.HRconv(fea)))
return out
网络规模:
- 输入:[B, 3, H, W](LR 低分辨率图像)
- 23 个 RRDB × 3 个 RDB × 5 个 conv = 345 个卷积层(主干部分)
- 输出:[B, 3, 4H, 4W](×4 超分辨率图像)
- 参数量:约 16.7M(64 特征通道,23 个 RRDB)
上采样设计选择:
| 方法 | 优点 | 缺点 |
|---|---|---|
| 转置卷积(反卷积) | 可学习上采样权重 | 容易产生棋盘格伪影 |
| 亚像素卷积(ESPCN) | 效率高 | 复杂度高,需要大通道数 |
| 最近邻 + 卷积(本方案) | 稳定无伪影,实现简单 | 上采样过程不可学习 |
7. 生成对抗训练:SRGANModel
7.1 网络架构总览
训练阶段:三个网络协同工作
┌─────────────────────────────────────────────────────────┐
│ netG(生成器):RRDBNet │
│ LR ──→ SR(超分图像) │
├─────────────────────────────────────────────────────────┤
│ netD(判别器):VGG-128 / NLayerDiscriminator │
│ SR / HR ──→ 真假判别 │
├─────────────────────────────────────────────────────────┤
│ netF(感知网络):VGG19(固定权重,不参与训练) │
│ SR / HR ──→ 深层特征(第 34 层) │
└─────────────────────────────────────────────────────────┘
7.2 生成器(网络中的噪声注入部分)
networks.py 中还定义了一个额外的 Generator 类(用于噪声注入实验),核心思路是将噪声图和输入图像分开处理再合并:
class Generator(nn.Module):
"""噪声注入生成器:学习如何将噪声图与 LR 图像融合"""
def __init__(self, n_res_blocks=8):
super().__init__()
# 输入端:3通道 → 64通道特征
self.block_input = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=3, padding=1),
nn.PReLU()
)
# 8 个残差块
self.res_blocks = nn.ModuleList(
[ResidualBlock(64) for _ in range(n_res_blocks)])
# 输出噪声图:64 → 3通道
self.block_output = nn.Conv2d(64, 3, kernel_size=3, padding=1)
def forward(self, x, z):
# z: 噪声图(扩展到与 x 同尺寸)
z = z.expand(x.shape)
# 通过残差块处理噪声图,学习噪声的空间分布
block = self.block_input(z)
for res_block in self.res_blocks:
block = res_block(block)
noise = self.block_output(block)
# 将学习到的噪声叠加到 LR 图像(值域钳制到 [0,1])
return torch.clamp(x + noise, 0, 1), noise
7.3 判别器架构
RealSR 提供两种判别器选择:
判别器一:VGG-128 风格(用于 Track1 DF2K)
class Discriminator_VGG_128(nn.Module):
"""基于 VGG 架构的判别器,输入 128×128 图像块"""
# 5 层步长=2 的卷积,逐步将 128×128 压缩到 4×4
# 最后通过全连接层输出标量判断
# 特征通道变化:
# 128×128 → 64 通道 → 128 通道 → 256 通道
# → 512 通道 → 512 通道 → 4×4 特征图
# → FC(8192 → 100) → FC(100 → 1)
def forward(self, x):
fea = self.lrelu(self.conv0_0(x))
fea = self.lrelu(self.bn0_1(self.conv0_1(fea)))
# ... 5层下采样卷积
fea = fea.view(fea.size(0), -1) # 展平
fea = self.lrelu(self.linear1(fea)) # 8192 → 100
out = self.linear2(fea) # 100 → 1
return out
判别器二:PatchGAN(用于 Track2 DPED)
class NLayerDiscriminator(nn.Module):
"""PatchGAN 判别器:输出每个局部 patch 的真假分数"""
# 设计思路:不对整张图像做全局判断,
# 而是对每个 patch 独立判断真假,
# 这样更关注局部纹理细节,适合超分任务
# 3层卷积(stride=2),输出 patch 级别的分类 map
# 每个输出像素对应输入图像的一个感受野区域
两种判别器对比:
| 判别器 | 感受野 | 优势 | 适用场景 |
|---|---|---|---|
| VGG-128 | 全局(整张图像) | 全局一致性 | 处理伪影图像(DF2K) |
| PatchGAN | 局部 patch | 局部纹理细节更真实 | 真实手机照片(DPED) |
7.4 损失函数体系
RealSR 的生成器总损失由三部分组成:
# ===== 生成器总损失 =====
l_g_total = 0
# 1. 像素损失(Pixel Loss):L1 距离
if step % D_update_ratio == 0 and step > D_init_iters:
if cri_pix:
l_g_pix = pixel_weight * L1Loss(fake_H, real_H)
# pixel_weight = 1e-2(权重很小,避免过度平滑)
l_g_total += l_g_pix
# 2. VGG 感知损失(Perceptual/Feature Loss)
if cri_fea:
real_fea = netF(real_H).detach() # 真实图像的 VGG 特征(固定)
fake_fea = netF(fake_H) # 生成图像的 VGG 特征
l_g_fea = feature_weight * L1Loss(fake_fea, real_fea)
# feature_weight = 1(主要损失项)
l_g_total += l_g_fea
# 3. 相对平均 GAN 损失(RaGAN Loss)
if gan_type == 'ragan':
pred_d_real = netD(real_ref).detach()
# 相对判别:生成图"比真实图更像真实图"的程度
l_g_gan = gan_weight * (
cri_gan(pred_d_real - mean(pred_g_fake), False) + # 真实图相对假图:应为假
cri_gan(pred_g_fake - mean(pred_d_real), True) # 假图相对真图:应为真
) / 2
# gan_weight = 5e-3(权重最小,用于风格化而非像素精度)
l_g_total += l_g_gan
RaGAN(Relativistic average GAN)解析:
传统 GAN:判别器只判断"生成图是否为真"
RaGAN:判别器同时评估"生成图相对于真实图哪个更真"
标准 GAN 损失:
D: max E[log D(real)] + E[log(1 - D(fake))]
G: min E[log(1 - D(fake))]
RaGAN 损失:
D: max E[log σ(D(real) - E[D(fake)])] + E[log(1 - σ(D(fake) - E[D(real)]))]
G: min E[log(1 - σ(D(real) - E[D(fake)])] + E[log σ(D(fake) - E[D(real)])]
RaGAN 的优势:
- 让真实图像的判别分数相对降低,生成图像分数相对提升,训练更稳定
- 同时更新真假两侧,梯度信号更丰富
- 更接近 Wasserstein 距离的优化目标
损失权重总结:
| 损失项 | 权重 | 作用 |
|---|---|---|
| 像素损失(L1) | 1e-2 | 保证像素级精度,防止严重失真 |
| VGG 感知损失(L1) | 1 | 主要优化目标,感知质量 |
| RaGAN 损失 | 5e-3 | 增强真实感纹理,风格化 |
8. Dataset 数据管道
8.1 LQGTDataset 实现
class LQGTDataset(data.Dataset):
"""
读取 LR(Low Quality)和 GT(Ground Truth)图像配对
支持:磁盘 PNG 文件 / LMDB 高性能数据库
支持:在线下采样生成 LR / 预计算 LR 直接读取
支持:训练时在线噪声注入增强
"""
def __getitem__(self, index):
# 1. 读取 GT(高分辨率参考图)
img_GT = util.read_img(GT_env, GT_path, resolution)
# 2. 获取 LQ(低分辨率输入)
if self.paths_LQ:
img_LQ = util.read_img(LQ_env, LQ_path, resolution)
else:
# 在线生成:使用 matlab 风格 imresize(与 matlab 结果一致,避免边界差异)
img_LQ = util.imresize_np(img_GT, 1/scale, True)
# 3. 训练时数据增强
if phase == 'train':
# 3.1 随机裁剪:取 LQ_size = GT_size/scale 的 patch
rnd_h = random.randint(0, max(0, H - LQ_size))
rnd_w = random.randint(0, max(0, W - LQ_size))
img_LQ = img_LQ[rnd_h:rnd_h+LQ_size, rnd_w:rnd_w+LQ_size, :]
img_GT = img_GT[rnd_h*scale:..., rnd_w*scale:..., :]
# 3.2 随机翻转 + 旋转
img_LQ, img_GT = util.augment([img_LQ, img_GT], use_flip, use_rot)
# 4. 颜色空间转换:BGR → RGB(OpenCV 读入是 BGR)
img_GT = img_GT[:, :, [2, 1, 0]]
img_LQ = img_LQ[:, :, [2, 1, 0]]
# 5. HWC → CHW,numpy → Tensor
img_GT = torch.from_numpy(np.ascontiguousarray(np.transpose(img_GT, (2,0,1)))).float()
img_LQ = torch.from_numpy(np.ascontiguousarray(np.transpose(img_LQ, (2,0,1)))).float()
# 6. 在线噪声注入(训练时)
if phase == 'train' and 'noise' in aug:
noise = self.noises[np.random.randint(0, len(self.noises))]
img_LQ = torch.clamp(img_LQ + noise, 0, 1) # 叠加真实噪声块
return {'LQ': img_LQ, 'GT': img_GT, 'LQ_path': LQ_path, 'GT_path': GT_path}
8.2 在线噪声注入增强机制
# data_loader.py 中的 noiseDataset
class noiseDataset:
"""
预加载所有噪声块到内存,训练时随机采样
噪声注入公式:
img_LQ_noisy = clamp(img_LQ + noise_patch, 0, 1)
特点:
- 同一 epoch 内噪声块是随机选取的,增加了训练数据多样性
- 噪声来自真实图像,而非高斯分布假设
- 不需要对噪声进行任何建模假设,端到端学习
"""
9. 训练策略与配置参数
9.1 Track1 DF2K 完整训练配置
# options/df2k/train_bicubic_noise.yml
#### 通用设置
name: Corrupted_noise # 实验名称(用于日志和保存目录)
model: srgan # 使用 SRGAN 对抗训练模式
scale: 4 # 超分倍数 ×4
#### 数据集配置
datasets:
train:
mode: LQGT # 读取 LR+GT 配对
aug: noise # 启用噪声增强
noise_data: ../datasets/DF2K/Corrupted_noise/ # 噪声块目录
dataroot_GT: ../datasets/DF2K/generated/tdsr/HR # 高分辨率图
dataroot_LQ: ../datasets/DF2K/generated/tdsr/LR # 低分辨率图
n_workers: 6 # 数据加载线程数
batch_size: 16 # 批次大小
GT_size: 128 # 训练 crop 尺寸(GT 是 128×128)
use_flip: true # 随机水平翻转
use_rot: true # 随机旋转(0/90/180/270度)
#### 网络结构
network_G:
which_model_G: RRDBNet # 生成器:RRDBNet
in_nc: 3 # 输入通道(RGB)
out_nc: 3 # 输出通道(RGB)
nf: 64 # 特征通道数
nb: 23 # RRDB 块数量
network_D:
which_model_D: discriminator_vgg_128 # VGG-128 判别器
in_nc: 3
nf: 64
#### 预训练模型
path:
pretrain_model_G: ../pretrained_model/RRDB_PSNR_x4.pth
# ★ 关键:从 PSNR 预训练模型初始化,而非随机初始化
# 这样可以避免 GAN 训练初期的不稳定,防止模式崩溃
#### 训练超参数
train:
lr_G: 1e-4 # 生成器初始学习率
lr_D: 1e-4 # 判别器初始学习率
beta1_G: 0.9 # Adam β1
beta2_G: 0.999 # Adam β2
lr_scheme: MultiStepLR # 多步学习率衰减
niter: 60001 # 总迭代次数
lr_steps: [5000, 10000, 20000, 30000] # 衰减节点
lr_gamma: 0.5 # 每次衰减为原来的 0.5 倍
pixel_criterion: l1 # 像素损失:L1
pixel_weight: 1e-2 # 像素损失权重(小权重避免过平滑)
feature_criterion: l1 # 感知损失:L1
feature_weight: 1 # 感知损失权重(最大,主要优化目标)
gan_type: ragan # GAN 类型:相对平均 GAN
gan_weight: 5e-3 # GAN 损失权重(最小,仅用于纹理增强)
D_update_ratio: 1 # G 和 D 的更新比例(1:1)
D_init_iters: 0 # D 预训练迭代数(0 表示直接 GAN 联合训练)
9.2 学习率调度策略
class MultiStepLR_Restart:
"""多步学习率衰减 + 热重启"""
# Track1 学习率变化过程:
# iter 0 : lr = 1e-4
# iter 5000 : lr = 5e-5 (×0.5)
# iter 10000: lr = 2.5e-5 (×0.5)
# iter 20000: lr = 1.25e-5 (×0.5)
# iter 30000: lr = 6.25e-6 (×0.5)
# iter 60001: 训练结束
从 PSNR 模型初始化的意义:
RealSR(和 ESRGAN)的训练采用两阶段策略:
- 阶段一: 纯像素损失(L1/L2)训练,得到 PSNR 最优的基础模型
- 阶段二: 以阶段一模型为初始化,加入感知损失 + GAN 损失微调
这样做的原因:随机初始化直接进行 GAN 训练极不稳定,容易陷入模式崩溃。从 PSNR 模型出发,生成器已经具备基本的超分能力,GAN 训练仅在此基础上增强感知质量。
10. 推理测试模块
10.1 多种推理模式
test.py 支持三种推理模式,通过配置文件中的参数控制:
# 根据配置选择推理模式
if opt['model'] == 'sr':
model.test_x8() # 模式1:8方向自集成(TTA)
elif opt['large'] is not None:
model.test_chop() # 模式2:分块推理(大图)
else:
model.test() # 模式3:直接推理
# 可选后处理
if opt['back_projection']:
model.back_projection() # Back Projection 迭代修正
10.2 x8 测试时增强(TTA)
def test_x8(self):
"""
8方向自集成:对输入图像进行 8 种变换,分别推理后取平均
8种变换 = 4次旋转(0/90/180/270度)× 2(原始/水平翻转)
优势:减少方向性偏差,显著提升 PSNR/SSIM 指标(约 +0.1~0.2 dB)
代价:推理时间增加 8 倍
"""
def _transform(v, op):
# op: v_flip, h_flip, transpose
if op == 'v_flip': return v.flip(2)
elif op == 'h_flip': return v.flip(3)
elif op == 'transpose': return v.permute(0, 1, 3, 2)
# 生成 8 个变换版本
lr_list = [self.var_L]
for tf in 'v', 'h', 't':
lr_list.extend([_transform(t, tf) for t in lr_list])
# 分别推理
sr_list = [self.netG(aug) for aug in lr_list]
# 逆变换后取平均
output_cat = torch.stack(sr_list, dim=0)
self.fake_H = output_cat.mean(dim=0)
10.3 分块推理(Chop Forward)
对于高分辨率大图(如 4K),直接推理会超出 GPU 显存。分块推理将图像切成 4 块分别处理:
def forward_chop(self, *args, shave=10, min_size=160000):
"""
分块推理,支持递归分块(图块太大时继续细分)
Args:
shave: 重叠区域大小(像素),用于消除块边界伪影
min_size: 最小块大小(像素数),超过则继续细分
流程:
1. 将图像分为 4 块(上/下 × 左/右),各带 shave 像素重叠
2. 对每块独立推理
3. 将推理结果拼回原始尺寸
4. 重叠区域取对应块的内容(不做混合)
"""
h, w = args[0].size()[-2:]
# 定义四个切割区域(带重叠 shave)
top = slice(0, h//2 + shave)
bottom = slice(h - h//2 - shave, h)
left = slice(0, w//2 + shave)
right = slice(w - w//2 - shave, w)
# 对每块推理后拼合(UpScale 后坐标同比例缩放)
h *= scale; w *= scale
top_out = slice(0, h//2)
bottom_out = slice(h-h//2, h)
left_out = slice(0, w//2)
right_out = slice(w-w//2, w)
y[..., top_out, left_out] = y_chops[0][..., top_out, left_out]
y[..., top_out, right_out] = y_chops[1][..., top_out, right_r] # 逆变换
y[..., bottom_out, left_out] = y_chops[2][..., bottom_r, left_out]
y[..., bottom_out, right_out] = y_chops[3][..., bottom_r, right_r]
10.4 Back Projection 后处理
Back Projection 是一种迭代优化方法,通过计算 SR 图像下采样后与原 LR 图像的误差来修正 SR 结果:
def back_projection(self):
"""
Back Projection 约束:
超分图像下采样后应该能重建出原始 LR 图像
公式:SR_refined = SR + λ × upsample(LR - downsample(SR))
"""
# 计算 LR 空间的误差
lr_error = self.var_L - F.interpolate(
self.fake_H,
scale_factor=1/self.opt['scale'],
mode='bicubic',
align_corners=False)
# 将误差上采样到 HR 空间
us_error = F.interpolate(
lr_error,
scale_factor=self.opt['scale'],
mode='bicubic',
align_corners=False)
# 修正 SR 结果(λ = back_projection_lamda)
self.fake_H += self.opt['back_projection_lamda'] * us_error
torch.clamp(self.fake_H, 0, 1)
Back Projection 的意义:确保超分结果与输入 LR 的"一致性"——SR 下采样后应该接近原始 LR,这是一个物理约束,可以减少幻觉细节(hallucination)。
11. 分布式训练支持
def init_dist(backend='nccl', **kwargs):
"""初始化 PyTorch DDP 分布式训练(使用 NCCL 通信后端)"""
if mp.get_start_method(allow_none=True) != 'spawn':
mp.set_start_method('spawn') # 防止 CUDA fork 问题
rank = int(os.environ['RANK']) # 当前进程的全局 rank
num_gpus = torch.cuda.device_count() # 本节点 GPU 数量
torch.cuda.set_device(rank % num_gpus)
dist.init_process_group(backend=backend, **kwargs)
# DistIterSampler:分布式数据采样器
# 确保每个 GPU 处理不重叠的数据子集
train_sampler = DistIterSampler(train_set, world_size, rank, dataset_ratio)
# dataset_ratio=200:将每个 epoch 的数据集扩大 200 倍
# 对于小数据集(如 DPED),防止 epoch 过短导致学习率调度不合理
# 多 GPU 训练命令示例:
# CUDA_VISIBLE_DEVICES=4,5,6,7 python3 train.py \
# -opt options/df2k/train_bicubic_noise.yml \
# --launcher pytorch
12. 量化评估指标
# 测试时评估 PSNR 和 SSIM(可选,需要 GT)
psnr = util.calculate_psnr(cropped_sr_img * 255, cropped_gt_img * 255)
ssim = util.calculate_ssim(cropped_sr_img * 255, cropped_gt_img * 255)
# Y 通道评估(更接近人眼感知):
sr_img_y = bgr2ycbcr(sr_img, only_y=True) # BGR → YCbCr,取 Y 通道
gt_img_y = bgr2ycbcr(gt_img, only_y=True)
psnr_y = util.calculate_psnr(cropped_sr_img_y * 255, cropped_gt_img_y * 255)
ssim_y = util.calculate_ssim(cropped_sr_img_y * 255, cropped_gt_img_y * 255)
NTIRE 2020 竞赛评估标准:
| 指标 | 说明 |
|---|---|
| PSNR | 峰值信噪比,量化像素误差 |
| SSIM | 结构相似度,评估结构保真度 |
| MOS | 人工主观评分(Mean Opinion Score),最终排名依据 |
| MOR | 平均意见排名(Mean Opinion Rank) |
注意: NTIRE 2020 最终排名基于 MOS/MOR(人工主观评分),而非 PSNR/SSIM。这正是 RealSR 强调感知质量(感知损失 + GAN 损失)而非像素精度的原因。
13. 快速使用指南
13.1 环境配置
# 创建 conda 环境
conda create -n realsr python=3.7
conda activate realsr
# 安装 PyTorch(GPU 版本)
pip install torch>=1.0 torchvision
# 安装依赖
pip install numpy opencv-python lmdb pyyaml tqdm Pillow scipy
# 安装 TensorBoard(可选)
pip install tb-nightly future # PyTorch >= 1.1
# 或
pip install tensorboardX # PyTorch == 1.0
13.2 快速测试(直接使用预训练模型)
cd Real-SR-master/codes
# DF2K 模型(处理含噪声/压缩伪影的图像):
# 1. 修改 options/df2k/test_df2k.yml:
# - line 1: name = "test_df2k_result"
# - line 13: dataroot_LR = "你的测试图像目录"
# - line 26: pretrain_model_G = "预训练模型路径"
CUDA_VISIBLE_DEVICES=0 python3 test.py -opt options/df2k/test_df2k.yml
# DPED 模型(处理手机拍摄的真实照片):
CUDA_VISIBLE_DEVICES=0 python3 test.py -opt options/dped/test_dped.yml
# 结果保存在:../results/ 目录
# 使用 ncnn 轻量推理(无需 GPU,支持 Windows/Linux/macOS):
./realsr-ncnn-vulkan -i in.jpg -o out.png # 基础推理
./realsr-ncnn-vulkan -i in.jpg -o out.png -x # 开启自集成(x8 TTA)
./realsr-ncnn-vulkan -i in.jpg -o out.png -g 0 # 指定 GPU
13.3 训练自己的模型
Track1(处理含噪声图像):
cd Real-SR-master/codes
# Step1:生成 bicubic 降采样数据集
python3 ./preprocess/create_bicubic_dataset.py \
--dataset df2k --artifacts tdsr
# Step2:从源图像提取噪声块
python3 ./preprocess/collect_noise.py \
--dataset df2k --artifacts tdsr
# Step3:启动训练(多 GPU)
CUDA_VISIBLE_DEVICES=4,5,6,7 python3 train.py \
-opt options/df2k/train_bicubic_noise.yml
# 检查点保存在:../experiments/
Track2(处理真实手机照片):
# Step1:使用 KernelGAN 估计下采样核
cd KernelGAN
CUDA_VISIBLE_DEVICES=0,1,2,3 python3 train.py \
--X4 --input-dir SOURCE_PATH
# 结果:每张图像生成 kernel_x4.mat 文件
# Step2:用估计的核生成 LR/HR 数据对
cd ../codes
python3 ./preprocess/create_kernel_dataset.py \
--dataset dped --artifacts clean --kernel_path KERNEL_PATH
# Step3:收集真实噪声块
python3 ./preprocess/collect_noise.py \
--dataset dped --artifacts clean
# Step4:启动训练
CUDA_VISIBLE_DEVICES=4,5,6,7 python3 train.py \
-opt options/dped/train_kernel_noise.yml
14. 总结与思考
14.1 RealSR 的核心贡献
| 贡献 | 具体内容 | 影响 |
|---|---|---|
| 真实退化框架 | KernelGAN + 真实噪声注入 | 解决了训练/推理域差异问题 |
| 两赛道差异化方案 | DF2K 用 bicubic,DPED 用估计核 | 针对不同退化类型的最优策略 |
| 预训练初始化 | 从 PSNR 模型初始化 GAN 训练 | 显著提高训练稳定性 |
| RaGAN 对抗损失 | 相对平均 GAN | 更稳定、更真实的纹理生成 |
| 灵活推理策略 | x8 TTA + 分块推理 + Back Projection | 适应不同场景需求 |
14.2 局限性与改进方向
- 固定 ×4 倍率: 目前只支持 ×4 超分,不支持任意倍率
- KernelGAN 速度慢: 每张图像独立训练 KernelGAN 耗时长,难以大规模应用
- 噪声独立性假设: 噪声注入时直接叠加,假设噪声与图像内容独立,实际情况更复杂
- 感知指标优先: 为追求感知质量(MOS),PSNR 有所牺牲,不适用于科学图像分析
14.3 后续工作参考
- Real-ESRGAN(2021): 引入高阶退化(多次退化的组合),更通用的真实世界退化建模
- BSRGAN(2021): 随机混合多种退化(模糊/噪声/下采样/JPEG),提升鲁棒性
- SwinIR(2021): 用 Swin Transformer 替换 CNN 骨干,在多个超分任务上超越 RRDBNet
参考文献
- Ji, X., et al. "Real-World Super-Resolution via Kernel Estimation and Noise Injection." CVPRW 2020.
- Wang, X., et al. "ESRGAN: Enhanced Super-Resolution Generative Adversarial Networks." ECCVW 2018.
- Bell-Kligler, S., et al. "Blind Super-Resolution Kernel Estimation using an Internal-GAN." NeurIPS 2019. (KernelGAN)
- Lim, B., et al. "Enhanced Deep Residual Networks for Single Image Super-Resolution." CVPRW 2017. (EDSR)
- Lugmayr, A., et al. "NTIRE 2020 Challenge on Real-World Image Super-Resolution: Methods and Results." CVPRW 2020.
📌 代码仓库: github.com/jixiaozhong…
📄 论文链接: openaccess.thecvf.com/content_CVP…
🔧 ncnn 可执行文件: github.com/nihui/reals…
如果本文对你有帮助,欢迎点赞收藏!有问题欢迎在评论区交流。