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 构建合成噪声数据集

有了上面的基础函数,构建数据集就很简单了。我通常的做法是:

  1. 准备一批干净图像(比如 BSD500、DIV2K 的子集)
  2. 对每张图,随机选择一种噪声类型和参数
  3. 生成噪声图,保存为 (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 本章知识体系总览

下面这张图,帮你把这一章的核心逻辑串起来:

噪声模型与数据准备知识体系 高斯噪声 椒盐噪声 泊松噪声 正态分布 p(x) 随机替换 0/255 泊松分布 P(λ) σ 控制强度 prob 控制密度 信号强度决定方差 合成噪声数据集 (clean, noisy) 对 训练去噪网络 混合噪声训练 提升泛化能力

嗯,到这里,噪声模型和数据准备这部分就讲完了。你可能会问,为什么我要花这么多篇幅讲这些基础?因为后面调参的时候,你会发现——噪声类型不同,最优的网络结构和超参数完全不同。高斯噪声用简单的 MSE 损失就能搞定,但泊松噪声可能需要泊松损失函数。这些细节,我们后面会一一展开。