2. 噪声模型:光子噪声、读出噪声、固定模式噪声与参数估计

做RAW域降噪,第一件事不是调模型,而是搞懂噪声从哪来。我刚开始接触这个方向时,也踩过坑——拿个通用降噪网络直接怼RAW图,结果效果时好时坏。后来才明白,噪声模型没搞对,后面全是白费功夫。

RAW域的噪声,说白了就三大类:光子噪声、读出噪声、固定模式噪声。它们叠加在一起,构成了传感器输出的原始数据。你想想看,如果连噪声的统计特性都不清楚,模型怎么可能学得好?

2.1 光子噪声(泊松噪声)

光子噪声是物理极限。光打到像素上,光子到达数量服从泊松分布。信号越强,噪声越大——这不是坏事,是量子力学决定的。

数学上很简单:

# 光子噪声模型
# 假设平均光子数为 λ,实际观测值 k ~ Poisson(λ)
# 噪声方差 = λ,信噪比 SNR = λ / sqrt(λ) = sqrt(λ)

import numpy as np

def add_photon_noise(clean_signal, gain=1.0):
    """
    模拟光子噪声
    clean_signal: 理想光子计数(单位:电子数)
    gain: 系统增益(e-/DN)
    """
    # 泊松噪声:方差等于均值
    noisy_electrons = np.random.poisson(clean_signal)
    # 转换为数字值
    noisy_dn = noisy_electrons / gain
    return noisy_dn

关键认知:光子噪声是信号相关的。暗区噪声小,亮区噪声大。很多初学者以为噪声是均匀的,这是第一个误区。

我在项目中遇到过一个问题:用合成数据训练模型时,光子噪声参数没对齐真实传感器,结果模型在暗部过平滑,亮部又欠降噪。后来我养成了一个习惯——先校准噪声参数,再谈模型设计。

2.2 读出噪声(高斯噪声)

读出噪声来自电路。像素电荷被读出时,放大器、ADC都会引入噪声。这部分噪声与信号无关,近似高斯分布。

它的特点是:

  • 信号无关:亮区和暗区的读出噪声水平一样
  • 时域随机:每帧独立,不随时间累积
  • 近似高斯:中心极限定理保证,多个独立噪声源叠加后趋于高斯

完整的噪声模型通常写成:

# 完整噪声模型(简化版)
# 总噪声方差 = 光子噪声方差 + 读出噪声方差
# σ²_total = α * S + β

# 其中:
# S: 信号强度(电子数)
# α: 光子噪声系数(通常为1)
# β: 读出噪声方差(常数)

def simulate_raw_noise(clean_signal, read_noise_std=2.0, gain=1.0):
    """
    模拟RAW域噪声
    """
    # 光子噪声
    photon_noise = np.random.poisson(clean_signal)
    # 读出噪声
    read_noise = np.random.normal(0, read_noise_std, clean_signal.shape)
    # 叠加
    noisy = photon_noise + read_noise
    # 转换为DN值
    noisy_dn = noisy / gain
    return noisy_dn
噪声类型 分布 信号相关性 典型来源
光子噪声 泊松 相关(方差∝信号) 光子到达的量子随机性
读出噪声 高斯 无关(常数方差) 放大器、ADC、复位
固定模式噪声 非随机 空间相关 像素工艺偏差、暗电流不均匀

经验之谈:读出噪声的标定很简单——盖上镜头盖拍一张全黑图,统计像素值的标准差就行了。我每次拿到新传感器,第一件事就是拍黑帧。

2.3 固定模式噪声

固定模式噪声(FPN)是传感器的“指纹”。每个像素的暗电流、增益都有细微差异,导致同一光照下不同像素输出不同。

FPN分两种:

  1. 暗信号非均匀性(DSNU):无光照时,像素输出的差异
  2. 光响应非均匀性(PRNU):有光照时,像素增益的差异

FPN的特点是空间固定、时间不变。这意味着——你可以通过校准来消除它。

# 固定模式噪声校准
# 假设我们有一组暗帧和亮帧

def calibrate_fpn(dark_frames, bright_frames):
    """
    校准固定模式噪声
    dark_frames: 多张暗帧 (N, H, W)
    bright_frames: 多张均匀亮帧 (N, H, W)
    """
    # DSNU: 暗帧均值
    dsnu = np.mean(dark_frames, axis=0)
    
    # PRNU: 亮帧归一化后的残差
    bright_mean = np.mean(bright_frames, axis=0)
    bright_avg = np.mean(bright_mean)
    prnu = bright_mean / bright_avg - 1.0
    
    return dsnu, prnu

def remove_fpn(image, dsnu, prnu):
    """
    去除固定模式噪声
    """
    # 先减DSNU,再除PRNU
    corrected = (image - dsnu) / (1.0 + prnu)
    return corrected

注意:FPN校准需要多帧平均。单帧噪声太大,校准结果不准。我一般至少拍16帧暗帧和16帧亮帧。曾经有一次我只拍了4帧,结果校准后反而引入了新的条纹噪声——教训深刻。

2.4 噪声参数估计

搞清楚了噪声类型,下一步就是估计参数。这是从理论到实践的关键一步。

常用的估计方法有两种:

  • 光子转移法(Photon Transfer Method):通过拍摄不同光照下的均匀画面,拟合噪声方差与信号强度的关系
  • 补丁法(Patch-based Method):从单张图中提取多个均匀区域,统计局部均值和方差

光子转移法更准确,但需要可控光源。补丁法更灵活,适合现场标定。

# 光子转移法估计噪声参数

def estimate_noise_params(images, gains):
    """
    估计噪声参数
    images: 不同光照下的均匀画面列表
    gains: 对应的ISO增益
    """
    means = []
    variances = []
    
    for img in images:
        means.append(np.mean(img))
        variances.append(np.var(img))
    
    # 拟合:方差 = a * 均值 + b
    # a: 光子噪声系数(通常接近1)
    # b: 读出噪声方差
    coeffs = np.polyfit(means, variances, 1)
    a, b = coeffs[0], coeffs[1]
    
    return {
        'photon_noise_coeff': a,
        'read_noise_variance': b,
        'read_noise_std': np.sqrt(b)
    }

我个人习惯用光子转移法做离线标定,补丁法做在线校验。两者结合,基本能覆盖90%的场景。

核心结论:噪声模型不是学术玩具。它直接决定了降噪模型的上限。模型再强,如果噪声参数偏了,效果也出不来。我见过太多团队花几个月调网络结构,结果问题出在噪声模型上——白费功夫。

下面这张图总结了本章的知识结构:

RAW域噪声模型知识体系 RAW域噪声模型 光子噪声(泊松) 读出噪声(高斯) 固定模式噪声 方差 ∝ 信号强度 暗区噪声小 亮区噪声大 方差常数 信号无关 时域随机 空间固定 时间不变 可通过校准消除 噪声参数估计 光子转移法(离线标定) 补丁法(在线校验)