ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

振动信号分析中STFT时频分析实战:从FFT局限到工程落地

振动信号分析中STFT时频分析实战:从FFT局限到工程落地

振动信号分析这个领域,很多人是从FFT入门的。把一段加速度传感器采集的数据丢进FFT,看到频谱上几根尖峰,觉得分析也就这么回事。但真正做过设备故障诊断或者语音信号处理的人都知道,FFT有个致命的局限:它把整个时间轴上的信息压缩成一张静态的频谱图,时间维度被彻底抹掉了。你只知道信号里有哪些频率成分,却不知道这些成分是什么时候出现的。对于旋转机械的启停过程、轴承的冲击性故障、语音的音素过渡段,这种"时间盲区"会让关键信息全部丢失。

短时傅里叶变换(STFT)就是为解决这个问题而生的。它的思路很朴素:既然FFT看不了时间,那我就把长信号切成很多短段,每段分别做FFT,再把结果按时间顺序拼起来。这样得到的时频图,横轴是时间,纵轴是频率,颜色深浅代表该时刻该频率的能量强度。这篇文章会从工程实操的角度,把STFT在振动数据处理中的完整落地路径讲清楚——包括窗函数怎么选、参数怎么定、代码怎么写、结果怎么读,以及我在实际项目中踩过的那些坑。

1. 从FFT到STFT:为什么非要做这个升级

1.1 FFT在振动分析中的真实短板

先看一个具体的场景。假设你在一台电机壳体上装了加速度传感器,采样率设为5120 Hz,采集了10秒钟的振动数据。这段数据里包含两种成分:一种是电机稳定运行时的基频振动,大概在50 Hz附近;另一种是启动瞬间的冲击振动,频率成分很丰富,但只持续了不到0.5秒。

如果你直接对这10秒数据做FFT,得到的频谱上会同时出现50 Hz的尖峰和一片高频区域的能量抬升。问题来了:你没法判断那片高频能量是启动瞬间产生的,还是整个10秒里一直存在的。如果高频能量只在启动瞬间出现,那说明是启动冲击导致的;如果一直存在,那可能是某个轴承或齿轮的持续故障。这两种情况的处理方案完全不同,但FFT给不了你答案。

这就是傅里叶变换的"全局性"带来的问题。它假设信号是平稳的,也就是说信号的频率成分不随时间变化。但实际工程中的振动信号,尤其是涉及启停、变速、冲击、调制等工况的,几乎都是非平稳的。用FFT分析非平稳信号,就像用一张全程曝光的照片去分析一场足球比赛的战术——你只能看到球场上所有人的平均位置,看不到传球路线和跑位时机。

1.2 STFT的核心思路:分而治之

STFT的做法很直接:既然整段信号不平稳,那我就假设它在很短的时间窗口内是平稳的。具体操作是,用一个固定长度的窗函数(比如汉宁窗)在时间轴上滑动,每滑动一个步长,就把窗内的信号截取出来做一次FFT,然后把每次FFT的结果按时间顺序排列。

这里有一个关键的权衡:窗长决定了时间分辨率和频率分辨率的平衡。窗越长,截取的信号越长,频率分辨率越高(能区分更接近的频率成分),但时间分辨率越差(无法精确定位频率变化发生的时刻)。窗越短,时间分辨率越好,但频率分辨率越差。这个矛盾本质上来源于海森堡不确定性原理在信号处理中的体现,你不可能同时获得无限高的时间分辨率和频率分辨率。

用一个生活化的类比:假设你要描述一首歌的旋律变化。如果你以整首歌为单位来描述,你能准确说出用了哪些音符(频率分辨率高),但说不清每个音符具体在哪个时间点出现(时间分辨率差)。如果你以每0.1秒为单位来描述,你能精确追踪每个瞬间的音高变化(时间分辨率高),但可能分不清两个很接近的音符(频率分辨率差)。STFT就是让你在这两者之间选一个平衡点。

1.3 STFT在振动诊断中的典型应用场景

在实际的旋转机械故障诊断中,STFT的价值主要体现在几个场景。第一是变速工况分析,比如风力发电机在风速变化时的齿轮箱振动,转速在变,啮合频率也在变,STFT能清晰展示频率随时间的变化轨迹。第二是冲击性故障检测,比如轴承外圈剥落产生的周期性冲击,每次冲击在时频图上表现为一条垂直的宽带能量线,冲击的间隔对应故障特征频率。第三是启停过程监测,电机启动时电流频率从0逐渐升到工频,对应的振动频率成分也会有一个爬升过程,STFT能完整记录这个过渡。

还有一个容易被忽略的场景是调制现象分析。齿轮箱中齿轮的啮合频率会被轴的转频调制,形成边带。如果只看FFT,边带和主频的关系不够直观;但在时频图上,你能看到能量在啮合频率和边带之间周期性地"跳动",调制的周期和深度一目了然。

2. 窗函数与参数选择:STFT效果好坏的分水岭

2.1 窗函数到底在做什么

很多人做STFT时直接调用scipy.signal.stft,窗函数用默认值,参数凭感觉设,结果出来的时频图模糊一片,然后得出结论说"STFT效果不好"。问题往往出在窗函数和参数选择上。

窗函数的作用是减少频谱泄漏。所谓频谱泄漏,是因为我们对信号做了截断(只取窗内的一段),截断处的突变会在频域产生额外的频率成分,就像你在一段平滑的曲线上硬生生切一刀,切口处会产生高频振荡。窗函数通过在两端逐渐衰减信号来平滑这个截断,减少泄漏。

但窗函数也有代价:它会让主瓣变宽(频率分辨率下降),同时旁瓣虽然被压低但不会完全消失。不同窗函数的区别就在于主瓣宽度和旁瓣高度的权衡。矩形窗主瓣最窄但旁瓣最高,汉宁窗主瓣较宽但旁瓣衰减快,汉明窗介于两者之间,布莱克曼窗旁瓣最低但主瓣最宽。

2.2 振动信号该选什么窗

对于振动信号,我的经验是:优先用汉宁窗(Hann)。原因有几个。第一,汉宁窗的旁瓣衰减速率快,对于振动信号中常见的强主频加弱边带的结构,汉宁窗能有效抑制强主频的泄漏对弱边带的掩盖。第二,汉宁窗的等效噪声带宽适中,不会让频率分辨率损失太多。第三,汉宁窗在工程界接受度高,结果的可比性好。

但有两种情况需要换窗。一种是需要精确测量幅值的场合,比如做振动烈度评价时,汉宁窗的幅值恢复系数不是1,需要做补偿。这时候可以用平顶窗(Flat Top),它的主瓣很宽但幅值精度极高,适合校准和计量。另一种是信号中包含瞬态冲击,需要精确定位冲击时刻,这时候可以用矩形窗或指数窗,牺牲频率分辨率换取时间定位精度。

下面这张表是我在实际项目中总结的窗函数选择参考:

窗函数主瓣宽度(相对)旁瓣峰值(dB)幅值精度适用场景
矩形窗最窄-13差瞬态冲击定位
汉宁窗中等-31中等通用振动分析
汉明窗中等-41中等语音、声学
布莱克曼窗宽-57中等强干扰下的弱信号
平顶窗最宽-90优幅值精确测量

2.3 窗长和重叠率怎么定

窗长的选择取决于你要分析的频率成分。一个实用的经验公式是:窗长至少包含你要分辨的最低频率的3到5个周期。比如你要分析50 Hz的基频,采样率5120 Hz,那么一个周期是102.4个采样点,窗长至少取307到512点。如果取512点,频率分辨率是5120/512=10 Hz,意味着频率间隔小于10 Hz的两个成分无法区分。

但窗长也不是越长越好。如果信号中有快速变化的成分,比如持续0.1秒的冲击,窗长取512点对应0.1秒(512/5120=0.1秒),刚好能覆盖,但如果取1024点就是0.2秒,冲击会被"稀释"在窗内,时频图上的能量会被摊薄,定位精度下降。

重叠率的选择相对灵活。重叠的目的是让时频图在时间轴上更平滑,避免因为窗的跳跃导致某些时刻的信号被遗漏。常用的重叠率是50%到75%。重叠率越高,时频图越平滑,但计算量也越大。对于大多数振动分析场景,75%重叠(即每次移动窗长的1/4)是一个不错的平衡点。

这里有一个容易踩的坑:重叠率不是越高越好。当重叠率超过90%时,相邻窗之间的相关性太强,时频图会出现"拖尾"效应,反而让时间定位变得模糊。而且计算量会急剧增加,对于长时间数据可能无法接受。

3. 用Python实现STFT:从原始数据到可读时频图

3.1 数据准备与预处理

在动手写STFT代码之前,有几个预处理步骤不能省。第一步是去均值,振动信号中往往包含直流偏置或趋势项,不去掉的话会在时频图的0 Hz附近形成一条很强的能量带,掩盖低频信息。用scipy.signal.detrend或者直接减去均值都可以。

第二步是检查采样率。STFT的结果中频率轴的范围是0到采样率的一半(奈奎斯特频率)。如果采样率设置错误,整个频率轴都是错的。我遇到过有人把采样率写成1000 Hz,实际采集是5120 Hz,结果时频图上的频率全部压缩了5倍多,故障特征频率完全对不上。

第三步是数据长度的考虑。STFT的输出是一个二维矩阵,行数对应频率点数,列数对应时间帧数。如果数据太长,比如几小时的连续采集,直接做STFT会得到一个巨大的矩阵,内存可能扛不住。这时候需要分段处理,或者先做降采样。

import numpy as np from scipy import signal import matplotlib.pyplot as plt # 模拟一段振动信号:50Hz基频 + 200Hz冲击成分(仅在0.5-1.0秒出现) fs = 5120 # 采样率 t = np.arange(0, 2, 1/fs) # 2秒数据 f_base = 50 # 基频 f_impact = 200 # 冲击频率 # 基频信号 x = 1.0 * np.sin(2 * np.pi * f_base * t) # 冲击成分:在0.5-1.0秒之间加入200Hz成分 impact_mask = (t >= 0.5) & (t < 1.0) x[impact_mask] += 0.5 * np.sin(2 * np.pi * f_impact * t[impact_mask]) # 加入少量噪声 x += 0.1 * np.random.randn(len(t)) # 去均值 x = x - np.mean(x)

3.2 调用scipy.signal.stft的正确姿势

scipy.signal.stft是SciPy 1.0之后引入的STFT函数,比旧版的spectrogram接口更清晰。它的核心参数包括window(窗函数)、nperseg(每段长度,即窗长)、noverlap(重叠点数)、nfft(FFT点数)、fs(采样率)。

这里有一个关键点:nperseg和nfft的关系。nperseg是实际截取的信号长度,nfft是FFT的点数。如果nfft大于nperseg,信号会在末尾补零,相当于对频谱做插值,频率轴上的点会更密,但不会提高实际频率分辨率。如果nfft小于nperseg,信号会被截断,信息会丢失。通常设nfft等于nperseg或者略大(比如取2的整数次幂方便计算)。

# STFT参数 nperseg = 512 # 窗长 noverlap = 384 # 重叠点数,75%重叠 nfft = 512 # FFT点数 f, t_stft, Zxx = signal.stft( x, fs=fs, window='hann', nperseg=nperseg, noverlap=noverlap, nfft=nfft, boundary=None, # 不补边界,避免时间轴偏移 padded=False # 不额外补零 ) # Zxx是复数矩阵,取模得到幅值 magnitude = np.abs(Zxx)

3.3 时频图的可视化技巧

拿到magnitude矩阵后,直接plt.pcolormesh出来的图往往不好看,因为振动信号的动态范围很大,强成分和弱成分可能差几个数量级。这时候需要对幅值做对数变换,用分贝(dB)表示。

# 转换为dB,参考值为最大值 magnitude_db = 20 * np.log10(magnitude / np.max(magnitude) + 1e-12) # 绘制时频图 plt.figure(figsize=(12, 6)) plt.pcolormesh(t_stft, f, magnitude_db, shading='gouraud', cmap='jet') plt.ylabel('频率 (Hz)') plt.xlabel('时间 (s)') plt.title('STFT时频图') plt.colorbar(label='幅值 (dB)') plt.ylim(0, 500) # 只显示0-500Hz范围 plt.tight_layout() plt.show()

这里有几个可视化上的经验。第一,cmap的选择很重要,jet虽然被很多人批评,但在时频图上它的对比度确实好,能清晰看出能量分布。如果要做正式报告,可以用viridis或inferno,更符合视觉感知均匀性。第二,shading='gouraud'能让图像更平滑,但计算量稍大;如果数据量大,用shading='auto'或'nearest'。第三,ylim一定要设,因为振动信号的高频部分往往能量很低,全频段显示会让低频区域的细节被压缩。

还有一个技巧:如果信号中有很强的单一频率成分(比如50 Hz基频),它会在时频图上形成一条贯穿始终的亮线,把其他弱成分掩盖。这时候可以在做STFT之前先做一次带阻滤波,把强主频滤掉,再看时频图,弱成分会清晰很多。这个操作在轴承故障诊断中特别有用,因为轴承故障特征频率往往在几千赫兹,能量比基频低几十个dB。

4. 时频图读图实战:从图案反推设备状态

4.1 稳态信号的时频图特征

先看最简单的情况:设备稳定运行,振动信号由几个固定的频率成分组成。这时候时频图上的表现是几条水平的亮线,每条线对应一个频率成分,线的粗细反映该成分的频率稳定度。如果线很细很直,说明频率非常稳定;如果线有轻微的抖动,可能是转速有波动。

这种时频图看起来"平淡",但信息量不小。水平线的数量告诉你有多少个主要频率成分,线的位置告诉你频率值,线的亮度告诉你幅值大小。如果某条线的亮度随时间有周期性变化,那说明这个频率成分被调制了,调制的周期可以从亮度变化的周期读出来。

4.2 冲击成分的识别

冲击在时频图上的表现非常有辨识度:一条垂直的宽带能量线。因为冲击在时域上是一个短促的脉冲,在频域上覆盖很宽的频率范围,所以在时频图上表现为某个时刻、从低频到高频都有能量。

轴承故障的冲击是周期性的,所以时频图上会看到一系列等间隔的垂直亮线。间隔时间就是冲击的周期,它的倒数就是冲击的重复频率。对于轴承外圈故障,这个重复频率就是外圈故障特征频率(BPFO);内圈故障对应BPFI;滚动体故障对应BSF。这些特征频率可以通过轴承几何参数和转速计算出来,和时频图上测到的间隔对比,就能判断是哪个部件出了问题。

这里有一个实操中的坑:冲击的间隔在时频图上不一定能直接测准。因为窗长有限,如果冲击间隔小于窗长,相邻冲击会在同一个窗内混叠,时频图上分不开。这时候需要减小窗长,提高时间分辨率。但窗长减小又会导致频率分辨率下降,可能看不清冲击的宽带特征。这个矛盾需要根据具体信号来平衡。

4.3 变速过程的时频图解读

变速工况是STFT最擅长的场景。电机启动时,转速从0逐渐升高,振动信号中的基频(对应转频)也会从0逐渐升高。在时频图上,这表现为一条从低频向高频倾斜的亮线。如果设备中有齿轮,齿轮啮合频率是转频的齿数倍,那么时频图上会看到多条平行的倾斜亮线,它们的斜率相同,但频率值成比例。

变速过程中还有一个重要现象:频率模糊。当转速变化很快时,在窗长内频率已经发生了显著变化,FFT的结果会把这段频率变化"平均"掉,导致时频图上的亮线变粗甚至分裂。解决方法是减小窗长,让窗内频率变化足够小。但窗长减小又会影响频率分辨率,所以需要在变速速率和频率分辨率之间做权衡。

一个实用的判断标准是:窗长内频率变化量应小于频率分辨率。假设频率分辨率是Δf,频率变化率是df/dt,那么窗长T应满足T < Δf / (df/dt)。比如Δf=10 Hz,df/dt=100 Hz/s,那么T < 0.1秒,对应512个采样点(采样率5120 Hz)。

5. 那些让我熬夜调试的STFT参数坑

5.1 边界效应导致的时间轴偏移

scipy.signal.stft默认的boundary='zeros'会在信号两端补零,这会导致时频图的时间轴比原始信号长。如果你用t_stft去对齐其他传感器数据,会发现对不上。我第一次遇到这个问题时,排查了半天以为是采样率设置错了,后来才发现是边界补零导致的。

解决方法很简单:设boundary=None,这样STFT只对信号实际存在的部分做分析,时间轴和原始信号一致。但代价是信号两端的窗可能不完整,时频图两端的数据会少一些。如果信号很长,这点损失可以忽略;如果信号很短,就需要考虑是否接受这个损失。

5.2 幅值标定的陷阱

STFT输出的幅值和原始信号的幅值之间有一个比例关系,这个比例取决于窗函数和窗长。如果你直接用np.abs(Zxx)作为幅值,得到的数值和实际振动幅值(比如加速度的m/s²)是对不上的。要做定量分析,必须做幅值标定。

标定的方法是:用一个已知幅值的正弦信号做STFT,看输出的幅值是多少,然后计算比例系数。对于汉宁窗,这个系数大约是窗长的一半(因为汉宁窗的相干增益是0.5)。但更可靠的做法是实际标定,因为不同实现可能有细微差异。

我在做振动烈度评价时踩过这个坑:直接用STFT的幅值去算RMS,结果比实际值小了将近一半。后来发现是窗函数的相干增益没有补偿。补偿之后,STFT算出来的RMS和直接对时域信号算的RMS就对上了。

5.3 频率轴的范围与混叠

STFT的频率轴范围是0到fs/2。如果信号中有高于fs/2的频率成分,它们会混叠到低频区域,在时频图上表现为虚假的低频成分。这个问题在振动信号中很常见,因为加速度传感器的高频响应很好,而很多故障特征频率都在几千赫兹。

避免混叠的方法是在采集时加抗混叠滤波器,把高于fs/2的频率成分滤掉。如果数据已经采集了,发现时频图上有不明低频成分,可以检查一下是不是高频混叠。一个简单的判断方法是改变采样率重新采集,如果那个低频成分的位置随采样率变化,那基本可以确定是混叠。

5.4 大数据量下的内存与速度优化

处理长时间振动数据时,STFT的计算量和内存占用是必须考虑的问题。假设采样率5120 Hz,采集1小时,数据量是1843万个点。如果窗长512,重叠384,那么时间帧数大约是1843万/(512-384)≈14.4万帧,频率点数256,STFT矩阵的大小是256×14.4万≈3700万个复数,每个复数16字节,总共约590 MB。这还只是一小时的数据,如果是多通道或者多天,内存很快就爆了。

优化方法有几个。第一,分段处理,每次处理几分钟的数据,分析完保存结果再处理下一段。第二,降采样,如果只关心低频成分,可以先降采样再STFT,数据量能减少一个数量级。第三,用scipy.signal.stft的return_onesided=True(默认),只返回正频率部分,数据量减半。第四,如果只需要时频图的图像而不需要原始矩阵,可以在计算完每个时间帧后立即转换为dB并存储为低精度格式(如float16),内存占用能减少一半以上。

6. STFT之外:什么时候该考虑其他时频方法

STFT不是万能的。它的核心限制是窗长固定,一旦选定,时间分辨率和频率分辨率就固定了。但实际信号中,不同时间段的特征可能需要在不同的分辨率下观察。比如变速过程中,低频段的频率变化慢,需要长窗来分辨;高频段的频率变化快,需要短窗来定位。固定窗长的STFT无法同时满足。

这时候可以考虑小波变换(CWT)。小波变换用可变的窗长:低频用长窗,高频用短窗,天然适合分析变速信号和瞬态信号。但小波变换的缺点是频率轴不是线性的,而是对数的,对于需要线性频率轴的振动分析,解读起来不如STFT直观。

另一种方法是Wigner-Ville分布(WVD),它的时频聚集性比STFT好得多,但会有交叉项干扰,对于多成分信号,交叉项会形成虚假的能量分布,需要做平滑处理(如平滑伪Wigner-Ville分布)。

我的建议是:先用STFT,如果发现分辨率不够再考虑其他方法。STFT的物理意义最清晰,参数最容易理解,结果最容易解释。在大多数旋转机械故障诊断场景中,STFT已经足够。只有在分析非平稳性极强的信号(如冲击响应、变速扫频)时,才需要考虑小波或其他方法。

还有一个折中方案:用多分辨率STFT。对同一段信号,用不同的窗长分别做STFT,然后对比不同分辨率下的时频图。短窗看时间细节,长窗看频率细节,结合起来能获得更全面的信息。这个方法在工程实践中很实用,虽然计算量翻倍,但避免了选择单一窗长的纠结。

7. 把STFT嵌入实际诊断流程的几个建议

7.1 和FFT配合使用

STFT不应该完全替代FFT。正确的流程是:先用FFT做全局频谱分析,确定主要的频率成分和关注频段;然后用STFT在关注频段做时频分析,看这些成分的时间行为。FFT负责"有什么",STFT负责"什么时候有"。两者配合,效率最高。

比如分析一台齿轮箱的振动,先用FFT找到啮合频率和边带的位置,然后针对啮合频率附近做STFT,看边带是否随时间周期性出现。如果边带在时频图上表现为周期性的能量跳动,那基本可以确认是齿轮故障。

7.2 特征频率的计算与标注

做STFT之前,先把可能的故障特征频率算出来,然后在时频图上标注这些频率的水平线。这样读图时能快速判断哪些能量带对应哪些故障。特征频率的计算需要轴承几何参数(节径、滚珠直径、接触角、滚珠数)和转速。如果这些参数拿不到,可以用经验公式估算,或者直接观察时频图上的周期性模式来反推。

标注的方法是在时频图上画水平虚线,用plt.axhline,颜色选对比度高的(比如白色或黑色),线型用虚线,避免遮挡时频图本身的能量分布。

7.3 批量处理与自动化

实际项目中往往有几十上百个测点的数据要分析,手动一个个做STFT不现实。需要写批处理脚本,自动读取数据文件、做STFT、生成时频图、保存结果。关键是要把参数配置化,不同测点可能用不同的窗长和频率范围,这些参数放在配置文件里,脚本读取配置后自动处理。

自动化还有一个好处是一致性。手动分析时,不同时间、不同人做的STFT参数可能不一样,结果没法对比。自动化保证所有数据用同一套参数处理,结果可比。

7.4 结果存档与追溯

时频图生成后,除了图片,建议把STFT的矩阵也保存下来(可以用np.save存为npy文件,或者用HDF5格式)。这样后续如果需要重新分析或者用不同的可视化方式,不需要重新计算。同时记录下STFT的所有参数(窗函数、窗长、重叠、采样率、时间范围),这些元数据对于结果的可追溯性至关重要。

我在一个项目中因为没有保存STFT参数,三个月后需要复现一张时频图时,完全想不起来当时用的什么窗长和重叠。只能重新试参数,浪费了大半天时间。从那以后,我养成了一个习惯:每次生成时频图,都在图片文件名里嵌入关键参数,比如stft_hann_512_75_5120.png,一看文件名就知道参数。

7.5 常见问题的快速排查清单

最后整理一个STFT分析中常见问题的排查清单,遇到问题时可以按这个顺序检查:

现象可能原因排查方法
时频图全黑或全白幅值未做对数变换检查是否转换为dB
频率轴数值不对采样率设置错误核对采集设备的实际采样率
时间轴对不上boundary参数导致设boundary=None
低频有虚假亮带直流偏置未去除做去均值或去趋势
高频有虚假成分频率混叠检查抗混叠滤波器
时频图模糊窗长或重叠不合适调整nperseg和noverlap
幅值偏小窗函数增益未补偿做幅值标定
计算太慢数据量太大分段处理或降采样

这个清单基本覆盖了我这些年遇到的大部分STFT相关问题。每次调试时按这个顺序过一遍,大部分问题都能快速定位。

STFT是一个看起来简单但细节很多的工具。窗函数、窗长、重叠率、边界处理、幅值标定,每一个环节都有坑。但一旦把这些细节都摸清楚了,它就成了振动分析中最得力的工具之一。我现在的习惯是,拿到任何一段非平稳振动信号,第一件事就是做STFT看时频图,先对信号的整体行为有个直观认识,然后再决定下一步用什么方法深入分析。这个习惯帮我避免了很多"盲人摸象"式的分析错误。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进