2. 图像噪声模型与数学建模
做低照度图像处理,第一件事就是搞清楚——噪声到底长什么样?
我刚开始接触这个领域时,总觉得噪声就是「画面花了」,后来才发现,不同来源的噪声,长相完全不同,处理方式也天差地别。这一章,咱们就把噪声的底裤扒干净。
2.1 噪声从哪来?
低照度环境下,光线不足,传感器就得拼命放大信号。放大信号的同时,噪声也被一起放大了。说白了,噪声就是传感器在「偷懒」时留下的痕迹。
常见的噪声来源有这么几类:
- 热噪声(暗电流噪声):传感器温度越高,电子越活跃,产生多余电荷。
- 读出噪声:电路在读取电荷时引入的随机误差。
- 散粒噪声(泊松噪声):光子到达传感器的时间随机性导致的。
- 量化噪声:ADC把模拟信号转成数字信号时的精度损失。
嗯,这里要注意:低照度场景下,散粒噪声和读出噪声是主角。其他噪声相对次要。
2.2 高斯噪声模型
高斯噪声是最常见的噪声模型。为什么?因为中心极限定理告诉我们,很多独立随机因素叠加起来,就近似高斯分布。
数学上,高斯噪声的概率密度函数长这样:
p(x) = (1 / (σ * √(2π))) * exp(-(x - μ)² / (2σ²))
其中:
- μ:均值,通常为0(无偏噪声)
- σ:标准差,决定噪声强度
在图像里,高斯噪声表现为每个像素独立叠加一个随机值。我习惯用 np.random.normal 来生成:
import numpy as np
import cv2
def add_gaussian_noise(image, mean=0, sigma=25):
noise = np.random.normal(mean, sigma, image.shape)
noisy = image + noise
return np.clip(noisy, 0, 255).astype(np.uint8)
我的经验:sigma取25时,噪声肉眼可见但不至于完全毁掉图像。做算法测试时,我一般从sigma=10到sigma=50做一组对比。
2.3 泊松噪声(散粒噪声)模型
泊松噪声跟高斯噪声完全不同。它来源于光子的量子特性——光子到达传感器是随机的,服从泊松分布。
数学上:
P(k) = (λ^k * e^(-λ)) / k!
其中 λ 是期望光子数,k 是实际到达的光子数。
关键特性:泊松噪声的方差等于均值。这意味着,信号越强的地方,噪声值越大,但信噪比反而更高。低照度区域,信号弱,噪声相对更明显。
生成泊松噪声的代码:
def add_poisson_noise(image):
# 将像素值映射到光子数
# 假设最大像素值255对应最大光子数
scale = 255.0 / np.max(image)
photons = image * scale
noisy_photons = np.random.poisson(photons)
noisy = noisy_photons / scale
return np.clip(noisy, 0, 255).astype(np.uint8)
避坑指南:我曾经在合成数据集时,直接用泊松分布采样整张图,结果高亮区域噪声过大。后来发现,泊松噪声的强度应该跟像素值挂钩——像素值越大,采样参数λ越大,噪声方差越大。
2.4 高斯 vs 泊松:核心差异
| 特性 | 高斯噪声 | 泊松噪声 |
|---|---|---|
| 来源 | 热噪声、读出噪声 | 光子到达随机性 |
| 与信号关系 | 独立于信号强度 | 方差 = 均值 |
| 低照度表现 | 均匀分布在整个画面 | 暗区噪声更明显 |
| 数学分布 | 正态分布 | 泊松分布 |
| 参数 | 均值μ、标准差σ | 期望值λ |
你想想看,实际传感器里,这两种噪声是同时存在的。所以更真实的模型是:高斯-泊松混合噪声。
2.5 噪声参数估计方法
拿到一张真实低照度图像,怎么知道它的噪声参数?
我常用的方法有三种:
- 平坦区域法:找图像中均匀的区域(比如天空、墙壁),计算局部方差。
- 差分法:拍两张完全相同的场景,相减后分析残差。
- 小波域估计:利用小波变换分离噪声和信号。
这里重点说平坦区域法,因为它最简单实用:
def estimate_noise_std(image, patch_size=32):
h, w = image.shape
stds = []
for i in range(0, h - patch_size, patch_size):
for j in range(0, w - patch_size, patch_size):
patch = image[i:i+patch_size, j:j+patch_size]
# 计算局部标准差
local_std = np.std(patch)
stds.append(local_std)
# 取最小的几个值作为噪声估计
stds.sort()
return np.mean(stds[:10])
为什么取最小的?因为图像中大部分区域都有纹理,纹理本身会贡献方差。只有最平坦的区域,方差才主要来自噪声。
2.6 合成含噪低照度数据集
做算法训练,没有真实数据怎么办?自己造!
我一般按这个流程来:
- 选底图:用高ISO、长曝光或HDR合成的高质量图像作为干净参考。
- 模拟低照度:降低整体亮度(乘以一个小于1的系数)。
- 加泊松噪声:模拟光子散粒效应。
- 加高斯噪声:模拟读出噪声和热噪声。
- 量化:转回8位整数。
完整代码示例:
def synthesize_lowlight(image, brightness_factor=0.3, sigma_gaussian=15):
# 1. 降低亮度
dark = image * brightness_factor
# 2. 加泊松噪声
scale = 255.0 / np.max(dark)
photons = dark * scale
noisy_photons = np.random.poisson(photons)
dark_poisson = noisy_photons / scale
# 3. 加高斯噪声
gaussian_noise = np.random.normal(0, sigma_gaussian, dark.shape)
noisy = dark_poisson + gaussian_noise
# 4. 裁剪并量化
noisy = np.clip(noisy, 0, 255).astype(np.uint8)
return noisy
我的习惯:合成数据集时,brightness_factor 从0.05到0.5均匀采样,sigma_gaussian 从5到30随机取值。这样训练出来的模型泛化能力更强。
2.7 本章知识体系
下面这张图,把本章的核心逻辑串起来了: