站在信号处理这个行当里摸爬滚打这些年,我越来越觉得“连续小波变换(CWT)”是个被低估的工具。很多人一听“时频局部分析”就觉得高深,其实它解决的是一个特别接地气的问题:傅里叶变换能告诉你信号里有什么频率,却说不清这些频率是什么时候出现的。而CWT就是把“频率随时间变化”这张图给画出来。这篇东西写给正在做故障诊断、生物电信号分析、地震勘探,或者单纯被时频分析搞得头大的朋友,我会从傅里叶的死穴讲起,把CWT的原理、参数、代码和坑一次说清楚。
1. 为什么傅里叶变换做不了“局部”分析
1.1 傅里叶变换的本质:把时间抹平掉的频谱
傅里叶变换的核心思想,是把一段信号分解成无穷多个正弦波的叠加。数学上长这样:
[ X(f) = \int_{-\infty}^{+\infty} x(t) e^{-j2\pi ft} dt ]
这个公式干了一件很“暴力”的事:它把时间变量 t 直接积分掉了。积分意味着什么?意味着信号在每一个时刻的大小,都被叠加到了同一个结果里。比如一段10秒的信号,前9秒是50Hz的纯音,最后1秒突然变成了200Hz的纯音。傅里叶变换看过去,只会告诉你“哦,这个信号里有50Hz和200Hz的成分”,至于200Hz是出现在开头还是结尾,它一概不知。
我经常跟人打比方:傅里叶变换像是把一整首交响乐搅拌成一份“食材清单”,清单上写着“有小提琴、有鼓、有钢琴”,但谁先响、谁后响、谁在哪一秒独奏,完全看不出来。对于平稳信号,这根本不是问题,反正频率成分从头到尾都一个样。可现实世界的信号——语音、机械振动、心电、地震波——几乎没有一个是平稳的。轴承磨损初期的高频冲击,只会在特定转角出现;癫痫发作时的棘波,也只是偶尔蹦出来。这些瞬态信息才是诊断的关键,而傅里叶变换恰恰把它们“平均”没了。
这就引出了时频分析的核心诉求:我们既要频率信息,又要时间信息,而且还要知道“某个频率成分到底在什么时刻活跃”。
1.2 短时傅里叶变换(STFT)和它的窗口困境
傅里叶做不了局部,那直接加个窗口不就行了?这就是短时傅里叶变换(STFT)的思路:用一个小窗口把信号切成一段一段,对每一段分别做傅里叶变换,然后把结果按时间顺序排成一幅“时间-频率”图。公式是:
[ STFT(t, f) = \int_{-\infty}^{+\infty} x(\tau) w(\tau - t) e^{-j2\pi f\tau} d\tau ]
窗口 w 的宽度决定了时间分辨率和频率分辨率的平衡。窗口越短,时间定位越准(能看到瞬态发生在哪一刻),但频率分辨率越差(频谱被拉宽,两个靠近的频率分不开);窗口越长,频率分辨得越清楚,但时间上就模糊了,短时间内发生的突变会被“摊”进整个窗口里。这个矛盾就是海森堡测不准原理在信号处理里的表现,一旦窗口长度固定,整个时频图上所有位置的频率分辨率都一样,不管信号是低频还是高频。
这种固定分辨率的做法非常不划算。现实问题里,低频成分往往持续很长时间(比如机器的转频),需要频率精度高一点;高频冲击往往只存在一瞬间(比如轴承点蚀的脉冲),需要时间精度高一点。STFT用一个固定窗口,等于逼你在“看整体”和“看细节”之间二选一,选完了还不能反悔。我当时第一次用STFT分析轴承振动信号时,为了看那个高频脉冲,窗函数选短了,结果转频附近的两个谐波糊成了一个;窗加长了,脉冲时间点又对不准,来回试了十几组窗口参数,最后只能折中。这就是STFT的先天缺陷——窗口长度是死的。
那有没有一种办法,让窗口自己会伸缩:看高频时自动变窄,看低频时自动变宽?有,这就是连续小波变换。
2. 连续小波变换的核心思路
2.1 小波到底是什么:有限长的波
“小波”(wavelet)这个词本身就挺直白——“小的波”。傅里叶变换用的基函数是正弦波,从负无穷延伸到正无穷,永不衰减。小波不一样,它是一个有限长度、平均值为零的波形,能量集中在一小段区间内,这段区间之外的振幅迅速衰减到零。
这个“有限长”是革命性的。正弦波像一盏永不熄灭的灯,照得整个房间都很亮,却分不清光是从哪个方向来的;小波像手电筒,光束集中,指哪打哪。用小波去做分解,就能在“某个时间位置”上看到“某个频率成分”的活跃程度。
常见的母小波有几种:Morlet小波是一个复值小波,形状是复正弦乘以高斯包络,适合分析振荡型信号,也是使用最广的;墨西哥帽小波(Ricker小波)是高斯函数的二阶导数,形状像一顶倒扣的帽子,适合检测脉冲和突变;Haar小波是个方波,虽然简单,但光滑性差,时频图很难看。选哪种小波没有绝对标准,要看信号形态:信号里是持续振荡就用Morlet,信号里是尖锐冲击就试试墨西哥帽,这个选择直接决定时频图的质量,后面我会详细讲。
2.2 CWT的数学定义:尺度与平移
连续小波变换的公式是:
[ CWT_x(a, b) = \frac{1}{\sqrt{|a|}} \int_{-\infty}^{+\infty} x(t) \psi^*\left(\frac{t-b}{a}\right) dt ]
这里的 \psi(t) 是母小波,a 是尺度参数(scale),b 是平移参数(shift),星号表示共轭。这个公式的直觉是:把母小波 \psi(t) 在时间轴上挪到位置 b,再拉伸或压缩成尺度 a,然后和信号 x(t) 做内积。内积越大,说明在这个位置、这个尺度下,信号和小波的相似度越高。
尺度参数 a 和频率是反向关系:a 越大,小波在时间上被拉得越宽,频率越低;a 越小,小波被压得越窄,频率越高。这正是“自适应窗口”的来由——当你在高频段扫查时,小波的等效窗口自然就变窄了。
这里有个关键点值得掰开揉碎:CWT的小波是对同一个母小波做“缩放+平移”得到的,所以本质上它的多分辨率是“内嵌”在基函数里的。STFT的窗口长度固定,频率分辨率和时间分辨率全局一致;CWT的窗口自动随尺度变化,低频段窗口宽、高频段窗口窄。这意味着同一幅时频图上,低频端的频率精度高,高频端的时间精度高——这正是现实信号分析最需要的分配方式。我第一次跑出CWT时频图时,看到低频段几条谱线清楚分开、高频段脉冲位置精确对应,真有那种“这玩意早该用上”的感慨。
2.3 尺度参数到频率的换算
用CWT最大的一个困惑就是:我把尺度 a 从1取到128,画出来的图横坐标是尺度,不是频率,怎么和实际故障特征频率对上?这中间隔着一个换算公式。
对于复数Morlet小波,中心频率为 fc(母小波本身的频率,通常在0.8到1.5之间),采样率为 fs,尺度 a 对应的“伪频率”是:
[ f_a = \frac{f_c \cdot f_s}{a} ]
举个例子,如果 fs = 1000 Hz,Morlet的中心频率 fc = 1,那么尺度 a = 100 对应的频率是 10 Hz,尺度 a = 10 对应的是 100 Hz。也就是说,如果你关心的是 1 Hz 到 100 Hz 的低频范围,尺度就要取 10 到 1000;如果你关心的是 100 Hz 到 1000 Hz,尺度就要取 1 到 10。
换算逻辑想清楚之后,选尺度范围就变成了一道简单的除法题:先确定目标频率区间下限 f_min 和上限 f_max,再反推尺度范围:
[ a_{max} = \frac{f_c \cdot f_s}{f_{min}}, \quad a_{min} = \frac{f_c \cdot f_s}{f_{max}} ]
很多人懒得算这步,直接用默认的尺度范围跑,结果画出来的图频率轴根本对不上实际物理频率,这是CWT入门最常见的翻车点。在这个问题上,花两分钟算一下尺度范围,能让后面省下几个小时的分析时间。
3. CWT实操:用Python把时频图跑出来
3.1 环境准备和信号构造
工欲善其事必先利其器。CWT在Python里有两条路:一条是用 SciPy 的scipy.signal.cwt,底层是自己传小波函数;另一条是用 PyWavelets(pywt)库,内置各种小波和连续小波变换的封装。我个人习惯用 pywt,因为内置小波种类多,且直接支持返回复数小波的结果,比手写小波函数省事。
先造一段测试信号。我建议用“chirp+突变”组合:一个频率线性扫频的chirp信号,中间叠加一个短时高频脉冲。这种信号是检验时频分析算法最好的试金石——chirp能看出频率分辨率,脉冲能看出时间分辨率。
import numpy as np import matplotlib.pyplot as plt import pywt fs = 1000.0 t = np.linspace(0, 1, int(fs), endpoint=False) # 频率从10Hz线性扫到100Hz的chirp信号 f0, f1 = 10, 100 chirp = np.sin(2 * np.pi * (f0 * t + 0.5 * (f1 - f0) * t**2)) # 在0.5秒处叠加一个200Hz的短时脉冲 pulse = np.zeros_like(t) idx_pulse = int(0.5 * fs) pulse[idx_pulse:idx_pulse+30] = np.sin(2 * np.pi * 200 * np.arange(30) / fs) * np.hanning(30) signal = chirp + pulse这段信号的第一个成分是平滑扫频,第二个成分是0.5秒处的一个极短振荡。拿它去跑STFT和CWT,能非常直观地看出两种算法在一个固定窗口下有多尴尬,而CWT又怎么避开这个坑。
3.2 用pywt实现CWT的核心代码
pywt的cwt函数用法很直接:传入信号、尺度数组、小波名,返回小波系数矩阵和对应的频率数组。
scales = np.arange(1, 128) coeffs, freqs = pywt.cwt(signal, scales, 'cmor1.5-1.0', sampling_period=1/fs) # 画时频图 plt.figure(figsize=(10, 6)) plt.imshow(np.abs(coeffs), extent=[0, 1, freqs[-1], freqs[0]], aspect='auto', cmap='jet') plt.colorbar(label='Magnitude') plt.ylabel('Frequency (Hz)') plt.xlabel('Time (s)') plt.title('CWT Scalogram') plt.ylim(0, 250) plt.show()'cmor1.5-1.0'这个字符串值得多说几句:cmor代表复数Morlet小波,1.5是带宽参数,1.0是中心频率。pywt里小波命名的格式是“名字+带宽-中心频率”,带宽越大,小波在频域的支撑越宽,频率分辨率越高,但时间分辨率变差;中心频率就是前文公式里的 fc,直接参与尺度到频率的换算。
这里有个特别容易踩的坑:pywt.cwt的返回值里,freqs是按尺度从小到大排列的,但小波系数的行顺序也是尺度从小到大。如果用extent参数画图,freqs的顺序必须和矩阵行对齐。freqs[0]对应的是最小尺度,也就是最高频率;freqs[-1]对应最大尺度、最低频率。如果你照抄网上的代码却把extent写成[0, 1, freqs[0], freqs[-1]],图就会上下颠倒,频率轴直接反了。我见过不止一个同事在这种细节上浪费了半天,就为了查“为什么时频图和想象的不一样”。
3.3 小波选择与参数调节经验
小波类型和参数的选择,我总结了几个原则,基本能覆盖绝大多数分析场景:
先看信号形态。如果你分析的是振动、声音、脑电这类振荡信号,首选复Morlet小波;如果你分析的是冲击脉冲、雷击、地层反射这类瞬态信号,墨西哥帽小波('mexh')往往更合适。复Morlet的好处是能同时给出幅值和相位信息,做包络分析很方便;墨西哥帽是实值小波,只有幅值,但对脉冲敏感,定位更锐利。
再看带宽参数。Morlet的带宽参数决定小波在频域里的宽度。带宽越大,频率分辨率越高,适合需要区分相邻频率分量的场景;带宽越小,时间分辨率越高,适合定位突变时刻。经验上做机械故障诊断时带宽取1.5比较均衡,做语音分析时可以试着调高到2.0以上,但这个参数没有通用最优值,必须结合信号特征反复试。
最后记住一个原则:时频图不是越“亮”越好。很多人看到结果里一片红色亮带就兴奋,其实那往往是小波参数不匹配导致的能量扩散,真正的特征应该是“清晰、局部、可解释的”。
4. CWT与STFT的对比实验
4.1 同一条信号、两种算法的效果对比
拿上面那段chirp+脉冲信号,分别用STFT和CWT跑一遍,差异非常明显。STFT选一个居中的窗口长度(比如128个点),跑出来的时频图里,chirp谱线是中段的直线,但在低频段和高频段两端都可能发糊,而那个0.5秒处的脉冲在图上被拉成一个竖直的宽线,时间上没法精确定位到0.5秒这个点。
同样信号跑CWT(尺度从1到128、Morlet小波),chirp谱线从头到尾都清晰连续,频率越高谱线越细;脉冲则在时频图上变成一个尖锐的垂直短线,时间位置正好卡在0.5秒处。这就是CWT的“自适应”带来的直接收益:高频细节用高时间分辨率去看,低频趋势用高频率分辨率去看,不用人为选窗口。
对比代码也很简单,STFT可以用SciPy的signal.stft:
from scipy.signal import stft f_stft, t_stft, Zxx = stft(signal, fs, nperseg=128, window='hann', noverlap=96) plt.pcolormesh(t_stft, f_stft, np.abs(Zxx), cmap='jet', shading='gouraud') plt.ylim(0, 250) plt.colorbar(label='Magnitude') plt.title('STFT Spectrogram')把两张图放一起看,STFT的固定窗口问题就暴露无遗了:窗口长则脉冲糊,窗口短则低频线糊。你没法让一个窗口同时照顾好两端,但CWT天然就做到了。
4.2 分辨率取舍的直观理解
我常跟人解释,时频分辨率的取舍本质上就是一个“亮度”问题。任何时频分析都不可能无限同时提高时间和频率精度,这是信息论决定的规律。STFT的选择是“全局统一”:整个时频平面上,所有位置的分辨率都一样,像一个固定焦距的镜头,你只能选聚焦中景还是聚焦特写。CWT的选择是“按需分配”:频率越低镜头焦距越长,频率越高镜头焦距越短,所有位置看起来都是清楚的。
这种自适应特性对工程分析意义很大。举个例子,诊断一台齿轮箱时,转频大概在20Hz上下,齿啮合频率可能在1000Hz上下,两者之间隔了几十倍。用STFT去分析,窗口得把20Hz的几级谐波分清楚,窗口就必须很长,结果高频冲击的时间精度就完蛋了;反过来窗口短了,20Hz附近的谱线糊成一团,连轴频都数不清。CWT则完全没有这个烦恼,低频区自动提高频率精度,高频区自动提高时间精度,一张图全搞定。
当然,CWT不是银弹。它算出来的时频图是冗余的,相邻尺度之间高度相关,直接拿去做定量绝对值的比较,得小心谨慎。下一节我会专门聊这些坑。
5. 常见问题与排查技巧实录
5.1 边界效应:时频图两端的“假信号”
CWT最容易被忽略的问题,是信号边界处的小波系数完全没有可信度。原因很容易理解:小波在尺度较大时宽度很宽,在信号开头处做内积时,小波会超出信号范围,超出部分没有数据可取,算法只能用补零或某种延拓方式去凑。凑出来的结果反映的不是真实信号,而是边界假象。
这种效应在时频图上表现为左右两侧各有一块“锥形”区域,尺度越大(频率越低)锥形越宽。我在一份实测数据里就栽过跟头:一个低频成分在信号末尾本来已经消失了,但时频图末端还亮着一大片,一开始我还以为发现了新特征,后来一查才发现是边界效应。识别和规避这个问题的办法不算复杂:
- 信号采集时尽量留出足够长的“前导”和“尾巴”,不要从信号开始就急着分析;
- 分析时把时频图两侧各截掉一部分,宽度参考最大尺度的小波有效支撑长度;
- 如果主要关心高频瞬态,边界影响通常不大,因为高频对应的小波尺度小,宽度窄,边界效应范围有限;
- 高频成分在信号开头或结尾出现时,结果只作定性参考,别用边界区域的幅值去做定量判断。
5.2 小波系数不是“功率”,别直接拿它和FFT幅值比
另一个常见的误区,是把CWT的小波系数振幅直接当成和FFT幅值类似的东西去比较。CWT是冗余变换,系数的大小受小波归一化方式、尺度密度、小波类型影响极大,不同尺度之间、不同小波之间的数值没有直接可比性。
如果你需要从时频图里提取定量指标(比如某个频带的总能量、某个故障特征频率的幅值趋势),建议先做一次“标定”:用一段已知幅值、已知频率的纯正弦信号,跑到CWT里,找到对应尺度位置的最大系数,反推一个缩放因子。后续分析用这个因子去换算,结果才靠谱。我实验室每次换小波参数,都会先拿正弦标准信号重新标定一遍,这条路走过一次之后,能少掉很多数据解释上的麻烦。
5.3 参数调整速查
我把CWT的参数调试经验整理成一个速查表,方便遇到问题直接对照:
| 现象 | 可能原因 | 处理方向 |
|---|---|---|
| 时频图整片发糊,看不到清晰谱线 | 尺度间隔太粗 | 增加尺度数量,或改用对数尺度分布 |
| 高频细节看不清 | Morlet中心频率过高、带宽偏小 | 降低中心频率,或增大带宽参数 |
| 低频谱线被切断、不连续 | 尺度范围不够大 | 按 f_min 反推增大最大尺度 |
| 时频图上下颠倒 | extent参数顺序错 | 确认freqs首尾顺序,低频在前则frequency轴从高到低 |
| 图两侧出现竖向亮带 | 边界效应 | 截掉两侧锥形区域再分析 |
| 脉冲定位到了,但频率对不上 | 尺度-频率换算用了默认fc | 用实际使用的Morlet中心频率重算频率轴 |
| 复小波画出的图振幅忽高忽低 | 未取模或未取绝对值 | 画图时对系数取 np.abs() |
这些参数里,最核心的还是“尺度范围”和“小波中心频率”这两个。尺度范围决定你看到的频率范围,中心频率决定尺度与频率的映射关系。先算清楚这两项,再谈细调带宽和尺度数量,基本就是CWT调参的标准路径。
5.4 尺度分布:均匀取还是对数取
pywt的cwt函数默认接受一个尺度数组,很多人习惯用np.arange(1, N)均匀取,这在尺度跨度不大时够用。但如果你要分析的频率范围很宽(比如10Hz到1000Hz,跨两个数量级),均匀尺度分布会让低频段相邻尺度对应的频率差很小(冗余度过高),高频段相邻尺度对应的频率差很大(可能漏掉特征)。
更优的做法是采用对数分布,让每个尺度对应频率间隔在相对意义上均匀:
scales = np.logspace(np.log10(2), np.log10(256), 128)这样低频段尺度密集取值,高频段自然变疏,配合CWT自身的多分辨率特性,时频图在超大频率跨度下看起来更均衡。我处理地震波数据时,频率跨度经常超过三个数量级,对数分布基本是必须的。
5.5 关于“傅里叶作画”的一个小旁注
最近“傅里叶作画”在网上挺火,很多人拿傅里叶变换做图像处理、做艺术效果。这从一个侧面说明:频域思维远不只是“算频谱”那么简单,它和时频分析、图像滤波、压缩编码是同一套哲学的不同应用。CWT在图像里也有对应物,比如二维小波变换常用于图像去噪和边缘检测。如果你被“傅里叶作画”勾起了对频域变换的兴趣,那CWT绝对值得纳入你的武器库——它比单纯傅里叶变换多了一个“局部”维度,而“局部”恰恰是信息最丰富的地方。
最后再分享一个我自己的操作习惯。每次拿到一段新信号,我不是直接跑CWT,而是先看原始波形和FFT频谱,确定目标频段的大致范围,然后反推尺度区间,再跑CWT。跑完第一版时频图,先检查两侧边界和整体背景噪声,确认没有大面积假亮带后,才会去抠具体的故障特征。CWT给了我们一张很“聪明”的放大镜,但放大镜本身不会告诉你该看哪里——知道自己想看什么,永远比工具本身更重要。