2. 图像噪声模型:高斯噪声、泊松噪声、椒盐噪声的数学原理与Python模拟生成

聊到图像降噪,咱们得先搞清楚一件事——噪声到底是什么?

说白了,噪声就是图像里不该有的随机信号。我刚开始做图像处理时,总觉得降噪就是“模糊一下”。结果呢?细节全没了,边缘也糊了。后来才明白:不同噪声的“脾气”完全不同,得对症下药。

这一章,咱们就把三种最常见的噪声模型掰开揉碎。你不仅能理解它们的数学原理,还能亲手用Python生成出来。嗯,动手才是硬道理。

2.1 噪声的“户口本”:三大模型对比

先给个全景图。我习惯把噪声分成三类,它们的来源和特性差异很大:

噪声类型 数学模型 典型来源 对图像的影响
高斯噪声 正态分布 N(μ, σ²) 传感器热噪声、暗电流 整幅图像均匀分布,像蒙了一层雾
泊松噪声 泊松分布 P(λ) 光子计数统计(低光照场景) 亮度越高噪声越明显,与信号相关
椒盐噪声 随机脉冲(0或255) 传输错误、坏像素 黑白点随机出现,像撒了胡椒和盐

你看,这三种噪声的“性格”完全不同。高斯噪声是“均匀派”,泊松噪声是“欺软怕硬派”(亮的地方噪声大),椒盐噪声是“极端派”。

核心观点:降噪算法必须匹配噪声模型。用均值滤波去椒盐噪声?效果会很差。用中值滤波去高斯噪声?也不理想。选对工具,事半功倍。

2.2 高斯噪声:最“老实”的噪声

高斯噪声是图像处理里最常见的噪声模型。为什么?因为根据中心极限定理,很多独立随机因素叠加起来,最终都会趋向正态分布。

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

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

其中 μ 是均值(通常为0),σ 是标准差(控制噪声强度)。

我在项目中遇到过一件事:有一次做工业检测,相机在高温环境下工作,图像全是“雪花点”。一分析,就是典型的高斯噪声。当时我直接用高斯滤波去噪,结果边缘全糊了。后来改用双边滤波,才保住了细节。

小技巧:高斯噪声的强度由 σ 控制。σ=10 时噪声较轻,σ=50 时图像基本看不清了。实际项目中,我一般先估算 σ,再选择滤波器的参数。

Python模拟生成高斯噪声

import cv2
import numpy as np
import matplotlib.pyplot as plt

def add_gaussian_noise(image, mean=0, sigma=25):
    """
    给图像添加高斯噪声
    :param image: 输入图像 (H, W, C)
    :param mean: 噪声均值
    :param sigma: 噪声标准差
    :return: 加噪后的图像
    """
    noise = np.random.normal(mean, sigma, image.shape)
    noisy = image + noise
    noisy = np.clip(noisy, 0, 255).astype(np.uint8)
    return noisy

# 读取图像
img = cv2.imread('lena.png', cv2.IMREAD_GRAYSCALE)

# 生成不同强度的噪声
noisy_light = add_gaussian_noise(img, sigma=15)
noisy_heavy = add_gaussian_noise(img, sigma=50)

# 显示结果
plt.figure(figsize=(12, 4))
plt.subplot(131), plt.imshow(img, cmap='gray'), plt.title('原图')
plt.subplot(132), plt.imshow(noisy_light, cmap='gray'), plt.title('σ=15')
plt.subplot(133), plt.imshow(noisy_heavy, cmap='gray'), plt.title('σ=50')
plt.show()

你看,σ=15 时图像还能接受,σ=50 时细节基本被淹没了。这就是高斯噪声的“均匀覆盖”特性。

2.3 泊松噪声:与信号“同甘共苦”

泊松噪声很有意思。它来源于光子计数的统计特性。你想想看,相机传感器接收光子是一个随机过程,光子数服从泊松分布。

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

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

其中 λ 是期望光子数,也是方差。关键点来了:泊松噪声的方差等于均值。这意味着图像越亮(λ越大),噪声方差越大,噪声越明显。

为什么会这样?因为光子计数是“信号依赖”的。亮区域光子多,统计波动也大。我在做天文图像处理时,这个问题特别突出——星空背景暗,噪声小;星体中心亮,噪声大。

注意:泊松噪声不是加性噪声,而是信号相关的。你不能像高斯噪声那样直接“加”上去。正确的做法是:把像素值当作 λ,从泊松分布中采样。

Python模拟生成泊松噪声

def add_poisson_noise(image):
    """
    给图像添加泊松噪声
    :param image: 输入图像 (H, W, C),值范围0-255
    :return: 加噪后的图像
    """
    # 将像素值转换为泊松分布的λ参数
    # 注意:泊松噪声的方差 = 均值
    noisy = np.random.poisson(image.astype(np.float64))
    noisy = np.clip(noisy, 0, 255).astype(np.uint8)
    return noisy

# 生成泊松噪声
noisy_poisson = add_poisson_noise(img)

# 对比不同亮度区域的噪声
plt.figure(figsize=(12, 4))
plt.subplot(121), plt.imshow(img, cmap='gray'), plt.title('原图')
plt.subplot(122), plt.imshow(noisy_poisson, cmap='gray'), plt.title('泊松噪声')

# 观察局部区域
roi_original = img[100:150, 100:150]
roi_noisy = noisy_poisson[100:150, 100:150]
print(f"原图区域均值: {roi_original.mean():.1f}, 方差: {roi_original.var():.1f}")
print(f"噪声区域均值: {roi_noisy.mean():.1f}, 方差: {roi_noisy.var():.1f}")

运行这段代码,你会发现:亮区域的噪声方差明显大于暗区域。这就是泊松噪声的“信号依赖”特性。

2.4 椒盐噪声:最“暴力”的噪声

椒盐噪声,也叫脉冲噪声。它不跟你玩“渐变”,直接随机把一些像素变成纯黑(0)或纯白(255)。

数学上,椒盐噪声可以描述为:

I_noisy(x,y) = 
    - 0, 以概率 p/2
    - 255, 以概率 p/2
    - I(x,y), 以概率 1-p

其中 p 是噪声密度。p=0.05 表示5%的像素被污染。

我记得有一次做文档扫描增强,扫描仪传输线缆接触不良,结果图像上全是黑白点。那就是典型的椒盐噪声。当时我用了中值滤波,效果出奇的好——因为中值滤波对极端值不敏感。

避坑指南:我曾经用均值滤波去椒盐噪声,结果黑白点被“抹开”成灰斑,图像更难看。后来才明白:椒盐噪声的“克星”是中值滤波,不是均值滤波。

Python模拟生成椒盐噪声

def add_salt_pepper_noise(image, prob=0.05):
    """
    给图像添加椒盐噪声
    :param image: 输入图像 (H, W, C)
    :param prob: 噪声密度(0-1)
    :return: 加噪后的图像
    """
    noisy = image.copy()
    total_pixels = image.size
    
    # 椒噪声(黑点)
    num_pepper = int(total_pixels * prob / 2)
    coords = [np.random.randint(0, i, num_pepper) for i in image.shape]
    noisy[coords[0], coords[1]] = 0
    
    # 盐噪声(白点)
    num_salt = int(total_pixels * prob / 2)
    coords = [np.random.randint(0, i, num_salt) for i in image.shape]
    noisy[coords[0], coords[1]] = 255
    
    return noisy

# 生成不同密度的椒盐噪声
noisy_sp_light = add_salt_pepper_noise(img, prob=0.02)
noisy_sp_heavy = add_salt_pepper_noise(img, prob=0.1)

plt.figure(figsize=(12, 4))
plt.subplot(131), plt.imshow(img, cmap='gray'), plt.title('原图')
plt.subplot(132), plt.imshow(noisy_sp_light, cmap='gray'), plt.title('密度=0.02')
plt.subplot(133), plt.imshow(noisy_sp_heavy, cmap='gray'), plt.title('密度=0.1')
plt.show()

你看,密度0.02时只有零星几个点,密度0.1时图像已经很难看了。椒盐噪声虽然“暴力”,但处理起来反而简单——中值滤波一上,基本搞定。

2.5 三种噪声的“全家福”对比

咱们把三种噪声放在一起对比一下,你就能更直观地感受它们的差异:

关键区别:

  • 高斯噪声:均匀分布,与信号无关,适合用高斯滤波、双边滤波
  • 泊松噪声:信号相关,亮区噪声大,适合用方差稳定变换(如Anscombe变换)
  • 椒盐噪声:随机脉冲,适合用中值滤波

下面这张SVG图,帮你理清三种噪声的核心逻辑:

图像噪声模型核心逻辑 输入图像 高斯噪声 N(μ, σ²) 加性噪声 泊松噪声 P(λ) 信号相关 椒盐噪声 随机脉冲 0/255 特性 均匀分布 与信号强度无关 特性 方差=均值 亮区噪声大 特性 极端值 随机位置 推荐:高斯/双边滤波 推荐:Anscombe变换 推荐:中值滤波

2.6 实战:混合噪声与参数调优

现实中的图像噪声往往是混合的。比如低光照下,既有泊松噪声(光子计数),又有高斯噪声(传感器热噪声)。

我建议你这样做:

  1. 先分析噪声类型:看直方图、看局部方差分布
  2. 分步处理:先处理椒盐噪声(中值滤波),再处理高斯/泊松噪声
  3. 参数调优:用PSNR和SSIM评估效果,不要凭感觉

重要提醒:不要一上来就用深度学习。传统方法在噪声模型已知时,效果稳定、计算快。我做过对比:在椒盐噪声密度0.1以下,中值滤波比CNN快10倍,效果还更好。

完整代码:生成三种噪声并对比

def generate_noise_comparison(image_path):
    """
    生成三种噪声的对比图
    """
    img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
    
    # 生成三种噪声
    gaussian = add_gaussian_noise(img, sigma=25)
    poisson = add_poisson_noise(img)
    salt_pepper = add_salt_pepper_noise(img, prob=0.05)
    
    # 计算PSNR
    def psnr(original, noisy):
        mse = np.mean((original - noisy) ** 2)
        return 10 * np.log10(255**2 / mse)
    
    print(f"高斯噪声 PSNR: {psnr(img, gaussian):.2f} dB")
    print(f"泊松噪声 PSNR: {psnr(img, poisson):.2f} dB")
    print(f"椒盐噪声 PSNR: {psnr(img, salt_pepper):.2f} dB")
    
    # 显示结果
    plt.figure(figsize=(15, 5))
    plt.subplot(141), plt.imshow(img, cmap='gray'), plt.title('原图')
    plt.subplot(142), plt.imshow(gaussian, cmap='gray'), plt.title('高斯噪声')
    plt.subplot(143), plt.imshow(poisson, cmap='gray'), plt.title('泊松噪声')
    plt.subplot(144), plt.imshow(salt_pepper, cmap='gray'), plt.title('椒盐噪声')
    plt.tight_layout()
    plt.show()

# 运行
generate_noise_comparison('lena.png')

运行这段代码,你会看到三种噪声的视觉效果差异。PSNR值也能告诉你:哪种噪声对图像质量影响最大。