资讯动态

Python绘制FFT频谱图:频率轴、窗函数与泄漏避坑指南

发布时间:2026/10/19 9:49:22 来源:尧图企业网站定制
简介傅里叶变换是信号处理中连接时域与频域的核心数学工具其离散形式FFT让计算机能够高效完成频谱分析。在工程实践中绘制一幅正确的频谱图并不仅仅是调用函数库更需要理解频率轴映射、采样率与奈奎斯特频率的关系以及窗函数对频谱泄漏的抑制作用。这些基础概念直接影响频谱图的准确性与可读性也是音频分析、振动监测、通信系统调试等场景中快速定位信号特征的前提。围绕一份信号处理领域的开源代码资源系统拆解用Python绘制FFT频谱图的关键流程从合成信号到真实语音覆盖单边谱绘制、Hann窗应用、dB刻度显示等常见操作并针对频率轴整体翻倍、尖峰旁瓣长草、加窗后幅值衰减、补零无法提高分辨率等高频问题给出可直接落地的排查方法与代码示例帮助开发者避开频谱图绘制中最容易翻车的四个陷阱。1. 傅里叶变换频谱图视频里看着轻松自己画全是坑傅里叶变换频谱图在演示视频里往往几秒钟就画出来了波形拉进去点击运行频率峰整整齐齐。真到自己动手时横轴按什么算、幅值要不要乘 2、窗函数加不加、补零能不能提高分辨率每一步都可能把结果带偏。这套资源正是某位信号处理方向的开发者做傅里叶变换频谱图演示时用的代码与数据里面有可直接运行的 Python 脚本、扫频信号、双正弦数据和一段短语音。适合正在学 DFT/FFT、被频谱图横纵轴绕晕或者想把手头音频信号可视化验证的工程师和学生。下面按拆解顺序讲清楚怎么跑通、参数怎么改以及最容易翻车的四个位置。2. FFT 频谱图的最短路径先弄懂频率轴再跑通第一张图2.1 从连续傅里叶变换到离散 FFT这张图的横轴到底是谁连续傅里叶变换的横轴是频率 f单位 Hz看起来不需要解释。但计算机只能处理采样后的离散序列FFT 输出的是 N 个复数下标是 k不是频率本身。这些下标对应的是数字频率 2πk/N换算成物理频率后是f_k k * sr / N其中 sr 是采样率N 是 FFT 点数。很多人第一张频谱图直接plt.plot(abs(np.fft.fft(x)))横轴写成 0、1、2……这样在不同采样率下根本没法对照。比如 N2000、sr2000 时k50 对应 50Hz但如果 sr16000、N4000k50 对应 200Hz。同样的下标物理意义完全不同。傅里叶变换频谱图第一个必须知道的事就是频率轴要用np.fft.fftfreq生成或者手动按np.arange(N) * sr / N算但别直接画下标。另一个容易忽略的点是 FFT 结果包含正频率和负频率。对实数信号来说正负频率幅度对称所以通常只取前半段画单边谱频率范围是 0 到 sr/2也就是奈奎斯特频率。视频里演示的频谱图如果是音频基本也都是这种单边显示。2.2 解压后的文件结构与运行环境资源包解压后我看到的目录结构大致是这个思路不一定每台机器解压出来文件名完全一致但一般会包含脚本、工具函数和数据目录fft_spectrum_demo/ ├── main_plot_spectrum.py ├── fft_utils.py ├── data/ │ ├── two_sines.npy │ ├── chirp_linear.npy │ └── voice_short.wav └── requirements.txt运行环境建议用虚拟环境隔离。requirements.txt 里锁定了依赖我实际操作时直接用 pip 安装python -m venv venv source venv/bin/activate pip install --upgrade pip pip install -r requirements.txt python main_plot_spectrum.py这段命令的逻辑是先建一个干净的 Python 环境再把资源包要求的库装进去。Windows 下激活虚拟环境的命令是venv\Scripts\activatemacOS 和 Linux 是source venv/bin/activate。如果直接装在全局环境里也能跑但之后换项目容易遇到 numpy、scipy 版本互相打架的问题。资源包里通常只需要 numpy、scipy、matplotlib最多加一个 wavfile 读取相关的依赖。第一次跑的时候如果报ModuleNotFoundError缺什么装什么就行不用一次装一堆这是最常见的做法。2.3 第一张图合成叠加信号用 numpy 输出单边谱先不碰真实音频用合成信号跑通最小链路。下面这段代码就是资源包里main_plot_spectrum.py的核心逻辑我加了关键注释import numpy as np import matplotlib.pyplot as plt sr 2000 # 采样率单位 Hz duration 1.0 # 信号时长单位秒 t np.arange(0, duration, 1/sr) # 时间序列 # 两个正弦一个 50Hz一个 120Hz叠加后作为输入 f1, f2 50.0, 120.0 x 1.0*np.sin(2*np.pi*f1*t) 0.5*np.sin(2*np.pi*f2*t 0.3) N len(x) X np.fft.fft(x) # 离散傅里叶变换 freqs np.fft.fftfreq(N, 1/sr) # 完整频率轴包含负频率 half N // 2 mag np.abs(X[:half]) / N mag[1:] * 2 # 单边谱正频率幅度乘以 2直流分量除外 plt.figure(figsize(9, 3)) plt.plot(freqs[:half], mag) plt.xlabel(Frequency (Hz)) plt.ylabel(Amplitude) plt.xlim(0, 500) plt.grid(alpha0.3) plt.title(Single-sided spectrum of two sines) plt.tight_layout() plt.savefig(single_side_spectrum.png, dpi150)逻辑说明np.fft.fft返回的复数数组每个元素的模就是该频率分量的幅度。DFT 的定义里一个幅度为 A 的正弦会被拆成正负两支每支幅度接近 A*N/2所以除以 N 之后要乘以 2才能把正频率那一支的幅度还原成原始信号的幅度。mag[1:] * 2是为了跳过第 0 个下标对应的直流分量直流只要除以 N不需要乘 2。np.fft.fftfreq(N, 1/sr)直接给出从负到正的完整频率轴取前 half 个正好对应 0 到接近 sr/2。参数说明sr2000决定横轴最高只画到 1000Hzduration1.0决定频率分辨率是 1Hz也就是两个相隔不到 1Hz 的峰无法在这个配置下被有效区分。f1、f2可以改成任意频率试。我这里故意选了 50Hz 和 120Hz在 1 秒时长内恰好是整数个周期边界没有跳变所以用矩形窗也不会出现明显泄漏。等后面换成任意频率比如 47Hz不加窗就会出现第 4.2 节说的“频谱长草”。先记下这个前提。3. 把频谱图调明白采样率、窗函数、动态范围和真实音频3.1 采样率与奈奎斯特频率参数错一个整张图全是假的采样率是决定频谱图是否能反映真实信号的第一个关键参数。假设信号里最高频率是 f_max采样率必须大于 2f_max才能避免高频分量折叠到低频区域。这个 2f_max 就是奈奎斯特率频谱图能显示的最高频率是 sr/2而不是 sr。举例来说sr1000 时理论上最多只能表示 500Hz 以下的信号。如果你拿一个 600Hz 的正弦波去采样采出来之后会看到一个 400Hz 的峰。这个现象叫混叠公式可以简单理解成f_alias abs(f - round(f / sr) * sr)600Hz、采样率 1000Hz 时round(600/1000)1所以 f_aliasabs(600-1000)400Hz。资源包里有线扫频数据就是专门用来演示这个现象的。代码里检查混叠的常见做法是sr_low 1000 f_test 600 t_low np.arange(0, 1, 1/sr_low) x_low np.sin(2*np.pi*f_test*t_low) X_low np.fft.fft(x_low) freqs_low np.fft.fftfreq(len(x_low), 1/sr_low) half len(x_low) // 2 peak_idx np.argmax(np.abs(X_low[:half])) print(f折叠后显示频率: {freqs_low[peak_idx]:.2f} Hz)逻辑说明这段代码刻意把采样率降到 1000Hz然后对一个 600Hz 信号做 FFT找到幅度最大的下标再映射回频率。输出会显示 400Hz而不是 600Hz。真实信号不会告诉你它发生了混叠所以你必须在采集端先确认信号带宽再选采样率。参数之间还有一层关系频率分辨率由 FFT 点数决定Δf sr / N。同样的 N 下采样率越高分辨率越差同样的采样率下N 越大分辨率越好。下面这组表可以直观看出采样率 sr可表示最高频率N1024 时频率分辨率2000 Hz1000 Hz约 1.95 Hz8000 Hz4000 Hz约 7.81 Hz44100 Hz22050 Hz约 43.07 Hz所以不要只盯着“FFT点数大不大”分辨率本质上是采样时长决定的。要提高频率分辨率最直接的办法是延长采样时间靠补零不算这个后面专门说。3.2 加窗与频谱泄漏为什么正弦波也有一堆旁瓣FFT 对一段有限长信号做处理时相当于把这段信号周期性延拓。如果截断边界不平滑延拓后就会出现跳变频谱上表现为一个本该集中的尖峰扩散成一片旁瓣这叫频谱泄漏。视频里如果只演示整周期采样这个现象会被隐藏一旦用真实音频或任意频率马上就会暴露。最常见的补救是加窗。Hann 窗的表达式是w[n] 0.5 - 0.5*cos(2πn/(N-1))它在两端平滑衰减到 0让截断边界更连续。代码里对比加窗前后的频谱import numpy as np import matplotlib.pyplot as plt sr 1000 N 1000 f 47.0 t np.arange(N) / sr x np.sin(2*np.pi*f*t 0.7) # 相位 0.7 导致边界不连续 w np.hanning(N) xw x * w def spectrum_db(raw): X np.fft.fft(raw) freqs np.fft.fftfreq(len(raw), 1/sr) half len(raw) // 2 spec 20*np.log10(np.abs(X[:half]) / len(raw) * 2 1e-10) return freqs[:half], spec freqs1, spec1 spectrum_db(x) freqs2, spec2 spectrum_db(xw) plt.plot(freqs1, spec1, labelrectangular (no window)) plt.plot(freqs2, spec2, labelhann) plt.ylim(-90, 0) plt.legend() plt.grid(alpha0.3)逻辑说明np.hanning(N)生成长度为 N 的 Hann 窗x 乘以窗后再做 FFT。对比两条曲线矩形窗的旁瓣明显更高Hann 窗把离主峰较远的旁瓣压下去很多但主瓣会比原来宽一些。这是个权衡想要频率分辨率极致高用矩形窗想要动态范围好、弱信号能看清用加窗。不同窗的选择可以参考下表窗函数主瓣宽度旁瓣抑制典型场景矩形窗窄约 -13 dB瞬态信号、校准Hann 窗中约 -31 dB通用音频、正弦测量Hamming 窗中约 -43 dB窄带信号、语音Blackman-Harris宽约 -58 dB 起强动态范围需求3.3 从音频文件到短时傅里叶变换STFT频谱图怎么来的普通频谱图是横轴频率、纵轴幅度的二维曲线适合看“某个时刻”的全局频率成分。但音频是随时间变化的比如语音里的元音和辅音频率分布完全不同。这时需要把长信号切成很多短帧每帧加窗做 FFT再把所有帧按时间排列成二维图也就是短时傅里叶变换通常叫 spectrogram。资源包里的voice_short.wav就是干这个用的。一个可以直接跑的版本from scipy.io import wavfile from scipy import signal import numpy as np import matplotlib.pyplot as plt sr, audio wavfile.read(data/voice_short.wav) if audio.ndim 1: audio audio.mean(axis1) audio audio.astype(np.float64) audio / np.abs(audio).max() f, t_spec, Sxx signal.spectrogram( audio, fssr, nperseg2048, noverlap1024, windowhann, scalingspectrum ) Sxx_db 10*np.log10(Sxx 1e-12) plt.figure(figsize(10, 4)) plt.pcolormesh(t_spec, f, Sxx_db, shadinggouraud, cmapmagma, vmin-80, vmax-20) plt.xlabel(Time (s)) plt.ylabel(Frequency (Hz)) plt.colorbar(labeldB) plt.ylim(0, 4000) plt.tight_layout()参数说明nperseg2048是每帧长度频率分辨率约等于 sr/2048noverlap1024是帧之间重叠一半让时间轴更平滑。windowhann降低旁瓣。scalingspectrum表示输出的是幅度谱平方因此后面用10*np.log10转 dB。vmin-80, vmax-20把颜色范围限制在 80dB 动态区间避免背景噪声把颜色拉平。如果scipy.signal.spectrogram不适合你的环境也可以用np.fft.rfft手动一帧帧叠但代码会更长。实际项目中我一般先跑通 spectrogram 再考虑自己实现除非要做特殊的时间分辨率控制。3.4 显示参数怎么调dB、色标、动态范围频谱图不是“算完就完”显示参数对可读性影响很大。最常见的错误是直接画线性幅度。线性刻度下强频率成分会把弱成分完全压成一片深色什么都看不清。改成 dB 刻度之后动态范围被压缩细节才出来。dB 换算要看数据是幅度还是功率。signal.spectrogram返回的是功率谱所以用10*np.log10如果是np.abs(np.fft.fft(x))这种幅度谱通常用20*np.log10。这个区别视频里不会特意讲但自己画图时特别容易踩坑。动态范围的设定也很关键。vmax 一般取谱图中的最大峰值vmin 往下拉 60 到 80dB。比如最大峰是 0dBvmin 取 -80dB含义是只关心比峰值低 80dB 以上的成分。如果 vmin 设到 -120dB很多数字噪声和数值误差也会被画出来图面上全是噪点如果 vmin 设得太高比如 -40dB弱信号又会被直接裁掉。色标建议用magma、viridis或灰度不要用彩虹色。人眼对彩虹色不同区域的亮度感知不均匀容易把同一强度的频率成分看成不同强度。视频里如果用了冷色调多半也是为了避免这个问题。4. 频谱图翻车排雷四种最常见的坑与排查方法4.1 现象频率轴整体翻倍50Hz 的峰跑到 1950Hz现象把 50Hz 正弦波画频谱图峰值出现在接近采样率的位置比如采样率 2000Hz 时峰在 1950Hz 附近。原因没有正确区分正频率和负频率。FFT 输出是双边谱前半段是正频率后半段是负频率。如果直接用np.arange(N)当下标再乘以sr/N横轴会从 0 一直延伸到 sr看起来像是多了一倍。峰值跑到 1950Hz其实是 50Hz 的负频率镜像。解决改用np.fft.fftfreq生成完整频率轴取前N//2个点作为单边谱频率轴freqs np.fft.fftfreq(N, 1/sr) half N // 2 plt.plot(freqs[:half], np.abs(X[:half]) / N)如果看到的是对称双峰也可以先用np.fft.fftshift(X)把零频率移到中心再画完整频率轴。实信号分析里单边谱是最实用的画法。4.2 现象尖峰旁边长草正弦波频谱不干净现象一个 47Hz 正弦采样率 1000Hz时长 1 秒FFT 之后主峰在 47Hz 附近但旁边拖出一大片衰减振荡甚至旁瓣高到看不清 30Hz 处的弱信号。原因47Hz 在 1 秒内是 47 个周期本来刚好是整数。但如果相位是任意值比如 0.7或者采样点数不是整周期长度边界处就会有跳变。FFT 周期延拓时把这个跳变当成高频成分于是频谱上出现大量本不该有的能量这就是频谱泄漏。解决先加窗最常用 Hann 窗。如果目标是测量正弦波的精确幅度也可以把采样时长调整成目标频率的整数倍。但真实音频不可能凑整周期所以音频场景下几乎都要加窗。判断是不是泄漏可以把窗去掉再画一次如果旁瓣明显降低说明就是边界跳变引起的。4.3 现象加窗后幅值降了一半峰值对不上标定值现象对一个幅度为 1 的正弦波加 Hann 窗再做 FFT峰值变成 0.5 左右和原始幅度对不上。原因窗函数把信号两端压低了整体能量下降。Hann 窗的平均值接近 0.5不校正直接用abs(X)/N*2自然只有一半。解决幅值归一化时不要只除以 N要除以窗函数总和w.sum()w np.hanning(N) xw x * w Xw np.fft.fft(xw) half N // 2 mag np.abs(Xw[:half]) * 2 / w.sum() mag[0] np.abs(Xw[0]) / w.sum()逻辑说明w.sum()等于 N 乘以窗均值。对 Hann 窗来说近似等于 N/2所以乘 2 再除 w.sum()刚好抵消窗函数带来的衰减。这里第 0 个点是直流不需要乘 2。如果做的是功率谱则建议用np.abs(Xw)**2 / (sr * np.sum(w**2))。不同资源包里的归一化方式可能有差异动手前打印一下w.sum()和w.sum()/N比盲目套公式可靠。4.4 现象补零后曲线变密但两个峰还是拉不开现象原始数据只有 1000 点为了“提高分辨率”调用np.fft.fft(x, 8192)频谱曲线确实变得很平滑但两个相隔 1Hz 的正弦峰依然无法分开。原因补零只是增加了 FFT 的输出点数并没有增加信号时长。频率分辨率的本质是 Δf sr / N这里的 N 是参与实际采样的点数不是 FFT 填充后的点数。补零后的曲线变密属于插值效果不会把原本重叠的两个峰分开。解决要真正区分两个相隔很近的频率必须延长采样时间。例如 1 秒数据对应 1Hz 分辨率想分辨 0.5Hz 差别的两个峰至少采 2 秒。补零可以用来让频谱曲线更平滑但别把它当成分辨率增强手段。测试方法很简单对比相同数据补零前后找一个频差小于 1Δf 的合成信号看峰值位置是否稳定。不稳定才是正常的说明还差时间不是差点数。5. 验证频谱图算得对不对用 Parseval 定理和合成回放做交叉检查5.1 Parseval 定理两条能量等式兜底频谱图画出来再好看也不能保证归一化完全正确。我每次拆这种资源第一步都会用 Parseval 定理做能量校验也就是时域能量应该等于频域能量energy_t np.sum(x**2) / sr energy_f np.sum(np.abs(X)**2) / (sr * N) print(energy_t, energy_f, energy_t - energy_f)逻辑说明离散 Parseval 定理要求时域平方和除以采样率和频域模平方和除以sr * N一致。对照两个数值如果差值大到小数点后几位还对不上说明 N、sr 或者归一化系数肯定有问题。加了窗之后需要先用加窗后的时域数据xw计算能量因为窗已经改写了信号。5.2 逆变换和回放验证“画出来的谱”没有改坏数据FFT 再逆变换回来是一条更直接的交叉验证。对没有改动的原始复数频谱做np.fft.ifft恢复的时域信号应该和输入几乎一致x_recover np.fft.ifft(X).real print(np.max(np.abs(x - x_recover)))如果是频谱图 demo还可以把合成信号写回 WAV 文件听一下。比如用正弦叠加信号生成一个短音频画出频谱图再用scipy.io.wavfile.write写回去播放。如果听感明显发闷或发炸通常是采样率参数和原始数据不匹配。这个习惯帮我过滤掉了好几个“图看着漂亮、数据实际跑偏”的案例。从那以后我每次拿到频谱图资源和代码都强制先跑一遍 Parseval 和 ifft 对比确认链路没有断再安心调窗函数和动态范围。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑