在调试信号处理流程时,很多人遇到过这样的场景:传感器数据采集回来,想通过滤波把波形处理得干净一些,再送给后续算法使用。第一版低通滤波器跑完,波形确实平滑了,可你真正关心的“尖峰”“突变沿”也一起消失了;第二版把截止频率抬高,噪声又卷土重来。翻资料、改参数、重新出图,半小时过去,盯着屏幕你只想问一句话:滤波器,我的刀呢?
这里的“刀”,并不是真正的手术刀或菜刀,而是滤波器的选型思路、截止频率和滤波器参数。滤波本身是一门关于“取舍”的技术:把不需要的频率分量切掉,把需要保留的成分完整留下来。如果只会照抄一段代码,不知道自己在频域上做了什么,就容易把信号磨圆、切钝,甚至把最有价值的特征一刀切没。
这篇文章会把数字滤波器从概念、频谱分析、SciPy 实现,到参数调整和实时嵌入式落地过程中最容易踩坑的地方完整过一遍。适合刚入门信号处理、正在做传感器数据分析,或者需要在 Python / C 中实现滤波但对参数选择没有把握的开发者。
1. 为什么我会喊出“我的刀呢”
1.1 一个典型的滤波故障现场
先看一个抽象但很常见的处理流程。
原始信号大概长这样:
- 包含一个缓慢变化的有用趋势;
- 叠加了一个 50Hz 的电源工频干扰;
- 还有传感器自身的高频随机噪声;
- 波形里可能存在一个短促的冲击或上升沿,这是后续算法判断“是否发生异常事件”的关键特征。
处理人员第一反应是:“噪声太多,做个低通滤波吧。”于是在网上找到一段类似代码,把截止频率设成 5Hz。结果波形干净得可怕,连那个冲击特征的“刀锋”也被磨成小土坡。后续算法基于这个波形开始漏报,项目结论变成了“这个特征不明显”。
问题出在哪?不是低通滤波这个操作错了,而是“用错了刀”。
1.2 “刀”到底指什么
在滤波器语境里,“找到合适的刀”,至少要回答下面几个问题:
- 有效信号的频段是多少?
- 噪声的频段是多少?
- 两者之间有没有明显的间隔?
- 我需要的特征是“平滑趋势”,还是“陡峭边沿”,还是“某一个窄带振动”?
不同答案对应不同滤波器:低通、高通、带通、带阻,甚至可能是组合滤波。
如果一开始不分析频谱,只凭感觉设置截止频率,就像不看病灶位置就在身上乱切一样,很容易切掉好的部分。很多时候,不是滤波器不好用,而是我们没有找到合适的“下刀点”。
2. 数字滤波器基本概念与使用场景
2.1 数字滤波器在做什么
模拟滤波器处理连续电压信号,数字滤波器则处理离散的采样序列。每次滤波,本质上是对一个时间序列做卷积或递归运算,让某些频率分量通过,让另一些频率分量衰减。
在频域里,一段信号可以拆成不同频率的正弦波叠加。比如一个 50Hz 的工频正弦干扰,如果把它从时域信号中移除,而尽量不影响 5Hz、20Hz 这样的有效分量,这就是滤波器的典型目标。
2.2 四种幅频特性
| 类型 | 通过频率 | 衰减频率 | 常见用途 |
|---|---|---|---|
| 低通 Low-pass | 低于截止频率 | 高于截止频率 | 去掉高频毛刺、平滑波形 |
| 高通 High-pass | 高于截止频率 | 低于截止频率 | 去除基线漂移、直流偏置 |
| 带通 Band-pass | 某个区间内 | 区间外 | 提取特定频段的振动特征 |
| 带阻 Band-stop | 区间外 | 某个区间内 | 滤除单一窄带干扰,如 50Hz 工频 |
实际项目中,低通最常用,但它不是万能的。如果一个干扰信号与有效信号在频带上存在重叠,单纯靠低通很难同时做到“有效信号不衰减、干扰信号被压掉”。这时候就需要考虑陷波滤波,也就是窄带带阻滤波器。
2.3 IIR 与 FIR 怎么选
数字滤波器从结构上分为两大类:无限脉冲响应滤波器(IIR)和有限脉冲响应滤波器(FIR)。
| 对比维度 | IIR | FIR |
|---|---|---|
| 相位线性 | 非线性,频率越高延迟越明显 | 可以做到严格线性相位 |
| 阶数与计算量 | 实现同等滤波效果时阶数更低 | 通常需要较高阶数 |
| 数值稳定性 | 存在反馈,设计不当可能不稳定 | 没有反馈,基本稳定 |
| 典型例子 | Butterworth、Chebyshev、陷波器 | 窗函数设计、等纹波滤波器 |
| 适合场景 | 资源敏感的嵌入式、实时滤波 | 对波形形态、相位延迟敏感的场景 |
工程里没有绝对好坏。IIR 计算效率高,适合实时和嵌入式;FIR 相位特性好,适合需要保留波形形态的离线数据分析。如果既想减少相位失真,又希望计算简单,也可以在离线环境使用filtfilt做零相位滤波,或者先用 IIR 再把延迟补偿掉。
3. 环境准备与测试信号
3.1 Python 环境约定
本文代码采用 Python + SciPy 实现,因为 SciPy 的signal模块封装了滤波器设计、滤波执行和频谱分析函数,非常适合快速验证算法。
建议环境如下:
- Python 3.9 及以上;
- NumPy 1.21 及以上;
- SciPy 1.8 及以上;
- Matplotlib 3.5 及以上。
版本不需要完全一致,只要不是特别老的版本,下面接口基本都能使用。如果你用的是公司内网环境,也可以把依赖下载后离线安装。
python -m venv venv source venv/bin/activate # Windows 下使用 # venv\Scripts\activate pip install numpy scipy matplotlib3.2 合成一段测试信号
在动手设计滤波器之前,先合成一段测试信号,避免用真实数据来回猜测。
import numpy as np from scipy import signal import matplotlib.pyplot as plt fs = 1000.0 # 采样率 1000Hz duration = 2.0 # 时长 2 秒 n = int(fs * duration) t = np.arange(n) / fs # 2Hz 方波,用于模拟带边沿的开关量或脉冲特征 base_wave = signal.square(2 * np.pi * 2 * t, duty=0.5) # 高频随机噪声,模拟传感器毛刺 rng = np.random.default_rng(42) noise = rng.normal(0, 0.3, size=t.shape) # 原始测试序列 raw = base_wave + noise这里故意把“有效信号”设计成方波,是因为方波包含丰富的奇次谐波。方波的边沿看起来锐利,其实需要较高频率的谐波来共同塑造。低通滤波器保留的谐波次数越少,方波边沿越模糊。这个特征可以非常直观地解释“为什么滤波后波形钝了”。
建议先用 Matplotlib 画出原始波形确认一下:
plt.figure(figsize=(10, 4)) plt.plot(t, raw, lw=0.8) plt.xlabel("Time (s)") plt.ylabel("Amplitude") plt.title("Raw Square Wave with Noise") plt.grid(True) plt.show()4. 先看频谱:不要凭感觉选截止频率
4.1 FFT 能告诉我们什么
滤波前先看频谱,是信号处理里非常基础且重要的习惯。快速傅里叶变换可以把时域信号转换到频域,让我们看清信号能量分布在哪些频率上。
def plot_spectrum(x, fs, title="Spectrum"): n = len(x) freq = np.fft.rfftfreq(n, d=1 / fs) mag = np.abs(np.fft.rfft(x)) plt.figure(figsize=(10, 4)) plt.plot(freq, mag, lw=0.8) plt.xlabel("Frequency (Hz)") plt.ylabel("Magnitude") plt.title(title) plt.grid(True) plt.xlim(0, fs / 2) plt.show() plot_spectrum(raw, fs, "Raw Signal Spectrum")如果运行这段代码,会看到方波频谱并不是单一谱线,而是在 2Hz、6Hz、10Hz、14Hz、18Hz 等奇次谐波位置有能量。频率越高,谐波幅值越低。随机噪声则像一层“地板”一样铺在整个频带上。
接下来的滤波器设计,就是根据这张频谱图判断:哪些频率成分可以留,哪些频率成分要切掉。
4.2 频谱图的观察要点
- 有用信号的频谱主要落在哪个区间: 如果只关心 2Hz 方波的基本形态,保留 6~10Hz 以内的谐波就够用;如果关心边沿是否陡峭,往往需要保留到几十甚至上百 Hz。
- 噪声频谱的主要集中位置: 如果是 50Hz 工频干扰,会在 50Hz 处出现很明显的谱峰;如果是白噪声,则均匀分布。
- 信号与噪声是否重叠: 重叠越严重,滤波难度越大。不存在完美一刀切,只能权衡。
先用频谱建立全局认知,再设置滤波器参数,比对着时域波形猜要高效得多。
5. 核心案例一:低通滤波为什么把边沿“磨圆”了
5.1 方波中的谐波与边沿
回到刚才的 2Hz 方波。方波的傅里叶展开可以近似看成:
基波 2Hz + 三次谐波 6Hz + 五次谐波 10Hz + 七次谐波 14Hz……
理论上,如果只保留基波,得到的波形就是一个 2Hz 正弦波,完全看不出方波形态。保留的谐波越多,波形的上升沿和下降沿越接近原始方波。
“刀锋般的边沿”之所以会消失,就是因为低通滤波器把高阶谐波全部削掉了。
5.2 不同截止频率对比
下面分别用 15Hz 和 80Hz 截止频率做低通滤波。
# 15Hz 低通 sos_15 = signal.butter(4, 15 / (fs / 2), btype="low", output="sos") filtered_15 = signal.sosfiltfilt(sos_15, raw) # 80Hz 低通 sos_80 = signal.butter(4, 80 / (fs / 2), btype="low", output="sos") filtered_80 = signal.sosfiltfilt(sos_80, raw) plt.figure(figsize=(12, 8)) plt.subplot(3, 1, 1) plt.plot(t, raw, lw=0.8) plt.title("Raw") plt.grid(True) plt.subplot(3, 1, 2) plt.plot(t, filtered_15, lw=0.8, color="tab:orange") plt.title("Low-pass 15Hz") plt.grid(True) plt.subplot(3, 1, 3) plt.plot(t, filtered_80, lw=0.8, color="tab:green") plt.title("Low-pass 80Hz") plt.grid(True) plt.tight_layout() plt.show()从输出结果可以清楚看到:
- 15Hz 低通后,波形非常光滑,高频噪声几乎消失,但方波变成了圆头波浪。如果真实业务中需要判断“开关切换”的时刻,这种波形很难给出精确边界。
- 80Hz 低通后,方波边沿明显更陡,基本保持了原始方波的形态,但噪声并没有完全滤除,因为高频噪声也被保留了一部分。
15Hz 和 80Hz 之间没有绝对的对错,只有适不适合业务目标。如果你要的是平滑趋势,15Hz 合适;如果你要找边沿、尖峰或者异常突变,那么 80Hz 甚至更高频带才更像一把趁手的“刀”。
5.3 为什么不能只看时域波形去调参
很多开发者习惯不去看频谱,只盯着时域波形调截止频率。看到一个毛刺就去压低截止频率,看到波形太平就把截止频率调高,反复拉扯。这种方式很容易陷入局部最优。
正确流程是:先用 FFT 看看有效信号的能量集中在哪个频段,再确定截止频率。比如想保留一个 100Hz 以内的振铃特征,普通低通截止频率就不能设在 20Hz 以下,否则该特征会被误伤。
如果目标只是去掉工频干扰,而有效信号在 100Hz 以下,那么更合理的是先做一个 50Hz 陷波器,再根据噪声情况决定后续低通截止频率。下面就来演示这种情况。