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 噪声参数估计方法

拿到一张真实低照度图像,怎么知道它的噪声参数?

我常用的方法有三种:

  1. 平坦区域法:找图像中均匀的区域(比如天空、墙壁),计算局部方差。
  2. 差分法:拍两张完全相同的场景,相减后分析残差。
  3. 小波域估计:利用小波变换分离噪声和信号。

这里重点说平坦区域法,因为它最简单实用:

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 合成含噪低照度数据集

做算法训练,没有真实数据怎么办?自己造!

我一般按这个流程来:

  1. 选底图:用高ISO、长曝光或HDR合成的高质量图像作为干净参考。
  2. 模拟低照度:降低整体亮度(乘以一个小于1的系数)。
  3. 加泊松噪声:模拟光子散粒效应。
  4. 加高斯噪声:模拟读出噪声和热噪声。
  5. 量化:转回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 本章知识体系

下面这张图,把本章的核心逻辑串起来了:

图像噪声模型与合成流程 噪声来源 高斯噪声模型 泊松噪声模型 参数估计方法 合成含噪数据集 核心思路 低照度噪声 = 泊松噪声(光子散粒效应)+ 高斯噪声(读出/热噪声) 合成数据集时,先降亮度 → 加泊松 → 加高斯 → 量化 训练降噪 / HDR 算法