信号转图像实战:从时频分析到故障诊断的完整指南 📅 发布时间:2026/9/16 1:57:33 👁 浏览次数: 在工业现场做设备诊断那几年我最大的感受就是光靠耳朵去听轴承的异响或者盯着示波器上一堆乱麻一样的波形真的会把人逼疯。同样一段振动信号健康状态下和出现点蚀故障时在时域波形上看顶多是幅值大了点但到底哪里有毛病、故障特征频率是多少根本说不清。直到我把信号转成图像再去做时域频域分析很多问题一下子就明朗了。这个思路说白了就一句话把一维时间序列变成二维图像让人眼和深度学习模型都能直观地“看见”信号里藏着的秘密。它的应用面极广从音频分类、语音识别到机械故障诊断、生物医学信号处理再到地震波分析几乎凡是跟波形打交道的领域都能用上这套方法论。这篇文章我会从原理讲到实操从代码写到踩坑把我这几年积累的经验完整拆给你看。1. 内容整体设计与思路拆解1.1 为什么非要把信号转成图像先回答一个最基础的问题原始信号是时间序列直接分析不行吗不是不行而是很多场景下“不够用”。拿振动信号举例。一段正常的电机振动信号频谱上主要是转频及其谐波一旦轴承外圈出现裂纹高频段会出现冲击成分同时调制出边频带。这些东西在时域波形上往往表现为“信号变毛了”“幅值变大了”但你很难从波形上直接读出“外圈故障特征频率是123.4Hz”这种精确结论。傅里叶变换能把信号拆成频率成分但它只告诉你“有哪些频率”却把“这些频率在什么时间出现”这个关键信息给丢了。现实中的绝大多数信号都是非平稳的频率成分随时间动态变化。语音里的共振峰、设备启动阶段的转速爬升、脑电信号里的突发棘波全是时变特征。这时就需要一种既能看频率又能看时间的分析工具而把信号变成图像恰恰是把这种时变信息可视化的最佳途径。更深一层的原因是深度学习时代“图像”是一种极度成熟的载体。CNN、Vision Transformer这些模型在图像分类、目标检测上已经被验证得足够可靠配套的数据增强、迁移学习工具链也非常完善。如果能把信号问题转化成图像识别问题就能直接借用整个计算机视觉领域的技术红利。这也是为什么“信号转图像”在近年的故障诊断、语音识别论文里几乎成了标配操作。1.2 主流的信号图像化路线有哪些业内常用的信号转图像方法我帮你盘一下每条路线的适用场景和坑都不太一样。短时傅里叶变换STFT/语谱图把长信号切成一帧一帧对每帧做FFT然后把时间、频率、幅值三个维度拼成一张二维图。这是最经典、最通用、最不容易出错的方案适合绝大多数场景。梅尔频谱图在STFT基础上把频率轴按梅尔刻度重新映射模拟人耳对频率的非线性感知。语音领域最常用因为人耳本来就不是线性分辨频率的。小波变换/尺度图用可变窗口替代STFT的固定窗口低频段用长窗、高频段用短窗兼顾时间和频率分辨率。适合处理瞬态冲击成分明显的信号比如轴承故障、心电信号。循环谱/包络谱图针对调制类故障先做包络解调再做频谱分析能突出故障特征频率。我当年做齿轮箱诊断时经常用这招。格拉姆角场/马尔可夫变迁场把时间序列编码成二维矩阵属于相对小众但效果惊艳的路线在时间序列分类任务里表现不错但计算量偏大解释性也不如时频图直观。我自己用得最多的还是STFT语谱图和梅尔谱原因很简单原理清晰、参数可控、工业落地容易而且符合直觉。你不需要当数学家就能理解这张图在说什么。下面重点讲这条路线。1.3 时域频域分析的核心价值有的朋友可能会问图文并茂说半天我做时域频域分析到底图什么我总结成三个作用。一是降维观察。原始信号动辄几万个采样点人眼根本看不过来但一张语谱图上横轴是时间、纵轴是频率、颜色代表能量整个信号的能量分布一目了然。哪里有了冲击、哪个频段出现了异常、持续时间多长全都在一张图里。二是特征增强。很多微弱故障特征在时域波形里被噪声淹没但在频域或时频域里会凸显出来。语谱图相当于在信号里做了一次“特征放大”让原本看不见的细节变得可见。三是打通深度学习的入口。图像化以后你可以用预训练的CNN提取特征甚至直接用现成的图像分类框架做端到端训练省去了大量手工特征工程的功夫。这也是近些年论文里比较主流的技术路线。2. 核心细节解析与实操要点2.1 STFT的原理与参数含义STFT的数学表达式很简单就是对信号加窗后做傅里叶变换X(t, f) ∫ x(τ)w(τ - t)e^(-j2πfτ)dτ通俗讲就是把信号乘以一个时间有限的窗函数窗函数的位置不断移动每移动一次就得到这一小段信号在各个频率上的幅值。把这些幅值按时间顺序排列就是一张语谱图。这里有几个参数你必须搞明白因为它们直接决定了图像的长相和信息量。窗长nperseg每一次FFT处理的样本点数。窗越长频率分辨率越高频率轴上每个bin的宽度越小但时间分辨率越差窗越短则反之。重叠率overlap相邻两帧之间的重叠样本数。重叠越多时间轴越平滑但计算量也越大。一般取窗长的50%~75%比如窗长1024点就设noverlap768或512。FFT点数nfft做FFT时的点数。它不必等于窗长可以做零填充来获得更细的频率网格但注意这不增加真实分辨率只是让图像看起来更平滑。窗函数类型矩形窗频率泄漏大汉宁窗、海明窗是折中最优布莱克曼窗旁瓣衰减更大但主瓣更宽。一般语音和振动分析首选汉宁窗。2.2 时间分辨率与频率分辨率的博弈这是整个STFT里最核心的权衡新手最容易栽在这儿。海森堡测不准原理在信号处理里的体现就是窗长决定的时间分辨率和频率分辨率是一对矛盾你不可能同时获得高时间分辨率和高频率分辨率。一个直觉化的类比你想知道演奏者在一首曲子里的每个音符的精确起止时间同时还想精确分辨每个音符的实际音高。如果拿一个非常细的梳子去梳时间轴你会发现能精确定位音符的起止但音符到底是一个音还是旁边跟着一个装饰音你很难分清反过来拿一把宽梳子能看清整体音高结构但单个音符的起始时间就模糊了。窗长就是这个梳子的齿距。实操中怎么选记住一个经验法则先估一下你要关注的最低频率然后保证窗长至少能覆盖几个周期。比如关注的目标特征频率是50Hz采样率是10kHz一个周期是200个采样点那么窗长至少要400~1000点才能把50Hz附近的频率分辨出来。如果想看启动过程的转速爬升动态变化快就牺牲频率分辨率换取时间分辨率窗长取128~256点就够了。2.3 梅尔谱与普通语谱图的差异梅尔频谱图是在语谱图基础上做了频率轴的非线性变换。梅尔刻度的公式是mel 2595 × log10(1 f/700)人耳对低频的辨识能力强于高频1000Hz以下接近线性1000Hz以上按对数增长。梅尔滤波器组就是一组按梅尔刻度均匀分布的三角滤波器把原始频谱能量映射到梅尔刻度上。这么做的直接好处是特征维度大幅降低。原始语谱图如果是1025个频率bin梅尔谱通常只保留40~128个梅尔频带数据量小了一个数量级训练起来更快且更贴合人耳感知特性。语音识别、音乐信息检索领域几乎统一用梅尔谱。但如果你做的是工业故障诊断频率轴需要精确到Hz级普通语谱图反而更直观因为梅尔刻度会扭曲频率之间的精确距离。2.4 小波变换和图谱的补充思路小波变换解决的是STFT窗口固定不变的痛点。它通过一个可伸缩平移的母小波在高频处用窄窗获得好时间分辨率在低频处用宽窗获得好频率分辨率因此对瞬态冲击更加敏感。对轴承故障诊断来说滚动体撞击缺陷点会产生非常短促的冲击脉冲这种信号在STFT里会被窗函数拖得模糊但在小波尺度图里非常锐利。所以如果你处理的信号有明显的瞬态成分不妨试一下小波变换。代价是计算量偏大、参数选择小波基函数、尺度取值范围更复杂对小白的友好度不如STFT。2.5 图像尺寸、颜色与保存标准的统一这是把信号转图像时最影响后续建模的细节也是很多论文复现里最容易被忽视的地方。你要喂给CNN的图像必须统一否则模型学到的不是信号特征而是画幅大小特征。先说尺寸。一次STFT得到的图像宽高取决于信号长度和参数。训练时通常统一缩放到224×224或256×256。但注意直接在参数层面调整窗长、hop、nfft来粗暴凑尺寸会破坏时频分辨率特性正确做法是先按标准参数生成完整语谱图然后再用图像缩放或中心裁剪到目标尺寸。再说颜色。语谱图本质是单通道的幅度值画出来的时候用伪彩色只是为了人眼看图更舒服。给模型训练时要注意要么统一用灰度图要么统一用同一种colormap映射后保存成三通道RGB不能混用。我曾经见过团队一半数据用jet映射一半用viridis导致模型在验证集上异常但一直找不到原因。这事看似无关紧要实际坑得很。最后是幅值缩放。振幅动态范围很大直接用线性幅值做图微弱细节全被强成分淹没。标准做法是取对数比如20log10(|X|)把动态范围压缩到可显示的区间。还可以做归一化映射到[0,1]或[0,255]。3. 实操过程与核心环节实现3.1 环境准备与依赖安装推荐直接用Python生态轻量、社区活跃、坑少。我做这个最常用的库是scipy.signal做时频变换librosa做音频加载和梅尔特征matplotlib做图像渲染numpy做数值计算。再加一个PIL做后处理缩放。如果你用的是conda环境一次性安装到位conda create -n sig2img python3.10 -y conda activate sig2img pip install numpy scipy matplotlib librosa pillowlibrosa体积较大如果只是做STFT和梅尔谱scipy的signal模块完全够用还能少装一个重依赖。不过librosa提供了很多数据集加载、滤波、增广的便利函数省心不少看你个人取舍。3.2 合成测试信号从正弦波到冲击脉冲工欲善其事必先利其器。为了说明整条流程我先生成一段模拟的机械振动信号基频50Hz的旋转分量叠加一个200Hz的调制边带再加上每隔0.5秒出现一次的冲击脉冲最后混入一点噪声。import numpy as np from scipy import signal import matplotlib.pyplot as plt fs 2000 t np.arange(0, 4, 1/fs) rng np.random.default_rng(42) # 基频分量 二次谐波 sig 1.0 * np.sin(2*np.pi*50*t) 0.4 * np.sin(2*np.pi*100*t) # 冲击脉冲每0.5秒一个衰减振荡 impulse_ts np.arange(0.2, 4, 0.5) impulse_amp 0.8 for it in impulse_ts: idx int(it * fs) length int(0.05 * fs) if idx length len(t): envelope np.exp(-np.arange(length)/ (0.005*fs)) sig[idx:idxlength] impulse_amp * envelope * np.sin(2*np.pi*800*np.arange(length)/fs) # 高斯白噪声 sig 0.05 * rng.standard_normal(len(t)) plt.figure(figsize(12, 3)) plt.plot(t, sig) plt.xlabel(Time [s]) plt.ylabel(Amplitude) plt.title(Raw Vibration Signal) plt.xlim(0, 1) plt.tight_layout() plt.show()这个信号模拟的场景很典型转频50Hz是轴的旋转频率100Hz是二倍频800Hz的衰减振荡是轴承冲击的共振响应。你用肉眼在时域图上可能能看出“有些地方毛刺变多”但无法准确说出冲击的重复频率。别急图像化之后就清楚了。3.3 从波形到语谱图核心代码拆解接下来是重头戏用scipy.signal.stft把这段信号变成语谱图。f, t_stft, Zxx signal.stft( sig, fsfs, windowhann, nperseg256, noverlap192, nfft512, scalingspectrum, boundaryNone, paddedFalse )参数说明nperseg256对应时间窗长256/fs128毫秒因为采样率2kHz每个分析窗内有5个50Hz周期频率分辨率约7.8Hz足够区分50Hz和100Hz。noverlap192是75%重叠时间轴更平滑。nfft512做了零填充让频率轴看起来更细腻但实际并未增加物理分辨率。scalingspectrum使得幅值对应真实信号的幅度谱。得到的Zxx是个复数矩阵取模平方得到功率谱然后转对数刻度power np.abs(Zxx) ** 2 log_power 10 * np.log10(power 1e-10) plt.figure(figsize(10, 6)) plt.pcolormesh(t_stft, f, log_power, shadingauto, cmapjet) plt.xlabel(Time [s]) plt.ylabel(Frequency [Hz]) plt.title(STFT Spectrogram (Log Power)) plt.colorbar(labeldB) plt.ylim(0, 1000) plt.tight_layout() plt.savefig(spectrogram.png, dpi150) plt.show()跑完这段你会看到图上非常清晰地出现三条水平亮线50Hz、100Hz处稳定存在800Hz处每0.5秒出现一个断续的亮斑。之前时域里“毛刺变多”的模糊直觉到这里就变成了可量化的“每隔0.5秒的频率特征”。这就是图像化的力量。3.4 梅尔频谱图的生成与保存如果做语音相关任务建议直接用梅尔谱。这里给出一个librosa风格的实现思路。为了减少依赖我不用librosa重函数用一个简洁的梅尔滤波器组实现但在实际项目中直接调librosa更省事import librosa y, sr librosa.load(vibration.wav, sr2000, monoTrue) mel_spec librosa.feature.melspectrogram( yy, srsr, n_fft512, hop_length64, win_length256, windowhann, n_mels64, power2.0 ) log_mel librosa.power_to_db(mel_spec, refnp.max) plt.figure(figsize(10, 6)) librosa.display.specshow(log_mel, srsr, hop_length64, x_axistime, y_axismel) plt.colorbar(format%2.0f dB) plt.title(Mel Spectrogram) plt.tight_layout() plt.savefig(mel_spectrogram.png, dpi150) plt.show()注意几个点hop_length64也就是每帧移动64个采样点时间轴上的清晰度由它决定。n_mels64表示把整个频带压缩成64个梅尔频带这个值不是越大越好过大反而引入冗余信息。保存时建议固定dpi和尺寸保证后续数据集统一。3.5 小波时间尺度图的快速实现对小波变换最省心的方式是直接用scipy.signal.cwt配合莫雷小波。我给出一个快速版本from scipy.signal import morlet2 from scipy.signal import cwt widths np.arange(1, 200) wavelet morlet2(64, w5.0) cwtmatr cwt(sig, wavelet, widths) plt.figure(figsize(10, 6)) plt.imshow(np.abs(cwtmatr), extent[0, 4, 1, 200], cmapjet, aspectauto) plt.xlabel(Time [s]) plt.ylabel(Scale) plt.title(CWT Scalogram) plt.colorbar(labelMagnitude) plt.ylim(1, 200) plt.tight_layout() plt.savefig(cwt.png, dpi150) plt.show()尺度轴和频率轴是反比关系小尺度对应高频率大尺度对应低频率。从图上你会发现800Hz的冲击在小尺度上方形成一条断续的亮带50Hz的转频在大尺度下方显示为稳定亮带。小波图对瞬态冲击的显示比STFT更锐利代价是纵向坐标需要习惯一下。3.6 把多段信号批量转为数据集单个文件分析只是练手真正常用的是批量生成数据集。假设你有一个文件夹里面按类别存放多段信号文件from pathlib import Path import pandas as pd def signal_to_image_path(signal_data, fs, out_path, nperseg256, noverlap192, nfft512): f, t, Zxx signal.stft(signal_data, fsfs, windowhann, npersegnperseg, noverlapnoverlap, nfftnfft, scalingspectrum, boundaryNone, paddedFalse) log_power 10 * np.log10(np.abs(Zxx) ** 2 1e-10) plt.imsave(out_path, log_power, cmapgray, originlower, dpi128) def build_dataset(root_dir, save_dir, class_label, file_listNone): root_dir Path(root_dir) save_dir Path(save_dir) save_dir.mkdir(parentsTrue, exist_okTrue) all_files list(root_dir.glob(*.wav)) if file_list is None else file_list for wav_file in all_files: y, sr librosa.load(wav_file, srNone, monoTrue) seg_len sr * 2 # 每段2秒 n_segments max(1, len(y) // seg_len) for i in range(n_segments): seg y[i * seg_len : (i 1) * seg_len] out_path save_dir / f{class_label}_{wav_file.stem}_{i:03d}.png signal_to_image_path(seg, sr, out_path) print(f类别 {class_label} 共生成 {n_segments} 张图)这里的关键设计是分段切割把长信号切成长度相等且短于原长的片段每段独立成图。好处有两点一是数据量放大二是模型输入尺寸可控。切分时注意段与段之间可以留一点重叠避免把瞬态事件正好切在边界上导致信息丢失。3.7 图像化后如何接入深度学习模型图像生成之后接CNN的训练流程就和普通图像分类一模一样。我贴一段用torchvision实现的小型CNN训练代码骨架import torch import torch.nn as nn import torchvision.transforms as T from torchvision.datasets import ImageFolder from torch.utils.data import DataLoader transform T.Compose([ T.Resize((224, 224)), T.ToTensor(), T.Normalize([0.5], [0.5]) ]) dataset ImageFolder(data/spec_images, transformtransform) loader DataLoader(dataset, batch_size32, shuffleTrue) model nn.Sequential( nn.Conv2d(3, 32, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding1), nn.ReLU(), nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(128, len(dataset.classes)) )训练环节有两点提醒第一数据增强要有时频意识。普通图像增强像随机裁剪、翻转在语音和振动任务里不完全适用尤其是垂直翻转会把频率轴翻转成完全物理上不可能的信号。推荐使用SpecAugment思路时间轴和频率轴做掩码。第二迁移学习要慎重。很多从事图像任务的朋友习惯用ImageNet预训练权重。但语谱图是灰度密度图和自然图像的特征相差极大。我的经验是先用小模型从头训效果不理想再考虑迁移。迁移时务必把所有输入改成单通道并调整第一层卷积权重或者把灰度图复制成三通道。3.8 数据增强的时频专用技巧这里单独聊聊适合语谱图的数据增强因为这是实际效果提升最明显的环节。时间掩码Time Masking随机选择一段时间区间把时间段内的数据置零。相当于模拟信号在某个时间段被干扰或丢失。频率掩码Frequency Masking随机选择一段频率区间把该频段的数据置零。相当于模拟某个频段被强噪声淹没。时间拉伸把整张图在时间轴上缩放。注意要同步调整频率坐标保持物理一致性。噪声注入直接对原始信号加随机噪声再重新生成语谱图比在图像上加噪声更物理真实。我的建议是时序相关的变换优先在信号域做频域和时域的掩码在图像域做。这样既保证物理一致性又降低计算成本。4. 常见问题与排查技巧实录4.1 语谱图时间轴和频率轴对不上这是刚上手时最常碰到的疑惑。图生成了横轴时间范围却和原始信号时长不一致或者频率轴范围不是预期的0到采样率的一半。原因通常出在STFT的时间轴计算方式和pcolormesh的映射上。signal.stft返回的t已经是每个帧的中心时间只要直接用pcolormesh(t_stft, f, ...)就不会错。频率轴上限等于(nfft/2 1)个bin对应的最大频率是fs/2也就是奈奎斯特频率。检查一下你的nfft是否等于采样率的一半再乘2。如果你用plt.imsave保存图像却不指定extent坐标信息会丢失所以保存时要么把坐标写进文件名要么做成可配置的meta信息。4.2 图像太暗或太亮导致细节丢失语谱图的动态范围问题很容易被忽视。如果你直接用线性幅值画图能量大的频带会压掉一切微弱细节整张图看起来像是一坨亮斑。这时需要做对数压缩。但要注意取对数后如果不变换单位很多人的大脑对dB值不敏感建议保存时把范围固定在比如[-80, 0]dB这样不同样本之间颜色才具备可比性。我习惯在保存前做一个百分位裁剪把1%和99%分位作为色标上下限能有效避免个别强成分主导色标范围。代码大概是vmin, vmax np.percentile(log_power, [1, 99]) plt.imsave(out_path, log_power, cmapgray, originlower, vminvmin, vmaxvmax, dpi128)4.3 故障特征频率在图上找不到你确信信号里存在某个故障频率但语谱图上就是看不到对应的亮线。这是最让人头疼的情况。排查顺序一般是窗长不够频率分辨率不足把两个接近的频率糊成了一个。加长nperseg试试。幅值太小被动态范围压住。用百分位裁剪或降低色标上限。故障频率不在分析频段内。确认一下你的关注频率在[0, fs/2]内。信号本身不包含该特征。排除信号采集通道接错、传感器测点不对等物理问题。这里插一句经验工业现场故障诊断先算理论故障特征频率再用STFT图去对应找效率远高于纯看图猜。知道该找什么图才不会白做。4.4 批量生成时图片尺寸不一致不同长度信号做STFT得到的图像宽高不同直接进CNN会报错。解决办法不是强行让参数去凑而是统一切割逻辑。建议在signal_to_image_path里强制规定时间轴上的帧数def fixed_length_stft(signal_data, fs, target_frames256): f, t, Zxx signal.stft(signal_data, fsfs, windowhann, nperseg256, noverlap192, nfft512) # 截取或填充到固定帧数 frames Zxx.shape[1] if frames target_frames: Zxx Zxx[:, :target_frames] else: pad_width ((0, 0), (0, target_frames - frames)) Zxx np.pad(Zxx, pad_width, modeconstant) return f, Zxx这样就能保证每张图的宽度一致配合固定的plt.figure(figsize(10, 6))保存即可。4.5 常见问题速查表问题现象可能原因解决方案频率轴范围不对nfft参数与采样率不匹配检查nfft fs确认nyquist频率为fs/2时间轴上出现垂直条纹帧重叠率过高或能量泄漏降低重叠率换窗函数高频段一片黑/一片白信号被低通滤波或动态范围被压低检查前端信号链路做百分位裁剪图片太模糊分辨不出频率变化窗长过长导致时间分辨率不足减小nperseg增加重叠率模型训练过拟合严重数据增强不足样本量太小增加掩码增强做信号域噪声注入梅尔谱低频段纹理异常mel滤波器组参数不合适采样率不匹配确认n_mels和fmin/fmax设置4.6 关于参数调优的一点心得最后分享一条最重要的经验STFT参数的选择没有放之四海而皆准的答案你要做的是“为了你的具体任务”去调参。我常用的调参顺序是先明确信号的最低关注频率和最高关注频率据此确定合适的采样率和nperseg范围然后用一个已知故障特征频率的样本做基准从nperseg256开始试依次看256、512、1024下的图像对比哪组参数让故障特征最清晰最后固定下来之后批量生成。还有一点很微妙如果模型最终是给业务方看的图像的可解释性比酷炫更重要。我认识不少工程师喜欢用小波图或格拉姆角场觉得更高级。但业务方的老师傅看了半天不知道纵轴是什么最后还是语谱图最容易被接受。你面对的是机器还是人直接决定选哪条路线。把信号转成图像这件事听起来像是花活做起来才知道它是连接传统信号处理和现代深度学习之间最顺手的一座桥。从时域频域分析的角度看一段枯燥的波形变成图像后隐藏的规律就赤裸裸摆在眼前。希望这篇分享能让你少走几段弯路真正用起来的时候记得多动手调一调参数。数据不会骗人图更不会。