快速傅里叶变换(FFT)的C#实现及详细注释
-------------------------------------------------------------------------------------------------------------------
作者:随煜而安
时间:2015/7/21
注:本文为作者原创文章,所有参考内容均在参考文献中列出,转载请注明出处。
参考文献:
数字信号处理第三版(华中科技大学出版社)
C#数字图像处理算法典型实例(人民邮电出版社)
-------------------------------------------------------------------------------------------------------------------
在阅读下面的快速傅立叶变换实现代码前,需要先了解FFT的基本原理及过程。可以参考我的上一篇博客快速傅里叶变换原理解析
下面给出的代码实现的是一维频率抽取的基2FFT算法,上面链接的博客中已经说过,对于快速傅立叶变换FFT,按时间抽取和按频率抽取的快速算法的计算量是相同的,所以这里只给出了按频率抽取的代码。
首先,我们知道,傅里叶变换的序列和变换结果都应该是复数序列,所以我们要封装一个复数Complex类。详细代码我已经在我的博客C#复数类的封装中给出(我今天发现其中有个ToString的重载方法写的有点问题,如果要使用注意一下,但无伤大雅也不影响本博客的任何内容)。
封装好了复数Complex类,我们就可以开始实现FFT算法了。首先给出两个私有方法,也是真正实现FFT的核心方法。
一维频率抽取基2的FFT算法:
/// <summary>/// 一维频率抽取基2快速傅里叶变换/// 频率抽取:输入为自然顺序,输出为码位倒置顺序/// 基2:待变换的序列长度必须为2的整数次幂/// </summary>/// <param name="sourceData">待变换的序列(复数数组)</param>/// <param name="countN">序列长度,可以指定[0,sourceData.Length-1]区间内的任意数值</param>/// <returns>返回变换后的序列(复数数组)</returns>private Complex[] fft_frequency(Complex[] sourceData, int countN){//2的r次幂为N,求出r.r能代表fft算法的迭代次数int r = Convert.ToInt32(Math.Log(countN, 2));//分别存储蝶形运算过程中左右两列的结果Complex[] interVar1 = new Complex[countN];Complex[] interVar2 = new Complex[countN];interVar1 = (Complex[])sourceData.Clone();//w代表旋转因子Complex[] w = new Complex[countN / 2];//为旋转因子赋值。(在蝶形运算中使用的旋转因子是已经确定的,提前求出以便调用)//旋转因子公式 \ /\ /k __// \/ \/N -- exp(-j*2πk/N)//这里还用到了欧拉公式for (int i = 0; i < countN / 2; i++){double angle = -i * Math.PI * 2 / countN;w[i] = new Complex(Math.Cos(angle), Math.Sin(angle));}//蝶形运算for (int i = 0; i < r; i++){//i代表当前的迭代次数,r代表总共的迭代次数.//i记录着迭代的重要信息.通过i可以算出当前迭代共有几个分组,每个分组的长度//interval记录当前有几个组// <<是左移操作符,左移一位相当于*2//多使用位运算符可以人为提高算法速率^_^int interval = 1 << i;//halfN记录当前循环每个组的长度Nint halfN = 1 << (r - i);//循环,依次对每个组进行蝶形运算for (int j = 0; j < interval; j++){//j代表第j个组//gap=j*每组长度,代表着当前第j组的首元素的下标索引int gap = j * halfN;//进行蝶形运算for (int k = 0; k < halfN / 2; k++){interVar2[k + gap] = interVar1[k + gap] + interVar1[k + gap + halfN / 2];interVar2[k + halfN / 2 + gap] = (interVar1[k + gap] - interVar1[k + gap + halfN / 2]) * w[k * interval];}}//将结果拷贝到输入端,为下次迭代做好准备interVar1 = (Complex[])interVar2.Clone();}//将输出码位倒置for (uint j = 0; j < countN; j++){//j代表自然顺序的数组元素的下标索引//用rev记录j码位倒置后的结果uint rev = 0;//num作为中间变量uint num = j;//码位倒置(通过将j的最右端一位最先放入rev右端,然后左移,然后将j的次右端一位放入rev右端,然后左移...)//由于2的r次幂=N,所以任何j可由r位二进制数组表示,循环r次即可for (int i = 0; i < r; i++){rev <<= 1;rev |= num & 1;num >>= 1;}interVar2[rev] = interVar1[j];}return interVar2;}
一维频率抽取基2的IFFT算法:
实现IFFT即逆变换的方法有多种,我采用的是能够重复调用写好的FFT方法的方式。这种方式需要在进行逆变换前对输入序列稍作处理取每个元素的共轭。然后调用FFT方法,最终同意对结果做除N处理即可。具体实现代码如下
/// <summary>/// 一维频率抽取基2快速傅里叶逆变换/// </summary>/// <param name="sourceData">待反变换的序列(复数数组)</param>/// <param name="countN">序列长度,可以指定[0,sourceData.Length-1]区间内的任意数值</param>/// <returns>返回逆变换后的序列(复数数组)</returns>private Complex[] ifft_frequency(Complex[] sourceData, int countN){//将待逆变换序列取共轭,再调用正变换得到结果,对结果统一再除以变换序列的长度Nfor (int i = 0; i < countN; i++){sourceData[i] = sourceData[i].Conjugate();}Complex[] interVar = new Complex[countN];interVar = fft_frequency(sourceData, countN);for (int i = 0; i < countN; i++){interVar[i] = new Complex(interVar[i].Real / countN, -interVar[i].Imaginary / countN);}return interVar;}
这样我们有了核心的两个方法,当然我们实现的是基2的FFT,对于其他情况,我打算在考完研后补充一个普通DFT的算法,一个针对N为合数的FFT算法。这样我们就可以封装一个供用户调用的公共方法,针对N的类型,智能的选择合适的算法。当然,目前就先实现到这里了,下面是我封装的公共方法,N为合数的FFT算法用注释代替了,虚位以待,考完研后补上!
/// <summary>/// 对给定的序列进行指定长度的离散傅里叶变换DFT/// 内部将使用快速傅里叶变换FFT/// </summary>/// <param name="sourceData">待变换的序列</param>/// <param name="countN">变换的长度N</param>/// <returns>返回变换后的结果(复数数组)</returns>public Complex[] DFT(Complex[] sourceData, int countN){if (countN > sourceData.Length || countN < 0)throw new Exception("指定的傅立叶变换长度越界!");//求出r,2的r次幂为Ndouble dr = Math.Log(countN, 2);int r = Convert.ToInt32(dr);//获取整数部分//初始化存储变换结果的数组Complex[] result = new Complex[countN];//判断选择合适的算法进行快速傅里叶变换FFTif ((dr - r) != 0){//待变换序列长度不是基2的}else{//待变换序列长度是基2的//使用一维频率抽取基2快速傅里叶变换result = fft_frequency(sourceData, countN);}return result;}