☰
MATLAB短时傅里叶变换实战:原理、参数选择与工程应用
2026/9/28 8:01:28 网站建设 项目流程

搞信号处理这一行,MATLAB里的短时傅里叶变换(STFT)是绕不开的工具。很多朋友一开始只学FFT,拿一段数据直接做变换,看幅度谱,觉得挺顺。可一旦碰到语音、振动、电机电流这类非平稳信号,频率随时间一直在变,整体FFT做出来的只是一锅平均过的“大杂烩”,看不到频率什么时候出现、什么时候消失。这时候就要把傅里叶变换从“全局视角”改成“局部扫描视角”,也就是短时傅里叶变换。这篇文章我把STFT的核心原理、MATLAB内置函数的使用方式、三个关键参数的取舍逻辑,以及我实际调试时踩过的坑,一次性讲清楚,代码也直接能跑。

1. 先想明白:STFT到底解决了什么问题

1.1 传统傅里叶变换的“平稳假设”陷阱

先从最朴素的场景说起。给定一段离散信号,你用FFT能得到它的频谱,比如100 Hz和300 Hz两个峰值,很漂亮。但问题是,这个频谱里没有时间坐标。你只知道信号里有这两个频率成分,却不知道它们在哪个时间段出现。对一段平稳信号来说无所谓,因为整段信号从头到尾频率结构都差不多;可对语音、刹车声、轴承故障振动这类信号,频率成分的出现时间和先后顺序恰恰是最关键的信息。

比如一段语音,前0.5秒是低沉的“呃”,后0.5秒变成尖锐的“啊”。如果直接把整段做FFT,低频和高频信息被混在同一张频谱里,你只能看到“这段声音有低频也有高频”,完全无法判断它们的时间顺序。更麻烦的是,如果这段信号里有一个转瞬即逝的冲击脉冲,整体FFT会把它的能量摊平到整个频率轴上,看起来像一层薄薄的“噪声底”,很难识别。

所以问题的本质是:标准傅里叶变换把信号当作无限长、平稳的序列来处理。但现实工程里的信号几乎都是非平稳的,频率结构随时间变化。于是我们需要一种带有时间定位能力的频谱分析手段。

1.2 用“窗口”把非平稳问题拆成局部平稳问题

STFT的思路很朴素:既然整段信号不稳定,那我就把它切成很多小段,每一段很短,短到可以认为在段内近似平稳。再把每一段分别做FFT,得到这一小段时间内的频谱。最后把各段的频谱按时间顺序排列起来,就得到一张“时间-频率-幅度”三位一体的二维图谱。

这个“切段”动作,在数学上相当于乘上一个随时间平移的窗函数。用公式表达就是:

[ X(t, f) = \int_{-\infty}^{+\infty} x(\tau) w(\tau - t) e^{-j2\pi f\tau} d\tau ]

其中 ( w(\tau - t) ) 是窗函数,它只在 ( t ) 附近一小段范围内有显著值,其余位置接近零。因此,计算某时刻 ( t ) 的频谱时,只有窗口覆盖范围内的信号参与计算。窗函数随 ( t ) 移动,就完成了对整个时间轴的扫描。

工程上的离散实现可以写成:

[ X(m, k) = \sum_{n=0}^{N-1} x[n + mH] w[n] e^{-j2\pi k n / N} ]

其中 ( N ) 是窗长,( H ) 是窗口移动的步长,也就是相邻两个窗之间的采样点偏移。最简单的实现方式是让窗口逐帧移动,比如每次移动 ( N/4 ) 个点,即重叠75%。

把它当作生活里的类比就是:传统FFT像给整条河拍一张全景照片,你只能看到河面上都有哪些漂浮物;STFT像拿一个探照灯沿着河岸扫,每扫过一段就拍一张局部照片,最后拼接起来,你不仅知道河里有漂浮物,还能知道它们在哪一段河面出现。

2. MATLAB里的三种实现路线

2.1 spectrogram、stft 和手写循环,怎么选

MATLAB里做STFT,最常见的三条路:直接用spectrogram函数,用较新的stft函数,或者自己写循环逐帧做FFT。三者底层逻辑相同,但使用场景和返回数据的组织方式有区别。

spectrogram是Signal Processing Toolbox里的老牌函数,返回时间、频率和谱值三个数组,同时自带绘图功能,一行代码就能出图。它的优势是方便观察,适合先看整体趋势,再做后续分析。如果你只需要图谱本身,不关心绘制细节,可以取输出的S矩阵,自己控制显示方式。

stft函数是MATLAB后来新增的接口,返回形式更规整,矩阵的行是频率,列是时间帧。它和spectrogram的计算结果本质一致,但参数命名更清晰,也更容易配合istft做逆变换。需要做信号重构、消噪、滤波等后续处理时,我习惯用stft。

手写循环的价值在于让你彻底理解内部原理。教学时特别推荐写一遍,因为只有自己处理过边界补零、重叠索引、窗函数归一化,才能真正明白spectrogram那些参数在背后做了什么。

2.2 手写一个最简STFT,加深算法理解

我建议所有人都先写一版最简单的循环实现,比如下面这样:

fs = 1000; t = (0:fs*0.5-1)/fs; x = chirp(t, 10, 0.5, 200, 'linear'); win = hamming(128); N = length(win); hop = 64; L = length(x); num_frames = floor((L - N) / hop) + 1; S = zeros(N/2, num_frames); center_times = zeros(1, num_frames); for m = 1:num_frames start_idx = (m-1)*hop + 1; seg = x(start_idx : start_idx + N - 1) .* win; spec = fft(seg, N); S(:, m) = abs(spec(1:N/2)); center_times(m) = (start_idx + N/2 - 1) / fs; end freqs = (0:N/2-1) * fs / N; imagesc(center_times, freqs, 20*log10(S + eps)); axis xy; xlabel('Time (s)'); ylabel('Frequency (Hz)');

这段代码里几个细节值得琢磨:hop=64表示每帧移动64个采样点,等于窗长的一半,重叠率50%。fft(seg, N)里的N表示做N点FFT,如果传入的信号段长度正好也是N,那这就是普通FFT。spec(1:N/2)取单边谱的前半段,因为实信号的FFT结果具有共轭对称性。

跑一下这段代码,你会看到chirp信号在时频图上呈现从10 Hz扫到200 Hz的斜线,非常直观。这套手写逻辑虽然简单,但已经把STFT的核心流程走了一遍:分帧、加窗、FFT、取幅度、拼矩阵。

2.3 用内置stft函数快速实现

理解原理之后,工程上还是推荐用内置函数。下面这段代码演示了stft的基本用法:

fs = 2000; t = (0:1/fs:1-1/fs)'; x = sin(2*pi*50*t) + sin(2*pi*400*t); win = hann(256, 'periodic'); [S, F, T] = stft(x, fs, ... 'Window', win, ... 'OverlapLength', 192, ... 'FFTLength', 512); figure; imagesc(T, F, 20*log10(abs(S)+eps)); axis xy; xlabel('Time (s)'); ylabel('Frequency (Hz)'); title('STFT结果'); colorbar;

这里窗口长256,重叠192,重叠率75%,FFT长度取512。FFTLength大于窗长时,相当于在窗口数据后面补零,再做512点FFT。补零不会提升真实频率分辨率,只会让频谱曲线看起来更平滑,这个后面详细解释。

stft返回的S是复矩阵,行数由FFTLength/2+1决定,列数由输入信号长度、窗长和重叠长度共同决定。如果你需要看功率谱,用abs(S).^2;如果要转成dB,用20*log10(abs(S))。注意幅度和功率的换算不要搞混,很多人在这里吃亏。

3. 三个关键参数背后的分辨率取舍

3.1 窗长N决定了频率分辨率和时间分辨率的此消彼长

STFT里最重要的参数是窗长N,它直接决定时间分辨率和频率分辨率。假设采样率是fs,窗长对应的物理时长是N/fs秒。窗越长,频域主瓣越窄,频率分辨率越好;但窗越长,窗口覆盖的时间范围也越大,时间定位就越模糊。

频率分辨率可以粗略按下式估算:

[ \Delta f \approx \frac{fs}{N} ]

比如fs=1000 Hz,窗长N=100,频率分辨率约为 10 Hz。也就是说,两个频率相差小于 10 Hz 的谱峰,在这个窗长下很难被区分开。如果把窗长增加到 1000,频率分辨率变为 1 Hz,但每个窗口的时间跨度也变成1秒,某时刻窗口内如果既有旧频率又有新频率,就会在时频图上糊成一片。

时间分辨率则取决于窗长本身。理想情况下,窗口中心时刻附近大约N/fs秒内的信号会共同决定该帧频谱。如果窗长为 100 个点、采样率 1000 Hz,那么一阵持续 0.05 秒的短暂声音就可能被“拉长”到 0.1 秒的模糊范围,你很难判断它的精确起止时间。

这就是STFT的物理宿命:时间分辨率和频率分辨率不可能同时无限好。要看清低频细节,就得用长窗,代价是时间轴变模糊;要捕捉瞬态冲击,就得用短窗,代价是频率轴变粗糙。实际使用时必须根据信号特征找平衡点,不存在万能参数。

3.2 重叠度不是“提高分辨率”,而是“提高显示连续性”

很多人误认为把重叠度设得越高,分辨率就越好。其实不是。重叠度影响的是时间轴上的采样密度,它决定了时频图上相邻两帧之间的时间间隔,而单帧的傅里叶计算本身并没有变化。

比如窗长256,重叠75%,意味着每64个点产生一帧。这意味着对于同样一段信号,得到的时频矩阵列数更多,图像看起来更平滑,追踪频率连续变化的信号时不会出现明显的“锯齿”。但频率分辨率仍然是fs/256,不会因为重叠多了就变好。

经验上推荐重叠75%。这个数值在连续性和计算量之间比较均衡。如果你做实时处理,算力有限,可以降到50%;如果是离线分析,信号频率变化又非常快,比如声音里的滑音,可以升到87.5%甚至更高,代价是计算量变大。

3.3 窗函数和FFT长度:提升细节之前的重复误区

窗函数的选择也很讲究。默认很多人用矩形窗,也就是什么窗都不加,直接截一段做FFT。矩形窗的优点是主瓣最窄,但旁瓣泄漏特别严重,频谱图上会出现一串虚假的“裙边”。更常用的Hann窗、Hamming窗、Blackman窗,都是通过牺牲一点主瓣宽度来压低旁瓣幅度。

Hann窗是STFT里最通用的默认选项,副瓣衰减快,主瓣比矩形窗口宽不到两倍,换来的是很好的频谱纯净度。Blackman窗旁瓣更低,但主瓣更宽,适合需要强力抑制泄漏的场合。我个人的经验是:没有特别理由就用Hann窗,如果只是看峰值位置,Hamming也差不多,效果差别不大。

FFTLength是另一个容易让人困惑的点。它不等于窗长。FFTLength大于窗长时,MATLAB自动在窗口数据后补零。补零的数学本质是在原来的频谱采样点之间做插值,让频谱曲线看起来更精细,但它不会让两个本来重叠的峰分开。真正提高频率分辨率,只有增加窗长,或者降低采样率。一句话:补零美化,加窗长才治病。

4. 完整实测案例:信号混叠了正弦、扫频和瞬态冲击

4.1 构造一段用于验证的多成分信号

只讲理论没意思,我构造一段更能说明问题的合成信号:一段2秒、采样率1600 Hz的信号,里面包含一个固定的100 Hz正弦、一个300 Hz的正弦、从150 Hz线性扫到700 Hz的chirp,以及一个发生在1.2秒附近、时宽极短的冲击脉冲。

fs = 1600; t = (0:1/fs:2-1/fs)'; x = 0.6*sin(2*pi*100*t) + 0.4*sin(2*pi*300*t); x = x + 0.5*chirp(t, 150, 2, 700, 'linear'); sigma_t = 0.002; impulse = exp(-((t - 1.2).^2) / (2*sigma_t^2)); x = x + 0.8*impulse; % 检查整体频谱 figure; pwelch(x, [], [], [], fs);

这段信号的特点在于:固定频率成分考验STFT的频率区分能力,chirp考验它对频率连续变化的追踪能力,冲击脉冲则考验它对瞬态事件的定位能力。三种成分同时存在,参数选不好就会顾此失彼。

4.2 STFT正反实验:短窗与长窗的对比

先用比较短的窗去分析:

figure; spectrogram(x, hann(64), round(0.75*64), 128, fs, 'yaxis'); title('短窗:时间定位好,频率分辨率差');

窗长只有64,频率分辨率约为1600/64=25 Hz。这个分辨率下,100 Hz和300 Hz这两个分量能显示出来,但它们的谱线宽度明显较大,而且在时频图上会显得“胖”一些。好处是1.2秒的冲击脉冲非常明亮,时间位置定位准确。

再把窗长换成256:

figure; spectrogram(x, hann(256), round(0.75*256), 512, fs, 'yaxis'); title('长窗:频率分辨率好,时间定位相对模糊');

这时频率分辨率约为6.25 Hz,100 Hz和300 Hz的谱线更细更锐利,chirp的斜线也干净得多。但冲击脉冲在时间轴上的宽度会被拉宽,看起来像一根“拖尾”的竖杠。如果再加大窗长到1024,冲击甚至会被摊到0.6秒的范围,在时频图上几乎看不出来是一个瞬态。

把上面两段代码跑一遍,你会对“此消彼长”有非常直观的感受。很多教材只说理论,不给这种对照实验,读者很容易一头雾水。

4.3 用stft提取时频矩阵做定量分析

看图形只是第一步,实际工程里往往要从STFT结果里提取数值特征。比如你关心chirp从300 Hz扫到400 Hz的时刻,可以把chirp所在的频带内的能量峰值点提取出来,拟合一条频率随时间变化的曲线。用stft返回矩阵来做就非常方便:

win = hann(128, 'periodic'); [S, F, T] = stft(x, fs, 'Window', win, ... 'OverlapLength', 96, 'FFTLength', 256, 'FrequencyRange', 'onesided'); S_mag = abs(S); freq_index = find(F > 250 & F < 750); time_energy = sum(S_mag(freq_index, :), 1); [~, peak_frame] = max(time_energy); disp(['峰值时刻约为: ', num2str(T(peak_frame)), ' 秒']);

这里我筛选了250到750 Hz频带,这个频带涵盖了chirp的大部分扫频范围。对每一帧求和得到频带总能量,再找最大值所在帧,就可以大致定位chirp能量最集中的时刻。

如果是做故障诊断,常需要某个频带内能量随时间的变化曲线。用sum(S_mag(freq_range, :), 1)就能轻松得到,省去反复切片滤波的麻烦。STFT矩阵化输出之后,分析手段可以变得非常灵活。

5. 参数选择与工程落地经验

5.1 不同应用场景的参数速查表

参数没有绝对最优,但工程上有一些常用的起步值。我把这些年做项目时习惯用的参数整理成一个速查表,方便你上手时直接参考:

应用场景采样率范围窗长建议重叠度FFT长度备注
语音分析8k~16k Hz256~51250%~75%与窗长相同或2倍兼顾清浊音和基频
电机轴承振动10k~50k Hz1024~409675%2倍窗长关注边带调制和高频冲击
电力谐波分析6.4k~51.2k Hz128~51250%与窗长相同需要精确定位谐波频率
声呐/雷达瞬时信号信号带宽而定64~25675%~87.5%2倍窗长尽量短窗,捕捉瞬变
生物医学信号100~1000 Hz128~51275%2倍窗长不同节律频率差异大

这个表不是死规矩,只是起步点。具体还要根据你关心的频率范围调整。比如只关心50 Hz工频附近的谐波,窗长就不能太短,否则相邻的谐波峰会叠在一起。

5.2 MATLAB批量处理信号时的效率优化

对长信号做STFT时,最常见的问题就是慢。如果不加思考地直接在循环里调用spectrogram,每次都生成图形和坐标轴,速度会慢得让人抓狂。比较好的做法是只调用一次spectrogram,把输出矩阵存下来,再做后续处理。

[s, f_spec, t_spec] = spectrogram(x, hann(256), 192, 512, fs); s_db = 20*log10(abs(s) + eps); imagesc(t_spec, f_spec, s_db); axis xy;

这样spectrogram只计算一次,后续所有画图和分析都用s矩阵,避免重复计算。

另一个容易忽视的问题是数据类型的坑。MATLAB默认矩阵都是double,如果你有一段很长的信号,比如几百万个采样点,生成的s矩阵会占掉大量内存。如果只是看谱图,可以转成single,内存占用减半。如果做实时或者实时近似的处理,还可以考虑分块处理:每次读入一段新数据,维护一个环形缓存,只对最新窗口内的数据做FFT,避免全部数据堆在内存里。

5.3 边界处的“半窗”问题

STFT在信号开头和结尾会出现边界效应。信号开头不够一个窗长时,要么补零,要么直接丢弃。MATLAB内置函数默认情况下会尽量保留能算的帧,但头尾几帧的信号不完整,谱可能会出现畸变。

做定量分析时,我建议只关注时间轴上离边界有一定距离的区域,或者提前把信号两端多采集一段数据,分析完再把边界部分裁掉。线上采集时尤其要注意,启动阶段的前几十毫秒数据可能不完整,别把边界伪影当成了真实特征。

6. 常见问题与实战排查

6.1 时频图里频率变成了“斜线”是怎么回事

很多人第一次看到STFT图里的斜线都会有点慌,其实这是正常的。频率随时间变化的信号,比如chirp、扫频信号,在STFT图上天然就是一条斜线。斜率的大小对应频率变化率。如果你看到的是正弦信号却变成了斜线,那就说明信号本身不平稳,或者分析对象里有频率调制成分。

还有一种情况,固定频率分量的曲线在图像上看起来左右漂移,是因为加窗后频谱主瓣有一定宽度,峰值估计在谱线上跳动。解决办法是提高频率分辨率(加长窗)或者对峰值做插值。

6.2 谱图上有“毛刺”或不连续

如果STFT图上一会儿亮一会儿暗,像掉帧一样,首先检查重叠度。重叠太低时,帧与帧之间衔接生硬,信号变化稍快就会出现明显的帧间跳变。把重叠度提高到75%以上通常能解决。

其次是窗函数类型问题。矩形窗旁瓣泄漏大,会让亮色区域向外“渗”,看像一团毛刺。换Hann窗后通常会干净很多。如果毛刺仍然存在,建议检查原始信号里是否混入了随机噪声或脉冲干扰,可以先做时域滤波再送STFT。

6.3 MATLAB画图时频率轴和显示范围对不上

用imagesc画STFT时,坐标轴范围来自f和t数组。很多人会把f和t搞反,画出图来频率轴变成时间轴,或者顶部和底部颠倒。记住imagesc的调用语法是imagesc(t, f, S),然后一定要加axis xy,否则y轴默认从大到小显示,频率轴是反的。

如果用了spectrogram(..., 'yaxis')直接出图,默认就是正确的显示方向,无需再多处理。但注意这个选项只影响画图,不影响返回矩阵。

6.4 STFT的逆变换与信号重构

有些朋友做完了STFT,还想从时频矩阵恢复出原始信号,用于滤除某个频带后重构。MATLAB里提供了istft函数,和stft正好配套:

win = hann(256, 'periodic'); [S, F, T] = stft(x, fs, 'Window', win, ... 'OverlapLength', 192, 'FFTLength', 256); % 修改S,比如把低频部分置零 S_filtered = S; S_filtered(F < 50, :) = 0; y = istft(S_filtered, fs, 'Window', win, ... 'OverlapLength', 192, 'FFTLength', 256);

这里要注意,istft的窗函数、重叠长度、FFT长度必须和stft完全一致,否则重构时会出现明显的幅度调制噪声。另外,如果你在修改S矩阵时破坏了共轭对称性,逆变换结果会产生相位噪声。处理单边谱时要特别小心。

如果对重构精度要求很高,不想用istft的默认方式,可以考虑LSEE-STFT框架:先用窗函数做加权,再通过重叠相加法(OLA)把各帧拼接起来。MATLAB的istft内部已经实现了类似逻辑,但默认情况下边缘若干点会有少量误差,工程上通常会让分析的信号段足够长,边缘误差被整体误差淹没。

我在实际项目里,最常用的组合就是stft加spectrogram配合:先用spectrogram快速出图看全局特征,再用stft拿矩阵做定量分析,最后如果需要做滤波重构,就用istft。三个函数各管一段,协作顺畅,很少出问题。

如果你刚开始接触STFT,建议别急着套工具箱,先把手写循环那个版本跑通,亲手改一改窗长、重叠度,看看时频图怎么变化。等理解了背后的计算流程,再用内置函数优化效率,踩坑的机会就会少很多。傅里叶变换、窗函数、频率分辨率这套体系,只有亲手调过参数,才能真正变成自己的判断力。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询