3. 移动平均滤波:原理、窗口选择策略、边界效应处理,Python实现与SciPy对比
移动平均滤波,说白了就是给光谱数据「抹平」一下。你想想看,光谱里那些毛刺噪声,就像脸上的痘痘,看着碍眼。移动平均就是拿一块小抹布,在数据上滑过去,把凸起的抹平,把凹陷的填上。
我刚开始做光谱分析那会儿,第一个接触的降噪方法就是它。为啥?因为简单、直观、效果好。但后来我发现,越是简单的东西,用好了是神器,用不好就是坑。今天咱们就把这个坑填上。
3.1 移动平均的原理
移动平均的核心思想就一句话:用局部平均值代替当前点。
具体来说,对于光谱数据 x[i],我们取它前后各 k 个点,一共 2k+1 个点,算个平均值,作为新的 y[i]。公式长这样:
y[i] = (1 / (2k+1)) * Σ x[i+j] (j 从 -k 到 k)
这里的 2k+1 就是窗口大小。窗口越大,抹得越平,但细节丢得也越多。我习惯把窗口大小叫做「平滑力度」——力度越大,噪声越少,但信号也越模糊。
核心理解:移动平均本质上是一个低通滤波器。它让低频的信号(光谱峰)通过,把高频的噪声(毛刺)滤掉。但问题是,光谱峰的边缘也是高频成分,所以峰会被展宽、变矮。
3.2 窗口选择策略
窗口怎么选?这是移动平均里最让人头疼的问题。我踩过不少坑,总结出三条经验:
- 看峰宽:窗口大小不要超过光谱峰半高宽的 1/3。比如你的峰半高宽是 30 个数据点,窗口就别超过 10。否则峰会被严重压扁。
- 看噪声水平:噪声越大,窗口需要越大。但别贪心,我见过有人为了降噪把窗口拉到 50,结果峰都看不见了。
- 奇数原则:窗口大小一定要是奇数。这样当前点正好在窗口中心,对称性好。
我在项目中遇到过一种情况:拉曼光谱里有个很窄的峰,只有 5 个点宽。用窗口 3 去平滑,峰还在;用窗口 5,峰直接没了。嗯,这里要注意——窄峰对窗口大小极其敏感。
| 窗口大小 | 降噪效果 | 峰形影响 | 适用场景 |
|---|---|---|---|
| 3 | 弱 | 几乎无影响 | 低噪声、窄峰 |
| 5 | 中等 | 轻微展宽 | 一般光谱 |
| 7-11 | 强 | 明显展宽 | 高噪声、宽峰 |
| >15 | 极强 | 严重变形 | 不推荐用于精细分析 |
3.3 边界效应处理
边界效应,说白了就是数据两端的问题。你想想看,光谱的第一个点,左边没有数据,怎么算平均值?
常见的处理方法有四种:
- 补零法:边界外补 0。简单,但会在边界引入突变,造成假峰。
- 镜像法:把边界内的数据镜像到外面。效果不错,我经常用。
- 截断法:直接丢掉边界处的
k个点。数据会变短,但内部质量好。 - 最近值法:用边界值填充外面。平滑后边界会有一段平线。
我曾经踩过的坑:有一次处理近红外光谱,用了补零法,结果在光谱两端出现了两个小鼓包。排查了半天才发现是边界效应搞的鬼。从那以后,我默认都用镜像法,除非有特殊需求。
3.4 Python 实现
自己写一个移动平均,其实就几行代码。但要注意边界处理,我习惯用镜像法:
import numpy as np
def moving_average(x, window_size):
"""
移动平均滤波,镜像法处理边界
x: 输入光谱数组
window_size: 窗口大小(奇数)
"""
if window_size % 2 == 0:
raise ValueError("窗口大小必须是奇数")
half = window_size // 2
# 镜像扩展
x_padded = np.pad(x, half, mode='reflect')
y = np.zeros_like(x)
for i in range(len(x)):
y[i] = np.mean(x_padded[i:i+window_size])
return y
# 示例
spectrum = np.array([1, 2, 3, 4, 5, 4, 3, 2, 1])
smoothed = moving_average(spectrum, 3)
print(smoothed)
这段代码里,np.pad 的 mode='reflect' 就是镜像法。我个人习惯用这个,因为它在边界处过渡最自然。
3.5 SciPy 对比
自己写归自己写,但实际项目中我推荐直接用 SciPy。为啥?因为人家优化得好,速度快,还支持多种边界模式。
SciPy 里对应的函数是 scipy.signal.savgol_filter?不对,那是 Savitzky-Golay 滤波。移动平均在 SciPy 里可以用 scipy.ndimage.uniform_filter1d 或者 scipy.signal.convolve 来实现。
最直接的方式是用卷积:
from scipy.signal import convolve
def moving_average_scipy(x, window_size):
kernel = np.ones(window_size) / window_size
return convolve(x, kernel, mode='same')
对比一下自己写的和 SciPy 的:
| 对比项 | 自己实现 | SciPy 实现 |
|---|---|---|
| 代码量 | 10-15 行 | 2-3 行 |
| 速度 | 慢(纯 Python 循环) | 快(底层 C 实现) |
| 边界模式 | 需手动实现 | 内置多种模式 |
| 灵活性 | 高(可自定义) | 高(参数丰富) |
我的建议:学习阶段自己写一遍,理解原理。实际项目中用 SciPy,省时省力。但要注意,SciPy 的 convolve 默认 mode 是 'full',记得改成 'same' 保持数据长度一致。
3.6 知识体系总览
下面这张图,是我梳理的移动平均滤波知识结构。你看一眼,心里就有谱了:
这张图把移动平均的核心内容都串起来了。你从中心节点出发,沿着分支走一遍,原理、窗口、边界、实现,全齐了。
最后说一句:移动平均虽然简单,但它是理解更高级滤波方法的基础。Savitzky-Golay、高斯滤波、小波降噪,都是在移动平均的思想上演化出来的。把这个吃透了,后面的路就好走了。
公众号:蓝海资料掘金营,微信deep3321