2. 图像噪声模型:高斯噪声、泊松噪声、椒盐噪声的数学建模与仿真
各位同学好,我是你们的讲师。今天咱们来聊聊图像噪声。说实话,噪声这东西,做图像处理的人天天见。你想想看,只要是真实传感器拍出来的照片,就一定有噪声。我刚开始做这个方向时,总觉得噪声是“脏东西”,恨不得一键清除。后来才明白——不了解噪声的脾气,就别想做好降噪。
这一章,我们就来解剖三种最常见的噪声:高斯噪声、泊松噪声、椒盐噪声。我会从数学建模讲到代码仿真,中间穿插一些我在项目里踩过的坑。
2.1 高斯噪声:最“标准”的噪声模型
高斯噪声,说白了就是加性白噪声。它的概率密度函数长这样:
p(x) = (1 / (σ * √(2π))) * exp(-(x - μ)² / (2σ²))
其中 μ 是均值,σ 是标准差。在图像里,我们通常假设 μ=0,σ 控制噪声强度。
我个人习惯用 np.random.normal() 来生成。但要注意——图像像素值范围是 [0, 255],加完噪声后一定要做截断处理。不然会出现像素值溢出,比如 260 这种“非法值”。
来看代码:
import cv2
import numpy as np
def add_gaussian_noise(image, mean=0, sigma=25):
"""
给图像添加高斯噪声
:param image: 输入图像 (uint8, [0,255])
:param mean: 噪声均值
:param sigma: 噪声标准差
:return: 加噪图像
"""
noise = np.random.normal(mean, sigma, image.shape)
noisy = image.astype(np.float32) + noise
noisy = np.clip(noisy, 0, 255).astype(np.uint8)
return noisy
# 使用示例
img = cv2.imread('lena.png', cv2.IMREAD_GRAYSCALE)
noisy_img = add_gaussian_noise(img, sigma=30)
这里有个坑——np.random.normal 生成的是 float64,直接加给 uint8 会溢出。我刚开始学的时候没注意,结果图像一片白,还以为是代码写错了。后来才反应过来,一定要先转 float 再截断。
2.2 泊松噪声:低光照下的“硬骨头”
泊松噪声,也叫散粒噪声。它跟高斯噪声最大的区别是——噪声强度跟信号强度有关。你想想看,在暗光环境下拍照,噪点是不是特别多?那就是泊松噪声在作怪。
数学上,泊松分布的概率质量函数是:
P(k) = (λ^k * e^(-λ)) / k!
在图像里,λ 就是像素的真实强度值。也就是说,像素越亮,噪声方差越大。
我在做夜间监控项目时遇到过这个问题。白天效果很好的降噪算法,到了晚上就崩了。后来分析才发现,是泊松噪声的“信号相关”特性没处理好。
cv2.randn() 不适用于泊松噪声。
正确的仿真代码:
def add_poisson_noise(image):
"""
给图像添加泊松噪声(散粒噪声)
:param image: 输入图像 (uint8, [0,255])
:return: 加噪图像
"""
# 将像素值映射到光子计数范围
# 通常需要缩放,因为泊松分布的 λ 不能太大
scale = 0.1 # 缩放因子,控制噪声强度
lam = image.astype(np.float32) * scale
noisy = np.random.poisson(lam)
# 还原到 [0,255] 范围
noisy = (noisy / scale).astype(np.uint8)
noisy = np.clip(noisy, 0, 255)
return noisy
这里 scale 参数很关键。它控制着“光子计数”的缩放比例。scale 越小,噪声越明显。我一般用 0.05~0.2 来模拟不同光照条件。
2.3 椒盐噪声:最“暴力”的噪声
椒盐噪声,顾名思义,就是图像上随机出现白点(盐)和黑点(椒)。它不像前两种噪声那样“温和”,而是直接把像素值替换成极端值。
它的数学模型很简单:
- 每个像素以概率 p/2 变成 0(黑点)
- 每个像素以概率 p/2 变成 255(白点)
- 每个像素以概率 1-p 保持不变
其中 p 就是噪声密度。
仿真代码:
def add_salt_pepper_noise(image, density=0.05):
"""
给图像添加椒盐噪声
:param image: 输入图像 (uint8, [0,255])
:param density: 噪声密度 (0~1)
:return: 加噪图像
"""
noisy = image.copy()
total_pixels = image.size
# 椒噪声(黑点)
num_pepper = int(total_pixels * density / 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 * density / 2)
coords = [np.random.randint(0, i, num_salt) for i in image.shape]
noisy[coords[0], coords[1]] = 255
return noisy
2.4 三种噪声的对比与选择
我把三种噪声的关键参数整理成了表格,方便你对照:
| 噪声类型 | 数学分布 | 与信号关系 | 关键参数 | 典型场景 |
|---|---|---|---|---|
| 高斯噪声 | 正态分布 | 加性,独立 | 均值 μ,标准差 σ | 传感器热噪声、放大器噪声 |
| 泊松噪声 | 泊松分布 | 信号相关 | λ(像素强度) | 低光照成像、医学影像 |
| 椒盐噪声 | 伯努利分布 | 像素替换 | 噪声密度 d | 传输错误、存储损坏 |
在实际项目中,我建议你这样选:
- 做通用算法验证:用高斯噪声就够了,简单可控
- 做低光照场景:必须加泊松噪声,否则算法在真实场景会失效
- 做鲁棒性测试:三种噪声都加,看看算法能扛到哪种程度
2.5 仿真实验:看看噪声长什么样
我们来做一个简单的对比实验。假设有一张 256x256 的灰度图,分别添加三种噪声:
import matplotlib.pyplot as plt
# 生成测试图像(渐变灰度图)
x = np.linspace(0, 255, 256)
y = np.linspace(0, 255, 256)
X, Y = np.meshgrid(x, y)
test_img = ((X + Y) / 2).astype(np.uint8)
# 添加噪声
gaussian_img = add_gaussian_noise(test_img, sigma=30)
poisson_img = add_poisson_noise(test_img, scale=0.1)
sp_img = add_salt_pepper_noise(test_img, density=0.05)
# 显示结果
plt.figure(figsize=(12, 3))
plt.subplot(141); plt.imshow(test_img, cmap='gray'); plt.title('原图')
plt.subplot(142); plt.imshow(gaussian_img, cmap='gray'); plt.title('高斯噪声 σ=30')
plt.subplot(143); plt.imshow(poisson_img, cmap='gray'); plt.title('泊松噪声 scale=0.1')
plt.subplot(144); plt.imshow(sp_img, cmap='gray'); plt.title('椒盐噪声 d=0.05')
plt.tight_layout()
plt.show()
你会发现:高斯噪声让整张图“蒙了一层雾”;泊松噪声在亮区更明显;椒盐噪声则是零散的黑白点。每种噪声的“长相”都不一样,对应的降噪策略也完全不同。
2.6 避坑指南:我踩过的三个坑
最后分享几个实战中容易犯的错误:
- 忘记截断像素值:加完噪声后像素可能超出 [0,255],不截断的话,imshow 会显示异常。我早期调试时经常被这个坑到。
- 泊松噪声的缩放因子:直接对 uint8 图像做泊松采样,λ 太大(比如 200)会导致分布近似高斯,失去泊松特性。一定要先缩放。
- 椒盐噪声的对称性:有些实现只加白点不加黑点,或者比例不对。记得保持椒盐各半,除非你有特殊需求。