简介:连续小波变换(CWT)是分析非平稳信号时频局部特征的常用工具,这份资源提供其MATLAB实现代码,面向信号处理、故障诊断、模式识别等方向的初学者和研究人员。压缩包内共2个文件,均为.m格式的MATLAB脚本,整体仅1KB,代码量精简、结构清晰,便于直接阅读与运行。已有4750人学习下载,是入门CWT编程实践的轻量参考。资源中的主程序演示了cwt函数的基本调用,包括小波基(如Morlet、Mexican Hat)选择、尺度范围定义以及时频结果的简单可视化;辅助脚本则围绕尺度-频率映射关系做了补充说明,帮助读者理解不同尺度对应的信号成分。通过运行示例,读者能快速掌握在MATLAB中搭建连续小波变换流程的方法,并可将这套基础代码迁移到信号去噪、特征提取等实际应用中。对于希望快速上手连续小波变换编程、避免从零摸索的读者,这套代码是一个实用的起点。
1. 连续小波变换 MATLAB 实现:为什么时频分析绕不开它
做非平稳信号分析的人,迟早会撞上连续小波变换(CWT)这道坎。傅里叶变换只能给你一个全局频谱,信号里哪个时刻发生突变、哪一段频率占主导,它一概不讲;短时傅里叶变换窗口一固定,时间和频率分辨率就锁死了。连续小波变换的MATLAB实现之所以被反复搜索,就是因为它能同时把时间和频率摊开,看突变、看趋势、看频带切换都靠这一张时间-频率图。这次要拆的这份资源,核心就是把这套算法落到可以出图、可以调参、可以复现的 MATLAB 脚本里。适合刚接触小波变换的新手,也适合做故障诊断、生物电信号分析、振动分析的老手拿来当参考底板。
2. CWT 的数学边界:母小波、尺度与频率,先立住再写代码
2.1 母小波与尺度-频率关系:一个公式讲清 CWT 黑匣子
连续小波变换的定义式并不复杂,工程上看懂这一个公式就够用了:
W(a,b) = (1/√a) ∫ x(t) · ψ*((t-b)/a) dt
这里 a 是尺度,b 是平移量,ψ 是母小波。尺度 a 越大,小波波形被拉伸得越宽,对应频率越低;a 越小,波形被压缩得越窄,对应频率越高。也就是说,尺度轴和频率轴是反着走的,这就是为什么很多初看 CWT 的人会被纵轴搞晕。
母小波的选择没有绝对标准,只有场景适不适合。工程上我见得最多的四类是 Morlet 族、Daubechies 族、Mexican Hat 和 Haar。它们的特点和适用场景我整理成了表:
| 母小波 | 实/复 | 频率定位 | 时间定位 | 工程场景 |
|---|---|---|---|---|
| Morlet / 解析 Morlet(amor) | 复 | 好 | 中 | 故障诊断、脑电/心电、振动信号分析,默认首选 |
| Daubechies(dbN) | 实 | 中 | 中 | 需要重构信号、做正交分解的场景 |
| Mexican Hat | 实 | 中 | 较好 | 峰值检测、脉冲定位,纵轴解释直观 |
| Haar | 实 | 差 | 极好 | 理论演示、突变点定位,频率信息基本放弃 |
实际写代码时,我一般直接把复值 Morphlet 系作为默认项,因为复数小波能同时给出幅度和相位两个自由度,时间-频率图上幅度谱的可读性比实值小波高一个档次。
2.2 从尺度到频率的换算表:再不被“尺度轴”骗
内置 cwt 函数在较新版本里会直接返回频率轴,但自写实现或者老版本脚本里,你拿到的仍然是尺度数组。尺度转傅里叶频率有一个经典换算公式:
fa = fc · fs / a
其中 fc 是母小波的中心频率,fs 是采样率,a 是尺度。同一个尺度在不同采样率下代表的实际频率完全不同,这就是为什么两个项目里拿到的尺度轴不能直接互相比较。
以 fs = 1000 Hz、fc = 1 为例,尺度 a 与频率 fa 的关系如下:
| 尺度 a | 对应频率(Hz) |
|---|---|
| 1 | 1000 |
| 2 | 500 |
| 4 | 250 |
| 8 | 125 |
| 16 | 62.5 |
| 32 | 31.25 |
看到没,尺度轴是指数级变化的。这也是为什么出图时我几乎不用线性坐标显示尺度,而是用 log 频率轴配合对数尺度。
2.3 MATLAB 里的三种实现路径:内置 cwt、工具箱老版本、自写
按现在的 MATLAB 版本,实现 CWT 有三条路可走。第一条是直接用内置 cwt 函数,从 R2016b 左右开始,它已经能返回时间向量 t、频率向量 f 和边界锥区 coi 三个输出,也是最省心的路径。第二条是用 Wavelet Toolbox 里的老接口,传尺度向量和母小波名,返回系数矩阵和尺度轴,适合你手上的代码是旧工程的情况。第三条是完全不用工具箱,自己写一个 CWT 核心函数,适合做算法验证或者在没有工具箱的环境里快速跑通逻辑。
自写实现我用得最多的是基于 FFT 卷积定理的版本,把时域卷积等价成频域相乘,代码短、速度快,还能让你真正看清小波变换在黑匣子里干了什么:
function cfs = simple_cwt_fft(x, fs, scales, center_freq) % 基于FFT卷积定理的自写连续小波变换 % 输入: x 一维信号 % fs 采样率(Hz) % scales 尺度向量 % center_freq 母小波中心频率, 典型值 1 cfs = zeros(numel(scales), numel(x)); n = numel(x); X = fft(x); % 信号频谱 freq = (0:n-1) * fs / n; % FFT频率轴 for k = 1:numel(scales) a = scales(k); % 频域构造Morlet母小波: 高斯带通 psi_hat = exp(-(freq * a - center_freq).^2 / 2); psi_hat = conj(psi_hat); cfs(k, :) = ifft(X .* psi_hat) * sqrt(1/a); end end逻辑说明:信号先做 FFT 得到频谱,然后在频域构造母小波的频域形式,每一行代码对应的都是一个带通滤波器;尺度越小,滤波器的中心频率越高,带宽越宽。最后用 IFFT 回到时间域,就得到了这一尺度下的小波系数。sqrt(1/a)是归一化因子,不加它,不同尺度之间的幅度不可比。
参数说明:scales建议按对数间隔生成,常见做法是scales = 2.^(2:0.2:8),这样低频到高频覆盖均匀;center_freq取 1 时频域中心对应fs / a,取 0.5 时中心频率减半。这段代码只是验证原理用的,真要大规模分析信号,我还是建议用内置 cwt,性能和边界处理都更完整。
3. MATLAB 落地流程:从原始信号到时间-频率图
3.1 先造一个带突变的模拟信号:这一小段相当于“实验台”
直接拿真实数据调 CWT,参数出了问题你都分不清是信号的问题还是代码的问题。我一般会在动手前先造一段已知成分的信号当实验台,确认算法没问题再换真实数据。
下面这段代码生成一个 50 Hz 正弦持续 0.5 秒后切换成 120 Hz 正弦的信号,并叠加 0.2 倍标准差的高斯噪声:
fs = 1000; % 采样率 1000 Hz t = 0:1/fs:1; % 1 秒时间轴 x = sin(2*pi*50*t) .* (t < 0.5); % 前半段 50 Hz x = x + sin(2*pi*120*t) .* (t >= 0.5); % 后半段 120 Hz x = x + 0.2 * randn(size(t)); % 加一点噪声,模拟真实采集逻辑说明:这段信号有两个特征,一是频率在 0.5 秒处跳变,二是整体是非平稳的,FFT 只能告诉你能量集中在 50 Hz 和 120 Hz,但说不清切换时刻,正好用来检验 CWT 的时间定位能力。噪声是故意加的,没噪声的纯正弦信号 CWT 结果太干净,掩盖边界效应。
参数说明:fs = 1000决定了所有频率轴的刻度;加噪幅度 0.2 是一个折中,太小没有真实感,太大特征频率会糊在底色里。如果你想做对比实验,建议给 randn 加种子再用rng(20240501),保证每次跑出来的信号一致。
3.2 用内置 cwt 跑通主线:参数值与运行逻辑
实验台搭好之后,主线代码就三五行,但它把 CWT 最常用的参数都暴露出来了:
[wt, f, coi] = cwt(x, fs, 'amor', ... 'VoicesPerOctave', 32, ... 'FrequencyLimits', [5 300]);逻辑说明:cwt 第一个输入是信号,第二个是采样率 fs,第三个'amor'指定母小波为解析 Morlet,默认参数无需额外调。VoicesPerOctave(每倍频程内细分多少个尺度)设为 32,表示每个倍频程之间会插值出 32 个尺度,数值越大频率轴越细腻,运行时间也越长。FrequencyLimits限定只计算 5 到 300 Hz 之间的频率,低于 5 Hz 和高于 300 Hz 的部分直接跳过,既省时间又避免无意义的高频噪声。
参数说明:VoicesPerOctave的默认值是 10,出图能看到明显“锯齿”感,我一般先用 10 快速验证逻辑,再提高到 24 到 32 出正式图。FrequencyLimits上界不要超过 fs/2,否则超出奈奎斯特频率的部分全是无效计算。如果信号里已知没有低频漂移,下限从 5 Hz 开始就够用。
输出变量里,wt是 频率×时间 的复数系数矩阵,f是对应的频率列向量,coi是边界锥区边界。很多教程只画 wt 不画 coi,后面我会专门讲它。
3.3 时间-频率图的工程画法:pcolor 与 imagesc 怎么选
拿到wt之后,绘图套路基本固定。我用的是 pcolor 加shading interp,因为 pcolor 能直接把数据网格映射到真实坐标,不会像 imagesc 那样把纵轴变成等间距索引:
figure; pcolor(t, f, abs(wt)); % 幅度谱,重点 shading interp; % 平滑颜色过渡 set(gca, 'YScale', 'log'); % 频率轴用对数坐标 ylim([5 300]); colormap('jet'); colorbar; xlabel('时间 (s)'); ylabel('频率 (Hz)'); hold on; plot(t, coi, 'k--', 'LineWidth', 1.2); % 边界锥区虚线 hold off;逻辑说明:abs(wt)取复数系数的模,得到的就是每个时刻、每个频率上的能量强度;set(gca,'YScale','log')非常关键,因为 CWT 在低频端尺度间隔小,线性坐标会把低频细节压缩成一条细缝,对数坐标才能让 5 Hz 到 300 Hz 全频段都看得清。coi虚线画的是边界锥,位置在它内侧的数据才是可信的。
参数说明:colormap 里'jet'是工程上最常用的,蓝到红表示能量从低到高;但要注意,jet 在高亮处容易形成视觉上的伪峰,正式报告里换成'parula'会更严谨。pcolor 和 imagesc 的选择上,我一般优先用 pcolor,只有当你需要把尺度轴直接映射成像素位置时才用 imagesc 并手动改YTickLabel。
4. 避坑:时间-频率图上最常见的四个翻车点
4.1 边缘一圈“天蓝色蘑菇云”:没管边界锥区 COI
现象:时间-频率图左右两端出现一片渐变色带,形状像一朵低垂的蘑菇云,能量并不是真实信号带来的,却占了图面很大面积。
原因:小波变换本质上是通过卷积计算每个点的小波系数,信号两端的样本不足,算法只能靠补零来凑,补零区域会产生虚假的低频能量。这个区域就叫边界锥区,代码输出里的 coi 就是它的边界线。
解决:绘图时先按 coi 把无效区域置为 NaN,让它们在图上直接显示为空白:
valid = f >= coi; % f 是 nf×1, coi 是 1×nt,自动广播 wt_masked = wt; wt_masked(~valid) = NaN; pcolor(t, f, abs(wt_masked)); shading interp;逻辑说明:valid是一个布尔矩阵,每个时间点、每个频率都标记了它是否落在可信区;将无效区域改成 NaN 后,pcolor 会自动跳过 NaN 对应的网格,图面干净得多。提取特征时也要注意,任何峰值出现在 coi 外侧,都先怀疑是边界伪影而不是真实信号。
4.2 纵轴显示的还是尺度,不是频率
现象:跑完代码纵轴刻度写的是 1、2、4、8、16,老大难地看不出对应的 Hz 数,和图注里写的“频率(Hz)”完全对不上。
原因:用了老版本 cwt 接口或者自写实现,返回的是尺度向量,没有经过频率换算;内置新版 cwt 默认给你频率轴,但老代码里这种情况很常见。
解决:手动换算频率轴并覆盖图像刻度:
fa = center_freq * fs ./ scales; % 尺度转频率 imagesc(t, 1:numel(scales), abs(cfs)); set(gca, 'YTick', 1:numel(scales), ... 'YTickLabel', round(fa, 2)); % 标成频率逻辑说明:fa 的计算就是第 2 章的换算公式,关键点是./点除,尺度是向量时不能写/。round(fa, 2)避免刻度标签太长盖住图面。这道换算翻车频率极高,几乎每个自写 CWT 的初学者都要踩一次。
4.3 复数小波画出来像“猫耳朵”:没取 abs
现象:pcolor 画出来的图里有大量正负交替的细条纹,频率轮廓像猫的耳朵一样一块凸一块凹,整体响应完全看不出频率切换点。
原因:复数小波系数包含实部和虚部,直接pcolor(t, f, wt)时实部会在正负值之间震荡,视觉上就是一堆高频脏条纹;能量信息其实藏在模里。
解决:统一画幅度谱:
pcolor(t, f, abs(wt)); % 幅度谱,不是实部如果你关心相位信息,用angle(wt)单独画相位图,不要和幅度谱混在一张图上。血泪经验:从那以后我写 CWT 绘图代码,abs 永远紧跟 pcolor,写错一次等于白跑一夜。
4.4 信号一长就卡死:参数在爆算力
现象:几百万点的原始信号直接丢进 cwt,跑了五分钟没结果,MATLAB 内存占用持续上涨,最后要么卡死要么自动清空变量。
原因:CWT 的计算量是 尺度数×时间点数,而 VoPerOctave 默认 10 已经不小,如果 FrequencyLimits 还设成 0 到 fs/2,等于每个倍频程都算满,时间轴又不降采样,矩阵直接爆掉。
解决:先降采样、再限频带、最后压 VoicesPerOctave:
x_down = downsample(x, 10); % 降采样 10 倍,fs 同步除以 10 fs_new = fs / 10; [wt, f, coi] = cwt(x_down, fs_new, 'amor', ... 'VoicesPerOctave', 8, ... 'FrequencyLimits', [5 100]);逻辑说明:降采样是治本的一步,前提是目标频率远低于原 fs/2;比如信号最高关心的频率是 100 Hz,原采样率 1000 Hz 有 5 倍裕量,降采样到 200 Hz 完全不影响结果。VoicesPerOctave 降到 8 会牺牲频率细腻度,但作为初筛足够。跑通之后再针对感兴趣频段做一次高参数的精算。
提示:如果信号太长没法整段处理,就分段做 CWT,每段加汉宁窗后再拼接特征,别让一个巨大矩阵把内存占死。
5. 参数调优:让 CWT 结果既可信又可复现
5.1 母小波中心频率 vs 带宽:cmor1.5-0.5 这种参数怎么定
内置 cwt 支持直接指定 cmor 小波,比如'cmor1.5-0.5',字符串里第一个数字是带宽,第二个是中心频率。这两个数字直接影响时间-频率图的“胖瘦”:带宽越小,频率峰越锐利;中心频率越高,整体频率定位的基准点越准。
对比实验可以直接做:
[wt1, f1] = cwt(x, fs, 'cmor1.5-0.5', ... 'VoicesPerOctave', 24, 'FrequencyLimits', [5 300]); [wt2, f2] = cwt(x, fs, 'cmor3-1', ... 'VoicesPerOctave', 24, 'FrequencyLimits', [5 300]); subplot(2,1,1); pcolor(t, f1, abs(wt1)); shading interp; title('cmor1.5-0.5'); subplot(2,1,2); pcolor(t, f2, abs(wt2)); shading interp; title('cmor3-1');逻辑说明:同一段 50 Hz 转 120 Hz 的信号,母小波带宽不同,图中频率脊的厚度完全不同。带宽 0.5 时频率脊很细,定位精准;带宽 1 及以上时频率脊变粗,时间分辨率提升但频率上两个相邻频率可能糊成一片。中心频率 1.0 和 0.5 的区别还体现在整个频率轴的偏置上,选小了频率刻度整体下移,容易把低频信号画得位置偏高。
参数说明:我的习惯是先固定带宽 1.0、中心频率 1.0 跑一遍,观察频率脊是否落在已知频率附近,再逐步减小带宽让脊变细。注意带宽过小时计算会变慢,因为滤波器在频域更窄,需要更多尺度覆盖同样频带。
5.2 FrequencyLimits 和 VoicesPerOctave:一次把谱图调清晰
FrequencyLimits 的取值决定计算范围,VoicesPerOctave 决定频率轴插值密度,两个参数配合起来直接决定这张图是“糊成一团”还是“分得清清楚楚”。
经验值上,VoicesPerOctave 我这么用:快速验证用 10,常规分析用 16,出正式图片用 24 到 32。超过 32 之后,频率轴已经肉眼感觉不到变化,只有运行时间在涨。FrequencyLimits 的下限不要低于 0.5 Hz,否则边界锥区占用图面一半以上;上限不要超过你信号里真实关心的最高频率的 1.5 倍,留一点余量就够了。
最常见的误用是把 FrequencyLimits 设成 0 到 fs/2,等于告诉算法把全部频带无差别算一遍。结果是高频噪声区域占满色阶,真正关心的低频段被压到图底一条窄缝里。正确做法是先算一次全频带,再用cwtfreqbounds查看当前母小波的有效频段,然后收紧:
[flow, fhigh] = cwtfreqbounds('amor', fs);这个函数会返回当前母小波和采样率下的有效频率下界和上界,把 FrequencyLimits 设在它返回的范围内部,既能保留有效信息,又不会浪费算力在无效频段。
5.3 用配置结构体锁住结果:参数放一处,结果可复现
调试 CWT 的过程中最烦的不是调参,而是调完一轮之后忘了上一轮用的什么参数。我现在的做法是,任何一次正式分析,先把全部参数放进一个配置结构体:
param = struct( ... 'fs', 1000, ... 'wavelet', 'cmor1.5-0.5', ... 'voices', 24, ... 'freq_limit', [5 300], ... 'downsample', 1, ... 'noise_seed', 20240501 ... ); rng(param.noise_seed); % 锁住噪声种子然后所有后续调用都从 param 取值:
[wt, f, coi] = cwt(x, param.fs, param.wavelet, ... 'VoicesPerOctave', param.voices, ... 'FrequencyLimits', param.freq_limit);逻辑说明:把采样率、母小波、细分参数、频带范围集中到一处,复盘时只看 param 就能完全还原当时的计算条件,不用在脚本里到处翻。noise_seed尤其重要,模拟信号里加随机噪声时,没有它,每次跑出来的图标点位置都不同,根本没法对比小波参数差异。
参数说明:downsample 字段我一般预留为 1,只有处理长信号时才改成 2 或 5,这样脚本结构不变,只是加一行x = downsample(x, param.downsample)。出图保存时,文件名里带上母小波名和 voices 数值,比如cwt_cmor1.5-0.5_v24.png,找图不用靠记忆。
6. 收尾技巧:用频率脊线验证 CWT 的频率对准
参数调得再漂亮,也要回答一个最基本的问题:图上的频率峰位,真的是信号的真实频率吗?CWT 不是黑匣子,但它很容易让你在参数迷雾里越走越偏。我常用的验证手段是提取频率脊线,即每个时刻找幅度最大值对应的频率,看它是否落在原始信号的已知频率上。
以第 3 章那段 50 Hz 转 120 Hz 信号为例,脊线提取就三行:
[val, idx] = max(abs(wt), [], 1); % 每个时刻找最大幅度索引 ridge_freq = f(idx); % 索引转频率 mean(abs(ridge_freq(t<0.4) - 50)) % 前半段应接近 50 Hz mean(abs(ridge_freq(t>0.6) - 120)) % 后半段应接近 120 Hz逻辑说明:max(abs(wt), [], 1)沿频率维取最大幅度,得到每个时刻最显著频率的索引;f(idx)把索引映射成实际频率。前半段信号只有 50 Hz,脊线应该稳定在 50 Hz 附近,后半段应该稳定在 120 Hz 附近。如果偏差超过 5%,先查 FrequencyLimits 是否把目标频率切掉了,再查母小波中心频率是否选偏,最后查采样率设定。
这个技巧还能直接用来看 CWT 的时间分辨率:把plot(t, ridge_freq)画出来,50 Hz 转到 120 Hz 的突变点处,脊线从 50 跳变到 120 的斜率越陡,说明时间定位越准。如果你想追踪某一频段随时间变化的趋势,比如心电信号的 RR 间期,这就是基础。
当初我在某个振动信号项目里跑了整夜 CWT,啸叫频率脊线稳定偏了 3 Hz,找了半天才发现是中心频率参数写错。从那以后我每次跑完 CWT 都强行过一遍脊线验证,再进入画图和特征提取,参数全部锁进配置结构体,不再靠脑子记。希望帮到你。
本文还有配套的精品资源,点击获取