DFT和FFT
为什么需要DFT
数字语音信号是离散时间信号,对其进行频域分析可以通过离散时间傅里叶变换(Discrete-time Fourier transform, DTFT)或者离散傅里叶变换(Discrete Fourier transform, DFT)。二者的区别在于,DTFT作用于时域离散的非周期信号,变换到频域后得到的是连续的周期信号;DFT作用于时域离散的周期信号,变换到频域后得到的是离散的周期信号。
当使用计算机处理信号时,由于其不能无限密集地存储连续时间信号,因此必须将信号+结果进行离散化处理。由于DFT的输入和输出都是离散信号,因此方便了使用计算机进行频域分析。
DFT公式与代码实现
对于一个周期为的序列,其可以被表示为:
我们称上式中的为傅里叶级数系数。这个公式的意义是,该信号可以表示为一系列离散时间复指数信号加权后的和。
经过一系列推导后,为了得到离散时间信号的傅里叶变换,我们将离散时间信号的傅里叶级数系数乘以其周期后可得:
这个公式的python代码为:
def dft_1(x):
# Original code, from https://pythonnumericalmethods.berkeley.edu/notebooks/chapter24.02-Discrete-Fourier-Transform.html
N = len(x) # 信号长度
n = np.arange(N)
k = n.reshape((N,1))
e = np.exp(-2j * np.pi * k * n /N)
X = np.dot(e, x)
return X
def dft_2(x):
# codes from https://www.zhihu.com/question/41427782
N = len(x)
WN = np.power(np.e, complex(0,-1) * (2*np.pi) / N) # 即dft_1()里的e
Xk = []
for k in range(N):
value = 0
for n in range(N):
value += x[n] * np.power(WN, n*k)
Xk.append(value)
return np.array(Xk)
第二个函数dft_2()
更容易理解,但计算时间很长。这个在接下来讲FFT时会展示。
跑下代码
现在,先演示下如何生成一段信号并将其转换到频域的:
import matplotlib.pyplot as plt
import numpy as np
plt.style.use('seaborn-poster')
# 首先生成一段信号
# sampling rate
sr = 100
ts = 1.0/sr
t = np.arange(0,1,ts)
# add signal
freq = 1.0
x = 3*np.sin(2*np.pi*freq*t)
freq = 4.0
x += np.sin(2*np.pi*freq*t)
freq = 7.0
x += 0.5*np.sin(2*np.pi*freq*t)
freq = 8.0
x += 0.5*np.cos(2*np.pi*freq*t)
freq = 8.0
x += 0.5*np.sin(2*np.pi*freq*t)
# do DFT
X = dft_1(x)
N = len(X)
n = np.arange(N)
T = N / sr
freq = n/T
n_oneside = N//2
f_oneside = freq[:n_oneside]
X_oneside = X[:n_oneside]/n_oneside
plt.figure(figsize = (16, 6))
plt.subplot(131)
plt.plot(t, x, 'r')
plt.ylabel('Amplitude')
plt.subplot(132)
plt.stem(f_oneside, abs(X_oneside), 'b', markerfmt=" ", basefmt="-b")
plt.xlabel('Freq (Hz)')
plt.ylabel('DFT Amplitude |X(freq)|')
plt.subplot(133)
plt.stem(f_oneside, abs(X_oneside), 'b', markerfmt=" ", basefmt="-b")
plt.xlabel('Freq (Hz)')
plt.xlim(0, 10)
plt.tight_layout()
plt.show()
最左边红色的是原始信号,中间的是转换后整个频域内的幅度,右边是在[0,10]上的放大图。可以看到经过多次叠加后,原始的时域信号已经分辨不出是如何得来的了,但经过DFT变换后,能够清楚地看到信号在1, 4, 7, 8Hz处有幅度,这跟我们定义信号时设置的freq
相同。
我们下来定义一个只有10个点的短信号,看一下在DFT前后的数值变化:
# sampling rate,只有10个点
sr = 10
ts = 1.0/sr
t = np.arange(0,1,ts)
# add signal
freq = 1.0
x = 3*np.sin(2*np.pi*freq*t)
freq = 2.0
x += np.cos(2*np.pi*freq*t)
freq = 3.0
x += 0.5*np.sin(2*np.pi*freq*t)
freq = 4.0
x += 0.5*np.cos(2*np.pi*freq*t)
freq = 4.0
x += 0.5*np.sin(2*np.pi*freq*t)
此时的x
为:
array([ 1.5 , 2.43728514, 1.42924017, 2.38029668, 1.84949989, 1.5 , -2.04048289, -3.68931368, -2.73825716, -2.62826814])
得到的DTF结果X_oneside
为:
array([-6.21724894e-16+0.00000000e+00j, 4.44089210e-16-3.00000000e+00j, 1.00000000e+00+7.10542736e-16j, 1.04360964e-15-5.00000000e-01j, 5.00000000e-01-5.00000000e-01j])
可以看到X_oneside
在实部上的值为定义信号时cos的系数,在虚部上的值为sin的系数(欧拉公式)。
计算耗时
前文里的两个DFT函数在计算时间上也不相同。在我的电脑上对一段2000点的信号进行DFT,dft_1()
的执行时间为226 ms,而尽管dft_2()
更方便看懂,它的执行时间达到了18.1 s。如果使用快速傅里叶变换(Fast Fourier transform, FFT)的话,这个数字是19.9 ms。为什么呢?
快速计算 -- FFT
由于DFT得到的信号是离散周期的,因此有:
同时由于 ,因此有:
所以,对任意的整数,有:
利用这个性质,其实我们在计算DFT时,并不需要将中间步骤中每一次的结果都算出来。例如,可以把结果按奇数和偶数分开:
这样,对一个长的序列进行DFT就变成了对两个长的序列进行DFT。现在这只是一个简单的示范,实际上我们还可以接着对这两个序列进行同样的分解操作,直到最后剩下两个数为止:
def FFT(x):
N = len(x)
if N == 1:
return x
else:
X_even = FFT(x[::2])
X_odd = FFT(x[1::2])
factor = np.exp(-2j*np.pi*np.arange(N)/N)
X = np.concatenate([X_even + factor[:int(N/2)]*X_odd,
X_even + factor[int(N/2):]*X_odd])
return X
跑下代码
看下这个FFT函数是怎么工作的。首先定义一个的输入:[ 0.0000000e+00 3.0000000e+00 3.6739404e-16 -3.0000000e+00]
Length = 4, X_even = [0.0000000e+00 3.6739404e-16], X_odd = [ 3. -3.]
1.0 X_even calling FFT([0.0000000e+00 3.6739404e-16])
1.1 Now, X_even_even = [0.], X_even_odd = [3.6739404e-16]
1.2 X_even_even calling FFT([0.]) gets [0.]
1.3 X_even_odd calling FFT([3.6739404e-16]) gets [3.6739404e-16]
1.4 Now X_even_even = [0.], X_even_odd = [3.6739404e-16]
X_1 = np.concatenate([X_even_even + factor[:int(N/2)]*X_even_odd ,
X_even_even + factor[int(N/2):]*X_even_odd ])
= [ 3.6739404e-16+0.00000000e+00j -3.6739404e-16-4.49927935e-32j]
2.0 X_odd calling FFT([ 3. -3.])
2.1 Now, X_odd_even = [3.], X_odd_odd = [-3.]
2.2 X_odd_even calling FFT([3.]) gets [3.]
2.3 X_odd_odd calling FFT([-3.]) gets [-3.]
2.4 Now X_odd_even = [3.], X_odd_odd = [-3.]
X_2 = np.concatenate([X_odd_even + factor[:int(N/2)]*X_odd_odd ,
X_odd_even + factor[int(N/2):]*X_odd_odd ])
= [0.+0.0000000e+00j 6.+3.6739404e-16j]
3.0 X = np.concatenate([X_1 + factor[:int(N/2)]*X_2,
X_1 + factor[int(N/2):]*X_2])
= [ 3.6739404e-16+0.j 3.6739404e-16-6.j 3.6739404e-16+0.j -1.8369702e-15+6.j]
[1] 定性聊聊DFT、DFS和DTFT之间的关系
[2] 重磅推出离散傅里叶变换
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix