先说个实际场景。我以前做传感器数据采集的时候,被50Hz工频干扰搞得焦头烂额,时域波形上那个毛刺怎么滤都滤不干净,FIR滤波器阶数加高了几十倍,延迟大得像慢动作,结果还不好。后来换了个思路,先把信号做FFT,看频谱,然后再动手滤,问题一下就清楚了。这就是FFT滤波的核心价值:用频域的视野解决时域的问题。
这篇东西我从原理到工程实现都会聊,适合刚接触信号处理和MATLAB的初学者,也适合那些已经在用MATLAB处理数据、但总觉得频谱分析这块差点火候的工程师。重点不是罗列函数,而是把"为什么要这样写"和"踩过的坑"都讲明白。读完你至少能把FFT分析、频域滤波、信号还原这条链路完整跑通,遇到常见的频谱泄漏、边界效应这类问题也知道怎么处理。
1. 先搞明白:FFT和滤波是怎么组合到一起的
1.1 FFT滤波到底在干什么
很多人一上来就写代码,Y = fft(x); Y(100:200) = 0; y = ifft(Y);三行搞定,但中间发生了什么完全没概念。这样糊弄一次可以,换个信号就翻车。
FFT滤波的本质,是用FFT这个工具,把信号从"时间域"搬到"频率域",在频率域里对不同的频率成分做"取舍",然后再用逆变换IFFT把处理后的信号搬回时间域。打个比方,你在菜市场买了一整块五花肉(时域信号),FFT就是把这块肉按"肥肉、瘦肉、皮"分开摆好(频域分解),滤波就是"我要瘦的,肥的扔了",最后IFFT就是把剩下的瘦肉重新拼成一盘菜(时域信号)。
这么说有点粗糙,但对于理解核心思路足够了。关键是你要意识到:滤波的本质不是"把波形拉平",而是"把某几个频率的成分干掉或者保留"。这个思路一旦建立,你以后看任何滤波问题都会通透很多。
1.2 为什么选FFT而不是直接上滤波器
有人会问:MATLAB里现成的滤波器设计函数一抓一大把,fir1、butter、designfilt,为什么还要绕这么大一圈用FFT?
我的答案很直接:因为FFT让你先"看见"再动手,而传统滤波器设计是"盲选"。做传统滤波器,你得先假设信号和噪声的频率范围,然后挑一个滤波器类型,定阶数、定截止频率,整个过程像在黑暗中摸索。而用FFT,你直接把信号频谱画出来,干扰在哪、有用信号在哪,一目了然。拿前面的电磁干扰说,频谱图上50Hz那根谱线高高耸立,你一刀切下去,干净利落。
另外,FFT滤波对"非稳态特征"的处理很灵活。比如你要保留的是信号里的某个瞬时冲击,它在频域上铺得比较宽,你可以在频域画一个任意形状的掩膜(mask),不像传统滤波器只能做低通、高通、带通这些规矩形状。这在图像处理领域叫频域掩膜,在信号处理里一样好用。
当然FFT滤波也不是万能的,它适合离线批量处理,实时性要求高的场景还是得用因果滤波器。这个后面我详细聊。
2. 动手前的必修课:频谱里的三个大坑
2.1 采样率、频率分辨率和奈奎斯特频率
做FFT滤波,第一步不是写代码,而是搞清楚你的信号是怎么被采样的。三个参数必须刻在脑子里:
- 采样率fs:每秒钟采多少个点。采样率决定了你能看到的最高频率,也就是奈奎斯特频率,等于fs/2。
- 采样点数N:一共采了多少个点。FFT的分辨率就是fs/N,意思是频谱上相邻两根谱线之间的频率间隔。
- 频率分辨率df:即fs/N。如果你采了1秒的数据,分辨率就是1Hz;采了0.1秒,分辨率就是10Hz。
这三个参数对滤波效果的影响极大。举个例子,你有一个100.5Hz的信号,采样率1000Hz,采了1秒,FFT分辨率是1Hz,100.5Hz这个分量会落在100Hz和101Hz这两根谱线之间,能量被"摊开"了,这就是频谱泄漏。这时候你想精准滤掉它,一刀切不干净;想保留它,又会发现幅度对不上。
所以做FFT滤波之前,先问自己一个问题:我的数据长度够不够让目标频率和噪声频率在频谱上分开?如果不够,要么加长采集时间,要么用后面要讲的窗函数。
2.2 频谱泄漏和窗函数的选择
频谱泄漏是FFT滤波最常遇到的坑。原因是FFT默认信号是周期的无限延拓,但实际信号截断后首尾不连续,这个不连续在频谱上会"溅"出很多假频率成分。
解决办法就是加窗。窗函数的本质是让信号两端平滑衰减到0,减少截断带来的突变。MATLAB里常用的有:
hann:汉宁窗,主瓣宽一点,旁瓣衰减比较好,通用性最强。hamming:海明窗,和汉宁很像,旁瓣略低一点。blackman:布莱克曼窗,旁瓣衰减更大,但主瓣更宽,频率分辨率牺牲更多。rectwin:矩形窗,实际上就是不加窗,主瓣最窄,但旁瓣泄漏最严重。
我的选择经验很简单:如果你要滤除的信号和干扰频率离得近,用矩形窗或汉宁窗,保住频率分辨率;如果干扰和有用信号离得远,用布莱克曼窗,把泄漏压到最低。滤波前加窗,滤波后记得在IFFT之前或者之后做相应的处理,否则幅度会偏。
2.3 实信号的频谱对称性与单边谱
MATLAB里fft算出来的结果是复数,长度和输入一致。很多人第一次看到这个复数数组就懵了,特别是后半段的值还挺大,这是什么鬼?
要理解这个,得知道一个基本性质:对实信号做FFT,频谱是共轭对称的。前N/2+1个点对应0到fs/2的正频率,后N/2-1个点对应从-fs/2到0的负频率。也就是说,后半段的信息和前半段是"镜像"的,没有额外信息量。
所以做频域滤波的时候,一个常见的错误是只处理前一半频谱,然后把后一半也置零,结果IFFT回来信号变成了复数,而且幅度只有原来的一半。正确做法是:设计掩膜的时候,要把正负频率都考虑进去。要么用fftshift把零频挪到中间,对称地处理;要么直接在原来的频谱上,按照[0:fs/N:(fs/2-fs/N), -fs/2:fs/N:-fs/N]的频率轴来设计掩膜。
我一般是先freq = (0:N-1) * fs / N;生成频率轴,做掩膜的时候判断物理频率是否落在目标区间,这样正负频率自然就处理了,不容易出错。
3. MATLAB代码实战:从信号生成到滤波还原
3.1 构造一个带噪信号:先看清楚频谱
光说不练没用,咱们直接上手。我先构造一个仿真信号:一个50Hz的工频干扰叠加一个5Hz的有用信号,再混入一点随机噪声。
fs = 1000; % 采样率 1000 Hz N = 2000; % 采样点数 2 秒 t = (0:N-1) / fs; % 时间轴 % 有用信号:5Hz 正弦 x_signal = 1.5 * sin(2 * pi * 5 * t); % 工频干扰:50Hz 正弦,幅度2 x_noise_50 = 2.0 * sin(2 * pi * 50 * t); % 随机噪声 x_rand = 0.3 * randn(1, N); x = x_signal + x_noise_50 + x_rand;先画时域波形,你会看到信号被50Hz振荡"骑"在上面,5Hz的趋势被完全淹没了。这就是很多人在时域里做滤波做得痛苦的原因:有用信号和噪声的频率差很远,时域波形上全是高频抖动,你看着就头大。
现在做FFT看频谱:
X = fft(x); freq = (0:N-1) * fs / N; % 画幅度谱,取前一半,因为后半是镜像 figure; plot(freq(1:N/2), abs(X(1:N/2))); xlabel('频率 (Hz)'); ylabel('幅度'); title('原始信号频谱');跑完你会看到在5Hz和50Hz处各有一根明显的谱线,50Hz那根特别高,其余地方是一些低矮的噪声底。这一步之所以重要,是因为它直接告诉你滤波的目标是什么:把50Hz那根线削掉,保留5Hz那根。
3.2 频域掩膜设计:做一个"窄带陷波"
接下来设计一个陷波器,把50Hz附近的能量滤掉。这里有个关键点:不能只把50Hz那一根谱线置零,因为频谱泄漏可能让干扰能量扩散到49.5Hz和50.5Hz,甚至更宽。所以陷波器要有一个"宽度"。
% 设计频域掩膜:将 49Hz~51Hz 的成分置零 mask = ones(1, N); % 注意:要同时处理正负频率,这里用频率轴判断 for k = 1:N if abs(freq(k) - 50) < 1.5 % 49~51Hz 范围内的都置零 mask(k) = 0; end end % 应用掩膜 X_filtered = X .* mask;这段代码有几个问题需要注意。第一,循环在MATLAB里效率低,实际工程中建议用向量化写法,比如mask(abs(freq-50)<1.5) = 0;。第二,你可能会问:为什么mask只置零了前一半的50Hz,后一半呢?因为频率轴freq是0~999Hz,当k走到950Hz附近时,abs(freq(k)-50)是900多,不会被置零。但别忘了实信号频谱是共轭对称的,50Hz的"镜像"在1000-50=950Hz处。所以这个写法其实漏掉了负频率的镜像。这是新手最容易犯的错误。
正确做法是:要么先生成-fs/2到fs/2的频率轴,用fftshift处理频谱,再设计对称的掩膜;要么直接用我推荐的逻辑:掩膜只定义为"物理频率是否落在陷波区间",然后对整个频率轴做判断,这样正负频率都会覆盖到。上面这个循环写法其实做到了这一点,因为freq覆盖0~999Hz,950Hz对应的物理频率是950Hz,它不等于50Hz。哎,这里确实漏了。
我换个写法,把负频率也纳入:
% 方法一:先 fftshift,把频率轴变成 -500~500 X_shifted = fftshift(X); freq_shifted = (-N/2:N/2-1) * fs / N; mask = ones(size(X_shifted)); mask(abs(freq_shifted - 50) < 1.5) = 0; mask(abs(freq_shifted + 50) < 1.5) = 0; X_filtered_shifted = X_shifted .* mask; X_filtered = ifftshift(X_filtered_shifted);这个方法更直白,也不容易漏。把零频挪到数组中间,正负频率对称分布,然后一刀一刀切。
3.3 IFFT还原与幅度校正检查
滤波完成后,用IFFT还原时域信号:
y = real(ifft(X_filtered));这里用real()取实部是安全的,因为理论上来讲,只要我们对称地处理了频谱,IFFT结果就是实数。如果处理不对称,取real会丢掉一部分信息,造成幅度偏差。所以前面频域操作一定要对称,这比real本身重要得多。
然后画图对比:
figure; subplot(3,1,1); plot(t, x); title('原始信号'); subplot(3,1,2); plot(t, y); title('FFT滤波后'); subplot(3,1,3); plot(t, x_signal); title('理想有用信号');跑完你会发现,滤波后的线和理想有用信号基本重合,只是噪声底还有一些。幅度上要验证一下:滤波后的5Hz成分幅度是不是接近1.5?因为前面信号是1.5*sin(5Hz)。如果幅度不对,八成是频谱处理的时候把有用信号的幅度也削了,或者前面加窗没做幅度恢复。
还有一个很容易忽略的点:频域滤波的本质是线性时不变系统的一个特例,所以如果掩膜在边界处"突变"得很厉害(比如从1直接跳到0),时域上会产生振铃效应(Gibbs现象)。你会在滤波后的波形两端看到明显的高频抖动,这就是掩膜不光滑导致的。解决办法是让掩膜的边缘有个过渡带,比如用fdesign.arbmag设计任意幅度响应,或者手动生成一个缓变的边缘。
3.4 完整流程封装:一个可复用的FFT滤波函数
把上面这套逻辑封装成函数,以后调用就方便了。我平时是这么写的:
function y = fft_filter(x, fs, freq_range, mode) % FFT频域滤波 % x: 输入信号 % fs: 采样率 % freq_range: [f1, f2] 保留的频率范围(Hz) % mode: 'bandpass' 或 'bandstop' N = length(x); X = fft(x); freq = (0:N-1) * fs / N; mask = ones(1, N); if strcmp(mode, 'bandpass') % 保留 f1~f2,其余置零 mask(freq < freq_range(1) | freq > freq_range(2)) = 0; elseif strcmp(mode, 'bandstop') % 滤除 f1~f2 mask(freq >= freq_range(1) & freq <= freq_range(2)) = 0; end X_filtered = X .* mask; y = real(ifft(X_filtered)); end这个函数虽然能用,但有几个边界问题要自己注意:第一,正负频率对称性问题。上面这个写法只处理了0~fs/2的物理频率,负频率没处理。如果freq_range在正频率范围内,负频率镜像没被处理,IFFT会产生误差。所以我实际用的时候会在频率轴构造时直接覆盖到-fs/2,或者输入的时候就要求用户提供正负频率范围。
第二,掩膜突变造成的振铃。就是前面说的Gibbs现象,所以实际工程我更喜欢用过渡带设计,不搞这种硬切。下面我讲实战案例的时候会展示怎么处理。
4. 实战案例:把50Hz工频干扰从传感器信号里揪出来
4.1 问题场景与频谱初诊
说一个我以前实际碰到过的案例。现场采集一个振动传感器的信号,采样率是10kHz,采了10秒。信号有用成分集中在200Hz~500Hz,但40Hz到70Hz附近有一大坨干扰,明显是工频及其谐波。时域波形上,干扰使得特征频率完全看不清。
按老办法,我先不加滤波,直接对整段数据做FFT,频谱图上看得很清楚:50Hz处一根大谱线,旁边还有100Hz、150Hz的谐波。为什么会有谐波?因为工频干扰往往不是纯正弦,而是带畸变波形,畸变会产生整数倍的谐波分量。如果只滤掉50Hz,100Hz那个尖峰还会残留,所以要多做几个陷波。
这就是先用FFT"看清敌情"的价值:我知道敌人长什么样,才知道刀该怎么下。
4.2 设计带过渡带的陷波器:避免振铃
硬切的掩膜会造成振铃,这在振动信号里是很致命的,因为振铃的伪特征可能被误判为机械故障。所以我在实际工程里不会用mask(mask==0)这种硬切,而是用平滑的过渡带。
实现思路很简单:在掩膜边缘用cos窗生成一个斜坡,而不是直接跳变。比如要滤除49Hz~51Hz,掩膜值在49Hz~50Hz之间从1逐渐降到0,在50Hz~51Hz之间从0逐渐升回1。这个可以用下面的方式生成:
% 定义一个带过渡带的陷波掩膜 function mask = notch_mask(N, fs, fc, bw) % fc: 中心频率 % bw: 过渡带宽度的一半 freq = (0:N-1) * fs / N; mask = ones(1, N); f_low = fc - bw; f_high = fc + bw; % 在 f_low~fc 之间线性下降,fc~f_high 之间线性上升 idx_low = find(freq >= f_low & freq < fc); idx_high = find(freq >= fc & freq <= f_high); if ~isempty(idx_low) mask(idx_low) = 0.5 - 0.5 * cos(pi * (freq(idx_low) - f_low) / (fc - f_low)); end if ~isempty(idx_high) mask(idx_high) = 0.5 - 0.5 * cos(pi * (fc - freq(idx_high)) / (f_high - fc) + pi); end end这个掩膜本质上是一个余弦斜坡,边缘不再是1到0的跳变,而是平滑过渡。实际效果是:频谱上的主瓣和旁瓣都会被压下去,但同时不会产生剧烈的时域振铃。
4.3 边界效应:首尾不连续的麻烦
还有一个容易忽视的问题:FFT假设信号是周期延拓的。如果你滤波后的信号首尾不连续,IFFT还原时会在首尾产生很大的振荡。这个在信号长度较短时尤其明显。
我曾经采了一段1秒的信号,FFT滤波后开头和结尾各出现了一个大的脉冲,把前面的有用信号全盖住了。排查了半天,最后发现是边界不连续导致的。解决的办法有几条:
- 滤波前对信号做数据延拓:把首尾各延拓一段数据(比如镜像延拓),滤波后再裁掉。
- 对信号做重叠保留法(overlap-save)或重叠相加法(overlap-add),把长信号分块处理,避免边界效应累积。这两个方法在
fftfilt函数里就有实现。 - 如果信号比较长,边界效应只影响开头和结尾一小段,直接把这两段裁掉,不纳入后续分析。
我的习惯是先用简单的延拓解决,因为大部分场景下边界效应只占信号总长度的不到1%,裁掉就行。但如果信号总共就几百个点,那必须用重叠法。
5. 进阶对比:FFT滤波和滑动窗口滤波到底怎么选
5.1 滑动窗口滤波的适用场景
随着嵌入式系统越来越普及,"滑动窗口滤波"这个词的出现频率越来越高。它的思路是在时域上开一个窗口,窗口内做均值、中值或加权运算,然后窗口每次移动一个点,得到新的滤波输出。
滑动窗口滤波的优势是实时性:每个点来了都能立刻算出滤波后的值,不需要等一整段数据采完。所以单片机、FPGA这类资源受限的平台上,滑动窗口滤波(比如滑动均值、滑动中值、滑动FIR)是主流。
但它的代价也很明显:窗口长度和滤波效果是矛盾的。窗口越长,平滑效果越好,但延迟越大,对快速变化的信号响应越迟钝。而且滑动窗口滤波只能做低通性质的平滑,你要做带通、带阻,得设计很复杂的窗口系数。
5.2 什么时候用FFT滤波,什么时候用滑动窗口
我的选择原则是这样的:
- 离线分析、频谱特征明显、需要精细的频率选择:用FFT滤波。比如做振动诊断,我要滤掉某个特征频率,保留另一个特征频率,FFT滤波最直观。
- 实时控制、嵌入式平台、计算资源受限:用滑动窗口滤波。比如ADC采样后做简单的均值平滑,消除随机噪声。
- 数据量大、需要流水线处理:可以用滑动窗口FIR,但也可以把FFT滤波做成重叠保留的流式处理,两者性能和延迟可以做到接近。
这里多说一句,现在很多做预测性维护的同行喜欢把FFT和包络谱分析结合。包络谱的做法是:先对信号做带通滤波(比如用FFT滤波只保留高频故障特征频段),然后做Hilbert变换求包络,再对包络做FFT。这一步里FFT滤波的价值就非常大了:你想保留哪个频段就保留哪个频段,直接对频谱做掩膜,比设计一个高阶带通滤波器省事得多。
5.3 图像处理里的"FFT滤波"怎么理解
热搜词里有个"matlab图片处理",我顺带提一嘴。图像本质上是二维信号,FFT滤波在图像里的逻辑和一维信号一模一样:把图像做二维FFT,频谱中心是低频,边缘是高频。低通滤波就是保留中心、削掉边缘,高通滤波就是反过来,带通就是保留一个环形区域。
MATLAB里fft2、ifft2就是干这个的。我在做图像噪声消除的时候,会在频域里把特定方向的条状噪声(比如扫描条纹)用掩膜干掉,这在空间域(时域)里很难实现,但在频域里就是画一条线的事情。这就是FFT滤波对比空间域滤波的杀手级场景:空间域难以表达的"形状",频域里用一个掩膜就能做。
6. 常见问题与排查技巧实录
6.1 我的调试笔记:FFT滤波翻车现场
这里整理几个我实际踩过的坑,每一个都卡过我好几个小时,写出来大家少走弯路。
问题1:滤波后信号幅值整体减半
现象:IFFT还原后的信号波形形状正确,但幅度只有原来的一半,有时还带着相位翻转。
原因:负频率分量没处理好。实信号FFT结果正负频率是对称的,如果你只把正频率的掩膜置零或保留,负频率那边没做同样处理,IFFT还原时实部就只剩一半能量。
解决:要么用fftshift把频率轴挪到-500~500Hz再处理,要么把频率轴构造为(0:N-1)*fs/N之后再额外处理N/2+2到N的部分,保持对称。
问题2:滤波后开头和结尾出现剧烈振荡
现象:波形两端有大幅度的振铃,中间正常。
原因:边界不连续引起的Gibbs现象。FFT把信号当成周期延拓,滤波后首尾不匹配,IFFT就产生振铃。
解决:滤波前做数据延拓(前延后延各几十个点),滤波后裁掉;或者用overlap-add/overlap-save方法分段滤波。
问题3:有用信号的幅度变了,或者频率"歪了"
现象:5Hz的有用信号,滤波后变成了5.2Hz,或者幅度从1.5变成了0.8。
原因:频域掩膜把有用信号附近的能量也削掉了。比如陷波带宽设得太宽,把5Hz附近的频谱也波及了。或者信号长度太短,频率分辨率不够,5Hz和50Hz的谱线相互泄漏。
解决:先看频谱确认有用信号和干扰之间的频率间隔,再设置合理的带宽。加窗时选旁瓣衰减好的窗,比如Blackman,减少泄漏。
问题4:IFFT结果有虚部,取real之后波形明显不对
现象:real(ifft(X_filtered))出来和预期完全不一样。
原因:频域操作破坏了共轭对称性。常见于只对正频率做了操作,负频率没动,或者手工修改了单个频点的复数幅值,破坏了对称。
解决:检查所有频域操作是否对称。如果要修单个频点,永远同时修改X(k)和X(N-k+2)两个位置。
以下是我整理的速查表:
| 现象 | 可能原因 | 排查步骤 | 解决办法 |
|---|---|---|---|
| 幅值减半 | 负频率没处理 | 检查频谱是否共轭对称 | fftshift对称处理 |
| 首尾振铃 | 边界不连续 | 看滤波后两端波形 | 数据延拓/overlap |
| 幅度偏差 | 掩膜误伤 | 对比滤波前后频谱图 | 调带宽/过渡带 |
| 虚部极大 | 共轭对称破坏 | 检查频域修改点 | 成对修改频点 |
| 50Hz滤不干净 | 分辨率/泄漏 | 看频谱图是否有残留 | 加窗/加长数据 |
| 全部变成NaN | 中间步骤除零 | 检查是否有0长度的数组 | 调试断点逐行看 |
6.2 几个让调试效率翻倍的小习惯
最后分享几个我在调试FFT滤波时养成的习惯,虽小但很管用:
画图一定要叠加对比。滤波前和滤波后的频谱图用subplot放在一起,别单独开窗口。你对比着看,一眼就能发现滤波把不该滤的东西滤掉了。
先仿真再实测。拿到真实信号之前,先用已知频率的仿真信号把代码跑通。如果仿真都过不了,别指望实测能用。我一般先用sin构造一个包含已知频率的信号,跑通后换成真实数据,这样能把算法问题和数据问题分开。
用变量检查幅度恢复。滤波后我通常会找一个已知幅度的正弦成分,对比滤波前后的幅度变化。如果这个都算不准,后面定量分析直接白搭。
不要在一个巨大的脚本里写死参数。滤波器参数、采样率、数据长度都抽出来作为变量,调参方便,也能避免改一步全盘崩。
最后再分享一个小技巧
做FFT滤波到现在,我最大的体会是:滤波不是目的,看清信号才是。很多人急着把滤波器怼上去,却忘了先做频谱分析。我建议每一次滤波之前,都强制自己先画一张频谱图,哪怕你心里已经知道干扰频率是多少,也画一下。因为信号是会变的,环境噪声也是会变的,这张图能告诉你"当下这一刻"的实际情况。
另外,如果你的数据量特别大(比如几十M个点),一次fft没问题,但IFFT之后首尾裁切很浪费。这时候我推荐用fftfilt这个MATLAB内置函数,它自动做分块处理,省去边界效应的麻烦,就是少了一些灵活性。需要精细控制掩膜形状的场景再回来自写。
手里有频谱图,心里就不慌。这行做得越久,越觉得FFT滤波的门槛不在数学,而在你有没有养成"先看谱,再下刀"的直觉。希望这篇东西能帮你把这个直觉建立起来。