本文还有配套的精品资源,点击获取
简介:傅里叶变换是信号处理中的核心工具,可将时域信号转换为频域以分析其频率成分。快速傅里叶变换(FFT)作为离散傅里叶变换(DFT)的高效算法,显著降低了计算复杂度,广泛应用于频谱分析、滤波、去噪和图像处理等领域。本文深入讲解FFT的基本原理,并结合Python(使用numpy库)和MATLAB两种主流工具,提供FFT实现的代码示例与分析方法,帮助读者掌握频域信号处理的核心技能,适用于通信工程、数据分析和IT相关领域的实践应用。
傅里叶变换的核心思想是将任意信号表示为正弦函数的线性组合。对于周期信号 $ x(t) $,其傅里叶级数展开为:
x(t) = sum_{k=-infty}^{infty} c_k e^{j k omega_0 t}, quad c_k = frac{1}{T} int_{-T/2}^{T/2} x(t) e^{-j k omega_0 t} dt
$$
其中 $ omega_0 = 2pi/T $ 为基频,$ c_k $ 表示第 $ k $ 阶谐波的复振幅。该式揭示了信号在频域中的离散谱结构。
对于非周期信号,引入 连续傅里叶变换(CFT) :
X(f) = int_{-infty}^{infty} x(t) e^{-j 2pi f t} dt, quad x(t) = int_{-infty}^{infty} X(f) e^{j 2pi f t} df
$$
此即时域与频域之间的双向映射。以矩形脉冲为例,其频谱为 sinc 函数,直观体现“时宽越窄,频带越宽”的物理规律。
import numpy as np
import matplotlib.pyplot as plt
# 生成矩形波并计算其理论频谱
t = np.linspace(-2, 2, 1000)
x = np.where(np.abs(t) <= 0.5, 1, 0)
f = np.linspace(-10, 10, 1000)
X = np.sinc(f) # sinc(f) = sin(pi*f)/(pi*f)
plt.plot(f, np.abs(X))
plt.title("Rectangular Pulse Spectrum |X(f)|")
plt.xlabel("Frequency (f)")
plt.ylabel("Magnitude")
plt.grid(True)
plt.show()
上述代码展示了矩形信号的频谱形态,sinc 函数的主瓣与旁瓣结构清晰呈现能量分布特性,为后续离散化分析提供直观参照。
在现代数字信号处理体系中,连续时间信号的傅里叶变换已无法直接应用于计算机系统,因为数字设备只能处理有限长度、离散采样的数据。因此, 离散傅里叶变换(Discrete Fourier Transform, DFT) 成为了连接理论分析与工程实现的关键桥梁。DFT不仅保留了连续傅里叶变换的核心思想——将信号从时域映射到频域,还通过数学上的离散化和周期性假设,使频谱分析能够在有限资源下高效执行。本章深入探讨DFT的数学建模过程、关键性能参数的影响机制,并结合音频、振动与图像等实际应用场景,揭示其在多领域中的实用价值。同时,指出DFT高计算复杂度所带来的瓶颈问题,为后续引入快速算法FFT奠定必要基础。
DFT的本质是将一个长度为 $ N $ 的有限长离散时间序列 $ x[n] $ 映射为其在频域上的复数表示 $ X[k] $,从而揭示该信号中包含哪些频率成分及其相对强度。这一过程建立在对连续信号进行理想采样与截断的基础上,具有明确的数学形式与物理意义。
设有一个长度为 $ N $ 的离散信号序列 $ x[0], x[1], dots, x[N-1] $,其 离散傅里叶变换(DFT) 定义如下:
X[k] = sum_{n=0}^{N-1} x[n] cdot e^{-j frac{2pi}{N} kn}, quad k = 0, 1, dots, N-1
其中:
- $ X[k] $ 表示第 $ k $ 个频点的复数频谱值;
- $ j = sqrt{-1} $ 是虚数单位;
- $ e^{-j frac{2pi}{N} kn} $ 是旋转因子(也称相位因子),记作 $ W_N^{kn} = e^{-j frac{2pi}{N} kn} $;
- 求和范围覆盖整个信号长度 $ N $。
该公式的物理含义是:每个频点 $ k $ 对应一个频率为 $ f_k = frac{k f_s}{N} $ 的复指数基函数(即正弦与余弦的组合),通过对输入信号与该基函数做内积运算,提取出信号在该频率下的投影幅度和相位。
对应的 逆离散傅里叶变换(IDFT) 公式为:
x[n] = frac{1}{N} sum_{k=0}^{N-1} X[k] cdot e^{j frac{2pi}{N} kn}, quad n = 0, 1, dots, N-1
IDFT的作用是从频域恢复原始时域信号,验证变换的可逆性。注意此处存在归一化系数 $ frac{1}{N} $,通常放在 IDFT 而非 DFT 中,以保持能量守恒(Parseval 定理成立)。
import numpy as np
import matplotlib.pyplot as plt
def dft(x):
N = len(x)
X = np.zeros(N, dtype=complex)
for k in range(N):
for n in range(N):
X[k] += x[n] * np.exp(-1j * 2 * np.pi * k * n / N)
return X
def idft(X):
N = len(X)
x = np.zeros(N, dtype=complex)
for n in range(N):
for k in range(N):
x[n] += X[k] * np.exp(1j * 2 * np.pi * k * n / N)
x[n] /= N
return x
# 测试信号:两个正弦波叠加
fs = 64 # 采样率
T = 1 # 时间长度
N = fs * T # 数据点数
t = np.linspace(0, T, N, endpoint=False)
x = np.sin(2 * np.pi * 5 * t) + 0.5 * np.sin(2 * np.pi * 13 * t)
# 执行DFT
X = dft(x)
x_recovered = idft(X)
# 验证误差
error = np.max(np.abs(x - x_recovered))
print(f"最大重构误差: {error:.2e}")
逻辑分析与参数说明 :
dft()函数实现了双重循环结构,外层遍历频点 $ k $,内层计算求和项,符合原始定义。- 使用
np.exp(-1j * ...)实现复指数运算,1j表示虚数单位。- 循环次数为 $ N^2 $,体现 O(N²) 复杂度。
idft()在最后除以 $ N $ 完成归一化。- 输出的最大误差小于 $ 10^{-14} $,表明数值精度良好,证明 DFT/IDFT 可逆。
此代码虽效率低,但清晰展示了 DFT 的数学本质,适用于教学理解或小规模数据处理。
要理解 DFT 的来源,需回顾连续傅里叶变换如何过渡到离散域。考虑一个连续时间信号 $ x_c(t) $,其傅里叶变换为:
X_c(f) = int_{-infty}^{infty} x_c(t) e^{-j2pi ft} dt
当使用采样间隔 $ T_s = 1/f_s $ 进行理想采样时,得到离散信号:
x[n] = x_c(nT_s)
根据采样定理,若信号带宽不超过奈奎斯特频率 $ f_s/2 $,则不会发生混叠。然而,在频域上,采样操作会导致频谱发生 周期延拓 ,即:
X_s(f) = sum_{k=-infty}^{infty} X_c(f - k f_s)
这说明离散时间信号的频谱是原连续信号频谱的无限重复。
进一步地,当我们仅取有限长度 $ N $ 的样本进行 DFT 分析时,相当于对信号加矩形窗:
x_w[n] =
begin{cases}
x[n], & 0 leq n < N
0, & ext{otherwise}
end{cases}
加窗操作在频域表现为卷积,导致真实频谱被“模糊”,产生 频谱泄漏 现象(将在 2.2.3 节详细讨论)。
此外,DFT 隐含假设输入序列是 周期性的 ,即认为 $ x[n+N] = x[n] $。这种周期延拓假设可能导致边界不连续,引发虚假高频分量。
graph TD
A[连续时间信号 xc(t)] --> B[理想采样]
B --> C[离散时间信号 x[n]]
C --> D[截断至长度 N]
D --> E[加窗操作 xw[n]]
E --> F[DFT计算 X[k]]
F --> G[周期性频谱输出]
style A fill:#f9f,stroke:#333
style G fill:#bbf,stroke:#333
图解说明:从模拟信号出发,经过采样、截断、加窗后进入 DFT 计算流程,最终输出的是周期性频谱。每一步都会影响最终频谱质量,尤其是截断带来的频谱泄漏不可忽视。
DFT 的核心优势在于它能对任意有限长序列提供完整的频域描述。但由于其基于离散且有限的数据集,带来了若干重要限制:
例如,若我们采集一段含有 50Hz 和 50.5Hz 成分的电力信号,而采样率为 1kHz,记录时间为 0.2 秒($ N=200 $),则频率分辨率为 $ Delta f = 1000 / 200 = 5 $ Hz,远大于两者的差值,导致两者无法分辨。
解决方法包括:
- 增加观测时间以提升 $ N $;
- 使用补零(zero-padding)改善栅栏效应(但不提高真实分辨率);
- 应用窗函数抑制频谱泄漏;
- 结合插值法进行频率精修。
这些优化策略将在后续章节展开。
DFT 的实用性高度依赖于多个关键参数的选择,这些参数直接影响频谱分析的准确性与可靠性。合理配置采样率、数据长度及窗函数类型,是获得高质量频域信息的前提。
根据香农采样定理,为了无失真地重建信号, 采样率 $ f_s $ 必须至少为信号最高频率 $ f_{max} $ 的两倍 ,即:
f_s > 2 f_{max}
否则会发生 频谱混叠(Aliasing) ,即高频成分“折叠”到低频区域,造成错误解释。
例如,若真实信号包含 70Hz 成分,而采样率为 100Hz,则奈奎斯特频率为 50Hz。此时 70Hz 将被混叠为 $ |100 - 70| = 30Hz $,表现为虚假的 30Hz 成分。
以下 Python 代码演示混叠现象:
import numpy as np
import matplotlib.pyplot as plt
fs_low = 10 # 低采样率(不足)
fs_high = 100 # 高采样率(充足)
t_low = np.arange(0, 2, 1/fs_low)
t_high = np.arange(0, 2, 1/fs_high)
f_true = 7 # 真实频率
x_low = np.sin(2*np.pi*f_true*t_low)
x_high = np.sin(2*np.pi*f_true*t_high)
plt.figure(figsize=(10,4))
plt.plot(t_high, x_high, 'b-', alpha=0.5, label='真实波形 (100Hz)')
plt.stem(t_low, x_low, 'r', markerfmt='ro', basefmt=" ", label='采样点 (10Hz)')
plt.xlabel('时间 (s)')
plt.ylabel('幅值')
plt.title('混叠现象示例:7Hz信号以10Hz采样')
plt.legend()
plt.grid(True)
plt.show()
逻辑分析 :
- 真实信号为 7Hz 正弦波;
- 以 10Hz 采样时,每周期仅采约 1.4 个点,严重欠采样;
- 观察离散点呈现类似 3Hz 的波动趋势(因 $ 10 - 7 = 3 $),形成视觉上的“走样”。
该图直观展示了混叠的危害,强调了正确设置采样率的重要性。
频率分辨率 $ Delta f $ 定义为相邻 DFT 频点之间的间隔:
Delta f = frac{f_s}{N}
其中 $ N $ 为有效数据点数。由于 $ N = f_s cdot T $,也可写作:
Delta f = frac{1}{T}
这意味着: 分辨率只取决于观测时间 $ T $ ,与采样率无关。
举例:若想分辨 1Hz 差异的频率,必须保证 $ T geq 1 $ 秒。
可见,即使提高采样率(如从 1000 到 2000 Hz),若时间不变,分辨率仍不变。只有延长采集时间才能真正提升分辨能力。
当信号频率不在 DFT 的离散频点上,或信号在窗口边缘不连续时,会出现能量向邻近频点扩散的现象,称为 频谱泄漏 。
根本原因在于:DFT 假设信号是周期延拓的,若原始信号在 $ x[0]
eq x[N-1] $,则会产生跳变,引入额外高频成分。
使用窗函数的步骤如下:
from scipy.signal import windows
# 生成测试信号
N = 64
n = np.arange(N)
x = np.cos(2*np.pi*15.3/N*n) # 非整数周期频率
# 应用汉宁窗
win = windows.hann(N)
x_win = x * win
# 计算DFT
X_rect = np.fft.fft(x)
X_hann = np.fft.fft(x_win)
# 绘制频谱
freq = np.fft.fftfreq(N)
plt.figure(figsize=(10,4))
plt.plot(freq[:N//2], 20*np.log10(np.abs(X_rect[:N//2])), label='矩形窗')
plt.plot(freq[:N//2], 20*np.log10(np.abs(X_hann[:N//2])), label='汉宁窗')
plt.xlabel('归一化频率')
plt.ylabel('幅值 (dB)')
plt.title('窗函数对频谱泄漏的抑制效果')
plt.legend()
plt.grid(True)
plt.show()
参数说明 :
windows.hann(N)生成长度为 N 的汉宁窗;x * win实现逐点乘法(加窗);- 使用 dB 刻度(
20*log10)便于观察旁瓣衰减;- 汉宁窗显著降低旁瓣,但主瓣展宽,牺牲部分分辨率。
选择窗函数需权衡主瓣宽度与旁瓣抑制能力,依据具体应用场景决策。
DFT 不仅是理论工具,更是众多工程系统的基石。以下三个案例展示其在音频、机械监测与图像处理中的广泛应用。
音频信号本质上是声压随时间变化的波形。通过 DFT 可将其分解为各谐波成分,用于音调识别、噪声分析等任务。
from scipy.io import wavfile
import numpy as np
# 读取音频文件
sample_rate, audio_data = wavfile.read('test_tone.wav')
if audio_data.ndim > 1:
audio_data = audio_data.mean(axis=1) # 立体声转单声道
# 取一帧数据
frame_size = 1024
frame = audio_data[:frame_size]
windowed_frame = frame * np.hanning(frame_size)
# 执行FFT
X = np.fft.fft(windowed_frame)
freqs = np.fft.fftfreq(frame_size, 1/sample_rate)
magnitude = np.abs(X[:len(X)//2])
# 找出主导频率
peak_freq = freqs[np.argmax(magnitude)]
print(f"检测到主导频率: {peak_freq:.2f} Hz")
扩展说明 :
wavfile.read()返回采样率和整数型样本;- 多通道音频需合并为单声道;
- 使用 Hanning 窗减少频谱泄漏;
- 主导频率可用于判断音符或检测异常音。
机械设备运行中产生的振动信号常包含轴承故障、齿轮啮合等特征频率。通过 DFT 可定位这些频率,实现早期故障诊断。
假设某电机转速为 1800 RPM(30 Hz),其一级齿轮啮合频率为 150 Hz。采集振动信号后进行频谱分析:
# 模拟振动信号
fs_vib = 1000
t_vib = np.linspace(0, 2, 2000, endpoint=False)
vibration = (0.5 * np.sin(2*np.pi*30*t_vib) +
0.3 * np.sin(2*np.pi*150*t_vib) +
0.1 * np.random.randn(len(t_vib)))
# 加窗并计算频谱
X_vib = np.fft.fft(vibration * np.blackman(len(vibration)))
freq_vib = np.fft.fftfreq(len(vibration), 1/fs_vib)
mag_vib = np.abs(X_vib[:len(X_vib)//2])
# 查找显著峰值
peaks = np.where(mag_vib > 0.5 * np.max(mag_vib))[0]
for p in peaks:
print(f"检测频率: {freq_vib[p]:.1f} Hz")
输出可能包含 30Hz 和 150Hz,对应转频与啮合频率,辅助判断设备状态。
图像可视为二维空间信号。对其应用二维 DFT:
F(u,v) = sum_{x=0}^{M-1} sum_{y=0}^{N-1} f(x,y) e^{-j2pi (ux/M + vy/N)}
纹理通常表现为特定方向和间距的空间模式,在频域中体现为对称的能量集中点。
from skimage import data
import numpy as np
import matplotlib.pyplot as plt
# 加载纹理图像
img = data.coins()[100:200, 100:200] # 截取局部
F = np.fft.fft2(img)
F_shift = np.fft.fftshift(F)
magnitude_spectrum = np.log(1 + np.abs(F_shift))
plt.subplot(121), plt.imshow(img, cmap='gray'), plt.title('原始图像')
plt.subplot(122), plt.imshow(magnitude_spectrum, cmap='gray'), plt.title('频谱图')
plt.show()
频谱中心代表直流分量,外围亮点指示周期性纹理方向与频率。可用于分类或缺陷检测。
尽管 DFT 功能强大,但其计算代价高昂。直接实现需 $ N^2 $ 次复数乘法与加法,当 $ N=8192 $ 时,运算量达六千多万次,难以满足实时处理需求。
对于一个 $ N $ 点 DFT:
以 ARM Cortex-M4 单片机为例,每秒仅能完成约 100 万次浮点运算。处理 1024 点 DFT 需:
1024^2 = 1,048,576 ext{ 次复数乘法 } approx 4M ext{ 实数乘法}
耗时超过 4 秒,完全不可接受。
为突破此瓶颈, 快速傅里叶变换(FFT) 利用旋转因子的对称性和周期性,采用分治策略将 DFT 分解为更小规模的子问题,将时间复杂度降至 $ O(N log N) $。
例如,对于 $ N=1024 $,FFT 仅需约 $ 1024 imes 10 = 10,240 $ 次操作,速度提升近百倍。
下一章将系统讲解 FFT 的算法原理与实现机制,彻底解决 DFT 的效率难题。
快速傅里叶变换(Fast Fourier Transform, FFT)是离散傅里叶变换(DFT)的一种高效实现方式,其核心价值在于将原本 $ O(N^2) $ 的计算复杂度降低至 $ O(N log N) $,使得频域分析在实时信号处理、通信系统、图像处理等领域得以广泛应用。本章深入剖析基2-FFT的核心思想、数学推导过程及其实际应用中的约束条件,并构建完整的高效频谱分析流程。
基2-FFT(Radix-2 FFT)是最经典且最广泛使用的FFT算法之一,适用于输入长度为 $ N = 2^m $ 的序列。它通过分治策略递归地将一个长序列的DFT分解为多个短序列的DFT,从而大幅减少复数乘法和加法的运算量。该方法主要有两种实现形式:时间抽取法(Decimation-in-Time, DIT)和频域抽取法(Decimation-in-Frequency, DIF),二者在结构上对称但数据流动方向不同。
时间抽取法(DIT-FFT)首先将输入序列按奇偶位置进行拆分,即将原序列 $ x[n] $ 分解为两个子序列:
x_{ ext{even}}[k] = x[2k], quad x_{ ext{odd}}[k] = x[2k+1], quad k=0,1,dots,N/2-1
然后分别对这两个子序列执行 $ N/2 $ 点DFT,再通过蝶形运算合并结果。其关键公式如下:
X[k] = X_{ ext{even}}[k] + W_N^k X_{ ext{odd}}[k]
X[k + N/2] = X_{ ext{even}}[k] - W_N^k X_{ ext{odd}}[k]
其中 $ W_N^k = e^{-j2pi k/N} $ 是旋转因子(twiddle factor),具有周期性和对称性:$ W_N^{k+N} = W_N^k $,且 $ W_N^{k+N/2} = -W_N^k $。
相比之下,频域抽取法(DIF-FFT)则是先对输入做前半后半分割,即:
x_1[k] = x[k] + x[k + N/2], quad x_2[k] = (x[k] - x[k + N/2]) cdot W_N^k
随后对 $ x_1 $ 和 $ x_2 $ 分别进行 $ N/2 $ 点DFT,输出自然顺序排列。DIF的特点是输入顺序不变,输出需要位反转重排;而DIT则输入需重排,输出为自然序。
下表总结了DIT与DIF的主要特性差异:
从算法设计角度看,DIT更适合软件实现,因为输出无需额外重排;而DIF由于其规则的数据流,在FPGA或ASIC等硬件平台中更易于流水化处理。
graph TD
A[N点输入序列] --> B{选择方法}
B --> C[DIT: 按奇偶拆分]
B --> D[DIF: 前后半段组合]
C --> E[计算N/2点DFT]
D --> F[计算N/2点DFT]
E --> G[蝶形合并]
F --> G
G --> H[最终N点DFT输出]
该流程图展示了DIT与DIF的共通分治逻辑:无论哪种方法,都依赖于将大问题分解为小规模DFT并利用蝶形单元完成合并。
蝶形运算是FFT中最基本的计算单元,每一对数据点经过一次蝶形操作即可生成新的频域值。以DIT为例,第 $ m $ 级蝶形运算中的任意一对节点满足以下关系:
# Python伪代码表示一个蝶形单元
def butterfly(a, b, W):
temp_a = a + W * b
temp_b = a - W * b
return temp_a, temp_b
参数说明:
- a , b :当前层级的两个输入复数;
- W :对应的旋转因子 $ W_N^k $;
- 返回值:更新后的两个输出值,用于下一级计算。
该运算之所以称为“蝶形”,是因为其图形表示形似蝴蝶翅膀交叉。在一个8点DIT-FFT中,共有 $ log_2 8 = 3 $ 层,每层包含 $ N/2 = 4 $ 个蝶形单元。
以下是8点DIT-FFT的完整蝶形网络示意图(使用Mermaid绘制):
graph LR
subgraph Stage 3
A6[X(0)] -- + --> O0
A7[X(4)] -- - --> O0
A4[X(2)] -- + --> O1
A5[X(6)] -- - --> O1
A2[X(1)] -- + --> O2
A3[X(5)] -- - --> O2
A0[X(3)] -- + --> O3
A1[X(7)] -- - --> O3
end
subgraph Stage 2
B0[x(0)] -- + --> A6
B1[x(4)] -- - --> A6
B2[x(2)] -- + --> A4
B3[x(6)] -- - --> A4
B4[x(1)] -- + --> A2
B5[x(5)] -- - --> A2
B6[x(3)] -- + --> A0
B7[x(7)] -- - --> A0
end
subgraph Stage 1
C0[x(0)] -- + --> B0
C1[x(1)] -- - --> B0
C2[x(2)] -- + --> B2
C3[x(3)] -- - --> B2
C4[x(4)] -- + --> B4
C5[x(5)] -- - --> B4
C6[x(6)] -- + --> B6
C7[x(7)] -- - --> B6
end
上述图示清晰展现了三级蝶形运算的逐层推进过程。每一级的旋转因子 $ W $ 取值不同,例如第一级所有 $ W=1 $,第二级使用 $ W_8^0=1 $ 和 $ W_8^2=-j $,第三级则涉及 $ W_8^0, W_8^1, W_8^2, W_8^3 $。
递归实现方面,可以采用如下Python风格的伪代码框架:
import cmath
def fft_dit(x):
N = len(x)
if N <= 1:
return x
# 分离奇偶项
even = fft_dit(x[0::2])
odd = fft_dit(x[1::2])
# 合并结果
X = [0] * N
for k in range(N // 2):
t = cmath.exp(-2j * cmath.pi * k / N) * odd[k]
X[k] = even[k] + t
X[k + N//2] = even[k] - t
return X
逐行解析:
- 第3行:递归终止条件,当序列长度为1时直接返回;
- 第5–6行:利用切片操作提取偶数位和奇数位子序列,递归调用自身;
- 第9行:计算旋转因子 $ W_N^k $ 并与奇部相乘;
- 第10–11行:执行蝶形加减运算,生成上下半部分的DFT结果;
- 第12行:返回合并后的完整频域序列。
此递归版本虽直观易懂,但由于频繁创建新数组和函数调用开销较大,工业级实现通常采用迭代+位反转重排的方式提升性能。
在DIT-FFT中,原始输入序列必须按照“位反转顺序”重新排列,才能保证各级蝶形运算正确对接。所谓位反转,是指将索引的二进制表示翻转后作为新的地址。
例如,对于 $ N=8 $,索引范围为0~7,其二进制表示及对应位反转如下:
因此,原始输入应按 [x[0], x[4], x[2], x[6], x[1], x[5], x[3], x[7]] 的顺序排列。
以下是一个高效的位反转重排函数实现:
void bit_reverse(double complex *x, int N)
int k = N >> 1;
while (k <= n) {
n -= k;
k >>= 1;
}
n += k;
}
}
参数说明:
- x :指向复数数组的指针;
- N :数组长度,必须为2的幂;
- logN :预先计算 $ log_2 N $;
- 内层循环模拟二进制加法的逆过程,逐步生成下一个位反转索引。
该算法避免了显式二进制转换,通过位操作高效跳跃,时间复杂度为 $ O(N) $,适合嵌入到FFT主循环之前预处理阶段。
FFT的效率提升源于对DFT核函数 $ W_N^{kn} = e^{-j2pi kn/N} $ 的三大性质的充分利用: 周期性 、 对称性 和 可约性 。
具体来说:
- 周期性 :$ W_N^{k+nN} = W_N^k $
- 对称性 :$ W_N^{k+N/2} = -W_N^k $
- 可约性 :$ W_{aN}^{akn} = W_N^{kn} $
在DIT-FFT推导中,原始DFT表达式为:
X[k] = sum_{n=0}^{N-1} x[n] W_N^{kn}
将其分为偶数项和奇数项:
X[k] = sum_{m=0}^{N/2-1} x[2m] W_N^{2mk} + sum_{m=0}^{N/2-1} x[2m+1] W_N^{(2m+1)k}
注意到 $ W_N^{2m k} = W_{N/2}^{m k} $,所以:
X[k] = 縛brace{sum_{m=0}^{N/2-1} x[2m] W_{N/2}^{mk}} {X { ext{even}}[k]} + W_N^k 縛brace{sum_{m=0}^{N/2-1} x[2m+1] W_{N/2}^{mk}} {X { ext{odd}}[k]}
这就得到了标准的蝶形结构。更重要的是,由于 $ X_{ ext{even}} $ 和 $ X_{ ext{odd}} $ 都是 $ N/2 $ 点DFT,可以继续递归下去,直到1点为止。
整个递归深度为 $ log_2 N $,每层有 $ N/2 $ 个蝶形单元,每个蝶形包含1次复数乘法和2次复数加法,故总运算量约为:
ext{乘法次数} approx frac{N}{2} log_2 N, quad ext{加法次数} approx N log_2 N
相较DFT的 $ N^2 $ 次乘法,当 $ N=1024 $ 时,FFT仅需约5120次乘法,效率提升近200倍。
尽管FFT减少了总体运算量,但在高性能计算中仍可通过底层优化进一步提速。典型手段包括:
实数输入特例优化(如Goertzel算法或DCT融合)
若输入为纯实数序列,则输出具有共轭对称性:$ X[k] = X^*[N-k] $,只需计算前半部分。
SIMD指令集加速(如AVX、NEON)
利用单指令多数据流并行处理多个复数蝶形。
缓存友好访问模式设计
采用缓存块划分(tiling)减少内存抖动。
以下为C语言中使用静态查表优化旋转因子的示例:
#define MAX_N 1024
double cos_table[MAX_N], sin_table[MAX_N];
void init_twiddle(int N) {
for (int k = 0; k < N; k++) {
cos_table[k] = cos(-2 * M_PI * k / N);
sin_table[k] = sin(-2 * M_PI * k / N);
}
}
// 使用查表替代实时计算
double complex get_W(int k, int N) {
return cos_table[k % N] + I * sin_table[k % N];
}
优势分析:
- 减少每次 exp() 或 sincos() 调用带来的CPU开销;
- 支持编译器向量化优化;
- 特别适合固定尺寸FFT(如OFDM系统中常用1024点)。
此外,现代库如FFTW、Intel MKL均采用“计划器”机制,在首次运行时探测最优执行路径(包括算法选择、缓存策略、线程调度等),实现极致性能。
DFT的直接计算需要对每个频率点 $ k $ 遍历所有时间点 $ n $,形成双重循环:
for k in range(N):
X[k] = 0
for n in range(N):
X[k] += x[n] * exp(-2j * pi * k * n / N)
该结构导致 $ O(N^2) $ 计算复杂度。当 $ N=8192 $ 时,需约67百万次复数乘法,耗时可达数百毫秒。
而FFT通过分治法打破这一瓶颈。设 $ T(N) $ 表示计算 $ N $ 点FFT所需时间,则有递推关系:
T(N) = 2T(N/2) + O(N)
根据主定理(Master Theorem),解得 $ T(N) = O(N log N) $。
以 $ N=2^{10}=1024 $ 为例:
- DFT:$ 1024^2 = 1,048,576 $ 次运算
- FFT:$ 1024 imes 10 = 10,240 $ 次运算(约)
性能提升比达100倍以上。这使得原本无法实时处理的音频、雷达信号现在可在微秒级完成频谱分析。
大多数基2-FFT实现要求输入长度为 $ 2^m $,否则无法整除分解。面对非2幂长度信号(如1000点),常见做法是 零填充(zero-padding) 至最近的2的幂(如1024)。
零填充不会增加新信息,但能提高频率分辨率的视觉表现(即插值效果)。其代价是引入旁瓣泄漏,需配合窗函数抑制。
import numpy as np
def pad_to_power_of_2(x):
N = len(x)
next_pow = 1 << (N - 1).bit_length() # 找到大于等于N的最小2的幂
padded = np.zeros(next_pow, dtype=x.dtype)
padded[:N] = x
return padded, next_pow
参数说明:
- x :原始信号数组;
- next_pow :目标长度;
- padded :补零后的数组。
虽然补零可使FFT顺利执行,但应注意:它并不真正提升物理分辨率,仅使频谱曲线更平滑。
对于实数信号 $ x[n] in mathbb{R} $,其DFT满足:
X[k] = X^*[N-k]
这意味着只需要计算 $ X[0] $ 到 $ X[N/2] $ 即可恢复全部信息。专业库(如 numpy.fft.rfft )专门为此类情况设计,仅返回前 $ N/2+1 $ 个点,节省内存和带宽。
突发信号在截断时会产生频谱泄漏。解决办法是在FFT前施加窗函数,如汉宁窗:
w[n] = 0.5 - 0.5 cosleft(frac{2pi n}{N-1}
ight)
window = np.hanning(N)
x_windowed = x * window
X = fft(x_windowed)
窗函数虽削弱泄漏,但也降低频率分辨率,需权衡选择。
完整流程包括:
1. 去除直流分量(减去均值)
2. 应用窗函数
3. 补零至2的幂
4. 执行FFT
5. 计算幅度谱并归一化
def analyze_spectrum(x, fs):
x = x - np.mean(x) # 去均值
window = np.hanning(len(x))
x_win = x * window
X = np.fft.fft(x_win)
freqs = np.fft.fftfreq(len(X), 1/fs)
magnitude = np.abs(X)
return freqs, magnitude
由于FFT频点离散,真实峰值可能落在栅格之间。可通过 质心法 或 抛物拟合 提高精度:
f_{ ext{peak}} = k + frac{1}{2} frac{|X[k+1]| - |X[k-1]|}{|X[k-1]| - 2|X[k]| + |X[k+1]|}
此技术广泛应用于音频音高检测、振动故障诊断等领域。
在现代信号处理实践中,快速傅里叶变换(FFT)已成为从时域到频域分析不可或缺的工具。随着数据科学与工程计算对高效数值处理的需求日益增长,Python 凭借其强大的开源生态,尤其是 NumPy 和 SciPy 库的支持,成为实现 FFT 分析的主流语言之一。其中, numpy.fft 模块提供了简洁、高效的接口用于执行离散傅里叶变换及其逆变换,广泛应用于音频处理、生物医学信号分析、通信系统建模等多个领域。
本章将深入探讨如何在 Python 环境下利用 numpy.fft 实现完整的频谱分析流程。内容涵盖核心函数的功能解析、典型信号的构建与变换、幅度谱与相位谱的正确解读方法,并结合实际应用场景进行代码级演示。通过系统性讲解,帮助具备一定编程基础和信号处理背景的开发者掌握从原始数据采集到可视化输出的全链路技术细节。
numpy.fft 是 NumPy 提供的一个专门用于执行傅里叶变换的子模块,封装了多种优化后的 FFT 算法实现,支持一维、二维及多维数组的快速频域转换。该模块不仅兼容 IEEE 浮点标准,还针对实数输入进行了特殊优化,显著提升了计算效率与内存利用率。理解其核心函数的工作机制是构建稳健频谱分析系统的前提。
fft() 是最基础的一维快速傅里叶变换函数,用于将长度为 $ N $ 的时域序列 $ x[n] $ 转换为复数形式的频域表示 $ X[k] $:
X[k] = sum_{n=0}^{N-1} x[n] cdot e^{-j2pi kn/N}, quad k = 0, 1, …, N-1
对应的逆变换由 ifft() 完成,公式如下:
x[n] = frac{1}{N} sum_{k=0}^{N-1} X[k] cdot e^{j2pi kn/N}
而 fftshift() 则负责重新排列频谱顺序,使零频率分量居中显示,便于观察对称结构。
import numpy as np
import matplotlib.pyplot as plt
# 构造一个简单正弦信号
N = 64
t = np.linspace(0, 2*np.pi, N, endpoint=False)
x = np.sin(5 * t) + 0.5 * np.random.randn(N)
# 执行FFT
X = np.fft.fft(x)
# 频率轴生成
freq = np.fft.fftfreq(N)
# 零频居中
X_shifted = np.fft.fftshift(X)
freq_shifted = np.fft.fftshift(freq)
# 可视化
plt.figure(figsize=(10, 4))
plt.plot(freq_shifted, np.abs(X_shifted))
plt.xlabel('Normalized Frequency')
plt.ylabel('Magnitude')
plt.title('Shifted Magnitude Spectrum')
plt.grid(True)
plt.show()
逻辑分析与参数说明
-np.fft.fft(x):输入实数或复数序列x,返回等长复数数组,每个元素代表对应频率下的复振幅。
-np.fft.fftfreq(N):根据采样点数N自动生成归一化频率轴,范围为 $[0, 1)$ 或 $[-0.5, 0.5)$,单位为“采样率的倍数”。
-np.fft.fftshift():交换数组前后半部分,适用于双边谱显示;若未调用此函数,则零频位于数组首部。
该三者构成频谱可视化的基础组合,在后续案例中频繁出现。
对于纯实数输入信号(如大多数传感器采集的数据),其频谱具有共轭对称性:$ X[k] = X^*[N-k] $。这意味着后半部分信息冗余,可仅保留前 $ N//2 + 1 $ 个非冗余频率点以节省存储空间和计算资源。
为此, numpy.fft.rfft() 提供了专门针对实数信号的 FFT 接口:
X_real = np.fft.rfft(x) # 输出长度为 N//2 + 1
相应地, irfft() 用于恢复原信号:
x_recovered = np.fft.irfft(X_real, n=N) # 需指定原始长度N以避免歧义
fft() rfft() 优势说明 :当处理大规模实数信号(如音频、振动)时,使用
rfft()可减少约 50% 的内存占用并提升缓存命中率,尤其适合嵌入式或实时系统部署。
要将 FFT 结果映射到物理频率轴,必须结合采样率 $ f_s $ 进行缩放。 fftfreq(N, d) 接收两个参数:
N : 信号长度 d : 采样间隔(秒) 返回一个长度为 N 的数组,表示每个频点对应的频率值(Hz)。例如:
fs = 1000 # 采样率 1000 Hz
T = 1/fs # 采样周期
N = 1000
freq = np.fft.fftfreq(N, T)
此时 freq 的取值范围为 [0, 499, -500, ..., -1] Hz(未 shift),符合奈奎斯特准则。
若配合 fftshift() 使用,则可得到中心对称的频率轴:
freq_centered = np.fft.fftshift(freq)
这在绘制功率谱密度图时至关重要,能直观反映低频与高频成分分布。
graph TD
A[原始时域信号 x[n]] --> B{是否为实数?}
B -- 是 --> C[rfft(): 仅计算非负频率]
B -- 否 --> D[fft(): 计算全部频率]
C --> E[生成频率轴 freq = rfftfreq(N,d)]
D --> F[生成频率轴 freq = fftfreq(N,d)]
E --> G[可选: fftshift() 居中显示]
F --> G
G --> H[计算 |X[k]| 或 angle(X[k])]
H --> I[matplotlib 可视化]
该流程体现了从原始数据到频谱展示的标准路径,强调了不同类型信号的选择策略。
完成一次准确的频谱分析,需遵循严谨的数据处理流程:从信号构造开始,经历预处理、变换、解析到最后可视化,每一步都直接影响结果的可靠性。
为验证 FFT 性能,常构造包含多个频率成分的合成信号。以下是一个典型示例:
import numpy as np
fs = 800 # 采样率 (Hz)
T = 1 # 信号持续时间 (s)
N = int(fs * T) # 总采样点数
t = np.linspace(0, T, N, endpoint=False)
# 三个正弦波叠加:50Hz, 120Hz, 300Hz
f1, f2, f3 = 50, 120, 300
A1, A2, A3 = 1.0, 0.7, 0.5
x = A1 * np.sin(2*np.pi*f1*t) +
A2 * np.sin(2*np.pi*f2*t) +
A3 * np.sin(2*np.pi*f3*t) +
0.2 * np.random.randn(N) # 加入高斯白噪声
参数说明 :
-fs=800Hz:满足奈奎斯特条件(最高频率 300Hz < 400Hz)
-T=1s:确保频率分辨率为 $ Delta f = 1/T = 1Hz $
- 噪声强度控制在 20% 幅度以内,模拟真实环境干扰
此信号可用于检测 FFT 对弱信号的识别能力以及抗噪性能。
接下来执行变换并解析结果:
X = np.fft.fft(x)
mag = np.abs(X) # 幅度谱
phase = np.angle(X) # 相位谱
freq = np.fft.fftfreq(N, 1/fs)
# 仅保留正频率部分(单边谱)
half_N = N // 2
mag_single = mag[:half_N]
phase_single = phase[:half_N]
freq_positive = freq[:half_N]
逐行逻辑分析 :
-np.fft.fft(x):执行 O(N log N) 复杂度的基2-FFT,输出复数数组
-np.abs(X):计算每个频点的模值,即 $ |X[k]| = sqrt{ ext{Re}^2 + ext{Im}^2} $
-np.angle(X):返回主值区间 $ (-pi, pi] $ 内的相位角
- 截取[:half_N]实现单边谱转换,适用于实数信号
注意:由于能量守恒,单边谱中直流分量(k=0)和 Nyquist 分量(k=N/2)不应加倍,其余频率点应乘以2以补偿对称丢失的能量。
最终通过图形展示频谱特征:
import matplotlib.pyplot as plt
plt.figure(figsize=(12, 5))
# 幅度谱
plt.subplot(1, 2, 1)
plt.plot(freq_positive, 2 * mag_single, 'b') # 双倍幅度
plt.xlim(0, fs/2)
plt.xlabel('Frequency (Hz)')
plt.ylabel('Amplitude')
plt.title('Single-sided Amplitude Spectrum')
plt.grid()
# 相位谱
plt.subplot(1, 2, 2)
plt.plot(freq_positive, phase_single, 'r')
plt.xlim(0, fs/2)
plt.xlabel('Frequency (Hz)')
plt.ylabel('Phase (rad)')
plt.title('Single-sided Phase Spectrum')
plt.grid()
plt.tight_layout()
plt.show()
图表意义 :
- 左图应在 50Hz、120Hz、300Hz 处出现明显峰值,验证频率提取准确性
- 右图相位应接近理论值(此处因加噪略有抖动)
该流程构成了工业级频谱分析的基础模板,可直接迁移至实际项目中。
尽管幅度谱广受关注,但相位信息同样承载着信号的时间结构特征,尤其在脉冲检测、回波定位等领域不可忽视。
FFT 输出为复数序列 $ X[k] = a_k + jb_k $,其模与幅角定义如下:
Python 中分别用 np.abs() 和 np.angle() 实现:
X = np.fft.fft(x)
magnitude = np.abs(X)
phase_raw = np.angle(X)
注意事项 :
- 当实部接近零时,相位跳变剧烈,可能造成误判
- 建议使用np.unwrap()消除 $ pmpi $ 跳变问题
双边谱包含全部 $ N $ 个频率点,适用于复数信号分析;而实数信号通常采用单边谱。
转换代码如下:
def to_single_sided(X, fs):
N = len(X)
half = N // 2
mag = np.abs(X[:half])
mag[1:-1] *= 2 # 中间点加倍
freq = np.fft.fftfreq(N, 1/fs)[:half]
return freq, mag
此函数自动处理边界情况,适用于通用场景。
功率谱密度(PSD)反映单位频率内的信号功率分布,常用 Welch 方法估计,但也可基于 FFT 粗略计算:
psd = (np.abs(X)**2) / (fs * N)
或使用更精确的周期图法:
from scipy.signal import periodogram
f, Pxx = periodogram(x, fs)
PSD 在机械故障诊断、脑电分析中有广泛应用,单位为 $ V^2/Hz $
读取 .wav 文件并分析其频谱:
from scipy.io import wavfile
sample_rate, audio_data = wavfile.read('test_tone.wav')
# 若为立体声,取左声道
if audio_data.ndim > 1:
audio_data = audio_data[:, 0]
# 截取前1秒
N = sample_rate
x_audio = audio_data[:N].astype(float)
# 执行FFT
X_audio = np.fft.rfft(x_audio)
freq_audio = np.fft.rfftfreq(N, 1/sample_rate)
mag_audio = np.abs(X_audio)
# 绘图
plt.plot(freq_audio, mag_audio)
plt.xlabel('Frequency (Hz)')
plt.ylabel('Magnitude')
plt.title('Audio Signal Spectrum')
plt.grid()
plt.show()
应用场景 :可用于识别音调、检测啸叫、分析乐器谐波结构
心电信号常受 50Hz 工频干扰影响:
# 假设 ecg_signal 已加载
X_ecg = np.fft.fft(ecg_signal)
freq_ecg = np.fft.fftfreq(len(ecg_signal), 1/500) # fs=500Hz
mag_ecg = np.abs(X_ecg)
# 查找50Hz附近峰值
idx_50 = np.argmin(np.abs(freq_ecg - 50))
print(f"50Hz处幅度: {mag_ecg[idx_50]:.2f}")
若该处存在显著峰值,表明需添加陷波滤波器抑制干扰
利用 FFT 实现理想低通滤波:
def ideal_lowpass_fft(x, cutoff, fs):
X = np.fft.fft(x)
freq = np.fft.fftfreq(len(x), 1/fs)
X_filtered = X * (np.abs(freq) <= cutoff)
return np.fft.ifft(X_filtered).real
# 应用滤波
cleaned = ideal_lowpass_fft(noisy_signal, cutoff=100, fs=1000)
注意:理想滤波器会导致吉布斯现象,建议改用窗函数平滑过渡
该表有助于快速判断信号所属类别及分析重点。
在MATLAB环境中, fft 函数是进行快速傅里叶变换的核心工具,其底层基于高度优化的FFTPACK库实现,支持高效处理实数和复数信号。掌握其基本语法是开展频谱分析的第一步。
fft(X) 对输入向量或数组 X 执行N点DFT(N为 X 的有效长度),而 fft(X, N) 显式指定变换点数为N。当 N > length(X) 时,MATLAB自动对信号补零(zero-padding);若 N < length(X) ,则截断信号。
% 示例:对比不同N值的FFT效果
Fs = 1000; % 采样率 1000Hz
t = 0:1/Fs:1-1/Fs; % 时间轴(1秒)
x = sin(2*pi*50*t) + 0.5*sin(2*pi*120*t); % 含50Hz和120Hz的信号
Y1 = fft(x); % 默认N=length(x)=1000
Y2 = fft(x, 1024); % 补零至1024点提升频率分辨率表观精度
补零虽不增加真实信息,但可使频谱曲线更平滑,便于峰值定位和可视化。
由于基2-FFT要求数据长度为2的幂次以达到最优性能,常用 nextpow2() 获取大于等于信号长度的最小2的幂:
N = length(x);
N_fft = 2^nextpow2(N); % 如N=1000 → N_fft=1024
Y = fft(x, N_fft);
这能显著减少运算时间,尤其在处理长序列时优势明显。
尽管 freqz 主要用于滤波器响应分析,但对于已知系统函数的情况也可辅助频域建模。一般频谱绘图流程如下:
f = (0:N_fft-1)*(Fs/N_fft); % 频率轴
P2 = abs(Y/N_fft); % 双边幅度谱归一化
P1 = P2(1:N_fft/2+1); % 单边谱
P1(2:end-1) = 2*P1(2:end-1); % 能量守恒修正
plot(f(1:N_fft/2+1), P1)
xlabel('Frequency (Hz)'), ylabel('Magnitude')
title('Single-sided Amplitude Spectrum')
该流程实现了从原始FFT输出到物理可解释频谱图的转换。
对于包含负频率的双边谱, fftshift 将零频分量移至中心,适用于图像处理或对称频谱展示:
Y_shifted = fftshift(Y);
f_shifted = (-N_fft/2:N_fft/2-1)*(Fs/N_fft);
plot(f_shifted, abs(Y_shifted))
xlabel('Frequency (Hz)'), ylabel('|Y(f)|')
grid on
为便于比较不同信号强度,常将线性幅度转为分贝(dB):
magnitude_dB = 20*log10(abs(Y) + eps); % 加eps防log(0)
imagesc(f_shifted, magnitude_dB); colorbar;
ylabel('Magnitude (dB)'), xlabel('Frequency (Hz)')
此方法广泛应用于音频、雷达等动态范围大的领域。
原始相位谱因 atan2 输出限制在 [-π, π] 内可能出现突变。使用 unwrap 消除相位卷绕:
phase = angle(Y);
phase_unwrapped = unwrap(phase);
plot(f(1:N_fft/2), phase_unwrapped(1:N_fft/2))
xlabel('Frequency (Hz)'), ylabel('Unwrapped Phase (rad)')
该操作对系统相位延迟分析至关重要,如滤波器群延迟估计。
Fs = 2000; t = 0:1/Fs:2;
vibration_signal = exp(-5*t).*sin(2*pi*200*t) + 0.3*randn(size(t)); % 阻尼振荡+噪声
Y = fft(vibration_signal, 2048);
P = abs(fftshift(Y));
f = (-1024:1023)*Fs/2048;
[~, idx] = findpeaks(P, 'MinPeakHeight', max(P)*0.3);
resonant_freqs = f(idx)
输出可能为:
resonant_freqs =
198.0469 201.9531
加载 .wav 文件并提取主频:
[x, Fs] = audioread('note_C4.wav');
x = x(1:4096); % 截取片段
Y = fft(x, 4096);
P = abs(Y(1:2048));
f = (0:2047)*Fs/4096;
[pks, locs] = findpeaks(P, 'NPeaks', 5, 'SortStr', 'descend');
fundamental = f(locs(1)) % 基频
harmonics = f(locs(2:5)) % 谐波
典型结果(C4音符约261.6Hz):
| 峰值序号 | 频率(Hz) | 类型 |
|----------|-----------|----------|
| 1 | 261.8 | 基频 |
| 2 | 523.6 | 2倍谐波 |
| 3 | 785.4 | 3倍谐波 |
| 4 | 1047.2 | 4倍谐波 |
| 5 | 1309.0 | 5倍谐波 |
[pks, freq_indices] = findpeaks(P1, 'MinPeakDistance', 10, 'Threshold', 0.1);
significant_frequencies = f(1:N_fft/2+1)(freq_indices);
可用于自动化故障诊断系统,如轴承磨损导致的特征频率偏移监测。
以下是在Intel i7-11800H平台上对1M点实数信号执行100次FFT的平均耗时:
测试代码(Python):
import numpy as np
from scipy.fft import fft
import time
x = np.random.randn(1_000_000)
start = time.time()
for _ in range(100): fft(x)
print(f"Average: {(time.time()-start)/100*1000:.2f} ms")
graph TD
A[需求类型] --> B{是否需工业级部署?}
B -->|是| C[MATLAB + Simulink]
B -->|否| D{是否强调成本与扩展性?}
D -->|是| E[Python + SciPy/MNE]
D -->|否| F[MATLAB仅桌面分析]
C --> G[嵌入式代码生成]
E --> H[AI融合分析 pipeline]
MATLAB优势在于集成化工作流、Simulink联动及硬件支持;Python胜在开源生态丰富,易于与机器学习框架(如PyTorch)结合。
实际工程中常采用混合策略:MATLAB用于算法验证,Python用于生产环境部署。
本文还有配套的精品资源,点击获取
简介:傅里叶变换是信号处理中的核心工具,可将时域信号转换为频域以分析其频率成分。快速傅里叶变换(FFT)作为离散傅里叶变换(DFT)的高效算法,显著降低了计算复杂度,广泛应用于频谱分析、滤波、去噪和图像处理等领域。本文深入讲解FFT的基本原理,并结合Python(使用numpy库)和MATLAB两种主流工具,提供FFT实现的代码示例与分析方法,帮助读者掌握频域信号处理的核心技能,适用于通信工程、数据分析和IT相关领域的实践应用。
本文还有配套的精品资源,点击获取