图像退化模型:模糊、噪声、下采样、压缩伪影的数学建模

做超分辨率这么多年,我最大的体会是:不懂退化模型,就别谈超分落地。

你想想看,模型训练时用的都是“完美高分辨率图→人工降质→低分辨率图”这样的配对数据。如果降质过程跟真实场景对不上,模型学到的就全是花架子。我早期接过一个安防项目,模型在测试集上 PSNR 飙到 38,一上真实监控画面直接糊成一片。后来排查才发现——真实摄像头有 sensor 噪声、有镜头模糊、有编码压缩,而我训练时只用了简单的双三次下采样。

所以这一章,咱们把退化模型的每个环节掰开揉碎。数学公式我会给,但更重要的是告诉你:每个参数在实际项目中该怎么调。

2.1 经典退化模型的统一表达

先看一个最通用的退化公式:

y = D(x) = ( (x ⊗ k) ↓_s + n )_c

其中:

  • x — 高分辨率清晰图像(你要恢复的目标)
  • k — 模糊核(点扩散函数 PSF)
  • ⊗ — 卷积操作
  • ↓_s — 下采样(尺度因子 s)
  • n — 加性噪声
  • (·)_c — 压缩伪影(可选)

这个公式看着简单,但每个环节展开都有坑。下面我按实际工程中的处理顺序,逐个拆解。

核心观点:退化模型不是越复杂越好,而是越贴近你的数据采集链路越好。做项目第一步,先搞清楚你的图像是怎么从场景变成数字文件的。

2.2 模糊建模:点扩散函数与卷积

模糊是超分里最容易被低估的环节。我见过不少团队,模糊核随便用个高斯核就开训,结果模型对运动模糊、失焦模糊完全没招。

2.2.1 高斯模糊(最常见)

数学形式:

G(x, y) = (1 / (2πσ²)) · exp(-(x² + y²) / (2σ²))

参数 σ 控制模糊强度。σ 越大,图像越糊。

工程经验:

  • σ 范围一般在 0.5 ~ 3.0 之间
  • 我习惯在训练时对 σ 做随机采样,而不是固定值。这样模型能适应不同模糊程度
  • 核大小(kernel size)取 6σ+1 向上取整,保证核边缘衰减到接近零

2.2.2 运动模糊(相机抖动、物体运动)

运动模糊核是一个沿运动方向的线段。数学上可以建模为:

k_motion(x, y) = 1/L,  if (x, y) 在线段上; 0,  otherwise

其中 L 是运动轨迹长度,方向 θ 是运动角度。

避坑指南:我曾经在手机拍照超分项目里只用高斯模糊,结果模型对“手抖”产生的运动模糊完全失效。后来加了随机角度(0~360°)和随机长度(5~25像素)的运动模糊,效果立竿见影。

2.2.3 失焦模糊(散焦)

失焦模糊近似于圆盘函数(pillbox):

k_defocus(r) = 1/(πR²),  if r ≤ R; 0,  otherwise

R 是模糊圆半径,单位像素。实际场景中,失焦模糊往往还伴随衍射效应,但工程上直接用圆盘模糊就够了。

2.3 噪声建模:从高斯到泊松

噪声是退化模型里最“脏”的部分。真实传感器的噪声是多种噪声的叠加,不是单一分布能描述的。

2.3.1 高斯噪声(最基础)

n ~ N(0, σ²)

每个像素独立添加服从高斯分布的随机值。σ 控制噪声强度,通常取 0~25(图像范围 0~255)。

2.3.2 泊松噪声(光子噪声)

低光照条件下,光子到达的随机性服从泊松分布:

n_poisson ~ Poisson(λ) — λ

λ 是期望光子数。泊松噪声的特点是:信号越强,噪声方差越大。这与高斯噪声的“均匀加性”完全不同。

2.3.3 真实噪声模型(异方差高斯)

我在工业相机项目里用的最多的是这个:

σ_total² = σ_read² + σ_shot² · I

其中:

  • σ_read² — 读出噪声(固定底噪)
  • σ_shot² — 散粒噪声系数(与信号强度 I 成正比)
  • I — 像素强度

这个模型比单纯高斯噪声真实得多。我建议你在训练时至少用这个异方差模型,而不是纯高斯。

噪声类型 分布形式 工程场景 参数范围
高斯噪声 N(0, σ²) 传感器读出、热噪声 σ: 0~25
泊松噪声 Poisson(λ) 低光照、光子计数 λ: 10~200
异方差高斯 σ² = σ_read² + σ_shot²·I 真实 CMOS 传感器 σ_read: 1~5, σ_shot: 0.01~0.1
椒盐噪声 随机置 0 或 255 传输错误、坏像素 概率: 0.001~0.05

2.4 下采样建模:不止双三次

下采样是超分任务的核心操作。很多人直接用 OpenCV 的 cv2.resize(..., interpolation=cv2.INTER_CUBIC) 就完事了。但真实场景的下采样远比这复杂。

2.4.1 理想下采样(抗混叠)

先低通滤波(抗混叠),再隔点采样:

y = (x ⊗ g_aa) ↓_s

g_aa 是抗混叠滤波器,通常用高斯核或 sinc 核。s 是缩放因子,超分里常见 2×、3×、4×。

2.4.2 传感器下采样(Bayer 模式)

真实相机不是直接输出 RGB 的。每个像素只采集一种颜色(R/G/B),然后通过 demosaic 插值得到全彩图。这个过程本身就是一个退化。

我做过一个项目,直接用双三次下采样训练,模型在合成数据上 PSNR 很高,但一上真实 RAW 图就崩。后来改成模拟 Bayer 模式 + demosaic 插值,效果才正常。

注意:如果你的应用场景是手机/相机拍摄的图像,强烈建议在退化模型里加入 Bayer 采样和 demosaic 模拟。否则模型学到的“超分能力”在真实数据上基本是废的。

2.4.3 任意尺度下采样

实际项目中,缩放因子不一定是整数。比如要把 1080p 图像缩到 720p,缩放因子是 1.5。这时候需要用连续域的下采样模型:

y(i, j) = ∫∫ x(u, v) · φ(u/s - i, v/s - j) du dv

φ 是插值核(双线性、双三次、Lanczos 等)。工程上我建议用 Lanczos 核,保边效果最好。

2.5 压缩伪影建模:JPEG 与 HEVC

压缩伪影是超分落地里最头疼的问题之一。很多公开数据集都是 PNG 无损格式,但真实场景的图像几乎都是 JPEG 或 HEVC 压缩过的。

2.5.1 JPEG 压缩流程

  1. 颜色空间转换(RGB → YCbCr)
  2. 分块(8×8 块)
  3. DCT 变换
  4. 量化(质量因子 Q 控制压缩率)
  5. 熵编码

量化这一步是伪影的主要来源。量化矩阵 Q 越大,高频信息丢失越严重,出现块效应和振铃效应。

2.5.2 工程化压缩模拟

我建议在训练时用 torchjpeg 或 diffjpeg 做可微分的 JPEG 模拟。这样梯度可以回传到退化模型,实现端到端训练。

# 伪代码:可微分 JPEG 退化
def jpeg_degrade(x, quality):
    # x: [B, 3, H, W], 范围 [0, 1]
    y = rgb_to_ycbcr(x)
    y = block_dct(y, block_size=8)
    y = quantize(y, quality)  # 可微分量化
    y = block_idct(y)
    y = ycbcr_to_rgb(y)
    return y

质量因子 Q 的范围:

  • 高质量:Q=90~95(几乎无可见伪影)
  • 中等质量:Q=50~75(常见于网络图片)
  • 低质量:Q=10~30(严重块效应,监控视频常见)

2.6 完整退化流水线:一张图看懂

下面这张 SVG 图把整个退化模型串起来了。从高分辨率清晰图像开始,经过模糊、下采样、噪声、压缩,最终得到低分辨率退化图像。训练时,模型要学的是这个过程的逆过程。

图像退化模型完整流水线 高分辨率图像 HR (清晰) 模糊 高斯/运动/失焦 下采样 2×/3×/4× 噪声 高斯/泊松/异方差 压缩伪影 JPEG/HEVC 低分辨率图像 LR (退化) 💡 工程要点 • 退化顺序:模糊 → 下采样 → 噪声 → 压缩 • 每个环节的参数都要随机化,增强模型鲁棒性 • 真实场景可能缺少某个环节,按数据链路裁剪 退化模型参数随机化策略 模糊核类型/尺寸/角度 → 下采样尺度/插值方式 → 噪声类型/强度 → 压缩质量因子

2.7 退化模型的工程化实现

理论说完了,上点干货。下面是我在项目中常用的退化模型实现框架:

class DegradationModel:
    def __init__(self, config):
        self.blur_types = config.get('blur_types', ['gaussian', 'motion'])
        self.noise_types = config.get('noise_types', ['gaussian', 'poisson'])
        self.scale_range = config.get('scale_range', [2, 4])
        self.jpeg_quality_range = config.get('jpeg_quality', [50, 95])
        
    def __call__(self, hr_image):
        # 1. 随机选择模糊核
        blur_type = random.choice(self.blur_types)
        hr_blurred = self.apply_blur(hr_image, blur_type)
        
        # 2. 随机下采样
        scale = random.uniform(*self.scale_range)
        lr_image = self.downsample(hr_blurred, scale)
        
        # 3. 随机加噪声
        noise_type = random.choice(self.noise_types)
        lr_noisy = self.add_noise(lr_image, noise_type)
        
        # 4. 随机 JPEG 压缩
        if random.random() < 0.5:  # 50% 概率加压缩
            quality = random.randint(*self.jpeg_quality_range)
            lr_compressed = self.jpeg_compress(lr_noisy, quality)
        else:
            lr_compressed = lr_noisy
            
        return lr_compressed
我的建议:不要把所有退化环节都固定死。每个环节的参数都做随机采样,让模型见过足够多的退化组合。这样模型在真实场景下才有泛化能力。我一般每个 batch 都重新采样参数,相当于每张图的退化方式都不一样。

2.8 真实场景退化模型的选择策略

不同场景对退化模型的要求不一样。我整理了一个对照表,方便你快速选择:

应用场景 必须包含的退化 可忽略的退化 特殊处理
手机拍照超分 运动模糊、异方差噪声、Bayer 采样 压缩伪影(RAW 格式) 模拟 ISP 流水线
监控视频超分 运动模糊、JPEG 压缩、高斯噪声 泊松噪声(光照充足) 考虑帧间退化一致性
卫星遥感超分 失焦模糊、泊松噪声、下采样 运动模糊(平台稳定) 考虑大气湍流效应
医疗影像超分 高斯噪声、下采样 压缩伪影(DICOM 无损) 保留纹理细节
老旧照片修复 高斯模糊、JPEG 压缩、椒盐噪声 运动模糊(扫描仪输入) 考虑胶片颗粒噪声
重要提醒:退化模型不是越复杂越好。加太多不存在的退化环节,反而会让模型学偏。我见过有人把高斯噪声、泊松噪声、椒盐噪声全加上,结果模型在真实数据上反而变差了。原因很简单——真实场景根本没有椒盐噪声,模型却花了大量 capacity 去学怎么去除它。