降噪算法(一):均值滤波与高斯滤波
图像降噪,说白了就是去掉画面里那些不该出现的「噪点」。你想想看,传感器感光不均匀、光线不足、传输干扰……这些都会让图像变得脏兮兮的。我刚开始做图像处理那会儿,总觉得降噪就是「模糊一下」,后来才发现——降噪的本质,是在平滑和细节之间找平衡。
这一章我们先聊两个最基础的线性滤波算法:均值滤波和高斯滤波。它们虽然简单,但却是理解更复杂降噪算法的基石。
核心观点:均值滤波是「平均主义」,高斯滤波是「远近亲疏」。两者都是线性滤波,但高斯滤波在保留边缘方面明显优于均值滤波。
均值滤波:最简单的降噪方式
均值滤波的思路非常直白——用一个窗口扫过图像,把窗口内所有像素的灰度值取平均,作为中心像素的新值。
举个例子,一个 3×3 的窗口:
原始像素值:
[10, 12, 11]
[ 9, 50, 10] ← 中心点 50 是噪点
[11, 13, 12]
均值滤波后中心值 = (10+12+11+9+50+10+11+13+12) / 9 = 138 / 9 ≈ 15.33
你看,原本 50 的噪点被拉低到了 15 左右,和周围像素更接近了。这就是均值滤波的降噪原理。
但问题也来了——它把所有像素一视同仁。窗口内的每个像素权重都是 1/9,不管它离中心多远,也不管它是不是边缘。我在项目中遇到过这种情况:用均值滤波去噪,结果人物的轮廓变得模糊,像没对上焦一样。
均值滤波的致命伤:它会把边缘也「平均」掉。边缘两侧的像素值差异很大,一旦被平均,边缘就消失了。所以均值滤波只适合噪声较小、对细节要求不高的场景。
高斯滤波:给像素「论资排辈」
高斯滤波和均值滤波的区别,就在于权重分配。它不再搞平均主义,而是根据像素到中心点的距离,分配不同的权重——离中心越近,权重越大;离中心越远,权重越小。
这个权重分布,就是高斯函数:
G(x, y) = (1 / (2πσ²)) * exp(-(x² + y²) / (2σ²))
其中 σ(sigma)控制着权重的衰减速度。σ 越大,权重分布越平坦,滤波效果越接近均值滤波;σ 越小,中心像素的权重越大,保留细节的能力越强。
我习惯把 σ 理解为「视野范围」——σ 小,只看眼前;σ 大,兼顾全局。
3×3 高斯核的计算示例
假设 σ = 1.0,我们计算一个 3×3 高斯核:
| 位置 (x, y) | 距离平方 x²+y² | 未归一化权重 | 归一化权重 |
|---|---|---|---|
| (-1, -1) | 2 | 0.1353 | 0.075 |
| (-1, 0) | 1 | 0.3679 | 0.204 |
| (-1, 1) | 2 | 0.1353 | 0.075 |
| (0, -1) | 1 | 0.3679 | 0.204 |
| (0, 0) | 0 | 1.0000 | 0.554 |
| (0, 1) | 1 | 0.3679 | 0.204 |
| (1, -1) | 2 | 0.1353 | 0.075 |
| (1, 0) | 1 | 0.3679 | 0.204 |
| (1, 1) | 2 | 0.1353 | 0.075 |
归一化后的高斯核:
[[0.075, 0.204, 0.075],
[0.204, 0.554, 0.204],
[0.075, 0.204, 0.075]]
你看,中心像素的权重 0.554 远高于角落的 0.075。这意味着高斯滤波在平滑噪声的同时,能更好地保留中心像素的原始值——也就是保留了更多细节。
我的经验:实际项目中,我一般用 5×5 或 7×7 的高斯核,σ 取 1.0~2.0。核太小降噪效果有限,核太大又容易模糊。你可以先试 σ=1.5,然后根据效果微调。
两种滤波的对比
| 对比维度 | 均值滤波 | 高斯滤波 |
|---|---|---|
| 权重分配 | 均匀分布 | 中心权重高,边缘权重低 |
| 边缘保留 | 差 | 较好 |
| 降噪效果 | 一般 | 较好 |
| 计算复杂度 | 低 | 略高(需计算权重) |
| 适用场景 | 噪声均匀、细节要求低 | 通用降噪、细节保留要求高 |
代码实现:从零手写高斯滤波
理论说完了,我们来写代码。我个人习惯用 Python + NumPy 实现,这样能清楚看到每一步在做什么。
import numpy as np
import cv2
def gaussian_kernel(size, sigma):
"""生成高斯核"""
kernel = np.zeros((size, size))
center = size // 2
for i in range(size):
for j in range(size):
x = i - center
y = j - center
kernel[i, j] = np.exp(-(x**2 + y**2) / (2 * sigma**2))
# 归一化
kernel = kernel / np.sum(kernel)
return kernel
def gaussian_filter(image, kernel_size=5, sigma=1.5):
"""高斯滤波"""
kernel = gaussian_kernel(kernel_size, sigma)
pad = kernel_size // 2
# 边界填充(镜像模式)
padded = np.pad(image, pad, mode='reflect')
# 卷积操作
output = np.zeros_like(image)
for i in range(image.shape[0]):
for j in range(image.shape[1]):
region = padded[i:i+kernel_size, j:j+kernel_size]
output[i, j] = np.sum(region * kernel)
return output
# 使用示例
img = cv2.imread('noisy_image.jpg', 0) # 灰度图
result = gaussian_filter(img, kernel_size=5, sigma=1.5)
边界处理要注意:卷积操作时,图像边缘的像素没有完整的邻域。我建议用「镜像填充」(reflect mode),而不是补零。补零会在边缘产生突兀的暗边,影响视觉效果。
核心知识体系
下面这张图帮你理清本章的知识脉络:
避坑指南
- 核大小必须是奇数:3×3、5×5、7×7……这样才能保证中心像素存在。偶数核会导致输出图像偏移半个像素。
- σ 和核大小要匹配:核太小而 σ 太大,高斯核的权重在边缘处几乎为 0,相当于浪费了计算资源。一般建议核大小 ≥ 6σ+1。
- 彩色图像要逐通道处理:对 RGB 三个通道分别做高斯滤波,不要直接对三通道取平均,否则颜色会乱掉。
我曾经踩过的坑:有一次做工业检测,用均值滤波去噪,结果把产品表面的细微划痕也给抹平了,导致漏检。后来换成高斯滤波,σ 调到 1.2,既去掉了传感器噪声,又保留了划痕特征。所以——选对算法比调参更重要。