手把手教系列之快速傅立葉算法

[導讀] 今天來聊聊如何實現快速傅立葉變換FFT及其應用,希望大家喜歡。直接談FFT,可能沒這方面基礎的同學,不太能明白,先看看它的相近較容易理解的幾個概念吧。

啥是傅立葉級數?

在數學中,傅里葉級數(Fourier series)是把類似波的函數表示成簡單正弦波的方式。更正式地說法是,它能將任何週期性函數或週期信號分解成一個(可能由無窮個元素組成的)簡單振盪函數的集合,即正弦函數和餘弦函數(或者,等價地使用復指數),從數學的定義來看,是這樣地:

設x(t)是一週期信號,其週期爲T。若x(t)在一個週期的能量是有限的,有即

 

則,可以將x(t)展開爲傅立葉級數。怎麼展開呢?計算如下:

公式中的k表示第k次諧波,這是個什麼概念呢?不容易理解,看下對於一個方波的前4次諧波合成動圖就比較好理解了。這裏的合成的概念是時域上的疊加的概念,圖片來源wikipedia

啥是傅里葉變換?

在數學中,傅里葉變換(Fourier transform FT )是一種數學變換,它將一個函數(通常是一個時間的函數,或一個信號)分解成它的組成頻率,例如用組成音符的音量和頻率表示一個音樂和絃。傅里葉變換指的是頻域表示和將頻域表示與時間函數相關聯的數學運算。其本質是一種線性積分變換,用於信號在時域(或空域)和頻域之間的變換,在物理學和工程學中有許多應用。因其基本思想首先由法國學者約瑟夫·傅里葉系統地提出,所以以其名字來命名以示紀念。實際上傅里葉變換就像化學分析,確定物質的基本成分;信號來自自然界,也可對其進行分析,確定其基本頻率成分。其數學定義爲:

對於連續時間信號x(t),若x(t)在時間維度上可積分,(實際上並不一定是時間t維度,這裏可以是任意維度,只需在對應維度空間可積分即可),即:

 

那麼,x(t)的傅立葉變換存在,且其計算式爲:

其反變換爲:

上面這兩個公式是啥意思呢?在度量空間可積可以理解成其在度量空間能量有限,也即對其自變量積分(相當於求面積)是一個確定值,那麼這樣的函數或者信號就可以進行傅立葉變換展開,展開得到的就變成是頻域的函數了,如果對頻率將函數值繪製出曲線就是我們所說的頻譜圖,而其反變換就比較好理解了,如果我們知道一個信號或者函數譜密度函數,就可以對應還原出其時域的函數,也能繪製出時域的波形圖。

當然,本文限定討論時域信號是因爲我們電子系統中的應用最爲普遍的就是一個時域信號,當然推而廣之,其他的多維度信號也能利用上面定義進行推廣,同樣在多維空間信號也非常有應用價值,比如2維圖像處理等等。

上面兩個概念是一個東東麼?

  • 傅立葉級數對應的是週期信號,而傅立葉變換則對應的是一個時間連續可積信號(不一定是週期信號)

  • 傅立葉級數要求信號在一個週期內能量有限,而後者則要求在整個區間能量有限

  • 傅立葉級數的對應是離散的,而傅立葉變換則對應是連續的。

故而,兩者的物理含義不同,且其量綱也是不同的,代表週期信號的第k次諧波幅度的大小,而則是頻譜密度的概念。所以答案是這兩者從本質上不是一個概念,傅立葉級數是週期信號的另一種時域的表達方式,也就是正交級數,它是不同的頻率的波形的時域疊加。而傅立葉變換則是完全的頻域分析,傅里葉級數適用於對週期性現象做數學上的分析,傅里葉變換可以看作傅里葉級數的極限形式,也可以看作是對週期現象進行數學上的分析,同時也適用於非週期性現象的分析。傅里葉級數適用於對週期性現象做數學上的分析,傅里葉變換可以看作傅里葉級數的極限形式,也可以看作是對週期現象進行數學上的分析,同時也適用於非週期性現象的分析。

啥是離散傅立葉變換?

離散傅里葉變換(Discrete Fourier Transform,縮寫爲DFT),是傅里葉變換在時域和頻域上都呈離散的形式,將信號的時域採樣變換爲其DTFT的頻域採樣。

在形式上,變換兩端(時域和頻域上)的序列是有限長的,而實際上這兩組序列都應當被認爲是離散週期信號的主值序列。即使對有限長的離散信號作DFT,也應當將其看作其週期延拓的變換。在實際應用中通常採用快速傅里葉變換計算DFT。

對於N點序列,它的離散傅立葉變換爲(DFT)爲:

其中k=0,1,....,N-1,上面的式子展開一下:

啥是快速傅立葉變換?

快速傅立葉變換(Fast Fourier Transform:FFT)是一種計算數字信號序列的離散傅立葉變換(Discrete Fourier Transform:DFT)或其逆變換(IDFT)的算法。傅里葉分析將信號從其原始域(通常是時間或空間)轉換爲頻域的表示,反之亦然。DFT是通過將一系列值分解成不同頻率的分量來獲得的。這個操作在很多領域中都很有用,但是直接從定義中計算它通常太慢而不實際。FFT通過將DFT矩陣分解成稀疏(大部分爲零)因子的乘積來快速計算這種轉換。所以其本質是實現離散傅立葉變換的一種優化算法,將時間複雜度從降低爲,其中N爲待計算序列的長度。當N非常大時,這種優化在時間維度上提升是非常顯著的。尤其在嵌入式應用領域,由於受限於採用的芯片算力往往不強,所以FFT算法較之於DFT的效果是非常有應用價值的。

1994年,Gilbert Strang將FFT描述爲“我們一生中最重要的數值算法”,並被IEEE雜誌《計算科學與工程》列入20世紀十大算法之一,它深遠的影響了我們世界與日常生活。說這個算法改變了世界也不爲過。在我們日常生活中很多設備裏面都有它的影子,比如手機、比如photoshop,比如數字音響等等。

快速傅立葉算法的最核心思想就是計算機科學裏面常見的分治思想,即把一個複雜的問題,分解爲一個小的類似問題進行求解。

FFT基本上可分爲兩類,時間抽取法和頻率抽取法,而一般的時間抽取法和頻率抽取法只能處理長度N=2M的情況,另外還有組合數基四FFT來處理一般長度的FFT。所謂抽取,就是把長序列分爲短序列的過程,可在時域也可在頻域進行。最常用的時域抽選方法是按奇偶將長序列不斷地變爲短序列,結果使輸入序列爲倒序,輸出序列爲順序排列,這就是Coolly—Tukey算法。

假定待變換離散時間序列信號長度爲,將x(n)按照奇偶分組:

上式可變換爲:

其中,k取0,1,...,N/2-1

從而,

 

由於A(k),B(k)都是點的DFT,X(k)爲N點的DFT。那麼這一分治思想還可以進一步做下去,這裏就不贅述了。

下圖就是一個時間抽取的基2FFT算法的示意圖:

對於頻率抽取基2的示意圖其原理類似,這裏放個圖:

不同點:

  • DIT2 FFT是在時域先進行奇歐倒序,頻域輸出爲正序

  • DIF2 FFT其輸入序列在時域是正序,而頻域輸出爲奇偶分開的倒序。

代碼實踐

好了,前面碼了這麼多字,還是不夠直觀,爲了更好說明前面的分治思想,這裏放了個遞歸實現代碼測一下看看療效:

#include <assert.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>

#define q 8      /* 2^q 點,256 */
#define N (1<<q)  /* N點 FFT, iFFT */

typedef float real;
typedef struct{
  real Re; 
  real Im;
} complex;

#ifndef PI
# define PI 3.14159265358979323846264338327950288
#endif

/*爲了更好說明分治思想,這裏採用遞歸實現,結束條件爲N<=1*/
void fft( complex *v, int n, complex *tmp )
{
  if(n>1) {   /* N如小於1,直接返回*/
    int k,m;    complex z, w, *vo, *ve;
    ve = tmp; vo = tmp+n/2;
    for(k=0; k<n/2; k++) {
      ve[k] = v[2*k];
      vo[k] = v[2*k+1];
    }
    fft( ve, n/2, v );  /* FFT 偶數序列 v[] */
    fft( vo, n/2, v );  /* FFT 偶數序列 v[] */
    for(m=0; m<n/2; m++) {
      w.Re = cos(2*PI*m/(double)n);
      w.Im = -sin(2*PI*m/(double)n);
      z.Re = w.Re*vo[m].Re - w.Im*vo[m].Im; /* Re(w*vo[m]) */
      z.Im = w.Re*vo[m].Im + w.Im*vo[m].Re; /* Im(w*vo[m]) */
      v[  m  ].Re = ve[m].Re + z.Re;
      v[  m  ].Im = ve[m].Im + z.Im;
      v[m+n/2].Re = ve[m].Re - z.Re;
      v[m+n/2].Im = ve[m].Im - z.Im;
    }
  }
  return;
}

/*爲了更好說明分治思想,這裏採用遞歸實現,結束條件爲N<=1*/
void ifft( complex *v, int n, complex *tmp )
{
  if(n>1) {   
    int k,m;    complex z, w, *vo, *ve;
    ve = tmp; vo = tmp+n/2;
    for(k=0; k<n/2; k++) {
      ve[k] = v[2*k];
      vo[k] = v[2*k+1];
    }
    ifft( ve, n/2, v );  /* FFT 偶數序列 v[] */
    ifft( vo, n/2, v );  /* FFT 奇數序列 v[] */
    for(m=0; m<n/2; m++) {
      w.Re = cos(2*PI*m/(double)n);
      w.Im = sin(2*PI*m/(double)n);
      z.Re = w.Re*vo[m].Re - w.Im*vo[m].Im; /* Re(w*vo[m]) */
      z.Im = w.Re*vo[m].Im + w.Im*vo[m].Re; /* Im(w*vo[m]) */
      v[  m  ].Re = ve[m].Re + z.Re;
      v[  m  ].Im = ve[m].Im + z.Im;
      v[m+n/2].Re = ve[m].Re - z.Re;
      v[m+n/2].Im = ve[m].Im - z.Im;
    }
  }
  return;
}

#define SAMPLE_RATE (10000.0f)
int main(void)
{
  complex v[N], scratch[N];
  float amp[N];
  int k;

  /*模擬一個採樣系統,採樣率爲10KHz,有兩個信號:500Hz/2kHz*/
  for(k=0; k<N; k++) {
    v[k].Re = 1*sin(2*PI*500*k/SAMPLE_RATE)+0.5*sin(2*PI*2000*k/SAMPLE_RATE);
    v[k].Im = 0;//實際信號處理時,虛部常爲0
  }
    
  /*輸出模擬信號*/
  for(int i=0;i<N;i++)
  {
      printf("%f,",v[i].Re);      
  }
  printf("\n");  
  fft( v, N, scratch );

  for( int i=0;i<N;i++)
  {
      printf("%f,",sqrt(v[i].Re*v[i].Re+v[i].Im*v[i].Im));
  }
  printf("\n");  
  while(1);
}

代碼來源:https://www.math.wustl.edu/~victor/mfmm/fourier/fft.c

爲華盛頓大學的教學代碼,上面代碼僅測試了正變換,對於逆變換有興趣的可以試試。

總結一下

本文目的爲了方便理解快速傅立葉的算法思想,如果需要將算法實際應用到單片機或者DSP中,還需要做進一步的優化,實際使用時,一般會將蝶形算子做成一個表,另外也會做定點優化。對於ARM芯片而言,其CMSIS庫有現成的實現例子可以直接使用,對於TI系列DSP而言,也內置了FFT代碼庫,可直接使用。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章