探秘!有限冲激响应(FIR)数字滤波器竟如此通俗易懂!
1. 前言
数字滤波器凭借高精度、稳定性、灵活编程等特点,在通信、音频、图像等领域得到了广泛应用。
今天,我们将探索一下数字滤波器的理论基础,运用 Python(SciPy 库)构建一个常见类型的滤波器 —— FIR 滤波器。希望借此让大家了解数字滤波器这一强大工具,开启信号处理的全新旅程。
2. 数字滤波器简介
滤波器的核心作用是对信号的频率成分进行筛选和阻挡。接下来,我们将从滤波器的频率响应、时域(冲激响应)、运算(卷积)以及硬件实现等方面展开介绍。
2.1 频率响应
滤波器对不同频率的响应可以划分为通带、过渡带和阻带,典型情况如下:
一些重点关注的指标有:
- 通带(Passband):在此区域内,信号的频率成分基本无变化通过。
- 阻带(Stopband):在此区域内,信号频率成分会遭受大幅度衰减。
- 过渡带(Transition band):位于通带与阻带之间,信号的频率成分会有一定程度的衰减,但不会被大幅剔除。
- 纹波(Ripple):描述滤波器在频带内增益的波动情况。纹波越小,说明滤波器的响应越稳定,也就越符合理想要求。
- 相位(Phase):输入信号经过滤波器后会产生相位变化。在线性相位滤波器中,所有频率的信号都会经历同样的相位变化。
一般而言,纹波与过渡带之间存在着权衡关系。即纹波越小,过渡带就越大;反之,纹波越大,过渡带则越小。因此,在设计滤波器时需要折中考虑。
2.2 冲激/阶跃响应
滤波器在时域上也有表征,称为 “冲激响应”(Impulse Response)。之所以这样命名,是因为当我们将一个脉冲信号输入到滤波器时,观察到的时域输出信号就是 “冲激响应” 的样子。
此外,我们还经常会看到 “阶跃响应”(Step Response),它是指当输入为阶跃信号时,时域输出信号的形态。阶跃响应是冲激响应的累加之和,反映了滤波器从初始到稳定的变化,有点类似于设备的开关机过程。
下图展示的是图 1 所示滤波器的冲激响应与阶跃响应:
冲激响应分为有限和无限两种,具有有限冲激响应的是 FIR (Finite Impulse Response) 滤波器,具有无限冲激响应的是 IIR (Infinite Impulse Response) 滤波器。
本文重点关注 FIR 滤波器,其冲激响应中的每个脉冲称为 “抽头”(Tap)。例如,上图所示的是一个约 100 抽头的 FIR 滤波器。一旦确定了抽头系数,DSP 程序就可以做相应的运算了(见下文)。
2.3 卷积运算
之所以提及 “冲激响应”,是因为对信号进行滤波时,只需将冲激响应与信号进行 “卷积”(Convolution)即可。当然,也可以通过频域 “乘法” 实现滤波,从结果看,两者是等价的。只不过在实际应用中,需要从成本、效率等方面考量,选择更优的方式。
“卷积” 这个词听起来有些抽象,实际计算过程上并不复杂。它是将一个信号在另一个信号上进行滑动,然后将对应位置的元素相乘并累加,最终得到一个新的信号,在数学上通常用 “*” 符号表示:
由于卷积具有滑动特性,其输出信号长度比输入信号更长。举例来说,如果冲激响应长度 K 点,输入信号长度 N 点,那么二者卷积后会产生 N + K - 1 点。不过在具体实现时,我们能够指定输出信号的点数。对于刚开始接触卷积的概念而言,我觉得无需纠结这些细节,只需明白卷积输出的长度并非等同于输入信号的长度即可。
2.4 硬件实现
从硬件角度,FIR 滤波器通过一系列延迟器、乘法器和加法器实现,其结构如下:
其中,乘法器所采用的系数就是滤波器冲激响应的 “抽头”(Tap),并且乘法器的数量与抽头的数量相等。只要抽头数量足够多,它几乎可以实现任意类型的频率响应,这也正是数字滤波器的特点所在。
3. Python 实现 FIR 滤波器
3.1 FIR 滤波器设计
FIR 滤波器设计的核心任务是确定其 “抽头” 系数和长度。如今,有很多编程及仿真工具可帮助我们设计这些参数,如 MATLAB、 Python(SciPy 库)等。无论选用哪种工具,都有大致以下流程:
- 确定期望保留或滤除的频率范围;
- 确定滤波器类型,例如 FIR 滤波器或者 IIR 滤波器,算力消耗(如长度);
- 计算生成滤波器的抽头系数与长度;
- 查看频率响应图,关注通带、阻带、过渡带、纹波以及相位等指标;
- 生成一个带有噪声的信号,检验滤波器能否有效滤除目标频率成分。
本文将以 Python(SciPy 库) 为例,进行说明与演示 。
3.2 scipy.signal 及函数
Python 用于滤波器设计和运算的代码位于 scipy.signal 模块,其下有很多函数,这里我们挑选几个具有代表性的函数进行介绍 [4]:
|
|
滤波器设计
主要有两个函数,第 1 个是 scipy.signal.firwin
,它最为直接,能为 FIR 滤波器生成抽头系数,使用时只需指定抽头数量和截止频率即可。第 2 个是 scipy.signal.firwin2
,它更为灵活,适用于具有自定义频响曲线的滤波器,需要提供一个频率列表以及每个频率处期望的增益值。它们返回的结果均为一组抽头系数。
滤波器验证
使用 scipy.signal.freqz
计算滤波器的频率响应,接收的输入就是上述 FIR 滤波器的抽头系数,返回两个数组,一个是频率点,另一个是对应频率点上的复频率响应。如果对于复频率不了解,可以翻看一下前文的 FFT 介绍。利用这些结果,我们就可以绘制滤波器的频率响应。
滤波器运算
scipy.signal
中的这三个函数各有特点。scipy.signal.lfilter
接收 FIR 滤波器的抽头系数,用于沿一维对数据进行滤波;scipy.signal.convolve
对两个 N 维的时域数组进行卷积操作;scipy.signal.fftconvolve
同样是对两个 N 维数组进行卷积,但它借助 FFT 实现,即 频域相乘,这样在处理大规模数据时运算速度更快。
有人做过运算速度的对比,对于输入是 10 万个样本的情况,滤波速度如下图所示[5]:
可以看到 scipy.signal.lfilter
很慢,np.convolve
(来自于 NumPy)也很慢,scipy.signal.convolve
根据数据数量会自动切换为 scipy.signal.fftconvolve
,所以两者一样,但比 scipy.signal.lfilter
快很多,这就是 FFT 的优势!
3.3 FIR 滤波器案例
本案例主要基于 scipy.signal.firwin()
、 scipy.signal.freqz
、 scipy.signal.lfilter
等函数,更多高级用法请查阅 scipy.signal 模块。
代码中,首先构建了一个带有噪声的余弦输入信号,然后用 scipy.signal.firwin()
设计一个抽头长度为 74 、截止频率为 10 Hz 的 FIR 滤波器,再用 scipy.signal.freqz
绘制其频谱图,最后用 scipy.signal.lfilter
去噪,还原真实的余弦信号。
代码片段如下,运行于 Spyder 6 开发环境:
FIR 滤波器抽头系数,长度为 74 个:
FIR 滤波器频率响应:
输入信号 与 滤波后信号对比,滤波后信号有延迟,且有一段不确定的初始值:
代码托管地址见 4.4,可以下载运行。
4. 参考资料
4.1 引用出处
- https://pyageng.mpastell.com/book/dsp.html
- https://brianmcfee.net/dstbook-site/content/ch10-convtheorem/ConvolutionTheorem.html
- https://www.embedded.com/introduction-to-digital-filters/
- https://docs.scipy.org/doc/scipy/reference/signal.html
- https://pysdr.org/content/filters.html
4.2 滤波器扩展阅读
- https://www.dspguide.com/ch6.htm
- https://brianmcfee.net/dstbook-site/content/ch10-convtheorem/intro.html
- https://pysdr.org/content/filters.html
4.3 相关前文
- Python 带你深入浅出 DFT / FFT,就是这么简单!
- 用 Python 开启数字信号处理之旅 —— 生成 Sine 波形
4.4 代码托管
欢迎关注我的微信公众号“疯狂的运放”
,及时收到最新的推文。