離散傅里葉變換 - 快速計算方法及C實現 - 第一篇

DFT – Fast algorithms and C implementations - Part1


引言

算法中經常用到傅里葉變換,很長一段時間我都是使用FFTW("the fastest fft library in the west", 一個基於fortran語言的fft算法庫,該庫也爲Matlab以及intel MKL兩個計算軟件提供傅里葉算法,非常牛叉。但是最近我多次發現因爲在項目中使用FFTW導致程序莫名其妙的運行錯誤,花了我很長時間纔將錯誤定位出來,在於fftw_plan的創建過程存在一些隱藏的玄妙。在調試代碼以及研究算法的過程中,我一般只需要單線程,或者單實例,但是在工程部署中基本上都需要多線程和多實例同時運行,如果一個程序中包含多個並行的檢測器/跟蹤器,這些子程序又都需要獨立調用fftw,問題就發生了。網上有說法是fftw_plan的創建不能在多線程中同時進行,不過我的問題似乎有點難以理解,比如同樣的程序在windows下一般都沒問題,在Ubuntu下有可能會出問題,在TX2上總是會出問題,搞得我很崩潰。

自己寫FFT實現的想法由來已久,原因有三:(1)對fftw內部的報錯有些難以理解,大大增加我的算法的工程部署難度;(2)想要算法完全可控:fftw內部好像會自動調用多線程,或者需要在編譯的時候需要增加多線程開關選項,但是如果我想在一個程序中某些fft過程中調用多線程、某些fft過程不調用多線程,就難以實現,因爲fftw沒有爲各個fft函數增加線程開關;(3)程序源代碼完全封閉:經過我之前的努力,我已基本將opencv的常見功能用自己編寫的C代碼替代了,效率相比oepncv有一定的提升,目前唯一的遺憾是fft這一塊還要依賴fftw庫,雖然fftw也是跨平臺的,但是windows下使用只能通過官網下載dll和lib,Ubuntu下也是需要安裝一個deb包,幾乎無法通過下載源碼編譯,而且源碼是fortran的,不編譯成庫沒法在C中調用。

自己寫fft難度很大。如果只是實現fft的功能,那實在簡單,按照公式直接兩個for循環就搞定了,但是fft的計算其實有太多的技巧在裏面,這些技巧並不是信手拈來的,而是通過複雜的數學推導得出來的複雜的計算公式。最近查了一些資料,Winograd的經典論文"On Computing the Discrete Fourier Transform"(1978),Springer出版的圖書"Fast Fourier Transform: Algorithms And Applications"(2010),國內出版的圖書"快速傅里葉變換及其C程序"(2004)。粗略掃了掃,都沒看懂(cry...),另外還找了些網絡教材,比如傅里葉變換的矩陣分析FFT快速傅里葉變換(蝶形算法)詳解FFT快速傅里葉變換的工作原理。不得不感慨一句:真TM難!去年還是前年,商湯來我們學校做了一個專場報告,現場我找他們的技術總監問了一下他們有沒有傅里葉算法,回覆說他們之前也嘗試寫fft,用了幾個月,發現還是不如fftw快,然後就放棄了,最後是用了買的MKL庫。我以前重寫過開源C庫kissfft,寫得很痛苦,因爲當時並不懂它的計算原理,只能完全按照它的步驟來,雖然我重寫的版本比原版快了不少(內存優化、使用SSE),但是不得不承認,跟fftw/matlab(據說也是調用的fftw)/MKL(應該是目前CPU上最快的fft算法了,可以看作是針對英特爾CPU優化過的fftw)相比,差了不止一個量級。然後我把這個我自己寫的fft算法用在了LayeredKCF程序裏面,這是我第一個無任何依賴庫的的完整C++程序,做目標跟蹤的。注:GPU方法不在討論範圍。

最近因爲一度在工程部署中被fftw耽誤了很多時間,後來臨時通過使用opencv替換了相關功能(效率低且沒有我想要的接口),促使我決定再寫一個fft算法,即使慢一點,我也要想程序完全可控。


基礎知識

1. DFT的定義

N長序列\lbrace x_j \rbrace _{j=0}^{N-1}的DFT爲\lbrace X_k \rbrace_{k=0}^{N-1},其中:

X_k = \sum_{j=0}^{N-1} {x_j W_N^{jk}}

W_N=e^{-\frac{2\pi}{N}i}

W_N^{jk}\triangleq \left( W_N \right )^{jk}=e^{-\frac{2\pi jk}{N}i}

上面的i是虛數單位:i*i=-1

2. 歐拉公式

歐拉公式:e^{ix}=\cos(x)+i\sin(x)。因此:

W_N^k=\cos\left(-\frac{2\pi k}{N}\right)+i\cdot\sin\left(-\frac{2\pi k}{N}\right)

比如:W_N^0=1, W_2^1=-1

3. 離散函數f_N(k)=W_N^k的基本性質:

(1)f_N(k)N爲週期:

W_N^{k\pm N}=W_N^k

(2)f_N(k)具有共軛對稱性:

W_N^{-k}=\left(W_N^k\right)^*

其中(\bullet)^*表示對一個複數取共軛(實部不變,虛部取反)。

從而:W_N^{N-k}=\left(W_N^k\right)^*

(3) 將W_N^k的上下標同時乘以一個數,結果保持不變:

W_{sN}^{sk}=W_N^k

比如:W_8^4=W_4^2=W_2^1

4. DFT的基本性質

(1)標量的DFT

如果序列長度爲1(稱爲標量;長度大於1的序列可以稱爲一個向量),則它的DFT等於其本身。

(2)inverse DFT

逆傅里葉變換(inverse DFT)與正向傅里葉(forward DFT)的區別僅僅是傅里葉係數由W_N^k變成了W_N^{-k}

x_j = \sum_{k=0}^{N-1}{X_k W_N^{-k}}

對上式兩邊求共軛,由於“乘積的共軛=共軛的乘積”,即\left((a+bi)\cdot(c+di)\right)^*=(a+bi)^*\cdot (c+di)^*,因此:

x_j^*=\sum_{k=0}^{N-1}X_k^*W_N^k

因此,爲了求一個復序列的inverse DFT,可以先對序列的每個元素分別求共軛,然後執行forward DFT,再對結果的每一個元素分別求共軛即可。求共軛不需要進行任何加減運算,只需要一次邏輯操作即可,因此極爲便捷,在後面代碼部分將會詳細介紹。

(3)2d DFT 

由於DFT是線性運算,因此二維DFT可以視爲兩個一維DFT的複合函數,參見《快速傅里葉變換及其C程序》P253:

簡言之:二維DFT = 先對每行分別進行DFT + 再對每列分別進行DFT = 先對每列分別DFT + 再對每行分別DFT

(4)radix-2 DFT

基-2 離散傅里葉變換是快速傅里葉變換(FFT)的重要內容,也是蝶形算法的核心。下面我們簡單推導一下radix-2 DFT算法:

前提假設:N可以被2整除

首先對DFT公式按奇偶項進行拆分:

\begin{align} X_k &=\sum_{j<N}{x_jW_N^{jk}} \nonumber \\ &=\sum_{j<N/2}{x_{2j}W_N^{2jk}}+\sum_{j<N/2}{x_{2j+1}W_N^{(2j+1)k}} \nonumber \end{align}

由於W_N^{2jk}=W_{N/2}^{jk},故而:

X_k=\sum_{j<N/2}{x_{2j}W_{N/2}^{jk}}+W_N^k\sum_{j<N/2}{x_{2j+1}W_{N/2}^{jk}}

\lbrace A_k \rbrace _{k=0}^{k=N/2-1},\lbrace B_k \rbrace _{k=0}^{N/2-1}分別爲N/2長序列\lbrace x_{2j} \rbrace _{j=0}^{N/2-1}和序列\lbrace x_{2j+1} \rbrace _{j=0}^{N/2-1}的DFT,則:

對任意k<N/2:X_k=A_k+W_N^kB_k

因此X的前一半可以通過A_k,B_k的線性組合來得到。對於X的後一半:

\begin{align} X_{k+N/2} & = \sum_{j<N/2}{x_{2j}W_N^{2j(k+N/2)}}+\sum_{j<N/2}{x_{2j+1}W_N^{(2j+1)(k+N/2)}} \nonumber \\ & = \sum_{j<N/2}{x_{2j}W_{N/2}^{jk}}-W_N^k\sum_{j<N/2}{x_{2j+1}W_{N/2}^{jk}} \nonumber \end{align}

其中用到了W_2^1=-1

則對任意k<N/2:X_k=A_k-W_N^kB_k

可以看到,一個N長DFT可以分解成兩個N/2長DFT的線性組合,其中兩個子序列分別是原始序列的偶數項以及奇數項組成的序列。

\begin{split}a b\end{split}(5)1d real dft

一維實數序列的離散傅里葉變換可以有不同的方式進行實現,最基本的就是構造一個同樣長度的複數序列,實部與實數序列相同,虛部全部爲0. 當實序列的長度N爲偶數時,則存在更加有效的計算方法,參見《快速傅里葉變換及其C程序》P75:

意思就是說,一個偶數長的實序列的DFT可以這麼做:構造一個N/2長的複數序列,其中第k項由原來的實序列的第k個偶數作爲實部、以原序列第k個奇數作爲虛部。然後對這個複數序列求DFT,再根據DFT結果來恢復兩個單獨的DFT序列——分別對應原實數序列的偶數子序列和奇數子序列。其中用到的一個原理就是:若\lbrace x_j \rbrace爲實序列,則

X_{N-k}=\sum_j{x_jW_N^{j(N-k)}}=\sum_j{x_j W_N^{-jk}}=\left( \sum_j{x_j W_N^{jk}}\right)^*=X_k^*

最後根據radix-2算法,既然我們已經知道了奇、偶子列的DFT,就可以恢復原序列的DFT了。

綜上所述:

逆DFT、二維DFT、實數DFT,都可以通過一維複數DFT來高效率地實現,因此我們要解決的重點問題就是高效率地實現一維複數DFT。

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