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長序列的DFT爲,其中:
上面的是虛數單位:。
2. 歐拉公式
歐拉公式:。因此:
比如:
3. 離散函數的基本性質:
(1)以爲週期:
(2)具有共軛對稱性:
其中表示對一個複數取共軛(實部不變,虛部取反)。
從而:
(3) 將的上下標同時乘以一個數,結果保持不變:
比如:
4. DFT的基本性質
(1)標量的DFT
如果序列長度爲1(稱爲標量;長度大於1的序列可以稱爲一個向量),則它的DFT等於其本身。
(2)inverse DFT
逆傅里葉變換(inverse DFT)與正向傅里葉(forward DFT)的區別僅僅是傅里葉係數由變成了:
對上式兩邊求共軛,由於“乘積的共軛=共軛的乘積”,即,因此:
因此,爲了求一個復序列的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公式按奇偶項進行拆分:
由於,故而:
令分別爲N/2長序列和序列的DFT,則:
對任意k<N/2:
因此的前一半可以通過的線性組合來得到。對於的後一半:
其中用到了
則對任意k<N/2:
可以看到,一個N長DFT可以分解成兩個N/2長DFT的線性組合,其中兩個子序列分別是原始序列的偶數項以及奇數項組成的序列。
(5)1d real dft
一維實數序列的離散傅里葉變換可以有不同的方式進行實現,最基本的就是構造一個同樣長度的複數序列,實部與實數序列相同,虛部全部爲0. 當實序列的長度N爲偶數時,則存在更加有效的計算方法,參見《快速傅里葉變換及其C程序》P75:
意思就是說,一個偶數長的實序列的DFT可以這麼做:構造一個N/2長的複數序列,其中第k項由原來的實序列的第k個偶數作爲實部、以原序列第k個奇數作爲虛部。然後對這個複數序列求DFT,再根據DFT結果來恢復兩個單獨的DFT序列——分別對應原實數序列的偶數子序列和奇數子序列。其中用到的一個原理就是:若爲實序列,則
最後根據radix-2算法,既然我們已經知道了奇、偶子列的DFT,就可以恢復原序列的DFT了。