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 边界效应处理

边界效应,说白了就是数据两端的问题。你想想看,光谱的第一个点,左边没有数据,怎么算平均值?

常见的处理方法有四种:

  1. 补零法:边界外补 0。简单,但会在边界引入突变,造成假峰。
  2. 镜像法:把边界内的数据镜像到外面。效果不错,我经常用。
  3. 截断法:直接丢掉边界处的 k 个点。数据会变短,但内部质量好。
  4. 最近值法:用边界值填充外面。平滑后边界会有一段平线。

我曾经踩过的坑:有一次处理近红外光谱,用了补零法,结果在光谱两端出现了两个小鼓包。排查了半天才发现是边界效应搞的鬼。从那以后,我默认都用镜像法,除非有特殊需求。

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.padmode='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 知识体系总览

下面这张图,是我梳理的移动平均滤波知识结构。你看一眼,心里就有谱了:

移动平均滤波 原理:局部平均 窗口选择策略 边界效应处理 低通滤波器 峰展宽效应 看峰宽 看噪声水平 奇数原则 补零法 镜像法 截断法 最近值法 Python实现 vs SciPy 移动平均滤波知识体系

这张图把移动平均的核心内容都串起来了。你从中心节点出发,沿着分支走一遍,原理、窗口、边界、实现,全齐了。

最后说一句:移动平均虽然简单,但它是理解更高级滤波方法的基础。Savitzky-Golay、高斯滤波、小波降噪,都是在移动平均的思想上演化出来的。把这个吃透了,后面的路就好走了。


公众号:蓝海资料掘金营,微信deep3321