从傅里叶级数到基于互相关运算的声音测距【学习笔记】

傅里叶变换

傅里叶级数

任何周期函数都可以用正弦函数和余弦函数构成的无穷级数来表示
设一函数周期为 2 l 2l 2l
f ( x ) = a 0 2 + ∑ n = 1 ∞ a n c o s ( n 2 π x 2 l ) + ∑ n = 1 ∞ b n s i n ( n 2 π x 2 l ) , n ∈ N ∗ f(x) ={a_0\over2}+\sum_{n=1}^\infin a_n cos({n2\pi x\over 2l})+\sum_{n=1}^\infin b_nsin({n2\pi x\over 2l}),n\in N^* f(x)=2a0+n=1ancos(2ln2πx)+n=1bnsin(2ln2πx),nN
可以找到一系列特定周期的正余弦函数,使它们互相正交,即乘积在最大周期内的积分为零
∫ − l l s i n ( n π x l ) d x = 0 ; \int_{-l}^lsin({n\pi x\over l})dx = 0; llsin(lnπx)dx=0;
∫ − l l s i n ( m π x l ) c o s ( n π x l ) d x = 0 , m , n ∈ N ∗ ; \int_{-l}^lsin({m\pi x\over l})cos({n\pi x\over l})dx = 0,m,n\in N^*; llsin(lmπx)cos(lnπx)dx=0,m,nN;
∫ − l l s i n ( m π x l ) s i n ( n π x l ) d x = 0 , m , n ∈ N ∗ ; \int_{-l}^lsin({m\pi x\over l})sin({n\pi x\over l})dx = 0,m,n\in N^*; llsin(lmπx)sin(lnπx)dx=0,m,nN;
……
据此可以以{ 1 , s i n ( n π x l ) , c o s ( n π x l ) 1,sin({n\pi x\over l}),cos({n\pi x\over l}) 1,sin(lnπx),cos(lnπx)}为基表示出一个周期函数f(x)
求f(x)与每一个基的内积,即相乘求积分,即可得到每个基所对应的系数
a 0 = 1 2 l ∫ − l l f ( x ) d x , a_0 = {1 \over 2l}\int_{-l}^l f(x)dx, a0=2l1llf(x)dx,
a n = 1 l ∫ − l l f ( x ) s i n ( n π x l ) d x , a_n = {1 \over l}\int_{-l}^l f(x)sin({n\pi x\over l})dx, an=l1llf(x)sin(lnπx)dx,
b n = 1 l ∫ − l l f ( x ) c o s ( n π x l ) d x , b_n = {1 \over l}\int_{-l}^l f(x)cos({n\pi x\over l})dx, bn=l1llf(x)cos(lnπx)dx,
n π l = 2 π f {n\pi\over l} = 2\pi f lnπ=2πf,若将频率 f f f作为自变量,将 a n , b n a_n,b_n an,bn作为因变量,便可以得到一个频域的图像,有助于我们分析一段信号里的频率特征

下面引入欧拉公式,可以将傅里叶级数写成复数形式:
f ( x ) = ∑ n = − ∞ + ∞ c n e i n π x l f(x) = \sum_{n=-\infin}^{+\infin} c_n e^{i{n\pi x\over l}} f(x)=n=+cneilnπx
c n = 1 2 l ∫ − l l f ( x ) e − i n π x l d x c_n ={1\over 2l}\int_{-l}^l f(x)e^{-i{n\pi x\over l}}dx cn=2l1llf(x)eilnπxdx

对于非周期函数,直接以周期无穷大处理,而对于定义域连续且有限的一段信号,可以以定义域长度为周期,进行傅里叶变换


离散傅里叶变换(DFT)

现实生活中,对信号的数字化采样是离散的,这时就需要离散形式的傅里叶变换(DFT)
主要的改变就是把积分形式改为求和
设有一信号采样频率为 f s f_s fs,采样点数为N,那么对它进行傅里叶变换后可以得到N个频率幅值
c n = 1 N ∑ k = 0 N − 1 f ( k ) e i 2 π n N k c_n ={1\over N}\sum_{k=0}^{N-1} f(k)e^{i{2\pi n\over N}k} cn=N1k=0N1f(k)eiN2πnk

  • 通常也把 e i 2 π n k e^{i{2\pi \over n}k} ein2πk记作 ω n k \omega_n^k ωnk , 之后推导FFT的时候会用到
  • (余以为,指数上的负号应该是用欧拉公式推导复数形式时产生的,这里直接用实部记录余弦分量,以虚部记录记录正弦分量,便省去负号)

n ∈ { 0 , 1 , . . . , N − 1 } n\in\{0,1,...,N-1\} n{0,1,...,N1}依次计算,对得到的 c n c_n cn取模, 便可以得到一个频率从0~ f s f_s fs的频谱图。自变量频率是等间距分布的,即 c n c_n cn对应的实际频率为 f s ∗ n N , n ∈ { 0 , 1 , . . . , N − 1 } f_s*{n\over N},n\in\{0,1,...,N-1\} fsNn,n{0,1,...,N1}
同时采样定理告诉我们若要完整地保留原始信号中的信息,采样频率应至少大于原始信号中最高频率的两倍。可以只采用前一半结果。(具体原因不懂我瞎说的)
综上,采样频率越高,可以准确测量的频率也就越高(采样频率的一半);参与DFT运算的采样点越多,频谱的分辨率( N f s N\over f_s fsN)就越高,同时也必然导致更长的采样时间和运算时间。

至此已经可以写出离散傅里叶变换(DFT)的代码:

#define N 1024
#define PI 3.1415926535
double x[N];//输入信号 
double Re[N],Im[N];//实部虚部分别为cos、sin
double c[N];//输出频谱
int n,k;
for(n=0;n<N;n++)
{
   for(k=0;k<N;k++)
   {
   	Re[n] += x[k]*cos(2*PI*k*n/N);
   	Im[n] += x[k]*sin(2*PI*k*n/N);
   }
   c[n] = sqrt(Re[n]*Re[n]+Im[n]*Im[n]);//取模
}

这种写法十分简洁易懂,但是很费时。在I.MX RT1064上测试计算1024个采样点需要约1000ms


快速傅里叶变换(FFT)

快速傅里叶变换是一种可以加速离散傅里叶(DFT)运算速度的算法,大大减少了运算量。同时得到傅里叶变换的准确结果。

单位根及其性质

单位根
在复平面的单位圆上取n个点,这些点将圆周n等分。那么它们的指数表示为 e i 2 π n k , k ∈ { 1 , 2 , . . . , n − 1 } e^{i{2\pi \over n}k},k\in \{1,2,...,n-1\} ein2πkk{1,2,...,n1}现将它们记为 ω n k , k ∈ { 1 , 2 , . . . , n − 1 } \omega_n^k,k\in \{1,2,...,n-1\} ωnkk{1,2,...,n1},直观来看下标就是圆周被等分的份数,上标是从实轴开始逆时针数过的圆弧个数
n=8的单位根取法
显然,这些复数的模长为1,且它们的n次方都为实数1,故称之为单位根。

ω n k = e i 2 π n k \omega_n^k = e^{i{2\pi \over n}k} ωnk=ein2πk

单位根的性质

ω n k = ω 2 n 2 k \omega_n^k = \omega_{2n}^{2k} ωnk=ω2n2k

将圆弧再等分依次,变成2n等分,取2k份圆弧,两者表示同一个复数。也可以直接通过指数形式理解。

ω n k + n 2 = − ω n k \omega_n^{k+{n\over2}} = -\omega_n^k ωnk+2n=ωnk

+ n 2 +{n\over2} +2n表示在单位圆上逆时针旋转了半圈,即与原点对称,实部虚部取相反数。

( ω n k ) m = ω n m k (\omega_n^k)^m = \omega_n^{mk} (ωnk)m=ωnmk

这可以由复数乘法性质得到,模长相乘(=1),幅角相加。


单位根如何加速DFT运算

再贴一下离散傅里叶的公式:
c n = 1 N ∑ k = 0 N − 1 f ( k ) e − i 2 π n N k c_n ={1\over N}\sum_{k=0}^{N-1} f(k)e^{-i{2\pi n\over N}k} cn=N1k=0N1f(k)eiN2πnk
e i 2 π n k e^{i{2\pi \over n}k} ein2πk记为 ω n k \omega_n^k ωnk形式:
c n = 1 N ∑ k = 0 N − 1 f ( k ) ω N n k c_n ={1\over N}\sum_{k=0}^{N-1} f(k)\omega_N^{nk} cn=N1k=0N1f(k)ωNnk
c n = 1 N ∑ k = 0 N − 1 f ( k ) ( ω N n ) k c_n ={1\over N}\sum_{k=0}^{N-1} f(k)(\omega_N^n)^k cn=N1k=0N1f(k)(ωNn)k
这是我们需要计算的任务。
A ( x ) = ∑ k = 0 N − 1 f ( k ) x k , A(x) =\sum_{k=0}^{N-1} f(k)x^k, A(x)=k=0N1f(k)xk并将 f ( k ) 用 a k f(k)用a_k f(k)ak表示
A ( x ) = ∑ k = 0 n − 1 a k x k = a 0 + a 1 x + a 2 x 2 + . . . + a n − 1 x n − 1 A(x) = \sum_{k=0}^{n-1}a_kx^k= a_0+a_1x+a_2x^2+...+a_{n-1}x^{n-1} A(x)=k=0n1akxk=a0+a1x+a2x2+...+an1xn1
这里开始采用分治的思想,按下标奇偶性将数列分成两半(此处规定n为偶数):
A ( x ) = ( a 0 + a 2 x 2 + . . . + a n − 2 x n − 2 ) + ( a 1 x + a 3 x 3 + . . . + a n − 1 x n − 1 ) = ( a 0 + a 2 x 2 + . . . + a n − 2 x n − 2 ) + x ( a 1 + a 3 x 2 + . . . + a n − 1 x n − 2 ) A(x)=(a_0+a_2x^2+...+a_{n-2}x^{n-2})+(a_1x+a_3x^3+...+a_{n-1}x^{n-1})\\ =(a_0+a_2x^2+...+a_{n-2}x^{n-2})+x(a_1+a_3x^2+...+a_{n-1}x^{n-2}) A(x)=(a0+a2x2+...+an2xn2)+(a1x+a3x3+...+an1xn1)=(a0+a2x2+...+an2xn2)+x(a1+a3x2+...+an1xn2)
A 1 ( x ) = ( a 0 + a 2 x + . . . + a n − 2 x n − 2 2 ) , A 2 ( x ) = ( a 1 + a 3 x + . . . + a n − 1 x n − 2 2 ) . A_1(x) = (a_0+a_2x+...+a_{n-2}x^{n-2\over2}),\\A_2(x) = (a_1+a_3x+...+a_{n-1}x^{n-2\over2}). A1(x)=(a0+a2x+...+an2x2n2),A2(x)=(a1+a3x+...+an1x2n2).
A ( x ) = A 1 ( x 2 ) + x A 2 ( x 2 ) A(x) = A_1(x^2)+xA_2(x^2) A(x)=A1(x2)+xA2(x2)
接下来开始带入单位根 ω n k \omega_n^k ωnk(这里令 k < n 2 k<{n\over2} k<2n,分治减少运算量的关键步骤):
A ( ω n k ) = A 1 ( ( ω n k ) 2 ) + ω n k A 2 ( ( ω n k ) 2 ) = A 1 ( ω n 2 k ) + ω n k A 2 ( ω n 2 k ) = A 1 ( ω n 2 k ) + ω n k A 2 ( ω n 2 k ) A(\omega_n^k)=A_1((\omega_n^k)^2)+\omega_n^kA_2((\omega_n^k)^2)\\ =A_1(\omega_{n}^{2k})+\omega_n^kA_2(\omega_{n}^{2k})\\ =A_1(\omega_{n\over2}^k)+\omega_n^kA_2(\omega_{n\over2}^k) A(ωnk)=A1((ωnk)2)+ωnkA2((ωnk)2)=A1(ωn2k)+ωnkA2(ωn2k)=A1(ω2nk)+ωnkA2(ω2nk)
对于 k > n 2 k>{n\over2} k>2n,不妨直接带入 ω n k + n 2 \omega_n^{k+{n\over2}} ωnk+2n(多用单位根的几何意义理解):
A ( ω n k + n 2 ) = A 1 ( ( ω n k + n 2 ) 2 ) + ω n k + n 2 A 2 ( ( ω n k + n 2 ) 2 ) = A 1 ( ω n 2 k + n ) + ω n k + n 2 A 2 ( ω n 2 k + n ) = A 1 ( ω n 2 k ∗ ω n n ) − ω n k A 2 ( ω n 2 k ∗ ω n n ) = A 1 ( ω n 2 k ) − ω n k A 2 ( ω n 2 k ) = A 1 ( ω n 2 k ) − ω n k A 2 ( ω n 2 k ) A(\omega_n^{k+{n\over2}})=A_1((\omega_n^{k+{n\over2}})^2)+\omega_n^{k+{n\over2}}A_2((\omega_n^{k+{n\over2}})^2)\\ =A_1(\omega_{n}^{2k+n})+\omega_n^{k+{n\over2}}A_2(\omega_{n}^{2k+n})\\ =A_1(\omega_{n}^{2k}*\omega_n^n)-\omega_n^kA_2(\omega_{n}^{2k}*\omega_n^n)\\ =A_1(\omega_{n}^{2k})-\omega_n^kA_2(\omega_{n}^{2k})\\ =A_1(\omega_{n\over2}^k)-\omega_n^kA_2(\omega_{n\over2}^k) A(ωnk+2n)=A1((ωnk+2n)2)+ωnk+2nA2((ωnk+2n)2)=A1(ωn2k+n)+ωnk+2nA2(ωn2k+n)=A1(ωn2kωnn)ωnkA2(ωn2kωnn)=A1(ωn2k)ωnkA2(ωn2k)=A1(ω2nk)ωnkA2(ω2nk)
观察以上结果,发现前一半( k < n 2 k<{n\over2} k<2n)和后一半的结果十分相似,仅仅差一个符号
根据这个结论,我们就可以只带入前一半单位根的同时得到整个序列的DFT结果

再看两个推导的最后一行, n 写 作 了 n 2 n写作了{n\over2} n2n,有了这一步,就可以发现 A 1 ( ω n 2 k ) 、 A 2 ( ω n 2 k ) 和 A ( ω n 2 k ) A_1(\omega_{n\over2}^k)、A_2(\omega_{n\over2}^k)和A(\omega_{n\over2}^k) A1(ω2nk)A2(ω2nk)A(ω2nk)其实是等价的,只不过取的单位根少了一半,而这两个一半长度的序列又可以通过上面的分治的方法减半运算……
(典型的递归思想,这时候就需要满足 n = 2 i , i ∈ N ∗ n = 2^i,i\in N^* n=2i,iN

总得来说,FFT是充分利用了复数表示中复数的特性(见单位根的性质),推导出了分治的计算方法,并且可以递归使用,从而大大减少了计算复杂度


递归实现
现在就可以用递归的方式完成FFT的代码:
C语言的复数库对于不同编译器使用不太相同,所以先写一个简单的复数计算需要用到的代码

typedef struct complex_t
{
    double x,y;
} complex;

complex plus(complex a,complex b)//+
{
    a.x += b.x;
    a.y += b.y;
    return a;
}

complex minus(complex a,complex b)//-
{
    a.x -= b.x;
    a.y -= b.y;
    return a;
}

complex multi(complex a,complex b)//*
{
    complex m;
    m.x = a.x*b.x - a.y*b.y;
    m.y = a.x*b.y + a.y*b.x;
    return m;
}

接下来是递归程序

#define N 1024  //序列总长度,需满足2的整数次幂
#define Pi 3.14159265359

void FFT_recursion(int len,complex *x)//当前序列长度、当前求解序列
{
    int i;
    complex x1[N],x2[N];	//放按下标奇偶分组的两个序列
    complex Wn = {cos(2.0*Pi/len),sin(2.0*Pi/len)};	//k=1单位根,计算A2前系数用
    complex w = {1,0},m;	//w用来保存单位根的幂
    if(len == 1)return;	//当长度为1时就可以直接返回,什么也不做
    for(i=0;i<len;i+=2)		//给半长序列填入相应的值
    {
         x1[i>>1] = x[i];
         x2[i>>1] = x[i+1];
    }
    FFT_recursion(len>>1,x1);	//求出两个序列分别带入len/2个单位根的值
    FFT_recursion(len>>1,x2);	//结果就存储在输入的数组中
    
    for(i=0;i<(len>>1);i++,w = multi(w,Wn))	//这里i相当于公式中的k
    {
         m = multi(w,x2[i]);	//保存w与A2的乘积
         x[i] = plus(x1[i],m);	//同时给前后两部分赋值
         x[i+(len>>1)] = minus(x1[i],m);
    }
}

如果使用递归写法,就需在调用函数后再对类型为complex的数组进行处理
可以看到这里求的是 A ( ω n k ) A(\omega_n^k) A(ωnk),要求严谨的幅值还需要再除以N,并且取模方便使用(其实工程中直接除以一个合适的值,方便观察就可以了)

用递归实现的FFT已经比之前的DFT高效很多了,但需要特殊功能的时候还得用另外的函数封装,大数组又占空间,如果能改成用for循环实现岂不美哉?


迭代实现
用递归函数写代码的时候,我们只需要思考其中一步需要完成的操作,以及回归的条件。
那么最终整个递归过程都做了些什么呢,下面我们以N=8为例,把每一步分治的过程写出来。

a0 a1 a2 a3 a4 a5 a6 a7
a0 a2 a4 a6
a1 a3 a5 a7
a0 a4
a2 a6
a1 a3
a5 a7

第一行是原始输入的一组数据,我们要做的是用它们分别乘上 ω 8 k , k ∈ [ 0 , 7 ] \omega_8^k,k\in[0,7] ω8k,k[0,7]然后求和
第二行采用分治方法,按奇偶性分成两列,对它们求值后得到两个长度4的数组,设为A1[ ],A2[ ],那么用A1[0]± ( ω 8 1 ) 0 (\omega_8^1)^0 (ω81)0就可以得到最终需要计算的A[0]和A[4]两个值,其余三对结果求法类似。

下面把第一行和最后一行的下标及其二进制写出来:

原序列01234567
二进制000001010011100101110111
重排后04261537
二进制000100010110001101011111

可以看出排列后递归最底层的顺序是有规律的——下标的二进制表示颠倒

再放一张N=16的表格:

原序列0123456789101112131415
二进制0000000100100011010001010110011110001001101010111100110111101111
重排后0841221061419513311715
二进制0000100001001100001010100110111000011001010111010011101101111111

下面研究一下如何用程序完成这个排列
不想研究的可以抄代码,反正就一行
如果把下标的二进制当成数组,一位一位地交换,随着n的增大会越来越麻烦

  • c语言中右移可以表示整除2,比如5>>1=2, 6>>1=3
  • 一个数(除了二进制最低位)可以看作它整除2的数左移一位,如13(1101),而6(0110)<<1=12(1100),前三位和13相同
  • 一个二进制数颠倒后(除了最高位),可以看作它整除2的数也颠倒右移一位,如13(1101) -颠倒-> 11(1011), 6(0110) -颠倒-> 6(0110) >> 1 = 3(0011),后三位相同
  • 剩下的一位单独取出(和1作逻辑与运算)进行位移后和其它位作或运算即可
  • 我们只要从0(0本身就对称,移位后也无变化)开始依次操作,每次去取整除2(右移1)的下标所对应的排列后标号,就能把所有排列计算出来

下面是代码:

#define N 1024	//总数据个数
#define L 10	//满足 N = 2^L	//同时也是下标二进制表示的位数

int r[N] = {0};	//可以认为下标是原排列,数据是二进制颠倒后的排列
int i;	//循环变量

for(i=0;i<N;i++)
{
    r[i] = (r[i>>1]>>1)|(i&1)<<(L-1); //计算重排后的数组下标
    //逻辑或符号左侧是取下标整除2的结果右移一位
    //右侧是单独处理剩下来的一位,将下标本身第一位取出,左移L-1位
}
//这部分代码可以直接放进迭代FFT函数中

现在我们已经成功得到了一个排列后的下标,只要按照这个数组给出的顺序循环填入数据就可以开始进行计算了

直接给出完整代码吧:

#define N 1024 //总数据个数
#define L 10 //满足 N = 2^L
#define Pi 3.14159265359

void FFT(complex *x)
{
    int r[N] = {0},i,l,j,k;
    complex Wn,w;
    complex temp,temp1;	//用于交换数组项,以及计算时保存数组原值
    for(i=0;i<N;i++)
        r[i] = (r[i>>1]>>1)|(i&1)<<(L-1); //计算重排后的数组下标

    for(i=0;i<N;i++)	//按r[i]所给顺序重排数组
        if(i<r[i])	//防止重复交换
        {
            temp = x[i];
            x[i] = x[r[i]];
            x[r[i]] = temp;
        }
    
    for(l=1;l<N;l<<=1)	//当前需要计算的数组长度的【一半】,l取1,2,4,8,...,N/2
    {
        Wn.x = cos(Pi/l);Wn.y = sin(Pi/l);	//准备单位根
        for(j=0;j<N;j+=l<<1)	//依次处理每个序列,j每次循环递增2l
        {
            w.x = 1;w.y = 0;	//记录单位根的幂
            for(k=0;k<l;k++,w = multi(w, Wn))	//用上一步算好的值带入公式计算当前序列
            {
                //由于迭代法只用了一个复数数组,结果也直接记录在本身,
                //所以要先用临时变量记录原值。
                temp = x[j+k];temp1 = multi(w,x[j+k+l]);
                x[j+k] = plus(temp, temp1);
                x[j+k+l] = minus(temp, temp1);
            }
        }
    }
}

如需作频谱分析,测得的信号填入数组实部,输出数组进行取模(平方和开根号)即可
实测以上代码在I.MX RT1064上仅需1.5ms!

至此FFT的研究就基本完成了!


多项式乘法

逛了几天,看到最多的对FFT作用的描述就是:加速多项式乘法。
苦恼了好长时间,始终想不通傅里叶级数里哪里用到多项式了。最后才明白,其实是用傅里叶变换的快速算法,去加速多项式的乘法计算。这里已经不谈FFT频域转换的功能了。

多项式的表示

讲多项式乘法还是离不开两种表示方法

系数表示
下面是一个普通的多项式,它有n项,次数是n-1
f ( x ) = a 0 + a 1 x + a 2 x 2 + . . . + a n − 1 x n − 1 f(x) = a_0+a_1x+a_2x^2+...+a_{n-1}x^{n-1} f(x)=a0+a1x+a2x2+...+an1xn1
把它的系数拿出来,列成一个向量,就得到了多项式的系数表示:
( a 0 , a 1 , a 2 , . . . , a n − 1 ) (a_0,a_1,a_2,...,a_{n-1}) (a0,a1,a2,...,an1)
这是符合我们对多项式的一般认识的表示方法

点值表示
我们选一些x的值带入表达式中,就可以求得多项式的值。如果带入了n个x,得到n个值,理论上就可以通过这一系列方程求解出原式多项式的每一个系数。
y k = f ( x k ) yk=f(x_k) yk=f(xk),然后带入 x k , k ∈ { 0 , 1 , . . . , n − 1 } x_k,k\in \{0,1,...,n-1\} xk,k{0,1,...,n1}
( y 0 , y 1 , y 2 , . . . , y n − 1 ) (y_0,y_1,y_2,...,y_{n-1}) (y0,y1,y2,...,yn1)
这就是点值表示法
点值表示的多项式作乘法运算时只需要n次乘法。


FFT如何加速多项式乘法

FFT作多项式乘法的过程主要包括求值(DFT)插值(IDFT) 两部分。
求值指带入x的值,将系数表示转为点值表示;插值指将点值表示转回系数表示。

所以,求两个n项n-1次多项式相乘的结果,首先要对它们分别带入n个x值,求出点值表达(求值),然后循环n次将点值表示相乘,最后把相乘的结果还原为系数表达(插值)。

听起来是不是十分复杂,甚至有点多此一举?而且插值过程可是要求解n元一次方程组啊!
不过要知道,FFT对DFT的加速可不止一倍两倍,而且如果我们求值的过程中选取上述的单位复根作为自变量带入,插值过程就完全不需要解方程!而且与求值过程仅仅一个符号只差(我看呆了 )!


求值

求值过程与之前讲的FFT类似,同样是带入单位复根 ω n k \omega_n^k ωnk,代码完全相同。
y [ k ] = ∑ i = 0 n − 1 x [ i ] ( ω n k ) i y[k] = \sum_{i=0}^{n-1}x[i](\omega_n^k)^i y[k]=i=0n1x[i](ωnk)i
x[ ]为原多项式的系数,y[ ]记录点值表示。而代码中结果直接输出到x[ ]本身。
这里直接调用该函数:

void FFT(complex *x);	//输入复数组,输出求值结果

插值

插值过程与点值过程的区别就在于:

  1. 带入自变量时取单位复根的共轭复数 ω n − k \omega_n^{-k} ωnk
  2. 结果要除以n

插值过程所作的事情见以下表达式:
c k = 1 n ∑ i = 0 n − 1 y [ i ] ( ω n − k ) i c_k = {1\over n}\sum_{i=0}^{n-1}y[i](\omega_n^{-k})^i ck=n1i=0n1y[i](ωnk)i
c k c_k ck为所求得的系数, y [ i ] y[i] y[i]为已经做过乘法的点值表达
可以看到这里似乎又把已求得的点值表达当成了系数带入求值了,为什么这样做就能解出系数呢?(又要敲公式了,不想看的可以跳过

以下是证明:
设所求多项式的系数表达为 ( a 0 , a 1 , a 2 , . . . , a n − 1 ) (a_0,a_1,a_2,...,a_{n-1}) (a0,a1,a2,...,an1),现有的是它经过FFT求值后的点值表达 ( y 0 , y 1 , y 2 , . . . , y n − 1 ) (y_0,y_1,y_2,...,y_{n-1}) (y0,y1,y2,...,yn1),(其实是通过前面求值的点值多项式相乘得到的)

现设一向量 ( c 0 , c 1 , c 2 , . . . , c n − 1 ) (c_0,c_1,c_2,...,c_{n-1}) (c0,c1,c2,...,cn1)满足 c k = 1 n ∑ i = 0 n − 1 y i ( ω n − k ) i c_k = {1\over n}\sum_{i=0}^{n-1}y_i(\omega_n^{-k})^i ck=n1i=0n1yi(ωnk)i,我们要证明 c k = a k c_k=a_k ck=ak.

首先带入 y i = ∑ j = 0 n − 1 a j ( ω n i ) j y_i=\sum_{j=0}^{n-1}a_j(\omega_n^{i})^j yi=j=0n1aj(ωni)j
c k = 1 n ∑ i = 0 n − 1 y i ( ω n − k ) i = 1 n ∑ i = 0 n − 1 ( ∑ j = 0 n − 1 a j ( ω n i ) j ) ( ω n − k ) i = 1 n ∑ i = 0 n − 1 ( ∑ j = 0 n − 1 a j ( ω n j ) i ) ( ω n − k ) i = 1 n ∑ i = 0 n − 1 ( ∑ j = 0 n − 1 a j ( ω n j ) i ( ω n − k ) i ) = 1 n ∑ i = 0 n − 1 ( ∑ j = 0 n − 1 a j ( ω n j − k ) i ) c_k = {1\over n}\sum_{i=0}^{n-1}y_i(\omega_n^{-k})^i\\ ={1\over n}\sum_{i=0}^{n-1}(\sum_{j=0}^{n-1}a_j(\omega_n^{i})^j)(\omega_n^{-k})^i\\ ={1\over n}\sum_{i=0}^{n-1}(\sum_{j=0}^{n-1}a_j(\omega_n^{j})^i)(\omega_n^{-k})^i\\ ={1\over n}\sum_{i=0}^{n-1}(\sum_{j=0}^{n-1}a_j(\omega_n^{j})^i(\omega_n^{-k})^i)\\ ={1\over n}\sum_{i=0}^{n-1}(\sum_{j=0}^{n-1}a_j(\omega_n^{j-k})^i) ck=n1i=0n1yi(ωnk)i=n1i=0n1(j=0n1aj(ωni)j)(ωnk)i=n1i=0n1(j=0n1aj(ωnj)i)(ωnk)i=n1i=0n1(j=0n1aj(ωnj)i(ωnk)i)=n1i=0n1(j=0n1aj(ωnjk)i)
调换求和次序
c k = 1 n ∑ j = 0 n − 1 a j ( ∑ i = 0 n − 1 ( ω n j − k ) i ) c_k={1\over n}\sum_{j=0}^{n-1}a_j(\sum_{i=0}^{n-1}(\omega_n^{j-k})^i) ck=n1j=0n1aj(i=0n1(ωnjk)i)
j = k j=k j=k时, ω n j − k = 1 \omega_n^{j-k}=1 ωnjk=1 ( ∑ i = 0 n − 1 ( ω n j − k ) i ) = n (\sum_{i=0}^{n-1}(\omega_n^{j-k})^i)=n (i=0n1(ωnjk)i)=n
j ! = k j!=k j!=k时,记 ω n j − k \omega_n^{j-k} ωnjk ω n m \omega_n^m ωnm
∑ i = 0 n − 1 ( ω n j − k ) i = 1 + ( ω n m ) + ( ω n m ) 2 + . . . + ( ω n m ) n − 1 ( ω n m ) ∑ i = 0 n − 1 ( ω n j − k ) i = ( ω n m ) + ( ω n m ) 2 + ( ω n m ) 3 + . . . + ( ω n m ) n \sum_{i=0}^{n-1}(\omega_n^{j-k})^i=1+(\omega_n^m)+(\omega_n^m)^2+...+(\omega_n^m)^{n-1}\\ (\omega_n^m)\sum_{i=0}^{n-1}(\omega_n^{j-k})^i=(\omega_n^m)+(\omega_n^m)^2+(\omega_n^m)^3+...+(\omega_n^m)^{n} i=0n1(ωnjk)i=1+(ωnm)+(ωnm)2+...+(ωnm)n1(ωnm)i=0n1(ωnjk)i=(ωnm)+(ωnm)2+(ωnm)3+...+(ωnm)n
做差,得
( ω n m − 1 ) ∑ i = 0 n − 1 ( ω n j − k ) i = ( ω n m ) n − 1 (\omega_n^m-1)\sum_{i=0}^{n-1}(\omega_n^{j-k})^i=(\omega_n^m)^{n}-1 (ωnm1)i=0n1(ωnjk)i=(ωnm)n1
∑ i = 0 n − 1 ( ω n j − k ) i = ( ω n m ) n − 1 ω n m − 1 \sum_{i=0}^{n-1}(\omega_n^{j-k})^i={(\omega_n^m)^{n}-1\over\omega_n^m-1} i=0n1(ωnjk)i=ωnm1(ωnm)n1
注意 ( ω n m ) n = 1 (\omega_n^m)^{n}=1 (ωnm)n=1
所以 ∑ i = 0 n − 1 ( ω n j − k ) i = 0 \sum_{i=0}^{n-1}(\omega_n^{j-k})^i=0 i=0n1(ωnjk)i=0

再看上面的式子:
c k = 1 n ∑ j = 0 n − 1 a j ( ∑ i = 0 n − 1 ( ω n j − k ) i ) = 1 n a k ( ∑ i = 0 n − 1 ( ω n 0 ) i ) = 1 n a k ∗ n = a k c_k={1\over n}\sum_{j=0}^{n-1}a_j(\sum_{i=0}^{n-1}(\omega_n^{j-k})^i)\\ ={1\over n}a_k(\sum_{i=0}^{n-1}(\omega_n^{0})^i)\\ ={1\over n}a_k*n\\ =a_k ck=n1j=0n1aj(i=0n1(ωnjk)i)=n1ak(i=0n1(ωn0)i)=n1akn=ak


证明完毕,可以开始写代码了!

#define N 8	//大于等于项数和且满足2的整数次幂
#define L 3

double* FFT_Convolution(double *a, double *b)
{
    int r[N] = {0},i,j,l,k;
    complex x[N],y[N];
    complex w,Wn,x1,x2,y1,y2;
    for(i=0;i<N;i++)
    {
        r[i] = (r[i>>1]>>1)|(i&1)<<(L-1); //计算重排后的数组下标
        x[i].x = a[r[i]];x[i].y = 0.0;	//按r[i]重排x,y
        y[i].x = b[r[i]];y[i].y = 0.0;
    }
    //数组次序排列完成
    for(l=1;l<N;l<<=1)  //求值
    {
        Wn.x = cos(Pi/l);Wn.y = sin(Pi/l);
        for(j=0;j<N;j+=l<<1)
        {
            w.x = 1.0;w.y = 0.0;
            for(k=0;k<l;k++,w = multi(w, Wn))
            {
                x1 = x[j+k];
                y1 = y[j+k];
                x2 = multi(w,x[j+k+l]);
                y2 = multi(w,y[j+k+l]);
                x[j+k] = plus(x1, x2);     //同时操作两个序列
                x[j+k+l] = minus(x1, x2);
                y[j+k] = plus(y1, y2);
                y[j+k+l] = minus(y1, y2);
            }
        }
    }
    
    for(i=0;i<N;i++)    //求积
        y[i] = multi(x[i],y[i]);
    
    for(i=0;i<N;i++)	//再次重拍
    	x[i] = y[r[i]];
    
    for(l=1;l<N;l<<=1)  //插值
    {
        Wn.x = cos(Pi/l);Wn.y = -sin(Pi/l);	//这里多了一个负号
        for(j=0;j<N;j+=l<<1)
        {
            w.x = 1.0;w.y = 0.0;
            for(k=0;k<l;k++,w = multi(w, Wn))
            {
                x1 = x[j+k];
                x2 = multi(w,x[j+k+l]);
                x[j+k] = plus(x1, x2);
                x[j+k+l] = minus(x1, x2);
            }
        }
    }
    
    for(i=0;i<N;i++)	//将结果填入数组b
        b[i] = x[i].x/N;

    return b;
}
  • 需要注意的是N必须要大于等于所输入的两个多项式项数和

卷积与互相关运算

现在我们已经能求出多项式相乘的结果了,那么它有什么用呢?显然不仅仅是求多项式。

卷积

当我们平时手算多项式乘法时,如果次数比较高,求出来的项会特别多,所以我一般会按次数顺序计算,即先从0次项,再去找1次项的系数,以此类推。
如果把每一项的系数按顺序写出来,就会发现只要把两个系数相向放置,然后一项一项错开,把对应的系数相乘再求和就可以得到该次数的项的系数。
a 3 , a 2 , a 1 , a 0 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . b 0 , b 1 , b 2 , b 3 a_3,a_2,a_1,a_0..............\\...............b_0,b_1,b_2,b_3 a3,a2,a1,a0.............................b0,b1,b2,b3 a 0 ∗ b 0 = c 0 ( 常 数 项 系 数 ) a_0*b_0=c_0(常数项系数) a0b0=c0
a 3 , a 2 , a 1 , a 0 . . . . . . . . . . . . . . . . . . . b 0 , b 1 , b 2 , b 3 a_3,a_2,a_1,a_0..........\\.........b_0,b_1,b_2,b_3 a3,a2,a1,a0...................b0,b1,b2,b3 a 1 ∗ b 0 + a 0 ∗ b 1 = c 1 ( 一 次 项 系 数 ) a_1*b_0+a_0*b_1=c_1(一次项系数) a1b0+a0b1=c1
a 3 , a 2 , a 1 , a 0 . . . . . . . . . b 0 , b 1 , b 2 , b 3 a_3,a_2,a_1,a_0....\\.....b_0,b_1,b_2,b_3 a3,a2,a1,a0.........b0,b1,b2,b3 a 2 ∗ b 0 + a 1 ∗ b 1 + a 0 ∗ b 2 = c 2 ( 二 次 项 系 数 ) a_2*b_0+a_1*b_1+a_0*b_2=c_2(二次项系数) a2b0+a1b1+a0b2=c2
. . . . . . ...... ......

c k = ∑ i = 0 k a i ∗ b k − i c_k=\sum_{i=0}^{k}a_i*b_{k-i} ck=i=0kaibki
这个操作就是卷积
卷积在工程信号处理中有着广泛的应用,遇到卷积时,我们就可以用上述FFT算法来计算。

double* FFT_Convolution(double *a, double *b);

互相关运算

与卷积的区别在于第一个序列不需要逆序
将序列 { a n } \{a_n\} {an}固定,移动 { b n } \{b_n\} {bn},从-(n-1)到(n-1)

最直观的物理意义就是经过位移,两个序列中相似的部分重叠的时候,得到的运算结果最大。由此可以作信号的识别。

在MATLAB中的函数为xcorr(x,y);
在上述卷积代码中只需在数组b赋值时反向填入即可

下面是一个例子,可以明显看出第二个序列向左移动600个单位时和第一个序列最相似(这里是重叠)
MATLAB互相关


声音测距

下图是一段Chirp信号

chirp信号

A chirp is a signal in which the frequency increases (up-chirp) or decreases (down-chirp) with time.
Chirp信号是一段频率随时间增加或降低的信号。

  • 为什么用Chirp信号测距呢?
    声音测距的基本原理就是测量声音在空气中传播的延迟,通过发出一段Chirp信号,再去对它进行采集,通过与原信号进行互相关运算就可以知道它延迟的时间,从而算出距离。
    那为什么不直接判断相位差呢?可以简单的计算一下,常温中声速约为334m/s,假设声音信号传播10米后被接收,这期间过去了大约30ms,而一个440hz的声音信号已经走过了13个周期,这中间跳过的周期是我们无法测量的。
    而使用Chirp信号,我们可以按需设置它一个周期的时长,就可以避免这个问题,而且调频信号一个周期内的特征点比一个简单的正弦波要多得多,测量也就更加精确。

为了得到精确的时间延迟,也就是希望信号相关结果出现的峰值约尖锐越好,作为测距的声音信号需要频谱较宽,比如时间很短的脉冲信号、具有变频的Chirp信号、白噪声信号等等。利用声音定位的动物们常常使用的是Chirp信号。

采样完整周期的声音信号后,和原式数据作互相关运算,得到峰值处的横坐标,乘上采样周期即可得到声音传播延迟,再乘上声速即可算出距离。


终于终于结束了!(累die
这一周在各大论坛逛了无数篇有关傅里叶的博客,但奈何数学已经还了好多给老师,废了很大的劲才勉强理解。得出的结论是,每个人自身薄弱的地方不同,学习时着重的点也不同,写的东西大多是针对自身认为的难点。于是决定自己针对我理解中的难点,结合自己的理解写一篇笔记,以便日后查阅。(如果能帮到大家理解就更好啦)
其中一些内容包含自身理解,可能有错误或者不严谨的地方,查阅时要注意。

下面贴上主要参考的博文:

——2020/3/25

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值