
如果你做过信号处理、故障诊断或者图像分析大概率绕不开 NumPy 的傅里叶变换接口——也就是numpy.fft那一组 API。我最开始在项目里用到它们是因为需要从一段振动传感器数据里找出异常频率。当时我还不太明白原理只知道调用np.fft.fft能得到一堆复数然后用np.abs取模就能画频谱图。后来踩的次数多了才慢慢摸清这套 API 的脾气它既要你理解数学定义又涉及工程习惯数据排布、频率轴、实数虚数处理、归一化方式每一个细节都可能让结果偏离预期。这篇文章就从最核心的思路讲起把 NumPy 傅里叶变换 API 的常用接口、参数含义、高阶应用和坑点一次讲透。无论你是刚接触傅里叶变换的入门读者还是已经会调用fft但总觉得结果哪里不对的进阶用户都能从这里面拿到一些能直接复用的经验。我会结合实际代码和踩坑记录尽量把“为什么这么做”也讲清楚而不是只贴 API 文档。1. 傅里叶变换的思想与NumPy的实现脉络1.1 为什么信号处理离不开傅里叶变换傅里叶变换的核心思想其实并不神秘任何一个随时间变化的信号都可以看作一系列不同频率、不同幅度、不同相位的正弦波的叠加。这就像一杯混合果汁你从时域看是混在一起的液体但傅里叶变换能帮你拆解出里面到底有多少比例的苹果汁、橙汁、菠萝汁——对应到信号里就是哪些频率成分占主导。这句话听起来简单但真正做工程的时候价值极大。比如旋转机械故障诊断正常状态下振动信号里主要是转频及少量谐波一旦出现轴承故障频谱上就会冒出特征频率的峰值再比如音频降噪语音和噪声往往在频带上分布不同把噪声频带滤掉再回到时域就能得到相对干净的语音。这些都是傅里叶变换的典型应用场景。在实际程序里计算机处理的是离散采样得到的有限长序列所以我们需要的是离散傅里叶变换DFT。NumPy 的fft模块提供的是快速傅里叶变换FFT算法的封装它把 DFT 的计算复杂度从 (O(N^2)) 降到了 (O(N \log N))这也是为什么我们能在一段几十万点的数据上做频谱分析而感觉不到明显卡顿。1.2 NumPy的fft模块设计哲学NumPy 的fft模块不是单独的一个函数而是一组面向不同场景的 API。按维度分有一维的fft、ifft二维的fft2、ifft2以及任意维度的fftn、ifftn按输入类型分有专为实数信号设计的rfft、irfft。这套划分背后是明确的工程设计逻辑不同类型的数据、不同维度的问题应该用最匹配的接口而不是一个函数包打天下。初次接触的人容易只盯住np.fft.fft这并不是不行但会漏掉很多更合适的工具。比如处理音频、振动信号这类实数序列时使用rfft可以得到同样的信息计算量却几乎减半输出也更容易解释。图像处理场景则需要fft2和fftshift因为图像是二维离散信号而且我们希望把低频成分移到图像中心方便观察和做频域掩模。还有一个容易忽略的点fft模块期望输入是 NumPy 数组但它也会对输入做自动类型转换。比如你传入一个 Python 列表它会先转成数组再计算返回的是复数ndarray。这一点对性能有隐性问题——如果在一个大循环里反复传列表类型转换开销会被放大后面的性能优化章节我还会细说。1.3 从数学公式到离散傅里叶变换从连续傅里叶变换到离散傅里叶变换公式形式上很规整。对长度为 (N) 的输入序列 (x[n])正变换定义为[ X[k] \sum_{n0}^{N-1} x[n] \cdot e^{-2\pi i \frac{k n}{N}}, \quad k0,1,...,N-1 ]逆变换对应为[ x[n] \frac{1}{N} \sum_{k0}^{N-1} X[k] \cdot e^{2\pi i \frac{k n}{N}}, \quad n0,1,...,N-1 ]这里最值得注意的是系数 (1/N) 的位置。NumPy 的fft默认不做归一化它的输出幅度会随着点数 (N) 成比例放大而ifft在逆变换时会自动除以 (N)。也就是说np.fft.ifft(np.fft.fft(x))能恢复出原始信号但如果只调用正变换想直接得到“物理幅度”还需要自己额外除以 (N) 或 (N/2)。这一点是新手最容易困惑的地方后面我会结合例子演示。还有一个工程习惯要注意np.fft.fft的输出顺序不是“从低频到高频”排列的而是从零频开始依次是正频率分量后半段才是负频率分量。这种排列方式源自 FFT 的蝶形运算结构而不是某个高深理论。理解了这个排列方式后面使用fftfreq和fftshift就顺理成章了。2. 常用API详解与参数剖析2.1 fft / ifft最基础的一对变换先看最常用的一维函数import numpy as np np.fft.fft(a, nNone, axis-1, normNone) np.fft.ifft(a, nNone, axis-1, normNone)参数解释a输入数组。如果是多维数组默认沿最后一个轴计算。n可选如果给定会把输入截断或补零到长度n后再变换。补零在频域起到了“插值”效果会让频谱曲线更平滑但并不能提高真实频率分辨率。axis多维时指定沿哪个轴变换。norm归一化模式可填backward、forward、ortho。默认是backward等价于正变换不乘系数、逆变换除以 Nforward是正变换除以 N、逆变换不除ortho是正逆都乘以 (1/\sqrt{N})保持变换前后能量一致适合需要保持功率相等的分析场景。下面用一个简单仿真信号来演示基础用法。假设采样率 (Fs200) Hz时长为 5 秒信号是 5 Hz 和 50 Hz 两个正弦波的叠加import numpy as np import matplotlib.pyplot as plt Fs 200 # 采样率 200 Hz T 5 # 时长 5 秒 N Fs * T # 总采样点数 t np.arange(N) / Fs # 构造信号5Hz 幅度 1.050Hz 幅度 0.8 x 1.0 * np.sin(2 * np.pi * 5 * t) 0.8 * np.sin(2 * np.pi * 50 * t) # 傅里叶变换 X np.fft.fft(x) # 单边频谱幅度只取正频率部分 n_oneside N // 2 freqs np.fft.fftfreq(N, 1 / Fs)[:n_oneside] amp 2.0 * np.abs(X[:n_oneside]) / N # 直流分量是否要乘2需要单独处理这里主要看交流分量 plt.figure(figsize(10, 4)) plt.plot(freqs, amp) plt.xlabel(Frequency (Hz)) plt.ylabel(Amplitude) plt.title(Single-Sided Amplitude Spectrum) plt.xlim(0, 100) plt.grid(True) plt.show()为什么幅度要乘以2 / N因为fft的结果不归一化一个幅度为 (A) 的正弦波其正频率对应的复数模值大约是 (A \cdot N / 2)负频率还有一半能量。我们通常只看单边谱所以需要乘上 (2/N) 还原真实幅度。如果是直流分量0 Hz它的模值是 (A \cdot N)归一化时只除以 (N)不用乘 2。对应的逆变换也要记住如果修改了频谱或者想恢复时域信号直接用np.fft.ifft(X)即可。由于浮点误差结果会有极小的虚部一般用.real提取实部就行。2.2 fftfreq与fftshift把频谱坐标摆正fft输出的是复数数组本身没有“每个元素对应多少 Hz”的信息。频率轴必须自己计算方法是用np.fft.fftfreq(n, d)freqs np.fft.fftfreq(n, d1/Fs)参数d是采样间隔单位是秒得到的频率单位是 Hz。如果知道采样率Fs就填d1/Fs。这个函数返回的数组长度和fft输出一致前半段是 (0 \sim Fs/2) 的正频率后半段是负频率。freqs np.fft.fftfreq(8, d0.01) print(freqs)得到类似[0. 12.5 25. 37.5 -50. -37.5 -25. -12.5]。注意看这里负频率并不是按单调顺序排列的而是最后一个元素对应 (-12.5) Hz这正是 FFT 的固有顺序。np.fft.fftshift做的是把零频搬移到数组正中间让负频率在左侧、正频率在右侧适合画图时更直观地展开频谱。ifftshift是它的逆操作用于把中心化的频谱恢复成 FFT 原生顺序再送进ifft。# 中心化频谱 shifted np.fft.fftshift(X) shifted_freqs np.fft.fftshift(freqs)在图像处理中fftshift几乎必用因为二维图像的零频在四个角上不 shift 的话很难看懂。具体用法我会在图像部分展开。2.3 rfft与rfftfreq实数场景下的半谱优化实数信号的 FFT 结果有一个天然特性负频率部分是正频率部分的共轭镜像所以信息是冗余的。rfft只计算正频率部分输出长度不是 (N)而是 (N//2 1)包括零频和奈奎斯特频率。X_half np.fft.rfft(x) freqs_half np.fft.rfftfreq(N, d1/Fs)对应地逆变换是np.fft.irfft。由于只有一半频谱恢复时它会自动补出共轭对称的负频率部分所以在工程里处理音频、加速度计信号时我几乎总是用rfft而不是fft好处是计算量更小尤其当 N 很大时差距可观输出数组更小内存占用少频率轴理解起来更简单因为没有负频率部分干扰。一个注意点rfft的幅度归一化和fft相同单边幅度依然要乘 (2/N)直流除外。如果你用irfft(X_half)它会自动做长度为 (N) 的逆变换需要确保传入的频谱长度正确。2.4 多维变换fft2、fftn与图像处理入口二维图像本质上是数字信号在行和列方向分别采样的结果所以可以分别沿两个方向做傅里叶变换这就是fft2import numpy as np F_img np.fft.fft2(gray_image) F_shifted np.fft.fftshift(F_img)fft2等价于先对每一列做fft再对每一行做fft但一次性调用更高效。ifft2做逆变换fftn则是扩展到任意维度的版本。图像频谱的可视化不能直接显示复数数组传统做法是取幅值的对数因为频谱能量动态范围很大不压缩的话低频会过亮、高频几乎看不到magnitude_spectrum np.abs(F_shifted) log_magnitude np.log1p(magnitude_spectrum) plt.imshow(log_magnitude, cmapgray)没错做机器视觉预处理时很多操作都是围绕这张频域图来的。比如低通滤波可以看成在频域乘一个中心亮、四周暗的掩模高通滤波则相反。这里只是开了个头后面案例部分再详细演示。3. 高阶应用案例从信号到图像3.1 信号频谱分析识别周期分量的实践频谱分析最直接的用途就是从一个看似混乱的波形里找出周期性成分。我实际处理过一个风机振动数据采样率 2048 Hz时长 10 秒初步判断存在一个大约 29 Hz 的异常振动。处理步骤可以用代码串起来Fs 2048 N 2048 * 10 t np.arange(N) / Fs # 模拟一段类似振动信号的数据29Hz主振动 58Hz二次谐波 高斯噪声 np.random.seed(42) x 2.5 * np.sin(2 * np.pi * 29 * t) 1.0 * np.sin(2 * np.pi * 58 * t) x 0.5 * np.random.randn(N) X np.fft.rfft(x) freqs np.fft.rfftfreq(N, d1/Fs) amp np.abs(X) / (N / 2) amp[0] np.abs(X[0]) / N # 找前几个峰值避开0频 idx np.argsort(amp[1:])[::-1][:5] 1 for i in idx: print(f{freqs[i]:.2f} Hz, amplitude{amp[i]:.2f})结果应该能识别出 29 Hz 和 58 Hz 附近的高峰值噪声则分布在整个频带上。需要注意的是直接找峰值时频率分辨率受限于 (1/T)也就是 0.1 Hz。如果两个频率相差小于 0.1 Hz这个方案是区分不开的。此时增加采样时长比单纯增加采样率更有效。3.2 频域滤波用ifft回到时域频域滤波的思路很直观把频谱中不需要的频率部分置零再ifft回时域。比如我要去除 50 Hz 以上分量可以写# 低通滤波只保留 0~40 Hz X_clean X.copy() X_clean[freqs 40] 0 x_clean np.fft.irfft(X_clean, nN)但这里有一个我踩过很多次的大坑简单硬截断会造成频谱不连续逆变换出来时域信号在边缘会出现明显的振铃Gibbs 现象听起来就像音频里突然加了“金属感”。实操上更好的做法是构造一个平滑的掩模过渡比如让滤波器边缘在几个频率点内从 1 线性降到 0。或者直接使用scipy.signal的滤波器设计函数生成滤波器系数后在频域应用。如果坚持在频域处理建议用余弦过渡或高斯过渡mask np.ones_like(freqs, dtypefloat) transition (freqs 35) (freqs 40) mask[transition] (1 np.cos(np.pi * (freqs[transition]-35) / 5)) / 2 mask[freqs 40] 0 X_low X * mask这样得到的滤波结果更平滑但代价是过渡带变宽会损失一部分稍高频率的信号。3.3 图像频域操作高通、低通与边缘提取用图像做频域处理比一维信号直观得多。下面是一个直接可复现的流程import numpy as np import matplotlib.pyplot as plt # 假设已经有灰度图 img F np.fft.fft2(img) F_shifted np.fft.fftshift(F) # 低通掩模中心低频保留四周高频衰减 rows, cols img.shape crow, ccol rows // 2, cols // 2 mask_low np.zeros((rows, cols), dtypenp.float64) D 30 # 截止半径 y, x np.ogrid[:rows, :cols] mask_low[(y - crow) ** 2 (x - ccol) ** 2 D ** 2] 1 # 应用掩模并逆变换 F_filtered F_shifted * mask_low F_back np.fft.ifftshift(F_filtered) img_denoised np.fft.ifft2(F_back) img_denoised np.real(img_denoised) # 去掉数值噪声虚部高通滤波则是把掩模取反再保留中间一小区块为零用于提取高频边缘细节。实际项目里我在镜头表面缺陷检测中用过高通滤波预处理效果不错。需要注意图像经过fft2后如果直接做ifft2由于掩模是对称中心的偶对称掩模能保证输出实部合理但一切操作都应使用np.fft.ifftshift把频谱恢复到原生排列再进行逆变换否则图像会错位。3.4 卷积加速与相关分析卷积定理告诉我们时域卷积等价于频域乘法。当卷积核比较大时直接滑动窗口计算非常慢而用 FFT 做卷积复杂度只有 (O(N \log N))可以明显提速。对一维信号 x 和核 h标准做法是n len(x) len(h) - 1 X np.fft.rfft(x, n) H np.fft.rfft(h, n) y np.fft.irfft(X * H, n) y y[:len(x) len(h) - 1]注意补零到n len(x) len(h) - 1是为了避免循环卷积造成的边缘混叠。如果不补够FFT 的周期性会把卷积结果尾部卷回头部得到错误结果。这个技巧不只用于卷积也常用于计算互相关。两个信号的互相关可以通过“其中一个翻转后卷积”来求工程上也可以用 FFT 快速地计算。我当时做声音到达时间差估计时就是用 FFT 互相关代替了时域滑动相关计算速度快了几个数量级。4. 性能优化与工程实践4.1 高效使用FFT的注意事项傅里叶变换虽然快但性能差距也可能非常大。我整理了几个高频注意点优先用rfft处理实数信号。同样长度和点数rfft比fft能省将近一半计算量因为赫米特对称被利用了。尽量让变换长度接近 2 的幂。FFT 算法对 (2) 的幂长度最友好很多库内部会优化这类长度。NumPy 的 pocketfft 对很多长度做了优化但 (2) 的幂通常还是最优。如果原始长度不是 2 的幂可以传n参数补零到这个值。避免反复传列表。在大循环里调用np.fft.fft(list_data)会重复进行数组转换最好在最外层统一转一次。尤其是实时处理场景预先分配好数组可以减少内存分配开销。尽量一次处理整批数据。NumPy 的 FFT 支持多维数组沿指定轴计算比如np.fft.rfft可以一次性对一批语音帧做频谱提取这比循环单帧调用快不少frames np.random.randn(1000, 512).astype(np.float32) spec np.fft.rfft(frames, axis-1)采样率匹配实际需求。如果目标分析的频率上限只有 500 Hz而采样率是 20 kHz那信号采集本身就是浪费滤波降采样之后再变换性能提升会非常明显。4.2 窗口函数与频谱泄漏频谱泄漏是傅里叶分析里躲不开的问题。如果一个正弦波频率不是正好在 FFT 的离散频率网格点上它的能量会“泄漏”到相邻频点导致主瓣变宽、旁瓣抬升甚至掩盖旁边的小幅值信号。解决方法是加窗。把信号先乘一个两端衰减的窗函数比如汉宁窗再送入 FFT。加窗后的频谱旁瓣明显降低但代价是主瓣稍微变宽频率分辨率会有一点损失。所以“加窗”和“分辨率”需要权衡。window np.hanning(N) x_windowed x * window X np.fft.rfft(x_windowed)加窗后算真实幅度要特别注意因为窗函数把大部分样本的幅度压低了幅度归一化必须考虑窗的平均功率或相干增益。举个例子汉宁窗的相干增益是 0.5这意味着如果直接套用 (2/N) 归一化幅度会偏小约一半需要乘以1 / np.mean(window)修正amp 2 * np.abs(X) / N / np.mean(window) amp[0] np.abs(X[0]) / N / np.mean(window)这个细节不写进文档但实际作图时非常影响结果。我当时用加窗后得到的峰值幅度做设备诊断不修正时明显低于理论值排查了好几天才意识到是窗增益没折算。4.3 处理大数据量时的分块策略如果是长时间连续采集的振动数据或心电数据动辄几百万点直接对整个序列做 FFT 可能内存占用太大或者频率分辨率太高但并没有那么多细节需要关注。实际工程里我更习惯对长序列分段处理每段长度根据需要的频率分辨率决定。比如采集了 10 个小时的振动数据采样率 10 kHz目标是监测设备转频变化。我不会对全量数据做一次 FFT而是每 10 秒切一帧对每帧做一次rfft提取峰值频率和幅值做成趋势图。这样既降低了计算量又能直观看到频率随时间的变化。如果非得做全量高分辨率的 FFT可以考虑用重叠相加法做长卷积加速或者把身分成多段并行处理。不过对大多数分析任务分段提取频谱特征才是性价比最高的做法。我还会把分段结果保存成二维矩阵行是时间帧列是频率这样后续做时频图或频谱瀑布图都非常方便。5. 常见问题与排查技巧实录5.1 频率轴怎么总是对不上这是提问率最高的问题明明信号里有个 50 Hz 分量画出来的图峰值却不在 50 Hz 位置。绝大多数原因是频率轴计算出了问题。常见错误一直接用np.arange(N)当频率轴但横轴是样本序号不是频率。正确应该是freqs np.fft.rfftfreq(N, d1/Fs)常见错误二d填成了采样率Fs而不是采样间隔1/Fs。如果Fs200把d200填进去实际计算出的频率间隔会缩小 200 倍画出的谱全部挤在左侧。我建议写代码时先打印一次freqs数组确认最大频率是否约等于Fs/2再继续分析。常见错误三采样率到底是多少没弄清楚。比如音频文件常见采样率是 44100 Hz但有人读文件时没拿到采样率用了个默认值 8000最后峰值位置自然不对。做信号分析的第一步永远是确认采样率字段。5.2 module numpy has no attribute trapz 与属性方法混淆这个热搜词虽然不直接属于 FFT但反映了 NumPy API 的一个常见问题版本升级后函数名变了旧代码直接报错。比如np.trapz在较新版本的 NumPy 中已经迁移为np.trapezoid如果项目里还在用np.trapz就会遇到module numpy has no attribute trapz。这和调用np.fft.fft时把fft误写成np.fft.fftpack类似——版本或子模块路径变了没有及时更新代码。排查时我习惯先查当前环境里的 NumPy 版本python -c import numpy; print(numpy.__version__)然后对着官方 API 文档确认函数名。对傅里叶变换相关代码要确认使用的是np.fft.fft而不是已经不存在的np.fft.fftpack_lite或类似旧接口。更多情况下这个错误提示是在提醒你不要把所有 numpy 功能都背在内存里版本迁移后命名和模块结构都可能变。5.3 环境安装与版本兼容问题很多人项目代码没问题卡在ModuleNotFoundError: No module named numpy。这个多半是 Python 环境和当前解释器不匹配。PyCharm 里明明“装过了”却提示没有模块通常是解释器选错了项目解释器指到了系统 Python而包却装在虚拟环境里。我的排查顺序是在 PyCharm 右下角确认当前解释器路径。在 Terminal 中执行pip install numpy确认安装环境是同一个解释器。如果安装时卡在Installing backend dependencies常见于网络问题和 pip 版本过旧可以换镜像源pip install numpy -i https://pypi.tuna.tsinghua.edu.cn/simple另外NumPy 二进制版本对 Python 版本有要求。装了 Python 3.12 但 pip 源版本低或者混装也可能出现导入异常。这时候直接升级 pip 后再装最新版 NumPy 就够了。5.4 实数输入为什么要用rfft我见过不少代码在处理实数信号时依然用fft全谱分析然后自己只取前半段。从功能上没错从效率和简洁程度上则是浪费一半算力。rfft输出的浮点数组长度是N//21正好是正频率部分对大多数分析场景是足够的。如果你要对rfft的结果做逆变换用irfft不要用ifft。irfft会自动构建共轭对称频谱并且默认输出长度为2*(len(X)-1)也就是恢复回原始N。一个易错点是如果修改了rfft结果的长度比如手动补零到更长需要同步指定n参数告诉irfft目标长度。y_recon np.fft.irfft(X_half, nN)5.5 归一化模式怎么选np.fft.fft的norm参数虽然不常用但选错了会让结果偏离预期。我理解最简单的方式backward默认正变换不缩放逆变换除以 N。适合日常频谱分析和信号重构因为正变换得到的复数值绝对值直观比较容易检查。forward正变换除以 N逆变换不缩放。适合希望正变换结果直接代表平均幅度或密度的场景但实际我很少用。ortho正逆变换都除以sqrt(N)。适合计算能量谱或需要满足 Parseval 定理的时频分析能保证变换前后总能量一致。如果你计算功率谱密度或信噪比建议用normortho或单独对逆变换做归一化。我自己做振动分析时更倾向使用默认backward再手动归一化因为这样每一步中间结果都可解释。6. 我的实操心得与一点额外建议6.1 几个容易被忽略的细节在实际项目里摸爬滚打之后我发现很多问题不是 API 背得不熟而是对“信号本身”的理解没跟上。比如采样时间长度决定了频率分辨率补零只会让频谱曲线更平滑但不会把两个本来混叠在一起的频率分开。这一点我没少吃亏后来遇到频率分辨率不够的场景不是急着补零而是回头看看数据采集时长够不够。另一个细节是调试傅里叶变换代码时一定要做“往返测试”。也就是随机生成一段信号先fft再ifft对比恢复结果和原始信号的最大误差。如果误差不是浮点级别小到 1e-12 左右说明n参数、归一化方式或者轴方向用错了。这个习惯帮我排查了不少隐蔽的问题。6.2 这套能力的扩展空间学会了 NumPy 的 FFT API后续可以顺利迁移到很多更专门化的工具比如 SciPy 的scipy.fft、图像频域处理库、以及各种时频分析算法。你会发现它们的概念和参数思路非常相似只是底层实现和扩展功能更丰富。等到需要做短时傅里叶变换STFT时原理也是一脉相承的把长信号切帧、逐帧 FFT最后拼接成时频谱。我的个人经验是不要急着背 API先把离散傅里叶变换的排列顺序、归一化方式和频率轴计算这三个概念吃透再去看rfft、fftshift、窗函数这些细节会自然很多。等你能闭着眼画出频谱图并解释每个峰值对应的物理含义NumPy 傅里叶变换 API 对你来说就不再是一个黑盒了。我在后续的项目里遇到新的频域问题还会回来重新看这套基础代码每次都有新的体会。