2. 噪声模型:高斯噪声、泊松噪声、椒盐噪声的数学建模与仿真

做多帧降噪,第一件事不是调算法,而是搞清楚——你面对的噪声到底是什么来路。

我刚开始接触这个方向时,踩过一个坑。当时拿到一组暗光视频,直接上了均值滤波,结果画面糊成一团。后来一分析,发现噪声根本不是高斯分布,而是泊松占主导。嗯,从那以后,我养成了一个习惯:先建模,再降噪。

这一节,我们就来聊聊三种最常见的噪声模型。我会把数学公式、物理含义、以及仿真代码都给你拆开揉碎。你想想看,搞懂了噪声的“脾气”,后面设计滤波器才能有的放矢。

2.1 高斯噪声:传感器读数的“底色”

高斯噪声,说白了就是传感器在读取信号时,电子热运动带来的随机波动。它无处不在,是所有成像系统都绕不开的底噪。

2.1.1 数学定义

高斯噪声的概率密度函数长这样:

p(x) = (1 / (σ * √(2π))) * exp(-(x - μ)² / (2σ²))

其中:

  • μ 是均值,通常为0(无偏噪声)
  • σ 是标准差,决定了噪声的强度

我个人习惯用 σ 来表征噪声水平,而不是方差。因为标准差和像素值的量纲一致,更直观。

2.1.2 物理来源

高斯噪声主要来自:

  • 热噪声:传感器温度越高,σ越大
  • 读出噪声:ADC转换过程中的量化误差
  • 放大器噪声:模拟增益引入的随机波动
实战经验:我在做手机摄像头调试时发现,ISO越高,高斯噪声的σ几乎线性增长。所以高ISO下,降噪算法的强度必须动态调整。

2.1.3 仿真代码

用Python生成高斯噪声很简单,但要注意一点——噪声要加在浮点域,而不是整数域。我曾经因为直接在uint8上加噪声,导致截断失真,花了半天才定位到问题。

import numpy as np
import cv2

def add_gaussian_noise(image, sigma=25):
    """
    给图像添加高斯噪声
    image: 输入图像,范围[0, 255]
    sigma: 噪声标准差
    """
    # 转浮点,避免截断
    img_float = image.astype(np.float32)
    
    # 生成高斯噪声
    noise = np.random.normal(0, sigma, img_float.shape)
    
    # 加噪并裁剪
    noisy = np.clip(img_float + noise, 0, 255).astype(np.uint8)
    
    return noisy

# 使用示例
img = cv2.imread('clean.png')
noisy_img = add_gaussian_noise(img, sigma=30)
注意:np.random.normal的第二个参数是标准差σ,不是方差。很多人在这里写成了方差,结果噪声强度完全不对。

2.2 泊松噪声:光子计数的“量子本性”

泊松噪声,也叫散粒噪声。它来源于光的量子特性——光子到达传感器是随机的,服从泊松分布。

你想想看,在暗光环境下,每个像素接收到的光子数很少,比如只有10个。这时候随机波动(√10 ≈ 3.16)相对于信号本身就很显著了。这就是为什么夜景照片噪点特别多的根本原因。

2.2.1 数学定义

泊松分布的概率质量函数:

P(k) = (λ^k * e^(-λ)) / k!

其中:

  • λ 是期望光子数(即信号强度)
  • k 是实际观测到的光子数

泊松分布有一个重要性质:均值 = 方差 = λ。这意味着噪声的强度与信号本身成正比。信号越强,噪声的绝对幅度越大,但信噪比(SNR)反而越高。

2.2.2 与高斯噪声的区别

特性 高斯噪声 泊松噪声
与信号的关系 独立 依赖(方差=信号)
主要场景 亮光、高ISO 暗光、低光子数
分布形状 对称 低λ时偏斜

2.2.3 仿真代码

仿真泊松噪声时,我建议用 np.random.poisson 直接生成。注意输入图像要先归一化到光子数范围。

def add_poisson_noise(image, peak=100):
    """
    给图像添加泊松噪声
    image: 输入图像,范围[0, 255]
    peak: 峰值光子数,控制噪声强度
    """
    # 归一化到光子数
    img_norm = image.astype(np.float32) / 255.0 * peak
    
    # 生成泊松噪声
    noisy = np.random.poisson(img_norm)
    
    # 归一化回[0, 255]
    noisy = np.clip(noisy / peak * 255, 0, 255).astype(np.uint8)
    
    return noisy

# 使用示例
noisy_img = add_poisson_noise(img, peak=50)
核心要点:泊松噪声的仿真,关键在于peak参数的选择。peak越小,噪声越明显。实际传感器中,peak对应着满阱容量,一般在几千到几万之间。

2.3 椒盐噪声:像素的“死与生”

椒盐噪声,顾名思义,就是图像上随机出现一些纯黑(椒)或纯白(盐)的点。它不像高斯和泊松那样是连续的,而是离散的、突变的。

我记得有一次在工业检测项目中,相机传输线缆屏蔽没做好,图像上全是椒盐噪声。当时我以为是算法问题,折腾了两天,最后发现是硬件干扰。嗯,从那以后,我拿到图像第一件事就是先看有没有椒盐噪声——它往往是硬件问题的信号。

2.3.1 数学定义

椒盐噪声的模型很简单:

  • 每个像素以概率 p 被污染
  • 被污染的像素中,一半概率变成0(椒点),一半概率变成255(盐点)

所以总的噪声密度是 p,其中椒点和盐点各占 p/2

2.3.2 仿真代码

def add_salt_pepper_noise(image, prob=0.05):
    """
    给图像添加椒盐噪声
    image: 输入图像
    prob: 噪声密度(0~1)
    """
    noisy = image.copy()
    total_pixels = image.size
    
    # 椒点数量
    num_pepper = int(total_pixels * prob / 2)
    # 盐点数量
    num_salt = int(total_pixels * prob / 2)
    
    # 随机位置
    coords_pepper = [np.random.randint(0, i, num_pepper) for i in image.shape]
    coords_salt = [np.random.randint(0, i, num_salt) for i in image.shape]
    
    noisy[tuple(coords_pepper)] = 0
    noisy[tuple(coords_salt)] = 255
    
    return noisy

# 使用示例
noisy_img = add_salt_pepper_noise(img, prob=0.1)
避坑指南:我曾经在彩色图像上直接加椒盐噪声,忘了处理三个通道。结果椒点变成了彩色斑点,完全不是椒盐的效果。正确的做法是:要么三通道同时置0/255,要么只在亮度通道上加。

2.4 三种噪声的对比与选择

实际场景中,噪声往往是混合的。比如暗光下,泊松噪声占主导,但读出噪声(高斯)也同时存在。我一般这样判断:

  • 亮光、高ISO:高斯噪声为主
  • 暗光、长曝光:泊松噪声为主
  • 传输干扰、坏像素:椒盐噪声为主

做仿真时,我建议把三种噪声都加上,然后调权重。这样生成的测试数据更接近真实场景。

2.5 知识体系总览

下面这张图,是我自己总结的噪声模型知识框架。你可以把它当作一个快速索引:

噪声模型知识体系 噪声模型 高斯噪声 泊松噪声 椒盐噪声 来源:热噪声、读出噪声 参数:均值μ、标准差σ 特点:与信号独立 来源:光子计数随机性 参数:期望λ(=方差) 特点:方差=信号强度 来源:传输干扰、坏像素 参数:噪声密度p 特点:离散突变 核心原则:先识别噪声类型,再选择降噪策略

这张图把三种噪声的源头、参数和特点都串起来了。我个人习惯把它打印出来贴在工位上,做算法选型时扫一眼,心里就有数了。

我的小技巧:做仿真时,不要只加一种噪声。真实场景往往是混合的。我一般用高斯+泊松模拟传感器噪声,再加少量椒盐模拟传输干扰。这样训练出来的降噪模型,泛化能力会好很多。

好了,噪声模型就聊到这里。搞懂了这些,后面讲多帧降噪算法时,你就能理解为什么有些滤波器对高斯噪声有效,对椒盐噪声却无能为力了。