3. 噪声模型与仿真:用Python模拟真实传感器噪声
做图像降噪,第一步不是调网络,而是搞清楚你面对的是什么噪声。
我见过不少团队,模型在公开数据集上刷到SOTA,一上硬件就崩。为什么?因为训练数据里的噪声跟真实传感器拍出来的根本不是一回事。说白了,你连敌人长什么样都没搞清楚,怎么打?
这一章,我们就来拆解传感器噪声的物理来源,然后用Python把它复现出来。这样你后面做合成数据、做数据增强,心里才有底。
3.1 传感器噪声的物理来源
先看一张图,帮你建立整体认知:
这张图里,我把传感器噪声拆成了三大类。其中,光子噪声和读出噪声是时变噪声,每次拍都不一样;固定模式噪声是空间噪声,同一颗传感器上位置固定。
关键理解:光子噪声的方差跟信号强度成正比——信号越亮,噪声越大。读出噪声的方差是常数,跟信号无关。这两者的叠加,构成了传感器噪声的基本形态。
3.2 高斯噪声:最简单的起点
高斯噪声是读出噪声的主要成分。它的特点是:每个像素独立,服从正态分布,均值为0,方差固定。
数学上很简单:
I_noisy = I_clean + n, n ~ N(0, σ²)
Python实现就几行:
import numpy as np
import cv2
def add_gaussian_noise(image, sigma=25):
"""
添加高斯噪声
sigma: 噪声标准差,单位是像素值(0-255)
"""
noise = np.random.normal(0, sigma, image.shape)
noisy = image.astype(np.float32) + noise
noisy = np.clip(noisy, 0, 255).astype(np.uint8)
return noisy
# 使用示例
clean = cv2.imread('clean.png', cv2.IMREAD_GRAYSCALE)
noisy = add_gaussian_noise(clean, sigma=25)
我的经验:sigma=25对应什么水平?在8位图像里,这相当于约10%的噪声强度。实际传感器读出噪声通常在sigma=2~10之间,但做数据增强时我经常把sigma拉到30~50,让模型见过更恶劣的情况。
3.3 泊松噪声:信号相关的本质
泊松噪声才是传感器噪声的核心。它来自光子到达传感器的随机性——光子到达数量服从泊松分布。
泊松分布有个重要性质:均值等于方差。也就是说,信号越强,噪声方差越大。
def add_poisson_noise(image):
"""
添加泊松噪声
输入图像像素值被解释为光子计数
"""
# 将像素值映射到光子计数
# 这里用了一个缩放因子,控制噪声强度
scale = 1.0 # 越小噪声越强
photons = image.astype(np.float32) / scale
# 泊松采样
noisy_photons = np.random.poisson(photons)
# 映射回像素值
noisy = (noisy_photons * scale).astype(np.uint8)
noisy = np.clip(noisy, 0, 255)
return noisy
你想想看,如果直接用这个函数处理一张图,暗区的噪声看起来很小,亮区的噪声反而很大。这就是信号相关噪声的直观表现。
注意:泊松噪声的强度跟图像本身的亮度绑定。如果你用一张曝光不足的暗图,泊松噪声几乎看不见;但一旦提亮,噪声就全出来了。这就是为什么夜景照片放大后全是噪点。
3.4 信号相关噪声:更真实的模型
实际传感器噪声不是单纯的高斯或泊松,而是两者的混合。学术界常用的模型是:
I_noisy = I_clean + n_p(I_clean) + n_g
其中n_p是泊松噪声(信号相关),n_g是高斯噪声(信号无关)。
更精确的建模方式是用方差稳定变换的思路:
def add_realistic_noise(image, alpha=0.01, sigma=10):
"""
模拟真实传感器噪声
alpha: 泊松噪声系数(信号相关)
sigma: 高斯噪声标准差(信号无关)
"""
img = image.astype(np.float32)
# 信号相关部分:方差与信号成正比
poisson_var = img * alpha
poisson_noise = np.random.normal(0, np.sqrt(poisson_var))
# 信号无关部分:固定方差
gaussian_noise = np.random.normal(0, sigma, img.shape)
# 叠加
noisy = img + poisson_noise + gaussian_noise
noisy = np.clip(noisy, 0, 255).astype(np.uint8)
return noisy
参数调优建议:alpha和sigma的取值需要根据实际传感器标定。我在项目中常用的做法是:拍一张均匀光照下的灰卡,然后分析不同亮度区域的噪声方差,拟合出alpha和sigma。这样得到的噪声模型跟真实传感器最接近。
3.5 固定模式噪声(FPN)
FPN是传感器制造工艺导致的固定偏移。每个像素或每列像素有一个固定的偏置,不随时间变化。
FPN有两种主要形式:
| 类型 | 来源 | 特点 | 消除方法 |
|---|---|---|---|
| 像素FPN | 每个像素的暗电流不均匀 | 空间随机,固定不变 | 暗帧减法 |
| 列FPN | 列放大器偏移 | 整列有固定偏移 | 列校准 |
模拟FPN的代码:
def add_fpn(image, pixel_offset_std=5, col_offset_std=3):
"""
添加固定模式噪声
pixel_offset_std: 像素级偏移标准差
col_offset_std: 列级偏移标准差
"""
h, w = image.shape[:2]
# 像素级FPN
pixel_offset = np.random.normal(0, pixel_offset_std, (h, w))
# 列级FPN
col_offset = np.random.normal(0, col_offset_std, (1, w))
col_offset = np.tile(col_offset, (h, 1))
# 叠加
fpn = pixel_offset + col_offset
noisy = image.astype(np.float32) + fpn
noisy = np.clip(noisy, 0, 255).astype(np.uint8)
return noisy
避坑指南:我曾经在项目中忽略了FPN,结果模型在合成数据上表现很好,一上真实硬件就出现竖条纹。后来才发现是列FPN在作怪。从那以后,我每次做数据合成都会加上FPN,哪怕强度调得很低。
3.6 完整噪声管线:把一切串起来
现在我们把所有噪声类型整合成一个完整的仿真管线:
def sensor_noise_pipeline(image, iso_level=1.0):
"""
完整传感器噪声仿真管线
iso_level: ISO感光度,越高噪声越强
"""
img = image.astype(np.float32)
# 参数随ISO变化
alpha = 0.005 * iso_level # 泊松系数
sigma = 5 * iso_level # 高斯标准差
fpn_pixel = 2 * iso_level # 像素FPN
fpn_col = 1 * iso_level # 列FPN
# 1. 光子噪声(泊松近似)
poisson_var = img * alpha
poisson_noise = np.random.normal(0, np.sqrt(poisson_var + 1e-8))
# 2. 读出噪声(高斯)
gaussian_noise = np.random.normal(0, sigma, img.shape)
# 3. 固定模式噪声
h, w = img.shape[:2]
pixel_offset = np.random.normal(0, fpn_pixel, (h, w))
col_offset = np.random.normal(0, fpn_col, (1, w))
col_offset = np.tile(col_offset, (h, 1))
# 叠加所有噪声
noisy = img + poisson_noise + gaussian_noise + pixel_offset + col_offset
noisy = np.clip(noisy, 0, 255).astype(np.uint8)
return noisy
# 使用示例
clean = cv2.imread('clean.png')
noisy_low_iso = sensor_noise_pipeline(clean, iso_level=1.0)
noisy_high_iso = sensor_noise_pipeline(clean, iso_level=4.0)
核心要点:这个管线模拟了传感器噪声的三个核心成分——信号相关的泊松噪声、信号无关的高斯噪声、空间固定的FPN。调整iso_level参数,就能模拟从低ISO到高ISO的不同噪声水平。
3.7 噪声评估:怎么知道模拟得像不像?
模拟完了,总得验证一下。我常用的评估方法有两个:
- 方差-均值曲线:取均匀区域,计算不同亮度下的噪声方差。真实传感器噪声的方差-均值曲线应该是线性的(泊松部分)加上一个常数偏移(高斯部分)。
- 噪声频谱分析:FPN在频谱上表现为特定频率的峰值,高斯噪声则是平坦的频谱。
代码实现:
def analyze_noise(noisy_image, clean_image=None):
"""
分析噪声特性
返回不同亮度区间的噪声方差
"""
if clean_image is not None:
noise = noisy_image.astype(np.float32) - clean_image.astype(np.float32)
else:
# 如果没有干净图像,用局部方差估计
noise = noisy_image.astype(np.float32)
# 按亮度分桶
bins = np.linspace(0, 255, 20)
bin_centers = (bins[:-1] + bins[1:]) / 2
bin_variances = []
for i in range(len(bins)-1):
mask = (noisy_image >= bins[i]) & (noisy_image < bins[i+1])
if mask.sum() > 100:
bin_variances.append(np.var(noise[mask]))
else:
bin_variances.append(0)
return bin_centers, bin_variances
我的习惯:每次做完噪声仿真,我都会画一张方差-均值曲线图,跟真实传感器的标定数据对比。如果曲线形状对不上,就调整alpha和sigma参数。这个迭代过程虽然枯燥,但能让你的合成数据质量提升一个档次。
3.8 本章小结
这一章我们做了三件事:
- 理解了传感器噪声的物理来源——光子噪声、读出噪声、FPN
- 用Python实现了每种噪声的仿真代码
- 整合成完整的噪声管线,并给出了评估方法
有了这个噪声仿真工具,你后面做合成训练数据、做数据增强,就有了坚实的基础。记住一句话:噪声模型越真实,降噪模型越鲁棒。