3. 传统降噪方法(下):中值滤波、双边滤波的原理与实现,边缘保留特性分析

上一节我们聊了均值滤波和高斯滤波。这两种方法,说白了都是「平均主义」——把邻域内的像素揉在一起取个平均值。平滑效果不错,但代价也很明显:边缘糊了。

那有没有一种方法,既能去掉噪声,又能把边缘这条「命根子」保住?

有。中值滤波和双边滤波,就是传统图像处理里两个最经典的「边缘保留」选手。我入行那会儿,第一个接触的非线性滤波器就是中值滤波,当时觉得这玩意儿太聪明了。后来做双边滤波,才真正体会到什么叫「既要又要」的工程艺术。

3.1 中值滤波:简单粗暴,但有效

3.4.1 原理:排序代替平均

中值滤波的思路非常直接:不用平均,用排序。

具体做法:

  1. 选定一个窗口(比如 3×3)
  2. 把窗口内所有像素值排序
  3. 取排序后的中间值,作为当前像素的新值

举个例子。一个 3×3 窗口里的像素值是这样的:

原始像素值:
[10, 12, 11]
[13, 255, 9]   ← 中间那个 255 是椒盐噪声
[8, 11, 10]

排序后:8, 9, 10, 10, 11, 11, 12, 13, 255
中间值(第5个):11

新像素值:11

你看,那个突兀的 255 直接被干掉了,换成了 11。这就是中值滤波的核心——对离群点免疫。

核心结论:中值滤波对椒盐噪声(salt-and-pepper noise)效果极佳,因为噪声点通常排在序列两端,永远不会被选中。

3.4.2 为什么能保留边缘?

这个问题我当年也困惑过。你想想看,边缘是什么?是像素值的「跳变」。比如从 50 跳到 150。

如果窗口刚好跨在边缘上:

窗口跨边缘:
[50, 50, 50]
[50, 150, 150]
[50, 150, 150]

排序后:50, 50, 50, 50, 50, 150, 150, 150, 150
中间值:50 或 150(取决于窗口中心在哪一侧)

结果要么是 50,要么是 150。边缘还在。而均值滤波会把 50 和 150 平均成 100,边缘就糊了。

这就是中值滤波的「边缘保留」本质——它不创造新值,只从现有值里选一个。

3.4.3 代码实现

OpenCV 里一句话搞定:

import cv2
import numpy as np

# 读取图像
img = cv2.imread('noisy_image.jpg', cv2.IMREAD_GRAYSCALE)

# 中值滤波,核大小必须为奇数
denoised = cv2.medianBlur(img, 3)  # 3x3 窗口

# 显示结果
cv2.imshow('Original', img)
cv2.imshow('Median Filter', denoised)
cv2.waitKey(0)

如果自己实现,核心逻辑也不复杂:

def median_filter(image, kernel_size=3):
    h, w = image.shape
    pad = kernel_size // 2
    padded = np.pad(image, pad, mode='reflect')
    result = np.zeros_like(image)
    
    for i in range(h):
        for j in range(w):
            window = padded[i:i+kernel_size, j:j+kernel_size]
            result[i, j] = np.median(window)
    
    return result

个人经验:中值滤波的窗口大小很关键。3×3 对轻度噪声够用,5×5 能去更重的噪声,但也会开始抹掉细小的纹理。我一般从 3 开始试,不行再往上加。

3.4.4 中值滤波的局限性

优点 缺点
对椒盐噪声效果极好 对高斯噪声效果一般
边缘保留能力强 计算量大(需要排序)
实现简单 窗口越大,细节丢失越多
没有平均带来的模糊 对纹理区域不友好

我曾经在一个工业检测项目里用中值滤波去椒盐噪声,效果非常好。但换成高斯噪声后,图像反而出现了「块状感」。后来我才意识到——中值滤波对高斯噪声的抑制能力有限,因为高斯噪声的每个像素都偏离了真值,排序后中间值也不准。

3.2 双边滤波:空间+灰度,双管齐下

3.2.1 原理:两个高斯核的乘积

双边滤波(Bilateral Filter)是我个人非常喜欢的一个滤波器。它的想法很优雅:不仅要考虑像素之间的距离,还要考虑像素值之间的差异。

公式长这样:

I_filtered(x) = (1/W_p) * Σ I(x_i) * G_s(||x - x_i||) * G_r(|I(x) - I(x_i)|)

其中:
- G_s: 空间高斯核(距离越近,权重越大)
- G_r: 灰度高斯核(灰度越接近,权重越大)
- W_p: 归一化系数

说白了就是:

  • 空间近的像素,权重高
  • 灰度值接近的像素,权重高
  • 两者都满足的像素,权重最高

这就意味着:在平坦区域,周围像素灰度值都差不多,双边滤波退化为高斯滤波,平滑效果很好。但在边缘处,边缘另一侧的像素灰度值差异大,权重被压低,所以边缘被保留了下来。

一句话总结:双边滤波 = 高斯滤波(空间域) × 灰度相似性(值域)。

3.2.2 两个关键参数

双边滤波有两个核心参数,调参的时候要特别留意:

参数 符号 作用 调参建议
空间标准差 σspace 控制空间邻域大小 越大,平滑范围越广
灰度标准差 σrange 控制灰度相似性阈值 越大,边缘保留越弱

我刚开始用双边滤波的时候,经常把 σrange 设得太大,结果边缘全没了,跟高斯滤波没区别。后来才明白——σrange 决定了「什么算边缘」。设小了,连纹理都当边缘保留;设大了,真正的边缘也被平滑掉。

避坑指南:我曾经在一个医学图像去噪项目里,把 σrange 设成了 100(图像是 8 位,范围 0-255),结果边缘全糊了。后来改成 20-30,效果才出来。记住:σrange 一般取图像灰度范围的 10%-15%。

3.2.3 代码实现

OpenCV 实现:

import cv2

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

# 双边滤波
# 参数:d=9(邻域直径),sigmaColor=75(灰度标准差),sigmaSpace=75(空间标准差)
denoised = cv2.bilateralFilter(img, d=9, sigmaColor=75, sigmaSpace=75)

cv2.imshow('Bilateral Filter', denoised)
cv2.waitKey(0)

自己实现的核心逻辑:

def bilateral_filter(image, d=9, sigma_color=75, sigma_space=75):
    h, w = image.shape
    pad = d // 2
    padded = np.pad(image, pad, mode='reflect')
    result = np.zeros_like(image, dtype=np.float32)
    
    # 预计算空间高斯权重
    x, y = np.meshgrid(range(-pad, pad+1), range(-pad, pad+1))
    space_weight = np.exp(-(x**2 + y**2) / (2 * sigma_space**2))
    
    for i in range(h):
        for j in range(w):
            window = padded[i:i+d, j:j+d].astype(np.float32)
            center_val = image[i, j]
            
            # 灰度高斯权重
            range_weight = np.exp(-((window - center_val)**2) / (2 * sigma_color**2))
            
            # 总权重 = 空间权重 × 灰度权重
            weight = space_weight * range_weight
            weight /= weight.sum()  # 归一化
            
            result[i, j] = np.sum(window * weight)
    
    return result.astype(np.uint8)

性能提示:双边滤波的计算量很大,因为每个像素都要重新计算灰度权重。实际项目中可以用「双边网格」(Bilateral Grid)加速,或者用 OpenCV 的优化版本。我一般先用小图调参,再跑全图。

3.3 边缘保留特性对比分析

为了让你更直观地理解,我画了一张对比图:

传统降噪方法边缘保留特性对比 原始信号(含边缘跳变) 均值滤波:边缘模糊 中值滤波:边缘保留,但阶梯化 双边滤波:边缘保留,平滑过渡 中值滤波保持阶跃边缘的锐利度,双边滤波在保留边缘的同时提供更自然的平滑过渡

3.4 如何选择?

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

  • 椒盐噪声 → 中值滤波。又快又好,没商量。
  • 高斯噪声 + 需要保留边缘 → 双边滤波。但要注意调参。
  • 实时性要求高 → 中值滤波。双边滤波太慢。
  • 纹理丰富的区域 → 双边滤波。中值滤波会把细纹理当成噪声抹掉。

我的建议:不要迷信任何一种滤波器。实际项目中,我经常把中值滤波和双边滤波组合使用——先用中值滤波去椒盐噪声,再用双边滤波做精细平滑。效果往往比单独用任何一种都好。

3.5 小结

这一节我们聊了中值滤波和双边滤波。中值滤波靠排序,对椒盐噪声有奇效;双边滤波靠空间+灰度双权重,能在去噪的同时保住边缘。两者都是传统图像处理里的「边缘保留」利器。

但说实话,这两种方法都有天花板。中值滤波对高斯噪声力不从心,双边滤波计算量大、参数敏感。到了深度学习时代,我们有更好的选择——那就是后面要讲的卷积神经网络去噪方法。

不过,理解这些传统方法很重要。它们的思想——利用局部统计特性、区分噪声和边缘——在深度学习里依然被广泛借鉴。很多去噪网络的设计,都能看到中值滤波和双边滤波的影子。