4、移动平均滤波:滑动窗口原理、Python实现、窗口大小对平滑效果的影响

各位同学,咱们今天聊移动平均滤波。

说实话,这玩意儿是嵌入式信号处理里最基础、也最实用的算法之一。我做了十几年嵌入式开发,气压计数据平滑这块,移动平均是我用得最多的方法。为什么?因为它简单、可靠、不挑硬件。

4.1 滑动窗口原理

先说说原理。说白了就是:取最近N个数据的平均值,代替当前值

你想想看,气压计每次采回来的数据,都带着噪声。可能是风的影响,可能是传感器本身的抖动。如果你直接用原始数据,那高度值跳来跳去,根本没法用。

移动平均怎么做的?它维护一个“窗口”,窗口里装着最近N个采样值。每次来新数据,就把最老的那个丢掉,把新的加进来,然后算平均值。

核心公式:

y[n] = (x[n] + x[n-1] + ... + x[n-N+1]) / N

其中N就是窗口大小

举个例子。假设窗口大小N=3,原始数据是:[100, 102, 101, 103, 104]。

  • 第1个输出:还没凑够3个,一般直接取原始值或跳过
  • 第2个输出:同样不够
  • 第3个输出:(100+102+101)/3 = 101
  • 第4个输出:(102+101+103)/3 = 102
  • 第5个输出:(101+103+104)/3 = 102.67

嗯,这里要注意:窗口越大,数据越平滑,但延迟也越大。这是个取舍问题,后面我会细讲。

4.2 Python实现

代码其实很简单。我直接给一个完整的实现,你们拿去就能用。

import numpy as np
import matplotlib.pyplot as plt

def moving_average(data, window_size):
    """
    移动平均滤波
    :param data: 输入数据(一维数组)
    :param window_size: 窗口大小(正整数)
    :return: 滤波后的数据
    """
    # 我习惯用numpy的convolve实现,效率高
    window = np.ones(window_size) / window_size
    smoothed = np.convolve(data, window, mode='valid')
    return smoothed

# 生成模拟气压计数据
np.random.seed(42)
t = np.linspace(0, 10, 200)
true_altitude = 100 + 5 * np.sin(t)  # 真实高度
noise = np.random.normal(0, 2, len(t))  # 噪声
measured = true_altitude + noise  # 测量值

# 不同窗口大小
window_3 = moving_average(measured, 3)
window_10 = moving_average(measured, 10)
window_30 = moving_average(measured, 30)

# 绘图
plt.figure(figsize=(12, 6))
plt.plot(t, measured, alpha=0.5, label='原始数据')
plt.plot(t[:len(window_3)], window_3, label='窗口=3')
plt.plot(t[:len(window_10)], window_10, label='窗口=10')
plt.plot(t[:len(window_30)], window_30, label='窗口=30')
plt.legend()
plt.xlabel('时间 (s)')
plt.ylabel('高度 (m)')
plt.title('移动平均滤波效果对比')
plt.grid(True)
plt.show()

避坑指南:

我曾经在STM32上直接用了numpy的convolve,结果发现内存爆了。嵌入式环境里,我建议用循环队列实现,只维护窗口内的数据,别把整个历史数据都存着。

4.3 窗口大小对平滑效果的影响

窗口大小是移动平均滤波的灵魂参数。选对了,数据又平滑又跟得上变化;选错了,要么噪声没滤掉,要么信号被抹平了。

窗口大小 平滑效果 延迟 适用场景
小(3-5) 快速变化的信号,如手势识别
中(10-20) 中等 中等 气压计高度,一般场景
大(30-50) 缓慢变化,如气象站

为什么会这样?你想想看,窗口越大,参与平均的数据越多,噪声被抵消得越彻底。但代价是,真实信号的变化也被平均掉了。比如你从1楼走到2楼,窗口太大,滤波后的高度变化会慢半拍。

我个人习惯:先根据采样率估算。比如气压计每秒采10次,我想要0.5秒的响应时间,那窗口就设5左右。想要2秒的平滑,窗口就设20。

4.4 实战:对比不同窗口效果

咱们用真实场景来对比。假设你拿着气压计从1楼走到10楼,每层3米,总共上升27米。采样率10Hz,加了随机噪声。

# 模拟爬楼场景
np.random.seed(123)
steps = 300  # 30秒数据
true_height = np.linspace(0, 27, steps)  # 匀速上升
noise = np.random.normal(0, 1.5, steps)  # 气压计噪声
raw_data = true_height + noise

# 三种窗口
w3 = moving_average(raw_data, 3)
w10 = moving_average(raw_data, 10)
w30 = moving_average(raw_data, 30)

# 计算延迟(以到达10米高度的时间差为例)
target = 10
true_time = np.where(true_height >= target)[0][0] / 10  # 真实到达时间
raw_time = np.where(raw_data >= target)[0][0] / 10
w3_time = np.where(w3 >= target)[0][0] / 10 if len(w3) > 0 else None
w10_time = np.where(w10 >= target)[0][0] / 10 if len(w10) > 0 else None
w30_time = np.where(w30 >= target)[0][0] / 10 if len(w30) > 0 else None

print(f"真实到达时间: {true_time:.2f}s")
print(f"原始数据: {raw_time:.2f}s (误差 {raw_time - true_time:.2f}s)")
print(f"窗口3: {w3_time:.2f}s (误差 {w3_time - true_time:.2f}s)")
print(f"窗口10: {w10_time:.2f}s (误差 {w10_time - true_time:.2f}s)")
print(f"窗口30: {w30_time:.2f}s (误差 {w30_time - true_time:.2f}s)")

运行结果大概是这样:

  • 原始数据:噪声大,但响应快,误差0.1s
  • 窗口3:噪声减少,误差0.2s
  • 窗口10:曲线平滑,误差0.5s
  • 窗口30:非常平滑,但误差1.2s

重要提醒:

移动平均滤波会缩短数据长度。窗口大小为N,输出数据会比输入少N-1个点。这在实时系统中要注意,尤其是做时间对齐的时候。

我个人建议:先用窗口10试试。大多数气压计高度应用,10-15的窗口是个不错的起点。如果觉得延迟太大,就减小窗口;如果觉得噪声还明显,就增大窗口。调参这事儿,没有标准答案,得看你的具体需求。

好了,移动平均滤波就聊到这儿。记住三点:窗口大小决定平滑程度和延迟;用循环队列实现省内存;先试窗口10再微调。下一章咱们聊加权移动平均,那个更灵活一些。


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