EMD-HHT时频谱可视化实战:从Sifting到HHT谱图绘制
2026/9/15 4:31:12 网站建设 项目流程

简介:这是一套面向 HHT(希尔伯特-黄变换)的 MATLAB 实现与可视化代码,聚焦 EMD 经验模态分解和希尔伯特谱分析,适合需要处理非线性、非平稳信号的研究人员、工程师及相关专业学生使用。压缩包采用 rar 格式,仅含 1 个 m 文件,整体大小 1KB,核心脚本将数据预处理、EMD 迭代分解、IMF 提取、残余项计算与希尔伯特谱绘制整合在一起,并提供原始信号、各 IMF 分量及谱图的直观展示。已有 643 人学习下载。运行该脚本,可以清晰看到 EMD 如何通过局部极值与 spline 插值逐层剥离本征模态函数,理解 HSA 如何获得瞬时频率与幅度,从而为地震波、生物医学信号、经济数据等复杂信号的时频分析搭建可直接复用的实验框架;对正在学习 HHT 原理或希望快速搭建分析工具的人来说,是一份紧凑、能上手的参考代码。

1. 把 EMD、HHT、时频图焊成一条线的可视化思路

emd_visu_hht_EMD_emd_visu_这个像函数名一样的长标题,其实对应着一条稳定的信号分析链:先用经验模态分解(EMD)把非平稳信号拆成一组频率由高到低的 IMF,再对每个 IMF 做 Hilbert 变换并叠出 HHT 时频谱,最后把时间域、IMF 列表和频谱结果放到同一幅图上互相印证。我在处理轴承振动、风速突变这类信号时,最常回答的问题是“FFT 已经够用了,为什么还要 HHT”。答案是瞬时频率比固定窗频谱更贴近物理过程,而emd_visu类可视化能让分解质量和频率变化一眼看出问题。接下来这套方案不依赖某份源码,是对这条分析链最短、最可复现的落地写法,适合做故障诊断、气象数据和生物电信号的工程技术人员。

2. EMD 分解原理与数据准备:先让 sifting 过程能复现

2.1 IMF 的两个硬条件与包络均值

EMD 的假设很朴素:任意复杂信号都可以看成若干本征模态函数(IMF,Intrinsic Mode Function)叠加。一个序列要被称为 IMF,必须同时满足两个条件。第一,在整个数据段里,极值点个数与过零点个数相等或至多相差 1;第二,在任意时刻,由局部极大值拟合的上包络与局部极小值拟合的下包络的均值为 0。这两个条件比“看起来像正弦波”严格得多,它保证了每个 IMF 都能做有意义的 Hilbert 变换。

找出 IMF 的过程叫筛分(sifting),每轮做三件事:用三次样条分别连接所有局部极大值和局部极小值,形成上下包络;计算上下包络的平均值;让原始序列减去这个平均包络,得到新序列,然后重复。这个减法在去除低频骑行分量的同时,也在不断修正波形,直到上下包络对称到肉眼几乎看不出偏差。工程上常用停止条件 SD 来卡住迭代:SD 等于相邻两次筛分结果的能量差归一化值,取值在 0.1 到 0.3 之间。SD 设得越小,IMF 越光滑、瞬时频率越连续,但计算量和对端点振荡的敏感度都会上升。

2.2 一个能用的 sifting 最小实现

很多封装库把筛分过程封装成了黑盒,排错时反而难下手。我习惯先在本地用 NumPy 加 SciPy 把 sifting 循环写出来,再替换成 PyEMD,这样每一步都能打印包络和均值。下面这段不是完整生产代码,但可以直接跑通并验证“每条 IMF 都满足包络均值接近 0”的结论。

import numpy as np from scipy.signal import argrelmax, argrelmin from scipy.interpolate import CubicSpline def envelope_mean(x): n = len(x) max_idx = argrelmax(x, order=2)[0] min_idx = argrelmin(x, order=2)[0] # 极值点不足两个时直接返回原序列,避免端点处样条外插失控 if len(max_idx) < 2 or len(min_idx) < 2: return x.copy(), np.zeros(n) up = CubicSpline(max_idx, x[max_idx])(np.arange(n)) low = CubicSpline(min_idx, x[min_idx])(np.arange(n)) return x - 0.5 * (up + low), up - low def sift_emd(x, max_imfs=6, sd=0.2): residue = x.copy() imfs = [] for _ in range(max_imfs): proto = residue.copy() for _ in range(200): prev = proto.copy() proto, _ = envelope_mean(proto) # 用相邻两次能量差判断是否收敛,而不是只看包络均值 energy = np.sum((prev - proto) ** 2) / (np.sum(prev ** 2) + 1e-12) if energy < sd: break imfs.append(proto) residue = residue - proto if np.std(residue) < 1e-8: break return np.array(imfs), residue if __name__ == "__main__": fs = 1000 t = np.linspace(0, 1, fs, endpoint=False) # 模拟 3Hz 调幅的 50Hz 载波叠加上 7Hz 正弦 x = (1 + 0.3 * np.sin(2 * np.pi * 3 * t)) * np.sin(2 * np.pi * 50 * t) \ + 0.8 * np.sin(2 * np.pi * 7 * t) imfs, r = sift_emd(x, max_imfs=6, sd=0.2) print("IMF 数量:", len(imfs), "残差能量占比:", f"{np.mean(r**2) / np.mean(x**2):.2e}")

代码里的order=2用于限制极值点间距,避免噪声制造出密集伪极值。envelope_mean返回均值和包络差,方便调试时画出上下包络曲线。停止条件取相邻能量差小于sd,这比固定迭代次数更合理,因为对采样率高的信号,可能 30 次就收敛,对低频主导的信号却要跑 100 次以上。max_imfs只是硬性上限,实际分解会在残差能量趋近零时提前结束。

2.3 数据准备:单位、长度、采样率如何影响 HHT 后续结果

EMD 对数据格式的要求比 FFT 更苛刻。采样率必须写进后续的 Hilbert 变换和频谱绘制里,否则瞬时频率算出来是无量纲的弧度步长。更隐蔽的问题是数据长度:少于 200 个点时包络样条会非常不稳定,我一般要求至少 1000 点,也就是 10 个以上完整周期;数据过长也会让 EMD 计算负担变大,超过 10 万点时应先分段或降采样。

单位也值得统一。加速度计输出的是重力加速度 g,气象站输出的是米每秒,电压传感器输出的是伏特。EMD 对幅值单位不敏感,但后续计算边际谱能量时,幅值平方意味着单位会变成 g²/Hz 或 (m/s)²/Hz,这个单位要写进图标注里,否则报告上的 Y 轴只是一个没有物理含义的数字。常见做法是先把信号减去均值,再除以标准差做 Z-Score 标准化。标准化会让瞬时频率保持不变,但 HHT 谱的幅值会变成相对强度,好处是不同测点之间可以直接比较。

注意:不要对信号做带通滤波后再交给 EMD。滤波会改变极值分布,导致分解出的 IMF 与原始物理模态脱节。如果必须去噪,只做 50Hz 工频陷波或去趋势,且把处理过程记录清楚。

3. 用 emd_visu_hht_EMD 绘制 HHT 时频谱

3.1 Hilbert 变换与瞬时频率:为什么不能直接用 FFT

对第 2 章分解出的每条 IMF 做 Hilbert 变换,得到一个解析信号,就能在任意时刻算出幅值和相位。瞬时频率定义为相位的导数,它反映的是“这一刻振荡多快”,而不是一段窗口里的平均频率。FFT 把信号当成无限周期信号的正弦叠加,对突变型信号会摊平能量;短时傅里叶变换(STFT)用固定窗长,窗口短则频率分辨率差,窗口长则时间定位差。HHT 的路径不同:先筛掉非平稳成分,再做相位求导,频率和时间都没有被窗函数约束。

emd_visu_hht_EMD的核心不是某个特殊算法,而是把“瞬时频率会不会出现负值”“边缘有没有频率跳变”这类结果可视化出来。瞬时频率为负值是常见问题,原因通常不是 Hilbert 变换出错,而是该 IMF 没有真正满足包络均值条件。这时要回头调大 sifting 迭代次数或减小 SD。可视化是把诊断和结果展示合二为一的关键步骤。

3.2 最小可运行绘图代码:IMF 波形与 HHT 谱同画

下面代码读取上一步分解出的imfs,把每条 IMF 的时间波形、瞬时幅值包络和时频谱放进同一张图。

import numpy as np import matplotlib.pyplot as plt from scipy.signal import hilbert def plot_emd_hht(imfs, fs=1000, t=None, freq_bins=128): n_imfs = len(imfs) n = imfs.shape[1] if t is None: t = np.arange(n) / fs fig = plt.figure(figsize=(12, 2 * n_imfs + 2)) spec_ax = None for i, imf in enumerate(imfs): analytic = hilbert(imf) amp = np.abs(analytic) phase = np.unwrap(np.angle(analytic)) # 用中央差分计算瞬时频率,首尾各补一个点 inst_freq = np.zeros_like(phase) inst_freq[1:-1] = np.diff(phase, 2) / (2 * np.pi / fs) inst_freq[0] = inst_freq[1] inst_freq[-1] = inst_freq[-2] ax_wave = fig.add_subplot(n_imfs + 1, 3, i * 3 + 1) ax_wave.plot(t, imf, lw=0.8) ax_wave.plot(t, amp, lw=0.6, linestyle="--", color="red") ax_wave.set_ylabel(f"IMF{i + 1}") # 将 (时间, 瞬时频率, 幅值) 统计到二维直方图,即 HHT 谱的雏形 freq_ok = np.isfinite(inst_freq) & (inst_freq > 0) H, xedges, yedges = np.histogram2d( t[freq_ok], inst_freq[freq_ok], bins=[n // 100, freq_bins], weights=amp[freq_ok] ** 2 ) if spec_ax is None: ax_spec = fig.add_subplot(n_imfs + 1, 3, (i + 1) * 3 - 1) spec_ax = ax_spec else: ax_spec = fig.add_subplot(n_imfs + 1, 3, (i + 1) * 3 - 1) pcm = ax_spec.pcolormesh( xedges, yedges * fs / (2 * np.pi), H.T, shading="auto", cmap="magma" ) ax_spec.set_xlim(t[0], t[-1]) ax_spec.set_ylim(0, fs / 2) ax_spec.set_yticks(np.linspace(0, fs / 2, 5).astype(int)) fig.suptitle("emd_visu_hht_EMD: IMF waveforms and HHT spectrum") fig.colorbar(pcm, ax=spec_ax, label="Energy (a.u.)") return fig if __name__ == "__main__": fs = 1000 t = np.linspace(0, 1, fs, endpoint=False) x = (1 + 0.3 * np.sin(2 * np.pi * 3 * t)) * np.sin(2 * np.pi * 50 * t) \ + 0.8 * np.sin(2 * np.pi * 7 * t) imfs, _ = sift_emd(x, max_imfs=6, sd=0.2) fig = plot_emd_hht(imfs, fs=fs, t=t) fig.savefig("emd_hht_spectrum.png", dpi=150)

这段代码把每条 IMF 的瞬时幅值包络画成红虚线,便于观察包络是否平稳。np.histogram2d的作用是把不规则的瞬时频率点聚合成规则网格,时间方向以每 10 个样本为一个网格,频率方向由freq_bins指定。权重取幅值平方,对应能量密度,所以 HHT 谱的颜色深浅代表瞬时能量的强弱。yedges * fs / (2 * np.pi)是把“每样本弧度”换算成 Hz 的关键一步,漏掉这个换算会让频率轴全部偏小。

3.3 频率分辨率与网格参数的取舍

HHT 谱的频率分辨率不是固定值,它由瞬时频率的离散化步长决定。瞬时频率本身是连续的,但直方图会按freq_bins量化。freq_bins偏大时谱图出现大量空洞,偏小时会把 49Hz 和 51Hz 混在一起。经验上把freq_bins设为采样率除以 2 再除以最小可分辨带宽。比如采样率 1000Hz,目标分辨 2Hz,就取 250 个频点。

时间方向的网格也有讲究。n // 100表示把 1000 个样本分成 10 段,每段有 100 个样本参与统计。对冲击性故障信号,时间网格应该更细,比如n // 200;对平稳旋转机械,可以粗到n // 50。网格过细会让同一个瞬时频率点被打散,谱图看起来像麻点。另一个常见问题是瞬时频率阵首尾各补了一个值,这两个点如果落在 0 频率附近,会把谱图底部拉出一条横线。处理办法是绘制时舍去前 5% 和后 5% 的时间区间,代价是图谱两侧各少一小段。

4. EMD 可视化中的边界条件、模式混叠与排错

4.1 端点效应的三种处理方式

EMD 的样条包络在数据首尾没有极值约束,拟合曲线会大幅外插,导致首尾的 IMF 明显摆动,HHT 谱图两端出现非物理的高频或负频率。处理端点效应有三种常见做法。第一种是镜像延拓,把数据左端向右翻转复制、右端向左翻转复制,让极值点变成周期性的,延拓长度通常取两端各一个显著极值点的距离。第二种是添加特征波形,在首尾各接上一小段与数据自身周期相近的正弦波,接缝处用余弦窗加权平滑。第三种最简单,直接在 sifting 循环里把首尾各 5% 的样本排除在包络拟合之外,只做校验不参与差值计算。

我一般优先用镜像延拓,因为它的参数最少,不会像正弦延拓那样需要估计频率。但镜像延拓要求数据两端斜率不要太大,否则镜像出来的极值位置仍不准确。如果 HHT 谱图两端还是上下乱跳,就在绘制时主动裁掉前 5% 和后 5%,工程报告里注明“边缘截断窗口”即可。没有延拓能彻底消除端点效应,这个认知比任何参数都重要。

4.2 模式混叠:mask 与集合 EMD 的取舍

模式混叠是 EMD 最典型的失败模式:一段频率接近的分量被拆进两条 IMF,或者一条 IMF 里同时装着相差较大的两个频率。直观表现是 IMF 波形间歇性消失又出现,HHT 谱上原本该是连续的一条频率带被打断成碎片。混叠的根源是极值点在时间轴上被强信号“淹没”,样条包络无法分辨出弱信号的极值。

解决模式混叠有两条路线。mask 方法加入一个高频正弦试探信号,让强信号的极值密度被重新分割,分解完再把试探信号减去;集合经验模态分解(EEMD 或 CEEMDAN)则反复注入白噪声并取平均,用它来填满频率缝隙。mask 方法速度快、可解释性强,缺点是掩膜幅值和频率要靠试,一般取目标高频率分量的 2 到 5 倍;EEMD 更自动化,但会引入噪声残留,且计算量是原始 EMD 的几十倍。在emd_visu_hht_EMD工作流里,先用 HHT 谱看碎片位置,再决定上哪种方案,比直接套用 EEMD 更省算力。

4.3 emd_visu 参数对照表与常见误用

写代码很容易,调参才是 HHT 可视化里真正的成本。下面这张表来自我多次试错后的经验值,适用于采样率在 256Hz 到 10kHz 的振动与生物电信号。

症状根因调整方向
首尾 IMF 幅值突然放大端点效应增加镜像延拓长度,或绘图时裁掉首尾 5% 区间
瞬时频率出现负值SD 设置过大导致包络不均将 SD 从 0.3 降到 0.1,提高最大迭代次数
HHT 谱出现横向条纹freq_bins设置过大减小freq_bins使每个频格覆盖更宽带宽
高频分量和低频分量挤在同一条 IMF模式混叠对目标频带做 mask EMD,或改用 CEEMDAN
谱图上能量判断不出来幅值未归一化对输入信号做 Z-Score 标准化后再分解
分解出的 IMF 数量超过 8 条将噪声当信号分解设定max_imfs=min(8, log2(n))并检查残差占比

另一个常见误用是把 EMD 当滤波器:希望靠丢弃前面几条 IMF 来去除高频噪声。实际上前几条 IMF 很可能包含真实物理特征,直接丢弃会把短时冲击的尖峰削平。正确做法是保留所有 IMF,把 HHT 谱的边带噪声单独做阈值处理,例如只显示能量大于最大能量 2% 的区域。

注意:如果两条 IMF 的瞬时频率曲线几乎重合,说明分解过分解或停止条件太松。此时不应人工合并 IMF,而是重新设置更小的 SD 值,增加 sifting 迭代次数。

5. 用正交性与边际谱验证,再进入故障特征提取

5.1 快速验证 IMF 是否“干净”

分解完成后别急着看图说话,先跑一段正交性检查。好的 EMD 分解要求任意两条 IMF 之间近似正交,也就是内积量级远小于各自能量。同时所有 IMF 加残差应该能还原原始信号,还原误差能量占比通常在 1e-6 量级。如果还原误差明显偏大,说明 sifting 循环提前退出或某条 IMF 被错误迭代。

def check_emd_quality(x, imfs, residue): recon = np.sum(imfs, axis=0) + residue rel_err = np.sqrt(np.mean((x - recon) ** 2)) / (np.sqrt(np.mean(x ** 2)) + 1e-12) n = len(imfs) orth = 0.0 for i in range(n): for j in range(i + 1, n): # 用 Pearson 相关系数代替内积,单位无关且更直观 coef = np.corrcoef(imfs[i], imfs[j])[0, 1] orth += abs(coef) return rel_err, orth / (n * (n - 1) / 2) rel_err, avg_coef = check_emd_quality(x, imfs, r) print("重构误差:", f"{rel_err:.1e}", "IMF平均相关系数:", f"{avg_coef:.3f}")

这段代码返回两个指标:重构误差和平均相关系数。重构误差大于 1e-4 时先检查残差标准差;平均相关系数大于 0.1 时,大概率存在模式混叠。注意相关系数不等于正交性,但在同一数据尺度下,它比内积更容易被非技术人员理解。报告里把这两个数字贴出来,比贴十张 HHT 谱图更有说服力。

5.2 边际谱与 FFT 幅值谱的差别

HHT 谱是时间和频率的二维函数,把时间方向积分,得到的就是边际谱。它表示每个频率点在观测时间内积累的总能量。边际谱与 FFT 谱很像,但含义不同:FFT 幅值谱描述的是固定频率分量的幅值,而边际谱描述的是瞬时频率落在某个区间的概率密度乘能量。对频率随时间变化的信号,FFT 会在基频周围展开一段边带,边际谱则把能量压缩在一个更窄的频率带内,这个压缩特性正是故障诊断里区分松旷和磨损的关键指标。

计算边际谱时不要直接对每条 IMF 的频率直方图求和,而是对 HHT 谱矩阵做np.sum(H, axis=0),然后按频率网格画阶梯图。这个计算思路简单,却能暴露一个隐藏陷阱:瞬时频率小于 0 的点会被histogram2d丢弃,如果丢弃比例超过 5%,说明 Hilbert 变换阶段就不干净,边际谱会整体少一块能量。

5.3 一个有用的双层拆解实验

最后一招是把可视化工具变成验证工具。对同一段数据做两次分解:第一次用常规sd=0.2,第二次用更严格的sd=0.1并把最大迭代次数翻倍。然后把两次 HHT 谱相减,得到差分谱。差分谱上能量集中的位置就是对停止条件最敏感的区域,那里通常不够稳定,也是现场分析真正需要关注的位置。比如轴承早期故障,差分谱会在某个特征频率附近出现白色条带,而其他位置接近零。用这个方法,emd_visu_hht_EMD就不只是展示工具,而是能反推算法参数合理性的诊断装置。

本文还有配套的精品资源,点击获取

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

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

立即咨询