频谱分析-FFT之后的那些事情

知乎上有几个比较好的讲解傅里叶变换的文章:
傅里叶分析之掐死教程(完整版)
通过这些文章都能对频谱有大致了解,但等你自己坐下了,要对一个信号进行频谱分析时,你会发现好多细微的问题其实并没有注意,下面,将讲讲那些细微的问题

实现快速傅里叶变换

忠告:除非你自己为了验证你的能力,或为了验证你对对快速傅里叶变换算法的了解,千万别用自己写的快速傅里叶变换算法,也别在网上随便找一个算法就拿来用,快速傅里叶变换算法全世界比较权威的有FFTW,是由其由MIT(麻省理工学院)的M.Frigo 和S. Johnson 开发,可计算一维或多维实和复数据以及任意规模的DFT。当然,你如果不屑于人家一个学院开发的快速傅里叶变换库,你用你自己写的快速傅里叶变换算法没人拦着你,不过你如果不是用汇编来写那你已经输在了起跑线上了。
废话说那么多就是让你别用自己写的快速傅里叶变换算法,推荐大家使用人家经过十几年磨练的成熟库,其中FFTW就是一个很好的选择

FFT

工程中遇到的问题都是这样的:有一个信号,采样率为fs,求频谱

首先用一段matlab的代码进行成信号

N = 256;
t = linspace(0,2*pi,256);
Fs = 100;
t = [0:N-1]./Fs;

wave = 1*cos(2*pi*10.*t) ...
    + 2*sin(2*pi*15.*t + deg2rad(30)) ...
    + 3*cos(2*pi*20.*t + deg2rad(-30)) ...
    + 4*sin(2*pi*26.5.*t + deg2rad(60)) ...
    ;
Fs = 1/(t(2)-t(1));                    

wave为生成的信号,Fs为采样率,这段信号由形如Acos(2πωt+θ)的基本信号组成,中学物理知识都知道,Acos(2πωt+θ)中,A代表幅值,ω为角频率,θ为相位角,傅里叶变换就是把一段信号分解为n个形如Acos(2πωt+θ)基本信号叠加的过程
上面那段信号进行绘制:

figure
plot(t,wave);
set(gcf,'color','w');
xlabel('time (s)');
ylabel('Amp');
title('信号');

频谱分析-FFT之后的那些事情_第1张图片
这段数据如下:

8.06217782649107 6.69261843058528 -3.35043720308466 -4.66253989242961 0.697246349738525 0.0753692287562088 -2.78513076487351 0.499925017089664 3.17606212123038 -0.277082029119007 -1.05610561921709 3.01302269453115 1.03514268896465 -5.45859004974005 -2.40431451359648 6.43871086475007 3.84304060556592 -6.54073038281710 -6.18090623900590 3.18876994983517 5.42972297462435 1.05400419914209 0.343298598331839 0.280851846782554
……
-3.33193590622286 4.25926639098748 4.59807621135344 -2.58753675611741 -3.56492073452458 1.00777782079415 0.978670962706604 -0.664391120096073

下面我们用matlab的fft函数对这个数据进行快速傅里叶变换:

7.38504326171959+0.00000000000000i 7.38708694461654+0.183505522698889i 7.39321771956158+0.368190251593997i 7.40343404206541+0.555266982451810i 7.41773083839269+0.746017837381402i 7.43609543142577+0.941834538704739i 7.45850113316254+1.14426613813919i 7.48489766109681+1.35507803033555i 7.51519706012674+1.57632754978198i 7.54925308382079+1.81046378661169i
……
7.58683083724872-2.06046299281095i 7.54925308382079-1.81046378661169i 7.51519706012674-1.57632754978198i 7.48489766109681-1.35507803033555i 7.45850113316254-1.14426613813919i 7.43609543142577-0.941834538704739i 7.41773083839269-0.746017837381402i 7.40343404206541-0.555266982451810i 7.39321771956158-0.368190251593997i 7.38708694461654-0.183505522698889i

傅里叶变换出来的是对应长度的一堆复数,
此复数数组符合如下规律:

  • 第一个(索引为0)和N/2的两个复数的虚数部分为0;
  • 下标为i和N-i的两个复数共轭,也就是其虚数部分数值相同、符号相反.[1]

把它的模绘图可以看到其特性

 Y = fft(wave);
figure
plot(abs(Y));
set(gcf,'color','w');

频谱分析-FFT之后的那些事情_第2张图片

由于虚数部共轭和虚数部为0等规律,真正有用的信息保存在下标从0到N/2的N/2+1个虚数中
下面让我们来看看FFT变换之后的那些复数都代表什么意思。

FFT之后得到的是什么数

FFT之后得到的那一串复数是波形对应频率下的幅度特征,注意这个是幅度特征不是复制,下面要讲两个问题:1.如何获取频率,2.如何获取幅值

获取频率

FFT变换如何获取频率?傅里叶变换并没对频率进行任何计算,频率只与采样率和进行傅里叶变换的点数相关,注意这里是进行傅里叶变换的点数而不一定是信号的长度。
FFT变换完第一个数时0Hz频率,0Hz就是没有波动,没有波动有个专业一点的说法,叫直流分量
后面第二个复数对应的频率是0Hz+频谱分辨率,每隔一个加一次,频谱分辨率$\Delta f $计算公式如下:
Δ f = F s N \Delta f = \frac{{Fs}}{N} Δf=NFs
式中:
Fs为采样率
N为FFT的点数
因此只要Fs和N定了,频域就定下来了。

FFT变换后的第一个实数 - 直流分量

FFT之后的第一个结果表示了时域信号中的直流成分的多少,所谓直流信号,代表和基准0的偏移量。
上面的结果不好说明,下面再看一个例子:

oneWave = [1,1,1,1,1,1,1,1];
fft(oneWave)

输出:

8 0 0 0 0 0 0 0

oneWave 的直流分量是1,但计算结果是8,这里又引入一个问题,FFT之后的数值不是真实的幅值,需要进行转转换,第一个点需要除以N,才能还原为原来的结果

FFT变换后的复数模 - 幅度

假设原始信号的峰值为A,那么FFT的结果的每个点(除了第一个点直流分量之外)的模值就是A
的N/2倍。而第一个点就是直流分量,它的模值就是直流分量的N倍

也就是说,要得出真实幅值,需要把除了第1个点(i=0)以及最后一个点(i=N/2)除以N以外,其余点需要求得的模除以N/2
这是因为傅里叶级数对应时域幅值,其中已经包含了1/N项,而fourier变换中没有该系数,
所以,进行完fft变换后需除以N/2才能与时域对应上。
FFT的计算公式
F n = ∑ i = 0 N − 1 x i e − 2 π j N n i Fn = \sum\limits_{i = 0}^{N - 1} {{x_i}{e^{ - \frac{{2\pi j}}{N}ni}}} Fn=i=0N1xieN2πjni
实际应用中,只有i=0~N/2是有用的
全世界绝大部分的FFT算法计算出来后都需要进行幅度的转换的,为何要这样设计,因为傅里叶变换在很多场合是不需要求幅度的,而只需要分贝就可以,因此,如果傅里叶变换做了乘以2除以n的处理反而在许多场合是多余的,就像求距离,好多情况只需要 x 2 + y 2 {x^2} + {y^2} x2+y2就可以了,并不需要 x 2 + y 2 \sqrt {{x^2} + {y^2}} x2+y2
幅值根据需求有不同需求,具体见下节

幅度谱,幅值谱?Magnitude,Amplitude?

  • 幅值 Amplitude
    幅值就是对于波形的幅值来说的,上面一节说的转换就是把fft计算的结果转化为幅值,英文叫Amplitude
    在工程中还经常看到分贝纵坐标的频谱,带分贝的频谱,使用分贝数的好处是,用较小的坐标可以描述很宽的范围。工程上会取20log(Amplitude)转变为分贝。
    幅值第n(其中n!=1)点处的fft计算的结果是复数a+bi,模值A=sqrt(a2+b2),那么实际信号的幅值是2*A/N;
    当n=0时(0Hz),也就是第一个点就是直流分量,它的模值就是直流分量的N倍,实际信号的幅值是A/N,注意N是采样点而不是进行FFT的点数

  • 幅度 Magnitude
    若对fft的结果不做任何处理,直接取模,那么这个值叫幅度,英文上叫Magnitude,

于是对fft计算的复数结果,其实数和虚数对应如下:

名称 计算公式
幅度(Magnitude) R e 2 + I m 2 \sqrt {{{{\mathop{\rm Re}\nolimits} }^2} + {{{\mathop{\rm Im}\nolimits} }^2}} Re2+Im2
幅值(Amplitude) i = ( 0 , n 2 ) , A = R e 2 + I m 2 n ; o t h e r w i s e , A = 2 R e 2 + I m 2 n i = \left( {0,\frac{n}{2}} \right),A = \frac{{\sqrt {{{{\mathop{\rm Re}\nolimits} }^2} + {{{\mathop{\rm Im}\nolimits} }^2}} }}{n};otherwise,A = \frac{{2\sqrt {{{{\mathop{\rm Re}\nolimits} }^2} + {{{\mathop{\rm Im}\nolimits} }^2}} }}{n} i=(0,2n),A=nRe2+Im2 ;otherwise,A=n2Re2+Im2
dB 20log(Amplitude)

根据这个表,就可以很明白FFT之后需要进行什么样的处理了。

信号补零-提高频谱显示分辨率

有了上面的知识,做出频谱是没有太大问题了,但在频谱分析前,还需要说另外一个问题,前文提到,获取频率频谱需要计算频谱分辨率$ \Delta f = \frac{{Fs}}{N} $。
频谱分辨率数值越小,频谱就越精细,分辨率越高,所以,在一个时间里,能采集的点越多越好。
特别是在采样率高的情况下,采样率作为分子,是降低分辨率的一个因素,因此高频采样中,能采集的点越多越好。

另外吐槽一下,采集前别乱选采样率,要对采集的信号有一定的了解,确定大致感兴趣的频率段,频谱分析的频率范围是[0~Fs/2],也就是采样率的一半是你频谱的极限。别瞎选,以前就看过不懂采集时选了一个最大的采样率,结果得出来的频谱质量非常差。

在采集点数不足时,有一个方法可以提高频谱分辨率,就是信号补零。注意,这个提高只是视觉上的提高,并没有再物理上有相应的提高。也就是没有的频率成分你补零之后还是没有。

一般如果信号不是2n的长度,会补零把信号补到2n的长度,这样是因为2^n长度的傅里叶信号计算会更快更准。

完整的频谱分析代码

简单起见,这里贴上matlab的代码和Python的代码:
首先matlab:
定义一个函数进行频谱分析
只要输入data波形,Fs采样率,就可以输出Fre和Amp
File:
frequencySpectrum.m

function [Fre,Amp,Ph,Fe] = frequencySpectrum( wave,Fs,varargin)
    %傅里叶变换
    %   data:波形数据
    %   Fs:采样率
    %   varargin:
    %		isaddzero->是否补零,默认为1,否则会按照data的长度进行fft
    %		scale->幅值的尺度,'amp'为幅值谱,'ampDB'为分贝显示的幅值谱,'mag'为幅度谱就是fft之后直接取模,'magDB'为'mag'对应的分贝
    %		isdetrend->是否进行去均值处理,默认为1
    %   得到的是[fre:频率,Amp:幅值,Ph:相位,Fe:原始的复数]

    if (size(wave,1)>1 && size(wave,2) > 1)
    	for i=1:size(wave,2)
    		[a,b,c,d] = frequencySpectrum_1dim( wave(:,i),Fs,varargin);
    		Fre(:,i) = a;
    		Amp(:,i) = b;
    		Ph(:,i) = c;
    		Fe(:,i) = d;
    	end
    else
    	[Fre,Amp,Ph,Fe] = frequencySpectrum_1dim( wave,Fs,varargin);
    end
end

function [Fre,Amp,Ph,Fe] = frequencySpectrum_1dim( data,Fs,varargin)
    %傅里叶变换
    %   data:波形数据
    %   Fs:采样率
    %   varargin:
    %		isaddzero->是否补零,默认为1,否则会按照data的长度进行fft
    %		scale->幅值的尺度,'amp'为幅值谱,'ampDB'为分贝显示的幅值谱,'mag'为幅度谱就是fft之后直接取模,'magDB'为'mag'对应的分贝
    %		isdetrend->是否进行去均值处理,默认为1
    %   得到的是[fre:频率,Amp:幅值,Ph:相位,Fe:原始的复数]
    isAddZero = 1;
    scale = 'amp';
    isDetrend = 1;
    while length(varargin)>=2
        prop =varargin{1};
        val=varargin{2};
        varargin=varargin(3:end);
        switch lower(prop)
            case 'isaddzero' %是否允许补0
                isAddZero = val;
            case 'scale'
                scale = val;
            case 'isdetrend'
                isDetrend = val;
        end
    end

    n=length(data);
    if isAddZero
        N=2^nextpow2(n);
    else
        N = n;
    end
    
    if isDetrend
        Y = fft(detrend(data,'constant'),N);
    else
        Y = fft(data,N);
    end
    
    Fre=(0:N-1)*Fs/N;%频率
    Fre = Fre(1:N/2);
    Amp = dealMag(Y,N,n,scale);
    ang=angle(Y(1:N/2));
    Ph=ang*180/pi;
    Fre = Fre';
    Fe = Amp.*exp(1i.*ang);
end

function amp = dealMag(fftData,fftSize,dataSize,scale)
	switch lower(scale)
		case 'amp'
			amp=abs(fftData);
			amp(1)=amp(1)/dataSize;
			amp(2:fftSize/2-1)=amp(2:fftSize/2-1)/(dataSize/2);
			amp(fftSize/2)=amp(fftSize/2)/dataSize;
			amp=amp(1:fftSize/2);
		case 'ampdb'
			amp=abs(fftData);
			amp(1)=amp(1)/fftSize;
			amp(2:fftSize/2-1)=amp(2:fftSize/2-1)/(fftSize/2);
			amp(fftSize/2)=amp(fftSize/2)/fftSize;
			amp=amp(1:fftSize/2);
			amp = 20*log(amp);
		case 'mag'
			amp=abs(fftData(1:fftSize/2));
		case 'magdb'
			amp=abs(fftData(1:fftSize/2));
			amp = 20*log(amp);
		otherwise
			error('unknow scale type');
	end
end

调用示例

N = 256;
t = linspace(0,2*pi,256);
Fs = 100;
t = [0:N-1]./Fs;

waveData = 1*cos(2*pi*10.*t) ...
    + 2*sin(2*pi*15.*t + deg2rad(30)) ...
    + 3*cos(2*pi*20.*t + deg2rad(-30)) ...
    + 4*sin(2*pi*26.5.*t + deg2rad(60)) ...
    ;
Fs = 1/(t(2)-t(1));          

figure
[Fre,Amp] = frequencySpectrum(waveData,Fs);
subplot(2,2,1)
plot(Fre,Amp);
set(gcf,'color','w');
title('amp')

subplot(2,2,2)
[Fre,Amp] = frequencySpectrum(waveData,Fs,'scale','ampdb');
plot(Fre,Amp);
set(gcf,'color','w');
title('ampDB')

subplot(2,2,3)
[Fre,Amp] = frequencySpectrum(waveData,Fs,'scale','mag');
plot(Fre,Amp);
set(gcf,'color','w');
title('mag')

subplot(2,2,4)
[Fre,Amp] = frequencySpectrum(waveData,Fs,'scale','magdb');
plot(Fre,Amp);
set(gcf,'color','w');
title('magDB')

频谱分析-FFT之后的那些事情_第3张图片

python版本的见GitHub:
https://github.com/czyt1988/DataProcess/blob/master/czy/signal.py
函数spectrum

C++版本的等我写下一篇文章吧:

总结

  • FFT之后的幅值是需要进行特殊处理才能使用的,并非直接对应我们物理上的幅值,需要进行换算,换算的方法见上文列表

  • FFT的数据点数可以是原有数据的点数,也可以认为补长,使其有一定的可视分辨率

其实还有:

  • 截断加窗问题

  • 加窗频谱幅值修正问题

等闲的时候再写写了

若有错误请大家指正

#参考文献
[1]用Python做科学计算

你可能感兴趣的:(数学算法,matlab)