离散傅里叶变换学习-从一维到海洋模拟(一)

离散傅里叶变换的实现

最近在朋友的点拨下,以及通过网上查阅的一些资料来看,实现了一维离散傅里叶变换到二维离散傅里叶变换,以至于到FFT的实现及相应的逆变换。对傅里叶变换这个很长时间以来都没有理解的东西,有了一个深刻的认识。所以就想总结一下其中的原理以及具体的实现过程。

本文将会从一维离散傅里叶变换开始,逐步讲解到FFT的实现及相应逆变换的实现方法。以及FFT实现时使用的蝶形变换的具体操作方法。其中,还会罗列出所参考的资料。

一维傅里叶变换

在实际操作之前,一定要有一个重要的认知,那就是傅里叶变换本质上是把一个函数转换成用另一种形式来表示。这就是经常说的,时域信号转成频域信号。但这都不重要,数学公式上来看,傅里叶公式如下,即等号左边的f(x)可以表示成等号右边的一个和式,这个公式的通俗解释就是,一个函数可以表示成正弦函数和余弦函数的和。而其中an和bn表示的就是一个权值。

an和bn也是可以通过公式计算得到的,公式如下,可以看到是两个积分式。

总结这两个公式,可以获得一个认知:

  • 一个函数可以表示成不同频率的正弦函数和余弦函数的加权和。权值可以通过公式计算。
  • 只要确定了权值an和bn,结合第一个公式,通过对不同频率的正弦函数和余弦函数进行加权求和,我们可以算出来对应f(x)的值。

这里面有一个容易被忽视的点,那就是不同频率的正弦函数和余弦函数的组合,是固定的。所以最后只要确定权值an和bn即可,至此就可以知道,对一个函数进行傅里叶变换,要做的事情就是确定an和bn的值。通过手动计算当然很难,但是借助计算机却是可以的,对于原函数很难确定的积分公式,计算机也只能进行离散地计算以获得一个趋近于正确答案的结果,只要误差允许范围内,这没什么大问题。

DFT做的事情,主要是在长度为N的离散信号中,针对k=(0,1,2...),分别找出在长度N内振动k个周期的三角波分量的权值。举个例子,针对某个余弦信号,在两个周期内采样40次:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> x [ n ] = c o s ( 2 π n 2 ) , n = 0 , 1 , . . . , 39 x[n] = cos(2\pi\frac{n}2),n = 0,1,...,39 </math>x[n]=cos(2π2n),n=0,1,...,39

然后通过DFT可以知道它在40次采样周期内,震动了几个周期。算法的处理很暴力:

首先,选取40个长度为40个点的基信号,它们分别长这样:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> c o s ( 2 π 0 n 40 ) , c o s ( 2 π n 40 ) , c o s ( 2 π 2 n 40 ) . . . c o s ( 2 π 39 n 40 ) cos(2\pi\frac{0n}{40}),cos(2\pi\frac{n}{40}),cos(2\pi\frac{2n}{40})...cos(2\pi\frac{39n}{40}) </math>cos(2π400n),cos(2π40n),cos(2π402n)...cos(2π4039n)

第一个,40次采样内振动0个周期: <math xmlns="http://www.w3.org/1998/Math/MathML"> c o s ( 2 π 0 n 40 ) cos(2\pi\frac{0n}{40}) </math>cos(2π400n),即常值:

第二个,40次采样内振动1个周期: <math xmlns="http://www.w3.org/1998/Math/MathML"> c o s ( 2 π n 40 ) cos(2\pi\frac{n}{40}) </math>cos(2π40n)

以此类推,一直到40个采样内振动39个周期。

接下来,对于上述每个基信号,判断它们跟原信号的相关程度,就是用他们在同一点的函数值相乘,并对结果求和(向量的内积),即如下的公式:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> c o r r e l a t i o n ( x , y ) = Σ k x [ k ] y [ k ] correlation(x,y) = \Sigma_k x[k]y[k] </math>correlation(x,y)=Σkx[k]y[k]

这个值越大,则x[k]与y[k]越像。于是DFT把 <math xmlns="http://www.w3.org/1998/Math/MathML"> c o s ( 2 π 0 n 40 ) cos(2\pi\frac{0n}{40}) </math>cos(2π400n)到 <math xmlns="http://www.w3.org/1998/Math/MathML"> c o s ( 2 π 39 n 40 ) cos(2\pi\frac{39n}{40}) </math>cos(2π4039n)这40个基函数与当前函数 <math xmlns="http://www.w3.org/1998/Math/MathML"> c o s ( 2 π 2 n 40 ) cos(2\pi\frac{2n}{40}) </math>cos(2π402n)比较了一下,发现 <math xmlns="http://www.w3.org/1998/Math/MathML"> c o s ( 2 π 2 n 40 ) cos(2\pi\frac{2n}{40}) </math>cos(2π402n)和 <math xmlns="http://www.w3.org/1998/Math/MathML"> cos ⁡ ( 2 π 38 n 40 ) \cos(2\pi\frac{38n}{40}) </math>cos(2π4038n)长得最像。

这也和那显然,因为 <math xmlns="http://www.w3.org/1998/Math/MathML"> c o s ( 2 π 38 n 40 ) = c o s ( 2 π n − 2 π 38 n 40 ) = c o s ( 2 π 2 n 40 ) cos(2\pi\frac{38n}{40}) = cos(2\pi n-2\pi \frac{38n}{40}) = cos(2\pi \frac{2n}{40}) </math>cos(2π4038n)=cos(2πn−2π4038n)=cos(2π402n)

下面,如果我们把这40次每次比较的correlation值记下来,就得到了原信号在每个频率上的分量大小。就得到了所谓原信号的频域X:

如果用频域信号替换原信号,则:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> X k = ∑ n = 0 N − 1 x [ n ] c o s ( 2 π k n N ) , ( N = 40 ) X_k = \sum_{n=0}^{N-1}x[n]cos(2\pi \frac{kn}{N}), (N=40) </math>Xk=n=0∑N−1x[n]cos(2πNkn),(N=40)

问题来了,虽然貌似联系很紧密,但这怎么跟DFT的公式长得不一样。。。DFT的公式应该是这样的:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> X k = ∑ n = 0 N − 1 x [ n ] e − 2 π j k n N X_k = \sum_{n=0}^{N-1} x[n]e^{-2\pi j \frac{kn}{N}} </math>Xk=n=0∑N−1x[n]e−2πjNkn

用欧拉公式展开,我们得到的时:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> X k = ∑ n − 0 N − 1 x [ n ] c o s ( 2 π k n N ) − j ∑ n = 0 N − 1 x [ n ] s i n ( 2 π k n N ) X_k = \sum_{n-0}^{N-1} x[n]cos(2\pi \frac{kn}{N}) - j\sum_{n=0}^{N-1}x[n]sin(2\pi \frac{kn}{N}) </math>Xk=n−0∑N−1x[n]cos(2πNkn)−jn=0∑N−1x[n]sin(2πNkn)

这又是为什么呢?这是因为,对于一个信号,如果只跟余弦函数比较,会损失一些信息,比如相位。如果原信号有一些相位偏移, <math xmlns="http://www.w3.org/1998/Math/MathML"> x = c o s ( 2 π 2 n 40 + π 4 ) x = cos(2\pi \frac{2n}{40} + \frac{\pi}{4}) </math>x=cos(2π402n+4π)

对这个函数同样按照上面的方法计算频域,结果会有些不一样:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> X 2 = X 3 8 = ∑ n = 0 39 c o s ( 2 π 2 n 40 + π 4 ) c o s ( 2 π 2 n 40 ) = 10 2 X_2 = X_38 = \sum_{n=0}^{39}cos(2\pi\frac{2n}{40} + \frac{\pi}{4})cos(2\pi \frac{2n}{40}) = 10\sqrt2 </math>X2=X38=n=0∑39cos(2π402n+4π)cos(2π402n)=102

如果再找一个信号y, 没有相位偏移,而是把幅值砍到 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 2 \frac{\sqrt2}{2} </math>22 ,即:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> y = 2 2 c o s ( 2 π 2 n 40 ) y = \frac{\sqrt 2}{2} cos(2\pi \frac{2n}{40}) </math>y=22 cos(2π402n)

那么这个信号的DFT结果:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> Y 2 = Y 3 8 = ∑ n = 0 39 2 2 c o s ( 2 π 2 n 40 ) c o s ( 2 π 2 n 40 ) = 10 2 Y_2 = Y_38 = \sum_{n=0}^{39}\frac{\sqrt 2}{2}cos(2\pi\frac{2n}{40})cos(2\pi\frac{2n}{40})=10\sqrt2 </math>Y2=Y38=n=0∑3922 cos(2π402n)cos(2π402n)=102

跟x信号的记过一模一样,这样就由于损失信息,无法通过频域恢复信号了。

解决方法是另选一组以正弦函数(实际上选了负正弦)为基准的"基信号",即 <math xmlns="http://www.w3.org/1998/Math/MathML"> − s i n ( 2 π 0 n 40 ) -sin(2\pi\frac{0n}{40}) </math>−sin(2π400n)到 <math xmlns="http://www.w3.org/1998/Math/MathML"> − s i n ( 2 π 39 n 40 ) -sin(2\pi\frac{39n}{40}) </math>−sin(2π4039n),计算另一组原信号与正弦基的相关系数,这两组系数一起作为DFT的最终结果。而复数只是一个工具,用来方便地同时存储两组计算结果。当然它还有一个好处就是能够比较直观地表现出模和相位。

选负正弦还是正弦做基信号其实无所谓,只是最后的结果算出来的相位反一下而已,幅值是一样的。如果一个信号跟某频率余弦和负正弦的相关系数分别为a,b,那么代表这个信号差不多型如:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> a c o s ( k n N ) − b s i n ( k n N ) acos(\frac{kn}{N}) - bsin(\frac{kn}{N}) </math>acos(Nkn)−bsin(Nkn)

根据高中数学可以求得其模为 <math xmlns="http://www.w3.org/1998/Math/MathML"> a 2 + b 2 \sqrt{a^2+b^2} </math>a2+b2 ,相对余弦的相位为 <math xmlns="http://www.w3.org/1998/Math/MathML"> a r c t a n ( b a ) arctan(\frac{b}{a}) </math>arctan(ab),这与复数 <math xmlns="http://www.w3.org/1998/Math/MathML"> a + b i a+bi </math>a+bi的模和相位是相同的,因此DFT的公式相当于同时把x[n]做了跟N个余弦基、N个负正弦基的比对,结果用N个复数存储。如果想要看某个频率的相位和模,就看对应复数的相位和模。

我们再来看看上面有相位偏移的那个例子:

原信号: <math xmlns="http://www.w3.org/1998/Math/MathML"> x [ n ] = c o s ( 2 π 2 n 40 + π 4 ) x[n] = cos(2\pi\frac{2n}{40} + \frac{\pi}{4}) </math>x[n]=cos(2π402n+4π)

与余弦比对: <math xmlns="http://www.w3.org/1998/Math/MathML"> X 2 = ∑ n = 0 39 c o s ( 2 π 2 n 40 + π 4 ) c o s ( 2 π 2 n 40 ) = 10 2 X_2 = \sum_{n=0}^{39}cos(2\pi\frac{2n}{40}+\frac{\pi}{4})cos(2\pi\frac{2n}{40})=10\sqrt{2} </math>X2=∑n=039cos(2π402n+4π)cos(2π402n)=102

与负正弦比对: <math xmlns="http://www.w3.org/1998/Math/MathML"> X 2 = ∑ n = 0 39 c o s ( 2 π 2 n 40 + π 4 ) s i n ( − 2 π 2 n 40 ) = 10 2 X_2=\sum_{n=0}{39}cos(2\pi\frac{2n}{40} + \frac{\pi}{4})sin(-2\pi\frac{2n}{40})=10\sqrt{2} </math>X2=∑n=039cos(2π402n+4π)sin(−2π402n)=102

在40个点内振动两个周期这个频率上,其DFT的结果为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 10 2 + 10 2 j 10\sqrt{2}+10\sqrt{2}j </math>102 +102 j

其模为20,与其相位偏移前相同

其相位也为 <math xmlns="http://www.w3.org/1998/Math/MathML"> π 4 \frac{\pi}{4} </math>4π,也没有问题

如果将原信号变为 <math xmlns="http://www.w3.org/1998/Math/MathML"> x [ n ] = s i n ( 2 π 2 n 40 + π 4 ) x[n]=sin(2\pi\frac{2n}{40}+\frac{\pi}{4}) </math>x[n]=sin(2π402n+4π),会求得该频率DFT结果为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 10 2 − 10 2 j 10\sqrt{2}-10\sqrt{2}j </math>102 −102 j,求得其相位为 <math xmlns="http://www.w3.org/1998/Math/MathML"> − π 4 -\frac{\pi}{4} </math>−4π。因此,根据DFT结果求得的相位是相对余弦信号的相位。****

一维离散傅里叶变换的逆变换

一维离散傅里叶逆变换的公式如下:

具体的公式推导我不太理解,所以就不讲了。其中,要还原的目标,即原函数的值。则是由经过DFT变换得到的结果,即一组各个频率正交基的系数。当初在大学期间,看这个公式时,有一个很疑惑的点是左边的值是一个实数,右边的公式中是DFT的结果,看着也像是实数,然后又是一个e的指数形式,很困惑。这个疑惑不知道大家有没有,这里就特别提一下,在这个公式中,其实是复数。是DFT的结果,也是复数,而,由欧拉公式可以知道,也是复数。所以这个公式中右边部分是复数相乘并求和的结果,左边自然是也是复数。最后的得到的复数,实部就是我们想要的结果,虚部会计算变为0

C++实现一维离散傅里叶变换

示例代码仓库地址

二维离散傅里叶变换及其逆变换

二维离散傅里叶变换的公式有一维离散傅里叶变换相比,就是多了一层求和,公式如下:

逆变换的公式如下:

在代码的实现上,就是从两层循环,变成了4层循环。我用一张图片作为例子,来实现二维离散傅里叶变换及其逆变换。程序加载图片但只对它的R通道进行处理,将傅里叶变换的结果输出成图像。并将逆变换的结果输出成图像,和原图像进行对比。

程序需要依赖stb_image.h及stb_image_write.h图形库,具体的使用方法可以直接参考代码的示例。图片用的是缩小版本的lena的照片,不过只取了R通道。

首先实现一个Image的类,用于对图形数据的提取,下面的图给出类的声明、构造和析构方法。实现加载图片的逻辑。加载出来的图片,用一维浮点数组保存,且拆分成RGB三个通道,索引时根据x和y计算整数索引来取数图片中的值。在这篇示例中,只需要关注R通道即可。

在main函数里,使用刚刚定义的类实现加载和保存的逻辑。

然后我们需要在加载图片之后,实现DFT方法,并将结果保存为dft_result.png

如果将DFT的模输出成图像,是下面这张图

为了让其显像,做一个取对数的操作,是下面这张图。

这也和我们常见频谱图像有区别,而如何从上面这张图变换得到下面这张图,下文再进行介绍。

接着实现IDFT方法,并将结果保存为output.png

可以看到,输出的结果与原图一致。如下图是转换过程,最左边是原图,最右边是复原图,中间是DFT的结果。肉眼无法分辨出差别,而实际上,两张图的差值是0。

而对于下面这张频谱图像的获得,实际上是对DFT的结果做了一些操作。并且对图像进行处理,使其能够显像,这在冈萨雷斯的《数字图像处理》一书中也有体现。代码就不贴了,可以看Image类的GenerateSpectrum方法。

所以,对于DFT结果和频谱图像之间的关系,就讲清楚了。其实如果只是写程序实现这个过程,频谱图像是不必要关心的,但是频谱图像对于。更应该关注的是DFT的结果是如何存储的,以及如何复原原图像。下面要介绍的FFT利用一些定理对DFT加速,得到的结果和DFT一致。二维DFT的代码也上传到仓库里。

二维离散傅里叶变换的代码仓库

参考资料

如何通俗地解释什么是离散傅里叶变换? - 知乎 (zhihu.com)

相关推荐
A懿轩A8 分钟前
C/C++ 数据结构与算法【数组】 数组详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·数组
古希腊掌管学习的神8 分钟前
[搜广推]王树森推荐系统——矩阵补充&最近邻查找
python·算法·机器学习·矩阵
云边有个稻草人12 分钟前
【优选算法】—复写零(双指针算法)
笔记·算法·双指针算法
半盏茶香13 分钟前
在21世纪的我用C语言探寻世界本质 ——编译和链接(编译环境和运行环境)
c语言·开发语言·c++·算法
忘梓.1 小时前
解锁动态规划的奥秘:从零到精通的创新思维解析(3)
算法·动态规划
tinker在coding3 小时前
Coding Caprice - Linked-List 1
算法·leetcode
XH华8 小时前
初识C语言之二维数组(下)
c语言·算法
南宫生8 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
不想当程序猿_8 小时前
【蓝桥杯每日一题】求和——前缀和
算法·前缀和·蓝桥杯
落魄君子8 小时前
GA-BP分类-遗传算法(Genetic Algorithm)和反向传播算法(Backpropagation)
算法·分类·数据挖掘