4. 离散傅里叶变换(DFT):DFT的数学原理,DFT与相位的关系
离散傅里叶变换,简称 DFT,是咱们数字信号处理里最核心的工具之一。说白了,它就是把一段有限长的离散信号,从时域搬到频域。你想想看,我们平时采集到的信号,比如麦克风录下的声音、传感器读到的振动,都是一串串离散的数值。DFT 能告诉你,这串数值里都藏着哪些频率成分,每个成分的幅度有多大,相位又是多少。
我个人习惯把 DFT 看作是「连续傅里叶变换的数字化版本」。因为计算机只能处理离散的、有限长的数据,所以我们必须把傅里叶变换改造成适合计算机运算的形式。嗯,这里要注意,DFT 并不是近似,它本身就是一种精确的数学变换,只是定义域和值域都是离散的。
4.1 DFT 的数学定义
先看公式,别怕,我会拆开讲。
对于一个长度为 N 的离散序列 x[n](n = 0, 1, ..., N-1),它的 DFT 定义为:
X[k] = Σ_{n=0}^{N-1} x[n] · e^{-j(2π/N)kn} , k = 0, 1, ..., N-1
这个公式里,X[k] 就是频域的第 k 个点。k 对应的是数字频率,它和实际频率的关系是 f_k = k · fs / N,其中 fs 是采样率。
我刚开始学的时候,总觉得这个公式很抽象。后来我把它拆成三步理解:
- 旋转因子:e^{-j(2π/N)kn} 是一个在复平面上旋转的向量,旋转速度由 k 决定。
- 加权求和:把原始信号 x[n] 和这个旋转向量相乘,再对 n 求和。
- 相关性检测:如果 x[n] 里包含频率为 k 的成分,求和结果就会很大;否则接近零。
说白了,DFT 就是在做「猜你喜欢」——用不同频率的旋转向量去试探信号,看哪个频率和信号最「合拍」。
4.2 DFT 与相位的关系
DFT 的结果 X[k] 是一个复数。这个复数包含了两个信息:幅度和相位。
我们通常这样提取:
- 幅度谱:|X[k]| = sqrt(Re(X[k])² + Im(X[k])²)
- 相位谱:∠X[k] = atan2(Im(X[k]), Re(X[k]))
相位谱告诉我们什么?它告诉我们每个频率分量在时间轴上的起始位置。举个例子,两个频率相同的正弦波,一个从 0 度开始,一个从 90 度开始,它们的 DFT 相位谱就会差 90 度。
核心要点:DFT 的相位信息是绝对相位,它依赖于我们选取的时域窗口起始点。如果你把信号平移几个采样点,相位谱会发生变化,但幅度谱不变。这一点在做相位计算时特别重要。
我在项目中遇到过这样一个坑:用 DFT 分析一个振动信号,发现相位谱乱糟糟的,完全看不出规律。后来才意识到,是因为信号没有做整周期截断,产生了频谱泄漏,相位信息被污染了。嗯,这个后面会详细讲。
4.3 DFT 的矩阵形式
DFT 还可以写成矩阵形式,这样更直观,也方便用 Python 实现。
定义 DFT 矩阵 W,其元素为:
W_{k,n} = e^{-j(2π/N)kn}
那么 DFT 可以写成:
X = W · x
其中 x 是 N×1 的时域向量,X 是 N×1 的频域向量,W 是 N×N 的矩阵。
这个矩阵 W 有一个非常重要的性质:它是正交矩阵(差一个缩放因子)。这意味着 DFT 是可逆的,逆变换就是:
x[n] = (1/N) · Σ_{k=0}^{N-1} X[k] · e^{j(2π/N)kn}
你看,逆变换和正变换只差一个符号和缩放因子。这种对称性让 DFT 在工程实现上非常优雅。
4.4 用 Python 实现 DFT
理论讲完了,咱们直接上代码。我习惯先写一个最朴素的 DFT,不调任何库,让你看清楚每一步在干什么。
import numpy as np
def dft_naive(x):
"""
最朴素的 DFT 实现
参数 x: 输入信号,长度为 N 的一维数组
返回 X: 频域复数数组
"""
N = len(x)
X = np.zeros(N, dtype=complex)
for k in range(N):
for n in range(N):
angle = -2 * np.pi * k * n / N
X[k] += x[n] * np.exp(1j * angle)
return X
# 测试:生成一个 10Hz 的正弦波,采样率 100Hz,时长 1 秒
fs = 100
t = np.arange(0, 1, 1/fs)
f0 = 10
x = np.sin(2 * np.pi * f0 * t)
# 计算 DFT
X = dft_naive(x)
# 提取幅度和相位
magnitude = np.abs(X)
phase = np.angle(X)
print(f"信号长度 N = {len(x)}")
print(f"幅度谱最大值索引: {np.argmax(magnitude)}")
print(f"对应频率: {np.argmax(magnitude) * fs / len(x):.1f} Hz")
print(f"该频率的相位: {phase[np.argmax(magnitude)]:.3f} 弧度")
运行这段代码,你会看到幅度谱最大值在索引 10 的位置,对应频率 10Hz,相位接近 -π/2 弧度。为什么是 -π/2?因为 sin 函数在 DFT 的定义下,初始相位就是 -π/2。这个细节我当年也困惑了很久。
小技巧:实际工程中千万别用这种双重循环的 DFT,复杂度是 O(N²)。N=1024 时还能忍,N=10000 时你会等到怀疑人生。FFT(快速傅里叶变换)把复杂度降到了 O(N log N),这才是工程实践的标准选择。但我们先用朴素 DFT 理解原理,后面再讲 FFT。
4.5 DFT 的性质一览
DFT 有很多漂亮的性质,我整理了一个表格,方便你查阅:
| 性质 | 时域 | 频域 |
|---|---|---|
| 线性 | a·x[n] + b·y[n] | a·X[k] + b·Y[k] |
| 时移 | x[n - n₀] | X[k] · e^{-j(2π/N)kn₀} |
| 频移 | x[n] · e^{j(2π/N)k₀n} | X[k - k₀] |
| 共轭对称 | x[n] 为实信号 | X[k] = X*[-k](共轭对称) |
| 帕塞瓦尔定理 | Σ|x[n]|² | (1/N) · Σ|X[k]|² |
| 卷积定理 | x[n] * y[n] | X[k] · Y[k] |
这里面,时移性质和我们做相位计算关系最密切。你看,时域平移 n₀ 个点,频域只是乘了一个相位因子 e^{-j(2π/N)kn₀},幅度谱完全不变。这就是为什么我说「相位反映的是时间偏移」。
避坑指南:我曾经在分析雷达回波信号时,直接用 DFT 的相位值去做距离解算,结果误差很大。后来发现是因为信号起始点没有对齐,导致相位里混入了固定的偏移量。正确的做法是先做互相关,找到时延,再补偿这个相位偏移。记住:DFT 的相位是相对于你选取的时域窗口起点的,不是绝对的。
4.6 知识结构图
下面这张图总结了 DFT 的核心知识体系,从时域到频域的映射关系一目了然:
4.7 小结
这一章我们讲了 DFT 的数学原理,以及它和相位的关系。核心就三点:
- DFT 把时域离散信号变换到频域,结果 X[k] 是复数
- 相位谱 ∠X[k] 反映每个频率分量的时间偏移
- DFT 的相位是相对于时域窗口起点的,不是绝对相位