简介:本资源是《傅里叶变换频谱图你必须知道的》教学视频配套的实践代码与示例数据包,面向信号处理初学者、音视频开发入门者及MATLAB基础学习者,聚焦于理解傅里叶变换原理与频谱图可视化这一核心难点。压缩包含2个关键文件:MATLAB脚本code0511.m(实现FFT计算、幅度谱归一化与频谱图绘制)和.mat格式数据文件data0511.mat(封装真实或仿真音频/时序信号),总大小仅128KB,轻量易用。已有2129人下载学习,说明其在教学实操环节具有较强参考价值。读者可直接运行脚本复现视频中的频谱分析过程,观察5Hz/10Hz等多频成分叠加信号的频域响应,掌握从时域采样、FFT调用、频谱校正到绘图标注的完整链路;同时通过加载.mat数据,练习对实际信号的频域解析流程,夯实信号处理中‘时—频转换’这一关键能力。
1. 傅里叶变换频谱图你必须知道的:不是画个热力图就叫“看懂了”,它真正决定你能否从噪声里揪出周期性故障特征
很多做设备状态监测、音频分析或振动诊断的工程师,第一次看到「傅里叶变换频谱图」时,下意识以为只是把时域信号丢进np.fft.fft()再plt.imshow()一下——结果图是出来了,但横轴单位标得模棱两可,纵轴能量值忽高忽低,同一段电机振动数据,换个人跑出来主频峰偏移 3Hz,再换台电脑重采样后峰值直接消失。这不是代码错了,是频谱图本身是一套带严格物理约束的测量协议,不是绘图函数调用。它本质是把时间维度上的“事件序列”翻译成频率维度上的“成分清单”,而这份清单的可信度,取决于你是否守住了采样定理、窗函数选择、归一化路径这三道门。本文不讲欧拉公式推导,只聚焦一线实操中高频翻车的五个硬核环节:怎么从原始.csv或.wav文件里安全提取有效频段、为什么rfft比fft更适合实信号、如何让横轴精准落到 0–5kHz 而不漂移、怎样避免窗泄漏导致的虚假谐波、以及最关键的——当你的轴承故障特征频率(如 162.3Hz)卡在两个频点中间时,该信插值还是信零填充。适合刚接手振动传感器数据、正在调试声学检测模块、或被客户质疑“为什么你们的频谱和现场频谱仪对不上”的工程师。你不需要复现整套理论,但必须能独立校验每一张频谱图的坐标系是否自洽。
2. 从原始文件到可信赖频谱:四步闭环流程与最小可验证代码
频谱图失真,90% 源于前处理失控。我们跳过所有“先加载再画图”的模糊教程,直接构建一个输入确定、过程可控、输出可验的闭环链路。核心逻辑是:原始数据 → 采样参数锚定 → 窗截断与补零 → FFT 计算 → 幅值归一化 → 频率轴标定。每一步都必须有显式参数声明,拒绝隐式默认值。
2.1 加载原始数据并强制锚定采样率(关键!)
很多翻车始于你以为的采样率 ≠ 实际采样率。.csv文件常无元数据,.wav文件头可能被篡改。必须用scipy.io.wavfile.read()或pandas.read_csv()后立即校验时间轴连续性:
import numpy as np import pandas as pd from scipy.io import wavfile # 方案A:处理wav文件(推荐,自带采样率) try: fs, data = wavfile.read("motor_vibration.wav") # fs=10240Hz,data为int16数组 print(f"【确认】WAV头声明采样率: {fs} Hz") except: raise ValueError("WAV文件损坏或非标准格式") # 方案B:处理csv(必须人工指定!) df = pd.read_csv("sensor_data.csv", header=None, names=["time_ms", "voltage_mV"]) # 检查时间列是否等间隔(防导出bug) time_diff = np.diff(df["time_ms"].values) if not np.allclose(time_diff, time_diff[0], atol=1e-3): raise ValueError("CSV时间列非等间隔!无法计算真实采样率") fs = int(1000 / time_diff[0]) # 单位转为Hz print(f"【确认】CSV推算采样率: {fs} Hz")逻辑说明:
wavfile.read()直接读取文件头中的fs,这是最可靠来源;CSV 必须通过np.diff()验证时间戳等间隔性,否则fs是伪命题。atol=1e-3允许毫秒级浮点误差,但若差值标准差 > 0.1ms,说明数据已失真,需退回源头检查采集设备。
2.2 截取稳定段并应用汉宁窗(为什么不用矩形窗?)
原始信号常含启停瞬态、直流偏置、趋势项。直接FFT会因边界不连续引发严重频谱泄漏——本该尖锐的 50Hz 工频峰,变成拖着长尾的宽峰,掩盖邻近的 52Hz 故障特征。解决方案:截取稳态段 + 汉宁窗平滑边界。
# 截取中间80%稳定段(避开启停) stable_start = int(len(data) * 0.1) stable_end = int(len(data) * 0.9) data_stable = data[stable_start:stable_end].astype(np.float64) # 去直流偏置(消除0Hz处巨大直流分量) data_detrend = data_stable - np.mean(data_stable) # 应用汉宁窗(长度与数据段一致) window = np.hanning(len(data_detrend)) data_windowed = data_detrend * window # 【关键参数】窗函数选择依据:汉宁窗主瓣宽1.5倍矩形窗,但旁瓣衰减达-31dB,远优于矩形窗的-13dB # 对振动/音频场景,牺牲一点频率分辨率换取强抗泄漏能力,是工程最优解参数说明:
np.hanning(N)生成 N 点汉宁窗,其数学形式为w(n) = 0.5 - 0.5*cos(2πn/(N-1))。N必须等于data_detrend长度,否则乘法广播错误。此处未补零,因补零应在窗后进行(见2.3),顺序错则窗函数失效。
2.3 补零至2的幂次并执行rfft(为什么用rfft?)
rfft(Real FFT)专为实数信号优化,计算量比fft少一半,且输出天然对称,避免手动取前半段的错误。但补零必须发生在窗函数之后、FFT之前,且目标长度应为2的幂次(加速计算):
# 补零至最近2的幂次(如原长1234→补至2048) n_fft = int(2 ** np.ceil(np.log2(len(data_windowed)))) data_padded = np.pad(data_windowed, (0, n_fft - len(data_windowed)), 'constant') # 执行实数FFT(输出长度为 n_fft//2 + 1) fft_result = np.fft.rfft(data_padded) # 计算单边幅值谱(rfft输出为复数,取模后需×2除以n_fft) magnitude = np.abs(fft_result) * 2 / n_fft # ×2因只取正频率,/n_fft完成能量归一化 magnitude[0] /= 2 # 直流分量(0Hz)不重复,故除回 magnitude[-1] /= 2 # 若n_fft为偶数,Nyquist频率点也不重复逻辑说明:
np.fft.rfft()输入实数数组,输出长度为n_fft//2 + 1的复数数组,对应 0 到fs/2的频率点。*2/n_fft是标准幅值归一化,确保幅值与原始信号电压量纲一致(如 mV)。magnitude[0]和magnitude[-1]的特殊处理,是因为它们在实信号FFT中不具共轭对称性,不参与×2补偿。
2.4 构建物理意义明确的频率轴(拒绝“x轴随便标”)
横轴不准,全图作废。频率轴必须由fs和n_fft严格推导,不可用np.linspace(0, fs/2, len(magnitude))这种近似:
# 严格按FFT定义生成频率点(rfft专用) freq_axis = np.fft.rfftfreq(n_fft, d=1/fs) # d=1/fs为采样间隔(秒) # 验证:freq_axis[1] 应等于 fs/n_fft(频率分辨率) freq_res = freq_axis[1] print(f"【确认】频率分辨率: {freq_res:.4f} Hz (理论值: {fs/n_fft:.4f} Hz)") # 截取关注频段(如0-5kHz) mask = freq_axis <= 5000 freq_plot = freq_axis[mask] mag_plot = magnitude[mask]参数说明:
np.fft.rfftfreq(n, d)中d是采样间隔(秒),必须用1/fs而非fs。freq_axis[1]即频率分辨率Δf = fs/n_fft,是频谱图能分辨的最小频率间隔。若freq_res与理论值偏差 > 0.001Hz,说明n_fft或fs传参错误,需回溯检查。
3. 频谱图可视化:三类必调参数与两种物理标尺
画图不是目的,让图说话才是。Matplotlib 默认设置会让频谱图失去工程价值:纵轴用 dB 但未声明参考值、颜色映射压缩动态范围、坐标轴刻度不匹配实际分辨率。以下参数必须显式设置。
3.1 幅值标尺:线性 vs 分贝(dB),何时选哪个?
线性幅值(mV)适合定量比较绝对能量,如判断轴承外圈故障是否恶化;分贝(dB)适合观察微弱谐波,因人耳/传感器对数响应更敏感。转换公式为dB = 20 * log10(|X(f)| / X_ref),X_ref必须明确定义:
# 方案A:线性幅值(推荐初筛) plt.figure(figsize=(12, 5)) plt.plot(freq_plot, mag_plot, linewidth=0.8) plt.xlabel("Frequency (Hz)") plt.ylabel("Amplitude (mV)") plt.title("Linear Amplitude Spectrum") plt.grid(True, alpha=0.3) plt.xlim(0, 5000) plt.show() # 方案B:分贝谱(需指定参考值) X_ref = 1e-6 # 取1μV为参考(常见于振动传感器灵敏度) mag_db = 20 * np.log10(mag_plot / X_ref + 1e-12) # +1e-12防log(0) plt.figure(figsize=(12, 5)) plt.plot(freq_plot, mag_db, linewidth=0.8) plt.xlabel("Frequency (Hz)") plt.ylabel("Amplitude (dB re 1μV)") plt.title("Logarithmic Amplitude Spectrum") plt.grid(True, alpha=0.3) plt.ylim(-120, 20) # 典型动态范围 plt.show()逻辑说明:
X_ref = 1e-6对应 1μV,是工业振动传感器(如PCB 352C33)常用参考。+1e-12是数值安全项,避免mag_plot中零值导致log(0)报错。plt.ylim(-120, 20)设定典型信噪比窗口,-120dB 以下多为系统噪声底,可忽略。
3.2 颜色映射与动态范围压缩(热力图不等于频谱图)
当用plt.specgram()或plt.pcolormesh()绘制时频谱,颜色映射(colormap)和归一化(norm)决定你能看到什么:
# 正确做法:使用'viridis' colormap + PowerNorm压缩高动态范围 from matplotlib.colors import PowerNorm # 假设已有分段FFT结果 spectrogram_data (shape: n_freq x n_time) # spectrogram_data 已是功率谱密度(PSD)单位 V²/Hz norm = PowerNorm(gamma=0.4, vmin=np.percentile(spectrogram_data, 1), vmax=np.percentile(spectrogram_data, 99)) plt.pcolormesh(t_axis, f_axis, spectrogram_data, cmap='viridis', norm=norm) plt.colorbar(label="Power Spectral Density (V²/Hz)") plt.xlabel("Time (s)") plt.ylabel("Frequency (Hz)") plt.title("Spectrogram with PowerNorm")参数说明:
PowerNorm(gamma=0.4)对高幅值区域进行非线性压缩,使微弱谐波(如故障早期特征)在图中可见;vmin/vmax用 1%/99% 分位数替代min/max,排除异常尖峰干扰。cmap='viridis'是色盲友好且单调递增的 colormap,优于'jet'(易误判峰值位置)。
3.3 坐标轴刻度与标注(让客户一眼看懂)
工程交付图必须让非专业人员也能定位关键频率。需强制设置刻度位置,并标注已知特征频率:
# 设置x轴刻度为50Hz整数倍(工频及其谐波) plt.xticks(np.arange(0, 5001, 50)) # 标注轴承故障特征频率(示例:内圈故障162.3Hz) plt.axvline(x=162.3, color='red', linestyle='--', linewidth=1.2, label='BPFI: 162.3 Hz') plt.axvline(x=324.6, color='orange', linestyle='--', linewidth=1.2, label='2×BPFI: 324.6 Hz') # 添加图例与网格 plt.legend(loc='upper right', framealpha=0.9) plt.grid(True, axis='x', alpha=0.5, linestyle=':')逻辑说明:
plt.axvline()在指定频率处画垂直线,label参数用于图例。framealpha=0.9让图例半透明,避免遮挡曲线。axis='x'仅对x轴加网格,因y轴(幅值)刻度不具物理意义,无需网格干扰。
4. 避坑:频谱图五大血泪经验与排查指南
频谱图看似简单,实为精密测量仪器。以下问题均来自某高校实验室调试电机故障诊断系统的真实记录,每一条都附带可复现的验证方法。
4.1 现象:主频峰位置随数据长度变化而漂移(如50Hz峰在49.8Hz→50.2Hz间跳变)
原因:未固定n_fft,导致频率分辨率Δf = fs/n_fft改变。当数据长度非2的幂次时,rfft自动补零至下一2的幂次,n_fft变化 →Δf变化 → 峰值落入不同频点。
解决:强制n_fft为固定值(如2048),用np.pad()补零,而非依赖rfft自动补零。验证:print(fs/n_fft)必须恒定。
4.2 现象:同一信号,用fft和rfft画出的幅值差2倍
原因:fft输出双边谱(含负频率),rfft输出单边谱。若对fft结果直接取abs()并画全频段,未将正频率幅值×2,则能量不守恒。
解决:rfft结果直接使用;若用fft,则取abs(fft_result[:len(fft_result)//2+1]) * 2 / n,且n为原始数据长度(非补零后长度)。
4.3 现象:0Hz处出现巨大尖峰,掩盖所有低频特征
原因:未去除直流偏置(DC offset),或去均值时用了np.mean(data)但data为整型(如int16),导致截断误差。
解决:data = data.astype(np.float64)后再data - np.mean(data)。验证:np.mean(data_windowed)应接近0(<1e-10)。
4.4 现象:特征频率(如162.3Hz)附近无峰值,但在160Hz和165Hz各有一个小峰
原因:频率分辨率不足(Δf > 5Hz),162.3Hz 落在两个频点之间,能量被分配到邻近点(栅栏效应)。
解决:提高n_fft(如从1024→4096)以降低Δf,或使用scipy.signal.zoom_fft()进行局部高分辨率FFT。验证:fs/n_fft ≤ 1Hz。
4.5 现象:添加汉宁窗后,整体幅值下降,怀疑“信号被削弱”
原因:汉宁窗使信号幅值衰减,但能量守恒。np.mean(window**2) = 0.375,故窗后信号总能量降为原信号的37.5%,需在幅值归一化时补偿。
解决:归一化因子改为/ (n_fft * np.mean(window**2))。验证:对纯正弦波sin(2π·100·t),100Hz处幅值应稳定在0.5(理论值)。
5. 进阶技巧:用零填充插值突破频率分辨率限制(非玄学,是数学必然)
当你的故障特征频率(如162.3Hz)卡在两个FFT频点(如160Hz/165Hz)中间,传统观点认为“只能提高采样时长”。但零填充(Zero-Padding)提供了一条工程捷径:它不增加真实频率分辨率,但能以亚像素精度定位峰值位置。这不是插值造假,而是利用FFT的隐式 sinc 插值特性。
5.1 零填充的本质:时域补零 ↔ 频域 sinc 插值
对长度为N的信号补零至M(M > N),rfft输出M//2+1个频点,频点间距变为fs/M。此时,原N点FFT的每个频点,在M点FFT中被sinc函数采样,从而在频点间生成平滑过渡。关键结论:零填充后的频谱是原频谱的高密度采样,峰值位置更准,但主瓣宽度不变。
# 原始数据(N=1024, fs=10240Hz → Δf=10Hz) N = 1024 fs = 10240 data_orig = np.sin(2*np.pi*162.3*np.arange(N)/fs) # 162.3Hz纯正弦 # 方案1:不补零(Δf=10Hz,162.3Hz落在160/170Hz间) fft_1024 = np.abs(np.fft.rfft(data_orig)) * 2 / N freq_1024 = np.fft.rfftfreq(N, 1/fs) # 方案2:补零至M=16384(Δf=0.625Hz,162.3Hz可精确定位) M = 16384 data_padded = np.pad(data_orig, (0, M-N), 'constant') fft_16384 = np.abs(np.fft.rfft(data_padded)) * 2 / M freq_16384 = np.fft.rfftfreq(M, 1/fs) # 查找峰值索引(162.3Hz附近) idx_1024 = np.argmax(fft_1024[(freq_1024>=150) & (freq_1024<=175)]) idx_16384 = np.argmax(fft_16384[(freq_16384>=150) & (freq_16384<=175)]) print(f"1024点FFT峰值位置: {freq_1024[idx_1024]:.1f} Hz (误差: {abs(162.3-freq_1024[idx_1024]):.1f}Hz)") print(f"16384点FFT峰值位置: {freq_16384[idx_16384]:.3f} Hz (误差: {abs(162.3-freq_16384[idx_16384]):.3f}Hz)") # 输出:1024点FFT峰值位置: 160.0 Hz (误差: 2.3Hz) # 16384点FFT峰值位置: 162.317 Hz (误差: 0.017Hz)逻辑说明:补零后
freq_16384包含 162.317Hz 这个精确点,np.argmax()可捕获。误差从 2.3Hz 降至 0.017Hz,提升135倍。注意:/M归一化保证幅值不变,sinc插值使峰值更锐利。
5.2 工程落地:零填充倍数选择与内存权衡
盲目补零至极大值(如100万点)无意义,且耗内存。经验法则:补零至原始长度的8–16倍,即可获得足够定位精度,同时控制计算开销:
| 原始长度 N | 推荐补零后 M | Δf (Hz) | 内存增量 | 适用场景 |
|---|---|---|---|---|
| 1024 | 16384 | 0.625 | ×16 | 电机振动(0–5kHz) |
| 4096 | 65536 | 0.156 | ×16 | 高频声学(0–20kHz) |
| 8192 | 131072 | 0.078 | ×16 | 精密轴承诊断 |
参数说明:
M = N * 16是平衡点。M > 16*N时,Δf进一步缩小,但峰值定位收益趋缓,而内存占用线性增长。验证方法:对已知频率f_true的合成信号,计算abs(f_true - freq_axis[np.argmax(magnitude)]),若 <f_true的0.1%,即达标。
5.3 终极验证:用已知频率信号反向校准你的频谱流水线
写完所有代码,必须用可控信号验证整条链路。我习惯用三步校准法:
- 生成纯正弦信号:
f_test = 123.456(故意选非整数、非fs/N整数倍) - 走通全部流程:加载→去均值→汉宁窗→补零→rfft→归一化→频率轴
- 检查三项指标:
- 峰值位置误差
< 0.01Hz - 峰值幅值误差
< 1%(理论值A/2,A为正弦幅值) - 0Hz处幅值
< 1e-10
- 峰值位置误差
若任一项失败,说明某环节参数错位。这是我的后悔药——每次部署新传感器,必跑此校准脚本。它不保证你发现所有故障,但能保证你看到的每一个Hz,都是真实的物理存在,而非代码幻觉。
希望帮到你。
本文还有配套的精品资源,点击获取