2. 传统降噪方法(上):均值滤波与高斯滤波

各位同学,欢迎来到第二章。上一章我们聊了噪声的来龙去脉,今天该动真格的了——咱们亲手把噪声“抹掉”。

传统降噪方法,说白了就是“用周围像素的平均值代替当前像素”。听起来简单吧?但这里面的门道,够你踩好几个坑的。我个人习惯把这类方法叫做“空间域滤波”,因为它们直接在像素坐标上操作,不涉及频域变换。

今天我们先讲两个最基础的:均值滤波和高斯滤波。别看它们老,很多现代深度学习降噪网络的设计思路,追根溯源都能回到这里。

2.1 均值滤波:最朴素的“和稀泥”

均值滤波的原理,一句话就能说清楚:用一个窗口扫过图像,窗口内所有像素求平均,用这个平均值替换中心像素。

你想想看,如果某个像素是噪声点(比如椒盐噪声里的白点),它和周围正常像素一平均,值就被拉回来了。这就是降噪的本质——平滑。

2.1.1 数学定义与核

均值滤波的“核”(Kernel)长这样:

K = (1 / (ksize * ksize)) * [[1, 1, 1],
                              [1, 1, 1],
                              [1, 1, 1]]

对于一个 3×3 的核,每个权重都是 1/9。说白了就是“大家投票,票数相等”。

用公式表达就是:

g(x, y) = (1 / N) * Σ f(i, j)   (i,j ∈ 窗口)

其中 N 是窗口内像素总数。

2.1.2 代码实现

OpenCV 里一行搞定:

import cv2
import numpy as np

img = cv2.imread('noisy_image.jpg', cv2.IMREAD_GRAYSCALE)

# 均值滤波,核大小 5×5
blurred = cv2.blur(img, (5, 5))

cv2.imshow('Original', img)
cv2.imshow('Blurred', blurred)
cv2.waitKey(0)

如果你想自己手写,也不难:

def my_average_filter(img, ksize=3):
    h, w = img.shape
    pad = ksize // 2
    padded = np.pad(img, pad, mode='reflect')
    result = np.zeros_like(img)
    
    for i in range(h):
        for j in range(w):
            window = padded[i:i+ksize, j:j+ksize]
            result[i, j] = np.mean(window)
    
    return result.astype(np.uint8)
我的经验:手写滤波是理解卷积操作最好的方式。我在带新人时,第一周就让他们手写均值滤波,写完了再讲深度学习里的卷积层,他们一下就懂了。

2.1.3 均值滤波的优缺点

优点 缺点
实现简单,计算速度快 所有像素一视同仁,不分远近
对高斯噪声有一定效果 严重破坏边缘和细节
适合做图像预处理中的粗平滑 对椒盐噪声效果很差(会扩散噪声)
—— 核越大,图像越模糊,细节丢失越严重
避坑指南:我曾经在一个工业检测项目里,直接用均值滤波去噪,结果把产品表面的细微划痕也给抹平了。甲方拿着检测报告来找我,说“你们这算法把次品都当成良品了”。从那以后,我再用均值滤波都会先问自己一句:我到底要保留什么?

2.2 高斯滤波:给邻居“分个三六九等”

均值滤波的问题很明显——它认为窗口里所有像素对中心像素的贡献是一样的。但直觉告诉我们:离中心越近的像素,应该越有发言权。

高斯滤波就是基于这个想法。它用高斯函数来生成权重,距离中心越近,权重越大;越远,权重越小。

2.2.1 高斯函数与核

二维高斯函数长这样:

G(x, y) = (1 / (2πσ²)) * exp(-(x² + y²) / (2σ²))

其中 σ(sigma)控制着权重的分布——σ 越大,曲线越平坦,远处的像素也能分到不少权重;σ 越小,曲线越尖,只有紧挨着的邻居才有话语权。

举个例子,σ=1.0 时,一个 3×3 的高斯核大概长这样(归一化前):

[[0.075, 0.124, 0.075],
 [0.124, 0.204, 0.124],
 [0.075, 0.124, 0.075]]

你看,中心像素的权重是 0.204,是角落像素(0.075)的近 3 倍。这就是“远近亲疏有别”。

2.2.2 代码实现

OpenCV 里同样简单:

# 高斯滤波,核大小 5×5,sigma=1.5
gaussian_blur = cv2.GaussianBlur(img, (5, 5), 1.5)

cv2.imshow('Gaussian Blur', gaussian_blur)
cv2.waitKey(0)

手写实现也不复杂,关键是要先生成高斯核:

def gaussian_kernel(ksize, sigma):
    kernel = np.zeros((ksize, ksize), dtype=np.float32)
    center = ksize // 2
    
    for i in range(ksize):
        for j in range(ksize):
            x, y = i - center, j - center
            kernel[i, j] = np.exp(-(x**2 + y**2) / (2 * sigma**2))
    
    # 归一化,让所有权重之和为 1
    kernel /= kernel.sum()
    return kernel

def my_gaussian_filter(img, ksize=5, sigma=1.5):
    kernel = gaussian_kernel(ksize, sigma)
    h, w = img.shape
    pad = ksize // 2
    padded = np.pad(img, pad, mode='reflect')
    result = np.zeros_like(img)
    
    for i in range(h):
        for j in range(w):
            window = padded[i:i+ksize, j:j+ksize]
            result[i, j] = np.sum(window * kernel)
    
    return result.astype(np.uint8)
一个实用技巧:OpenCV 的 GaussianBlur 有个隐藏参数——你可以只传 ksize 不传 sigma,它会自动根据核大小计算 sigma。公式是 sigma = 0.3 * ((ksize - 1) * 0.5 - 1) + 0.8。但我建议你还是手动指定 sigma,可控性更强。

2.2.3 高斯滤波的优缺点

优点 缺点
权重分布符合自然规律,平滑更自然 仍然会模糊边缘,只是比均值滤波好一些
可通过 σ 控制平滑程度,灵活性高 计算量比均值滤波稍大(需要浮点乘法)
对高斯噪声效果很好(“专业对口”) 对脉冲噪声(椒盐)效果依然不理想
可分离性:二维高斯可拆成两个一维高斯,加速计算 σ 选择不当,要么欠平滑,要么过平滑

2.3 核心知识图谱

下面这张图帮你理清今天讲的内容:

传统降噪方法(上):核心知识结构 空间域滤波 均值滤波 高斯滤波 核:所有权重相等 优点:简单快速 缺点:严重破坏边缘 核:距离越近权重越大 优点:平滑自然,可调σ 缺点:仍会模糊边缘 核心矛盾:降噪 vs 保边 后续章节将引入边缘感知的降噪方法,解决这一矛盾

2.4 如何选择:均值 vs 高斯

在实际项目中,我一般这样选:

  • 快速预览或粗处理:用均值滤波,速度快,看一眼效果就行
  • 噪声是高斯型的:首选高斯滤波,σ 设为 1.0~1.5 之间
  • 需要保留边缘:两个都不行,等后面章节讲的双边滤波或深度学习方案
  • 核大小怎么定:一般 3×3 或 5×5 就够了,7×7 以上基本只剩模糊
一个重要的认知:均值滤波和高斯滤波都是线性滤波。它们对整张图像“一视同仁”,不会区分哪里是平坦区域、哪里是边缘。这就是它们保边能力差的根本原因。后面我们要讲的双边滤波、非局部均值,都是在尝试打破这种“一视同仁”。

2.5 本章小结

今天我们学了两种最基础的降噪方法:

  1. 均值滤波:简单粗暴,所有邻居平等投票。适合快速去噪,但细节丢失严重。
  2. 高斯滤波:按距离分配权重,平滑更自然。通过 σ 控制平滑程度,是实际项目中最常用的基础滤波器。

这两种方法都有一个共同的“死穴”——不分区域地平滑。边缘和噪声在它们眼里是一样的,都会被抹平。