快速傅里叶变换算法探幽
学完《信号与系统》之后发现什么还是不会?感觉《数字信号处理》和《信号与系统》差不多?怎么办?期末快到了……是不是感觉《数字信号处理》学了和没学一样?对的,就是这种感觉。大一的时候用FFT算法做过音乐频谱,然而当时对里面的算法并不是很了解,之前面某俱乐部的时候被面试官问到,因此……虽然还是……。学完DSP之后感觉没自己来尝试下FFT算法真是人生一大憾事。于是赶着期末之前当做是复习下(虽然并不在考试范围)。
傅里叶其人以及FFT算法的由来想必在这里不必多讲,在FFT算法广泛应用的今天,其强大的威力也早已被人所熟知。FFT算法是一种可以较高效率的计算离散傅里叶变换的算法,N点的DFT共需要N2次复数乘法和N(N-1)次复数加法,而利用FFT算法,任何一个N为2的整数幂(即N= 2M)的DFT,都可以通过M次分解,最后成为2点的DFT来计算。M次分解构成了从x(n)到X(k)的M级迭代计算,每级由N/2个蝶形运算组成。完成一个蝶形计算需一次(复数)乘法和两次复数加法。因此,完成N点的时间抽选FFT计算的总运算量可以节省为M*N/2=log2N*N/2次复数乘法和M*2*N/2= log2N*N次复数加法。本文将以手算8点FFT变换,以及采用C和C++实现FFT变换算法,最后使用matlab里面的FFT来进行验算的方式带你领略FFT算法的博大精深。
用FFT手算8点DFT
本文采用的例子是余翻译的《数字信号处理》第四版(我们的教材)第165面的例题5.16.其原序列为
x[0] | x[1] | x[2] | x[3] | x[4] | x[5] | x[6] | x[7] |
1 | 2 | 2 | 2 | 0 | 1 | 1 | 1 |
首先我们要明白,利用单个N点的DFT计算一个2N点的DFT的基本思想。大概就是利用形成偶序号样本x0[n]=x[2n]和奇序号样本x1[n]=x[2n+1]形成的两个长度为N/2的子序列的两个N/2点的DFT的加权和,来计算原来样本序列的DFT变换。例如,如果先计算N/2点的DFT的话,形式大致是如下所示:
先计算两个N/2点的DFT变换,然后计算加权和。然而,每一个N/2点的序列又可以再分为2,即为原来的N/4点,依次类推,我们这里假设N是2^m,那么,最后总是可以分到只剩下2点的DFT变换。对于8点的DFT来说,N/4就已经是剩下2点了。对于N/4点,其计算的流图如下所示:
因为两点的DFT已经不可再分解了,并且两点DFT可以很容易计算,其计算的流图如下:
其中Wn是旋转因子,
上面的2点DFT运算也是FFT的基本运算模块,由其形状特点被称为蝶形运算,改进后的蝶形运算的基本模块流图如下:
所以8点的DFT变换的流图可以进一步变为如下:
对于8点的DFT,我们由Wn的计算公式可以得到如下的结果:
所以8点的整个FFT运算过程如下图所示:
由蝶形运算的基本模块公式一步步向后算,便可得到最终的结果,其结果和题目的答案是一致的。(其中小字体的表示旋转因子,横线上的表示该级蝶形运算的结果)我觉得如果我们自己手算过FFT变换的话,对其中的一些来龙去脉会比较清楚,也为后面的算法设计提供了基础。
FFT算法实现分析
码位倒置
假设一个输入序列为N点的,那么它的序号二进制数位数就是t=log2N(我们假设输入序列总是2的幂)。可以利用移位运算实现码位的倒置。假设输入为n2n1n0,则输出为n0n1n2。用一个变量j每次获得原序号的最低位,并不断右移使得低位变成高位,最终可以实现码位倒置的功能,码位倒置的程序可参考下面的代码:
/**
*reverse - 码位倒置
*@data:输入的序列
*/
void FFT::reverse(std::vector<complex>& data)//码位倒置函数
{
unsigned int i, j, k;
unsigned int t;
complex temp;//临时交换变量
for (i = 0;i<N;i++)//从第0个序号到第N-1个序号
{//每个大循环实现一次序号交换
k = i;//当前第i个序号
j = 0;//存储倒序后的序号,先初始化为0
for (t = 0;t<log2N;t++)//实现倒置 例如:0101 -> 1010
{//每个小循环得到一个交换的序号
j <<= 1;
j |= (k & 1);//j左移一位然后或上k的最低位
k >>= 1; //k右移一位,次低位变为最低位
}
if (j>i)//如果倒序后大于原序数,就将两个存储单元进行交换,根据对称性,这样可以减少交换的次数
{
temp = data[i];
data[i] = data[j];
data[j] = temp;
}
}
}
蝶形运算
通过观察上面的蝶形运算图,我们可以总结出下面的一些规律。
对于8点的FFT,一共有3级蝶形运算
第一级有4组蝶形运算,每组有1个蝶形运算,其中每个蝶形运算的两个节点的距离为1(相邻)
第二级有2组蝶形运算,每组有2个蝶形运算,其中每个蝶形运算的两个节点的距离为2
第三级有1组蝶形运算,每组有4个蝶形运算,其中每个蝶形运算的两个节点的距离为4
大致如下图所示:
可以推倒,如果是N点的FFT,则一共有log2N级的蝶形运算,第m级的蝶形运算,每个蝶形两节点的距离是L=2^(m-1),第m级有N/2L组蝶形,每组有2^(m-1)个蝶形运算(即蝶形节点距离就是该组的蝶形个数)。因此在实现的时候,可以采用3组嵌套循环,第一层为log2N级蝶形运算,第二层为N/2L组蝶形,第三层为2^(m-1)即L个蝶形运算。
旋转因子
for (k = 0; k < N / 2; k++)//计算旋转因子表
{
temp.real = (float)cos(2 * PI / N * k);
temp.imag = -(float)sin(2 * PI / N * k);
WN_array.push_back(temp);
}
另外,可以发现,我们上面整个过程中都只是用到了一半的旋转因子,因此上面的代码也只是生成前半部分的旋转因子。如果是在MCU上面跑的FFT的话,为了节省CPU的计算,建议先生成旋转因子表,然后复制到代码中即可。/**
*fft - fft变换的外部接口
*@data:原始的数据
*@n:点数
*/
void FFT::fft(std::vector<complex>& data, unsigned int n)
{
unsigned int i, j, k, l;
complex top, bottom;//蝶形运算的上下两个部分
complex temp;
init(n);//初始化
print_something();//查看一下
reverse(data); //码位倒序
for (i = 0;i < log2N;i++)//共log2N级
{ //一级蝶形运算
l = 1 << i;//l等于2的i次方
for (j = 0;j < N;j += 2 * l)//每l个蝶形是一组,每级有N/2l组,j += 2 * l得到下一组蝶形运算的下标
{ //一组蝶形运算
for (k = 0;k < l;k++)//每组有l个
{ //课本里改进后的蝶形运算,只做一次复数乘积
temp = complex_mul(data[j + k + l], WN_array[N / (2 * l)*k]);//碟间距为l//每组的第k个蝶形
top = complex_add(data[j + k], temp);//上面
bottom = complex_sub(data[j + k], temp);//下面
data[j + k] = top;
data[j + k + l] = bottom;
}
}
}
}
FFT算法C++实现
/**
*From zero to greatness
*@author:LinChuangwei
*@Email:979951191@qq.com
*@date:11/27/2015
*@brief:FFT类头文件
*/
#ifndef _FFT_H_
#define _FFT_H_
#include <iostream>
#include <vector>
#define PI 3.1415926
//复数结构体,用于复数类型
typedef struct complex
{
float real;//实部
float imag;//虚部
}complex;
//FFT 类
class FFT
{
public:
void fft(std::vector<complex>& data,unsigned int n);//fft变换接口
void print_something();//打印点数以及旋转因子表
private:
unsigned int N;//N点的FFT
unsigned log2N;
std::vector<complex> WN_array;//旋转因子数组
//内部接口
void init(unsigned int n);//初始化函数
complex complex_add(complex a, complex b);//复数加法
complex complex_sub(complex a, complex b);//复数减法
complex complex_mul(complex a, complex b);//复数乘法
void reverse(std::vector<complex>& data);//码位倒置函数
};
#endif /*FFT.h*/
实现文件如下所示:/**
*From zero to greatness
*@author:LinChuangwei
*@Email:979951191@qq.com
*@date:11/27/2015
*@brief:FFT实现文件
*/
#include "stdafx.h"
#include "fft.h"
/**
*fft - fft变换的外部接口
*@data:原始的数据
*@n:点数
*/
void FFT::fft(std::vector<complex>& data, unsigned int n)
{
unsigned int i, j, k, l;
complex top, bottom;//蝶形运算的上下两个部分
complex temp;
init(n);//初始化
print_something();//查看一下
reverse(data); //码位倒序
for (i = 0;i < log2N;i++)//共log2N级
{ //一级蝶形运算
l = 1 << i;//l等于2的i次方
for (j = 0;j < N;j += 2 * l)//每l个蝶形是一组,每级有N/2l组,j += 2 * l得到下一组蝶形运算的下标
{ //一组蝶形运算
for (k = 0;k < l;k++)//每组有l个
{ //课本里改进后的蝶形运算,只做一次复数乘积
temp = complex_mul(data[j + k + l], WN_array[N / (2 * l)*k]);//碟间距为l//每组的第k个蝶形
top = complex_add(data[j + k], temp);//上面
bottom = complex_sub(data[j + k], temp);//下面
data[j + k] = top;
data[j + k + l] = bottom;
}
}
}
}
/**
*print_something - 打印旋转因子表等
*/
void FFT::print_something()
{
std::vector<complex>::iterator it;
std::cout << "进行FFT变换的点数:"<< N << std::endl;
std::cout << "旋转因子表如下(前面一半):" << std::endl;
for (it = WN_array.begin();it != WN_array.end();it++)
{
std::cout << "real:" << it->real << "\t" << "imag:" << it->imag << std::endl;
}
}
/**
*init - 初始化函数
*@n:点数
*主要完成旋转因子表的计算
*/
void FFT::init(unsigned int n)
{
unsigned int k;
complex temp;
N = n;//初始化两个数
log2N = (unsigned int)log2(N);
for (k = 0; k < N / 2; k++)//计算旋转因子表
{
temp.real = (float)cos(2 * PI / N * k);
temp.imag = -(float)sin(2 * PI / N * k);
WN_array.push_back(temp);
}
}
/**
*complex_add - 复数加法
*@a:输入的复数
*@b:输入的复数
*返回运算的复数结果
*/
complex FFT::complex_add(complex a, complex b)//复数加法
{//复数加法:实部+实部,虚部+虚部
complex result;//用于保存一些临时的变量
result.real = a.real + b.real;
result.imag = a.imag + b.imag;
return result;
}
/**
*complex_sub - 复数减法
*@a:输入的复数
*@b:输入的复数
*返回运算的复数结果
*/
complex FFT::complex_sub(complex a, complex b)//复数减法
{//复数减法:实部-实部,虚部-虚部
complex result;//用于保存一些临时的变量
result.real = a.real - b.real;
result.imag = a.imag - b.imag;
return result;
}
/**
*complex_mul - 复数乘法
*@a:输入的复数
*@b:输入的复数
*返回运算的复数结果
*/
complex FFT::complex_mul(complex a, complex b)//复数乘法
{//按复数乘法规则
complex result;//用于保存一些临时的变量
result.real = a.real*b.real - a.imag*b.imag;
result.imag = a.real*b.imag + a.imag*b.real;
return result;
}
/**
*reverse - 码位倒置
*@data:输入的序列
*/
void FFT::reverse(std::vector<complex>& data)//码位倒置函数
{
unsigned int i, j, k;
unsigned int t;
complex temp;//临时交换变量
for (i = 0;i<N;i++)//从第0个序号到第N-1个序号
{//每个大循环实现一次序号交换
k = i;//当前第i个序号
j = 0;//存储倒序后的序号,先初始化为0
for (t = 0;t<log2N;t++)//实现倒置 例如:0101 -> 1010
{//每个小循环得到一个交换的序号
j <<= 1;
j |= (k & 1);//j左移一位然后或上k的最低位
k >>= 1; //k右移一位,次低位变为最低位
}
if (j>i)//如果倒序后大于原序数,就将两个存储单元进行交换,根据对称性,这样可以减少交换的次数
{
temp = data[i];
data[i] = data[j];
data[j] = temp;
}
}
}
测试的代码如下:/**
*From zero to greatness
*@author:LinChuangwei
*@Email:979951191@qq.com
*@date:11/27/2015
*@brief:FFT算法测试程序
*/
#include "stdafx.h"
#include "fft.h"
int main()
{
std::vector<complex> data = { {1,0}, {2,0}, {2,0}, {2,0} ,{0,0},{1,0},{1,0},{1,0} };
std::vector<complex>::iterator it;
FFT lcw_fft;//定义一个FFT类
std::cout << "FFT变换前的序列:" << std::endl;
for (it = data.begin();it != data.end();it++)
{
std::cout <<"real:" << it->real << "\t" <<"imag:" << it->imag << std::endl;
}
lcw_fft.fft(data,8);//进行FFT变换
std::cout << "FFT变换后的序列:" << std::endl;
for (it = data.begin();it != data.end();it++)
{
std::cout << "real:" << it->real << "\t" << "imag:" << it->imag << std::endl;
}
return 0;
}
可以看到,其运算的结果是正确的。
FFT算法C实现
因为我和MCU的关系比较密切,最后可能很多的算法是要在MCU上面跑的,为了增加算法的可移植性,将上面的C++程序改为了C语言的版本。/**
*From zero to greatness
*@author:LinChuangwei
*@Email:979951191@qq.com
*@date:11/27/2015
*@brief:fft算法C实现头文件
*/
#ifndef _FFT_H_
#define _FFT_H_
#define PI 3.141592
#define N 8 //点数
#define log2N 3
//复数结构体,用于复数类型
typedef struct complex
{
float real;//实部
float imag;//虚部
}complex;
void fft(complex data[]);//FFT变换函数
void reverse(complex data[]);//码位倒置函数
complex complex_add(complex a, complex b);//复数加法
complex complex_sub(complex a, complex b);//复数减法
complex complex_mul(complex a, complex b);//复数乘法
#endif/*FFT.h*/
实现文件如下:
/**
*From zero to greatness
*@author:LinChuangwei
*@Email:979951191@qq.com
*@date:11/27/2015
*@brief:FFT变换C实现头文件
*/
#include "FFT.h"
//旋转因子表
complex WN_array[N / 2] =
{//在嵌入式的处理器中,为节省CPU的计算,一般应先把旋转因子计算好
//可用如下的语句计算,因为只用到前面一半,可计算一半的表即可
/* for (k = 0; k < N / 2; k++)//计算旋转因子表
{
WN_array[k].real = cos(2 * PI / N * k);
WN_array[k].imag = -sin(2 * PI / N * k);
}
*/
{ 1,0 },{ 0.707107,-0.707107 },{ 2.67949e-08,-1 },{ -0.707107,-0.707107 }
};
/**
*fft - fft变换的外部接口
*@data:原始的数据
*/
void fft(complex data[])
{
unsigned int i, j, k, l;
complex top, bottom;//蝶形运算的上下两个部分
complex temp;
reverse(data); //码位倒序
for (i = 0;i < log2N;i++)//共log2N级
{ //一级蝶形运算
l = 1 << i;//l等于2的i次方
for (j = 0;j < N;j += 2 * l)//每l个蝶形是一组,每级有N/2l组
{ //一组蝶形运算
for (k = 0;k < l;k++)//每组有L个
{ //课本里改进后的蝶形运算,只做一次复数乘积
temp = complex_mul(data[j + k + l], WN_array[N / (2 * l)*k]);//碟间距为l//每组的第k个蝶形
top = complex_add(data[j + k], temp);//上面
bottom = complex_sub(data[j + k], temp);//下面
data[j + k] = top;
data[j + k + l] = bottom;
}
}
}
}
/**
*complex_add - 复数加法
*@a:输入的复数
*@b:输入的复数
*返回运算的复数结果
*/
complex complex_add(complex a, complex b)//复数加法
{//复数加法:实部+实部,虚部+虚部
complex result;//用于保存一些临时的变量
result.real = a.real + b.real;
result.imag = a.imag + b.imag;
return result;
}
/**
*complex_sub - 复数减法
*@a:输入的复数
*@b:输入的复数
*返回运算的复数结果
*/
complex complex_sub(complex a, complex b)//复数减法
{//复数减法:实部-实部,虚部-虚部
complex result;//用于保存一些临时的变量
result.real = a.real - b.real;
result.imag = a.imag - b.imag;
return result;
}
/**
*complex_mul - 复数乘法
*@a:输入的复数
*@b:输入的复数
*返回运算的复数结果
*/
complex complex_mul(complex a, complex b)//复数乘法
{//按复数乘法规则
complex result;//用于保存一些临时的变量
result.real = a.real*b.real - a.imag*b.imag;
result.imag = a.real*b.imag + a.imag*b.real;
return result;
}
/**
*complex_add - 复数乘法
*@data[]:输入的序列
*/
void reverse(complex data[])//码位倒置函数
{
unsigned int i, j, k;
unsigned int t;
complex temp;//临时交换变量
for (i = 0;i<N;i++)//从第0个序号到第N-1个序号
{//每个大循环实现一次序号交换
k = i;//当前第i个序号
j = 0;//存储倒序后的序号,先初始化为0
for (t = 0;t<log2N;t++)//实现倒置 例如:0101 -> 1010
{//每个小循环得到一个交换的序号
j <<= 1;
j |= (k & 1);//j左移一位然后或上k的最低位
k >>= 1; //k右移一位,次低位变为最低位
}
if (j>i)//如果倒序后大于原序数,就将两个存储单元进行交换,根据对称性,这样可以减少交换的次数
{
temp = data[i];
data[i] = data[j];
data[j] = temp;
}
}
}
测试的代码如下:/**
*From zero to greatness
*@author:LinChuangwei
*@Email:979951191@qq.com
*@date:11/27/2015
*@brief:FFT测试程序
*/
#include "FFT.h"
#include <stdio.h>
int main()
{
complex data[] = { { 1,0 },{ 2,0 },{ 2,0 },{ 2,0 } ,{ 0,0 },{ 1,0 },{ 1,0 },{ 1,0 } };
int i = 0;
printf("FFT变换前:\n");
for (i = 0;i < N;i++)
{
printf("real:%f\timag:%f\n", data[i].real, data[i].imag);
}
fft(data);
printf("FFT变换后:\n");
for (i = 0;i < N;i++)
{
printf("real:%f\timag:%f\n", data[i].real, data[i].imag);
}
return 0;
}
运行的结果和上图是类似的。matlab验算
function lcw_fft
close all;
data=[1 2 2 2 0 1 1 1];
x = fft(data,8);
real_ = real(x)
imag_ = imag(x)
运算的结果如下:对比一下,可以知道我们的结果是正确的。之后将会使用FFT算法做一些有趣的东西,敬请期待。