RealSR:CVPR 2020 NTIRE 真实世界超分辨率冠军方案全面解析

4 阅读26分钟

论文: 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 项目的每一个技术细节。


目录

  1. 真实世界超分辨率的挑战
  2. RealSR 整体框架
  3. 项目代码结构
  4. 核心创新:退化建模框架
    • 4.1 Track1:DF2K 赛道 —— 双三次下采样 + 噪声注入
    • 4.2 Track2:DPED 赛道 —— KernelGAN 估计下采样核
  5. 数据预处理模块详解
    • 5.1 噪声收集(collect_noise.py)
    • 5.2 双三次数据集生成(create_bicubic_dataset.py)
    • 5.3 核卷积数据集生成(create_kernel_dataset.py)
  6. 骨干网络:RRDBNet 深度解析
    • 6.1 ResidualDenseBlock(RDB)
    • 6.2 RRDB:残差中残差致密块
    • 6.3 RRDBNet 完整结构
  7. 生成对抗训练:SRGANModel
    • 7.1 生成器(Generator)
    • 7.2 判别器架构
    • 7.3 损失函数体系
  8. Dataset 数据管道
    • 8.1 LQGTDataset 实现
    • 8.2 在线噪声注入增强
  9. 训练策略与配置参数
    • 9.1 Track1 训练配置
    • 9.2 Track2 训练配置
    • 9.3 学习率调度
  10. 推理测试模块
    • 10.1 多种推理模式
    • 10.2 分块推理(Chop)
    • 10.3 Back Projection 后处理
  11. 分布式训练支持
  12. 量化评估指标
  13. 快速使用指南
  14. 总结与思考

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)

两步下采样设计:

  1. 第一步(清洗): bicubic ×2 下采样,目的是消除原始 HR 图像中已有的噪声和伪影,获得"干净"的 HR
  2. 第二步(退化): 使用 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 的优势:

  1. 让真实图像的判别分数相对降低,生成图像分数相对提升,训练更稳定
  2. 同时更新真假两侧,梯度信号更丰富
  3. 更接近 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)的训练采用两阶段策略

  1. 阶段一: 纯像素损失(L1/L2)训练,得到 PSNR 最优的基础模型
  2. 阶段二: 以阶段一模型为初始化,加入感知损失 + 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 局限性与改进方向

  1. 固定 ×4 倍率: 目前只支持 ×4 超分,不支持任意倍率
  2. KernelGAN 速度慢: 每张图像独立训练 KernelGAN 耗时长,难以大规模应用
  3. 噪声独立性假设: 噪声注入时直接叠加,假设噪声与图像内容独立,实际情况更复杂
  4. 感知指标优先: 为追求感知质量(MOS),PSNR 有所牺牲,不适用于科学图像分析

14.3 后续工作参考

  • Real-ESRGAN(2021): 引入高阶退化(多次退化的组合),更通用的真实世界退化建模
  • BSRGAN(2021): 随机混合多种退化(模糊/噪声/下采样/JPEG),提升鲁棒性
  • SwinIR(2021): 用 Swin Transformer 替换 CNN 骨干,在多个超分任务上超越 RRDBNet

参考文献

  1. Ji, X., et al. "Real-World Super-Resolution via Kernel Estimation and Noise Injection." CVPRW 2020.
  2. Wang, X., et al. "ESRGAN: Enhanced Super-Resolution Generative Adversarial Networks." ECCVW 2018.
  3. Bell-Kligler, S., et al. "Blind Super-Resolution Kernel Estimation using an Internal-GAN." NeurIPS 2019. (KernelGAN)
  4. Lim, B., et al. "Enhanced Deep Residual Networks for Single Image Super-Resolution." CVPRW 2017. (EDSR)
  5. 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…


如果本文对你有帮助,欢迎点赞收藏!有问题欢迎在评论区交流。