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分两种:
- 暗信号非均匀性(DSNU):无光照时,像素输出的差异
- 光响应非均匀性(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%的场景。
核心结论:噪声模型不是学术玩具。它直接决定了降噪模型的上限。模型再强,如果噪声参数偏了,效果也出不来。我见过太多团队花几个月调网络结构,结果问题出在噪声模型上——白费功夫。
下面这张图总结了本章的知识结构: