1. 噪声模型与数据准备:理解高斯噪声、椒盐噪声、泊松噪声的数学原理与生成方法,构建合成噪声数据集
做图像去噪,第一步不是调网络,而是搞清楚你的对手是谁。
我见过不少同学,一上来就搭个 U-Net,然后丢进去一堆图片训练。结果模型表现时好时坏,根本不知道问题出在哪。说白了,你连噪声长什么样都没搞明白,怎么指望网络学会去除它?
这一章,我们就来把三种最常见的噪声模型掰开揉碎。我会结合自己踩过的坑,带你从数学原理到代码实现,完整走一遍。
1.1 高斯噪声:最普遍的“背景噪音”
高斯噪声,也叫正态噪声。你在相机传感器上看到的暗部噪点,多半就是它。
它的概率密度函数长这样:
p(x) = (1 / (σ * √(2π))) * exp(-(x - μ)² / (2σ²))
其中 μ 是均值,σ 是标准差。我习惯把 μ 设为 0,这样噪声就是零均值的,不会改变图像的整体亮度。
关键参数:σ 控制噪声强度。σ 越大,图像越花。
生成代码很简单。用 NumPy 几行就搞定:
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)
这里有个坑——一定要做 clip。我曾经在项目里忘了这一步,结果图像像素值溢出到 300 多,显示出来全是白花花一片。排查了半天才发现是数值越界了。
我的经验:实际应用中,sigma 取 15~30 比较接近真实传感器噪声。太小了网络学不到东西,太大了又脱离实际。
1.2 椒盐噪声:随机“坏点”的模拟
椒盐噪声,说白了就是图像上随机出现的一些白点(盐)和黑点(椒)。
你想想看,老照片上的那种斑点,或者传输过程中丢包导致的像素损坏,就是它。
数学上很简单:每个像素以概率 p 被替换为 0(椒)或 255(盐)。
def add_salt_pepper_noise(image, prob=0.02):
noisy = image.copy()
total = image.size
# 椒噪声
num_pepper = int(total * prob / 2)
coords = [np.random.randint(0, i-1, num_pepper) for i in image.shape]
noisy[coords[0], coords[1], :] = 0
# 盐噪声
num_salt = int(total * prob / 2)
coords = [np.random.randint(0, i-1, num_salt) for i in image.shape]
noisy[coords[0], coords[1], :] = 255
return noisy
注意:概率 prob 一般不超过 0.1。超过 0.2 的话,图像基本就废了,网络也很难恢复。我试过 prob=0.3,结果模型直接摆烂,输出全是灰的。
1.3 泊松噪声:与信号相关的“光子噪声”
泊松噪声比较特殊。它跟信号强度有关——信号越强,噪声越大。
物理上,它来源于光子到达传感器时的量子特性。低光照环境下尤其明显。
数学上,泊松分布描述的是单位时间内随机事件发生的次数。在图像里,每个像素的观测值服从以真实强度为均值的泊松分布。
def add_poisson_noise(image):
# 先将图像缩放到合适范围
scale = 255 / image.max()
scaled = image * scale
noisy = np.random.poisson(scaled)
return np.clip(noisy / scale, 0, 255).astype(np.uint8)
这里有个细节:泊松噪声的方差等于均值。所以亮区噪点多,暗区噪点少。这和前面两种噪声完全不同。
核心区别:高斯噪声是加性的,与信号无关;椒盐噪声是替换性的;泊松噪声是信号依赖的。
1.4 三种噪声的对比与选择
我把它们放在一起对比一下,方便你理解:
| 噪声类型 | 数学本质 | 与信号关系 | 典型场景 | 调参关键 |
|---|---|---|---|---|
| 高斯噪声 | 正态分布 | 独立 | 传感器热噪声 | σ 控制强度 |
| 椒盐噪声 | 随机替换 | 独立 | 传输错误、老照片 | prob 控制密度 |
| 泊松噪声 | 泊松分布 | 依赖信号 | 低光照、医学成像 | 信号强度决定方差 |
我个人习惯,在训练去噪网络时,会混合使用这三种噪声。这样模型泛化能力更强。但要注意——混合比例要合理。我一般高斯占 50%,椒盐和泊松各占 25%。
1.5 构建合成噪声数据集
有了上面的基础函数,构建数据集就很简单了。我通常的做法是:
- 准备一批干净图像(比如 BSD500、DIV2K 的子集)
- 对每张图,随机选择一种噪声类型和参数
- 生成噪声图,保存为 (clean, noisy) 对
def build_noisy_dataset(clean_images, noise_types=['gaussian', 'salt_pepper', 'poisson']):
dataset = []
for img in clean_images:
noise_type = np.random.choice(noise_types)
if noise_type == 'gaussian':
sigma = np.random.uniform(10, 50)
noisy = add_gaussian_noise(img, sigma=sigma)
elif noise_type == 'salt_pepper':
prob = np.random.uniform(0.01, 0.1)
noisy = add_salt_pepper_noise(img, prob=prob)
else: # poisson
noisy = add_poisson_noise(img)
dataset.append((img, noisy))
return dataset
避坑指南:我曾经直接把所有噪声类型混在一起训练,结果模型在泊松噪声上表现很差。后来发现,泊松噪声的强度范围太大,需要单独做归一化。建议你给每种噪声分配一个独立的通道或标签,让网络知道当前处理的是哪种噪声。
1.6 本章知识体系总览
下面这张图,帮你把这一章的核心逻辑串起来:
嗯,到这里,噪声模型和数据准备这部分就讲完了。你可能会问,为什么我要花这么多篇幅讲这些基础?因为后面调参的时候,你会发现——噪声类型不同,最优的网络结构和超参数完全不同。高斯噪声用简单的 MSE 损失就能搞定,但泊松噪声可能需要泊松损失函数。这些细节,我们后面会一一展开。