《数字信号处理》课程实验1 – FFT的实现
一、按时间抽选的基-2 FFT实现原理
观察DIT(基2)FFT的流图(N点,N为2的幂次),可以总结出如下规律:
(1)共有\(L=\log_2N\)级蝶形运算;
(2)输入倒位序,输出自然顺序;
(3)第\(m\)级(\(m\)从1开始,下同)蝶形结对偶结点距离为\(2^{m-1}\);
(4)第\(m\)级蝶形结计算公式:
\(X_m (k)=X_{m-1} (k)+X_{m-1 } (k+2^{m-1} ) W_N^r\)
\(X_m (k+2^{m-1} )=X_{m-1} (k)-X_{m-1} (k+2^{m-1} ) W_N^r\)
\(m=1,2,…,L\)
(5)第\(m\)级不同旋转因子数为\(2^{m-1}\);
(6)第\(m\)级每一种旋转因子关联\(2^{L-m}\)个蝶形结;
(7)第\(m\)级一种旋转因子的两次相邻出现间隔\(2^m\)个位置;
(8)第\(m\)级第\(i\)种(\(i\)从0开始)旋转因子\(W_N^r\)的指数\(r\)为\(2^{L-m}i\);
(7)每个蝶形结运算完成后,输出的两节点值可以直接放到原输入的两节点的存储器中(原位运算)。
二、按时间抽选的基-2 FFT实现细节
依据如上观察,使用Python语言编写下列相关程序:
(1)倒位变址运算
自然序排列的二进制数,其下一个数总比前一个大1。而倒序二进制数的下一个数,是前一个数最高位加1,然后由高位向低位进位得到的。使用Rader算法,可以方便地计算倒位序。
Rader算法利用一个递推关系——如果已知第\(k\)步倒位序为\(J\),则第\(k+1\)步倒位序计算方法为:从\(J\)最高位看向最低位,为1则变0;为0则变1并立刻退出,即得到下一步倒位序。由于已知递推起点第1步的序号0的倒位序也为0,故可以使用该算法求出所有倒位序。
进行倒位变址运算时,为避免重复调换,设输入为\(x(n)\),倒位序后为\(x(m)\),当且仅当\(m>n\)时进行对调。
下面是相关代码:
new_index = [0]
J = 0 # J为倒位序
for i in range(N - 1): # i为当前数
mask = N // 2
while mask <= J: # J的最高位为1
J -= mask # J的最高位置0
mask = mask >> 1 # 准备检测下一位
J += mask # 首个非1位置1
new_index.append(int(J))
for i in range(N):
if new_index[i] <= i:
continue # 无需对调
seq[i], seq[new_index[i]] = seq[new_index[i]], seq[i] # 交换
(2)旋转因子的计算
利用欧拉公式可知:
\(W_N^k=\cos(2kπ/N)-j \sin(2kπ/N)\)
在\(k=0,1,…,N-1\)范围内循环一次,即可计算所有旋转因子。
一种优化策略是利用如下递推关系来加速计算,递推起点\(W_N^0=1\):
\(W_N^{(k+1)}=W_N^k \exp(-j 2π/N)\)
相关代码如下:
WNk = []
two_pi_div_N = 2 * PI / N # 避免多次计算
for k in range(N // 2):
# WN^k = cos(2kPI/N) - j sin(2kPI/N)
WNk.append(Complex(math.cos(two_pi_div_N * k), math.sin(two_pi_div_N * -k)))
(3)蝶形结按层运算
以旋转因子的种类为循环标准,每一轮就算掉该种旋转因子对应的\(2^{L-m}\)个蝶形结。结合观察(3)~(8),相关代码如下:
L = int(math.log2(N)) # 蝶形结层数
for m in range(1, L + 1): # m为当前层数,从1开始
distance = 2 ** (m - 1) # 对偶结点距离,也是该层不同旋转因子的数量
for k in range(distance): # 以结合的旋转因子为循环标准,每一轮就算掉该旋转因子对应的2^(L-m)个结
r = k * 2 ** (L - m) # 该旋转因子对应的r
for j in range(k, N, 2 ** m): # 2^m为每组旋转因子对应的分组的下标差
right = seq[j + distance] * WNk[r]
t1 = seq[j] + right; t2 = seq[j] - right
seq[j] = t1; seq[j + distance] = t2
三、反变换IFFT的实现
由于IDFT公式为:
\(x(n)={\rm IDFT}[X(k)]=\frac {1}{N} ∑_{k=0}^{N-1} X(k) W_N^{-nk}\)
将该式取共轭:
\(x^* (n)=\frac {1}{N} [∑_{k=0}^{N-1}X(k) W_N^{-nk} ]^*=\frac {1}{N} ∑_{k=0}^{N-1}[X^* (k) W_N^{nk} ]=\frac {1}{N} {\rm DFT}[X^* (k)]\)
那么:
\(x(n)=\frac {1}{N} {{\rm DFT}[X^* (k)]}^*\)
这意味着IFFT可以共用FFT程序。只要将\(X(k)\)取共轭后做FFT,结果再取共轭并除以\(N\)即完成了IFFT。相关代码如下:
def ifft(seq: list):
# 检查是否为2^L点序列
N = len(seq)
if int(math.log2(N)) - math.log2(N) != 0:
raise ValueError('[ifft] Not 2^L long sequence.')
# 先对X(k)取共轭
seq = list(map(lambda x : x.conjugate(), seq))
# 再次利用FFT
seq = fft_dit2(seq)
# 再取共轭
seq = map(lambda x : x.conjugate(), seq)
# 最后除以N
seq = map(lambda x : x * Complex(1 / N, 0), seq)
return list(seq)
四、程序测试
按照要求,分别使用如下三种信号测试算法。使用matplotlib库作图,使用numpy库的FFT结果与自行撰写的FFT结果进行对照。
(1)正弦序列
产生\([-π,π]\)上的16点均匀采样,计算相应的\(\sin\)函数值,进行FFT,结果如下,正确无误:
绘制序列及其幅度频谱图,同时绘制IFFT结果以测试IFFT程序:
(2)三角波序列
从0开始向正方向,以\(π/8\)为间隔,产生三角波的16点\(x\)值,并按\([0, 0.5, 1, 0.5, 0, 0.5, 1…]\)规律赋\(y\)值。作FFT,结果如下,正确无误:
绘制序列及其幅度频谱图,同时绘制IFFT结果以测试IFFT程序:
(3)方波序列
产生\([-π,π]\)上的32点均匀采样作为方波的\(x\)值,并按\([0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1…]\)规律赋\(y\)值(占空比50%,周期8点)。作FFT,结果如下,正确无误:
绘制序列及其幅度频谱图,同时绘制IFFT结果以测试IFFT程序:
五、补充说明
本程序的fft_dit2
函数默认对输入的\(N\)长度序列作\(N\)点FFT,即采样点数与FFT点数相同。但有时候,需要对\(N\)长度序列作\(L(L>N)\)点FFT。为此,程序提供了一个实用函数add_zero_to_length
来把\(N\)点序列末尾补0到指定长度以支持上述FFT运算。
使用一个矩形窗序列\(R_8 (n)\),对其作16点FFT,测试程序正确性,程序正确无误,如下所示:
该段测试用的代码由于不是实验要求,故在fft_demo.py中作注释处理。
除了补0函数外,程序还提供了下列这些实用函数来帮助更方便地使用FFT:
附录:全部代码
fft.py
# coding: utf-8
# 《数字信号处理》课程实验
# FFT与IFFT实现(一维)
# 09017227 卓旭
import math
'''
自定义复数类
'''
class Complex:
def __init__(self, re, im):
self.set(re, im)
def set(self, re, im):
self._re = re
self._im = im
def get(self, what):
return self._re if what == 're' else self._im
def __add__(self, other):
return Complex(
self._re + other._re,
self._im + other._im
)
def __sub__(self, other):
return Complex(
self._re - other._re,
self._im - other._im
)
def __mul__(self, other):
return Complex(
(self._re * other._re) - (self._im * other._im),
(self._re * other._im) + (self._im * other._re)
)
def conjugate(self):
return Complex(
self._re, -self._im
)
def __abs__(self):
return math.sqrt(self._re ** 2 + self._im ** 2)
def __str__(self):
return (str(round(self._re, 3)) + ' + j' + str(round(self._im, 3))) if (self._im >= 0) \
else (str(round(self._re, 3)) + ' - j' + str(round(-self._im, 3)))
PI=math.pi
'''
按时间抽选的基-2快速傅里叶变换(n点)
需要传入list<Complex>
'''
def fft_dit2(seq: list):
# 检查是否为2^L点FFT
N = len(seq)
if int(math.log2(N)) - math.log2(N) != 0:
raise ValueError('[fft_dit2] Not 2^L long sequence.')
# 输入数据倒位序处理
new_index = [0]
J = 0 # J为倒位序
for i in range(N - 1): # i为当前数
mask = N // 2
while mask <= J: # J的最高位为1
J -= mask # J的最高位置0
mask = mask >> 1 # 准备检测下一位
J += mask # 首个非1位置1
new_index.append(int(J))
for i in range(N):
if new_index[i] <= i:
continue # 无需对调
seq[i], seq[new_index[i]] = seq[new_index[i]], seq[i] # 交换
# 计算所有需要的旋转因子WN^k(k在0~N/2-1)
# 一种优化策略是使用递推式WN(k+1) = WN(k) * e^(-j 2PI/N)计算
WNk = []
two_pi_div_N = 2 * PI / N # 避免多次计算
for k in range(N // 2):
# WN^k = cos(2kPI/N) - j sin(2kPI/N)
WNk.append(Complex(math.cos(two_pi_div_N * k), math.sin(two_pi_div_N * -k)))
# 蝶形运算
L = int(math.log2(N)) # 蝶形结层数
for m in range(1, L + 1): # m为当前层数,从1开始
# 见课本P219表4.1
distance = 2 ** (m - 1) # 对偶结点距离,也是该层不同旋转因子的数量
for k in range(distance): # 以结合的旋转因子为循环标准,每一轮就算掉该旋转因子对应的2^(L-m)个结
r = k * 2 ** (L - m) # 该旋转因子对应的r
for j in range(k, N, 2 ** m): # 2^m为每组旋转因子对应的分组的下标差
right = seq[j + distance] * WNk[r]
t1 = seq[j] + right; t2 = seq[j] - right
seq[j] = t1; seq[j + distance] = t2
return seq
'''
快速傅里叶变换的反变换
需要传入list<Complex>
'''
def ifft(seq: list):
# 检查是否为2^L点序列
N = len(seq)
if int(math.log2(N)) - math.log2(N) != 0:
raise ValueError('[ifft] Not 2^L long sequence.')
# 先对X(k)取共轭
seq = list(map(lambda x : x.conjugate(), seq))
# 再次利用FFT
seq = fft_dit2(seq)
# 再取共轭
seq = map(lambda x : x.conjugate(), seq)
# 最后除以N
seq = map(lambda x : x * Complex(1 / N, 0), seq)
return list(seq)
'''
实用函数,将实序列转化为list<Complex>
'''
def convert_to_complex(seq: list):
return list(map(lambda x : Complex(x, 0), seq))
'''
实用函数,将list<Complex>转化为实序列(丢弃虚部)
'''
def convert_to_real(seq: list):
return list(map(lambda x : x.get('re'), seq))
'''
实用函数,获取Complex的幅度值
'''
def convert_to_amplitude(seq: list):
return list(map(lambda x: math.sqrt(x.get('re') ** 2 + x.get('im') ** 2), seq))
'''
实用函数,获取Complex的相位值
'''
def convert_to_phase(seq: list):
return list(map(lambda x: math.atan2(x.get('im'), x.get('re')), seq))
'''
实用函数,打印FFT结果
'''
def print_fft_result(seq: list):
toprint = '[\n'
modder = 0
for cplx in seq:
toprint += str(cplx)
toprint += '\t\t' if modder != 3 else '\n'
modder += 1
modder %= 4
toprint += ']'
return toprint
'''
实用函数,给实序列补0到指定长度,可用于采样点数小于FFT点数
'''
def add_zero_to_length(seq: list, n: int):
if len(seq) == n:
return seq
# 如果点数不足,把seq补到n点
if len(seq) > n:
raise ValueError('[add_zero_to_length] n < len(seq).')
if len(seq) < n:
res = [*seq]
while (len(res) < n):
res.append(0)
return res
fft_demo.py
# coding: utf-8
# 《数字信号处理》课程实验
# FFT与IFFT实测应用(作图)
# 09017227 卓旭
import matplotlib.pyplot as plt
import numpy as np
from fft import *
PI=math.pi
def ft_test(name, xs, ys):
# 产生测试点
y_points = ys
# 绘制原图
plt.subplots_adjust(hspace=0.7)
plt.subplot(311)
plt.title('Original Singal: ' + name)
plt.plot(xs, y_points, '.')
# 绘制FFT结果(幅度谱)
fft_res = fft_dit2(convert_to_complex(y_points))
plt.subplot(312)
plt.title('FFT Result (Amplitude Spectrum)')
fft_res_amp = convert_to_amplitude(fft_res)
plt.plot(xs, fft_res_amp, '.')
max_height = np.max(fft_res_amp)
for (idx, val) in enumerate(xs):
plt.axvline(val, 0, fft_res_amp[idx] / max_height) # 绘制竖线
# 绘制IFFT
ifft_res = convert_to_real(ifft(fft_res))
plt.subplot(313)
plt.title('IFFT Result')
plt.plot(xs, ifft_res, '.')
plt.show()
if __name__ == '__main__':
# 正弦函数测试
xs = np.linspace(-PI, PI, 16)
ys = [math.sin(x) for x in xs]
print("========正弦函数========")
print("np.fft标准结果:")
print(np.fft.fft(ys))
print("我的FFT结果:")
print(print_fft_result(fft_dit2(convert_to_complex(ys))))
ft_test('sin(x)', xs, ys)
print("========三角波========")
xs = [i * PI / 8 for i in range(16)]
ys = [0, 0.5, 1, 0.5, 0, 0.5, 1, 0.5, 0, 0.5, 1, 0.5, 0, 0.5, 1, 0.5]
print("np.fft标准结果:")
print(np.fft.fft(ys))
print("我的FFT结果:")
print(print_fft_result(fft_dit2(convert_to_complex(ys))))
ft_test('Tri(x)', xs, ys)
print("========方波========")
xs = np.linspace(-PI, PI, 32)
ys = [-1,-1,-1,-1, 1, 1, 1, 1] * 4
print("np.fft标准结果:")
print(np.fft.fft(ys))
print("我的FFT结果:")
print(print_fft_result(fft_dit2(convert_to_complex(ys))))
ft_test('Square(x)', xs, ys)
# print("========R_8========")
# xs = range(16)
# ys = [1, 1] * 4
# ys = add_zero_to_length(ys, 16)
# print("np.fft标准结果:")
# print(np.fft.fft(ys, 16))
# print("我的FFT结果:")
# print(print_fft_result(fft_dit2(convert_to_complex(ys))))
# ft_test('R_8(x), 16 dot', xs, ys)
《数字信号处理》课程实验1 – FFT的实现的更多相关文章
- 《数字信号处理》课程实验2 – FIR数字滤波器设计
一.FIR数字滤波器设计原理 本实验采用窗函数法设计FIR数字低通滤波器.我们希望设计的滤波器系统函数如下: \(H_{d}\left( e^{jw} \right) = \left\{ \begi ...
- 数字信号处理--FFT与蝶形算法
在数字信号处理中常常需要用到离散傅立叶变换(DFT),以获取信号的频域特征.尽管传统的DFT算法能够获取信号频域特征,但是算法计算量大,耗时长,不利于计算机实时对信号进行处理.因此至DFT被发现以来, ...
- 数字信号处理专题(3)——FFT运算初探
一.前言 FFT运算是目前最常用的信号频谱分析算法.在本科学习数字信号处理这门课时一直在想:学这些东西有啥用?公式推来推去的,有实用价值么?到了研究生后期才知道,广义上的数字信号处理无处不在:手机等各 ...
- FS,FT,DFS,DTFT,DFT,FFT的联系和区别 数字信号处理
DCT变换的原理及算法 文库介绍 对于初学数字信号处理(DSP)的人来说,这几种变换是最为头疼的,它们是数字信号处理的理论基础,贯穿整个信号的处理. 学习过<高等数学>和<信号与系统 ...
- Python中的音频和数字信号处理(DSP)
翻译自Python For Engineers. 1. 创建一个正弦波 在这个项目中,我们将创建一个正弦波,并将其保存为wav文件. 但在此之前,你应该知道一些理论. 频率:频率是正弦波重复一秒的次数 ...
- 几幅图片弄清DFT、DTFT、DFS的关系 数字信号处理
原址:http://www.cnblogs.com/BitArt/archive/2012/11/24/2786390.html 很多同学学习了数字信号处理之后,被里面的几个名词搞的晕头转向,比如DF ...
- 数字信号处理与音频处理(使用Audition)
前一阵子由于考博学习须要,看了<数字信号处理>,之前一直不清除这门课的理论在哪里应用比較广泛. 这次正巧用Audition处理了一段音频,猛然发现<数字信号处理>这门课还是很实 ...
- FPGA与数字信号处理
过去十几年,通信与多媒体技术的快速发展极大地扩展了数字信号处理(DSP)的应用范围.眼下正在发生的是,以更高的速度和更低的成本实现越来越复杂的算法,这是针对高级信息服更高带宽以及增强的多媒体处理能力等 ...
- 现代数字信号处理——AR模型
1. AR模型概念观 AR模型是一种线性预测,即已知N个数据,可由模型推出第N点前面或后面的数据(设推出P点),所以其本质类似于插值,其目的都是为了增加有效数据,只是AR模型是由N点递推, ...
随机推荐
- Angular(二)
Angular开发者指南(二)概念概述 template(模板):带有附加标记的模板HTMLdirectives(指令):使用自定义属性和元素扩展HTMLmodel(模型):用户在视图中显示的数据 ...
- Matlab高级教程_第二篇:Matlab相见恨晚的模块_02_并行运算-2
1 MATLAB并行计算-从个人桌面到远程集群和云(陈伟/魏奋)视频摘录笔记 https://cn.mathworks.com/videos/parallel-computing-with-matla ...
- ORs-1-introduction
introduction: 1.Olfactory receptors (ORs)很重要 2.已知的ORs的分子结构,但仍存在没清楚的地方: Though the relationship betwe ...
- 网站爬取-案例三:今日头条抓取(ajax抓取JS数据)
今日头条这类的网站制作,从数据形式,CSS样式都是通过数据接口的样式来决定的,所以它的抓取方法和其他网页的抓取方法不太一样,对它的抓取需要抓取后台传来的JSON数据,先来看一下今日头条的源码结构:我们 ...
- 1059 C语言竞赛 (20 分)
题目:1059 C语言竞赛 (20 分) C 语言竞赛是浙江大学计算机学院主持的一个欢乐的竞赛.既然竞赛主旨是为了好玩,颁奖规则也就制定得很滑稽: 0.冠军将赢得一份“神秘大奖”(比如很巨大的一本学生 ...
- K3CLOUD呼吸时间设置
- SQL数据库的查询方法
简单查询: 一.投影 select * from 表名 select 列1,列2... from 表名 select distinct 列名 from 表名 二.筛选 select top 数字 列| ...
- postman接口测试使用
在做测试之前,先要了解一下http相关的一些知识,这样做起来才会更加顺手. http的请求分为header 和 body,一般在header里面放一些其他的信息,比如cookie,浏览器信息 body ...
- 【Java集合】试读LinkedList源码
LinkedList的本质是双向链表.(01) LinkedList继承于AbstractSequentialList,并且实现了Dequeue接口. (02) LinkedList包含两个重要的成员 ...
- RDS的tar文件恢复到本地mysql5.7版本数据库
参考博客: 安装qpress软件 https://blog.csdn.net/a18838964650/article/details/82800621 文章介绍补充 https://www.cnbl ...