時頻分析之STFT:短時傅里葉變換的原理與代碼實現(非調用Matlab API)

1. 引言

在信號分析中,傅里葉變換可稱得上是神器。但在實際應用中,人們發現它還是存在一些不可忽視的缺陷。

爲了便於敘述考察以下兩種情形:

Case 1

考察這樣一個函數:

fs = 1000;
t = 0:1/fs:1 - 1/fs;
x = [10 * cos(2 * pi * 10 * t), 20 * cos(2 * pi * 20 * t),...
        30 * cos(2 * pi * 30 * t), 40 * cos(2 * pi * 40 * t)];

繪製這個函數的時域圖像和經過傅立葉變換後的頻譜圖像,長這個樣子:
在這裏插入圖片描述

現在把信號反轉過來:

x = [10 * cos(2 * pi * 10 * t), 20 * cos(2 * pi * 20 * t),...
        30 * cos(2 * pi * 30 * t), 40 * cos(2 * pi * 40 * t)];
x = x(end:-1:1);

再次繪製時域和頻域的圖像,它長這樣:

在這裏插入圖片描述
不難發現,儘管這兩個信號的時域分佈完全相反,但是它們的頻譜圖是完全一致的。顯然,FFT無法捕捉到信號在時域分佈上的不同。

Case 2

考察一個普普通通的信號:

fs = 1000;
t = 0:1/fs:1-1/fs;

x = 2 * cos(2 * 10 * t) + 4 * sin(2 * 30 * t);

同樣繪製它的時域以及頻域圖像:
在這裏插入圖片描述

現在給信號加入一個高頻突變:

sharp = zeros(1, length(x));
% 給信號中間加一個突變
sharp(501:510) = 5 * cos(2 * pi * 100 * linspace(0, 1, 10));
x = x + sharp;

然後繪圖:
在這裏插入圖片描述
對比兩個信號的時域圖,我們能很明顯發現在第二個信號中央的部分出現了一個突變擾動。然而在頻域圖中,這樣的變化並沒有很好的被捕捉到。注意到紅框中部分,顯然傅里葉變換把突變解釋爲了一系列低成分的高頻信號的疊加,並沒有很好的反應突變擾動給信號帶來的變化

爲什麼我們需要時頻分析

通過以上的兩個例子,我們不難發現傅立葉變換的缺陷。

第一個例子告訴我們,傅里葉變換隻能獲取一段信號總體上包含哪些頻率的成分,但是對各成分出現的時刻並無所知。因此時域相差很大的兩個信號,可能頻譜圖一樣。

第二個例子告訴我們,對於信號中的突變,傅里葉變換很難及時捕捉。而在有些場合,這樣的突變往往是十分重要的。

當然如果非要硬槓,也不是完全沒辦法——這就需要需分析相位譜了,但在實際應用中,有誰會不嫌麻煩地去看相位譜呢?

總而言之,傅里葉變換非常擅長分析那些頻率特徵均一穩定的平穩信號。但是對於非平穩信號,傅立葉變換只能告訴我們信號當中有哪些頻率成分——而這對我們來講顯然是不夠的。我們還想知道各個成分出現的時間。知道信號頻率隨時間變化的情況,各個時刻的瞬時頻率及其幅值——這也就是時頻分析(引用自知乎)。

所謂時頻分析,就是既要考慮到頻率特徵,又要考慮到時間序列變化。常用的有兩種方法:短時傅里葉變化,以及小波變換。本文我們只介紹短時傅里葉變換

2. 短時傅里葉變換原理

短時傅里葉變換的思路非常直觀:既然對整個序列做FFT會丟失時間信息,那我一段一段地做FFT不就行了嘛!這也正是短時傅里葉變換名稱的來源,Short Time Fourier Transorm,這裏的 Short Time 就是指對一小段序列做 FFT。

那麼怎麼一段一段處理呢?直接截取信號的一段來做 FFT 嗎?一般我們通過加窗的方法來截取信號的片段。定義一個窗函數 w(t)\textrm{w}(t),比如這樣。

在這裏插入圖片描述
將窗函數位移到某一中心點 τ\tau,再將窗函數和原始信號相乘就可以得到截取後的信號 y(t)。

y(t)=x(t)w(tτ) y(t) = x(t) \cdot \textrm{w}(t - \tau)

前面提到的直接截取的方法其實就是對信號加一個矩形窗,不過一般我們很少選用矩形窗,因爲矩形窗簡單粗暴的截斷方法會產生的頻譜泄露以及吉布斯現象,不利於頻譜分析。更多關於窗函數的內容,可以看這裏:加窗法


對原始信號 x(t)x(t) 做 STFT 的步驟如下。

首先將將窗口移動到信號的開端位置,此時窗函數的中心位置在 t=τ0t = \tau_0處,對信號加窗處理

y(t)=x(t)w(tτ0) y(t) = x(t) \cdot \textrm{w}(t - \tau_0)

然後進行傅里葉變換

X(ω)=F(y(t))=+x(t)w(tτ0)ejωtdt X(\omega) = \mathcal{F}(y(t)) = \int_{\infty}^{+\infty}x(t)\cdot \textrm{w}(t-\tau_0) e^{-j\omega t}dt

由此得到第一個分段序列的頻譜分佈 X(ω)X(\omega)。在現實應用中,由於信號是離散的點序列,所以我們得到的是頻譜序列 X[N]X[N]

爲了便於表示,我們在這裏定義函數 S(ω,τ)S(\omega, \tau),它表示,在窗函數中心爲 τ\tau 時,對原函數進行變換後的頻譜結果 X(ω)X(\omega),即:

S(ω,τ)=F(x(t)w(tτ))=+x(t)w(tτ)ejωtdt S(\omega, \tau) = \mathcal{F}(x(t)\cdot \textrm{w}(t-\tau)) = \int_{\infty}^{+\infty}x(t)\cdot \textrm{w}(t-\tau) e^{-j\omega t}dt

對應到離散場景中,S[ω,τ]S[\omega, \tau] 就是一個二維矩陣,每一列代表了在不同位置對信號加窗,對得到的分段進行傅里葉變換後的結果序列
在這裏插入圖片描述

完成了對第一個分段的FFT操作後,移動窗函數到 τ1\tau_1。把窗體移動的距離稱爲 Hop Size。移動距離一般小於窗口的寬度,從而保證前後兩個窗口之間存在一定重疊部分,我們管這個重疊叫 Overlap。

在這裏插入圖片描述
重複以上操作,不斷滑動窗口、FFT,最終得到從 τ0τN\tau_0 \sim \tau_N 上所有分段的頻譜結果:

在這裏插入圖片描述
最終我們得到的 SS,就是 STFT 變換後的結果。

3. STFT實現

以下代碼基於 Matlab 2019b。

3.1 算法實現

STFT 的實現如下,算法返回的三個參數:

  • f: m 維向量,表示傅里葉變換後每個點對應的頻率值,單位爲 Hz
  • t: n 維向量,表示 n 個窗口中心時間 τ1τn\tau_1 \sim \tau_n,單位爲秒
  • STFT: 一個二維矩陣 [m, n],每個列向量代表了在對應 τ\tau 上 FFT 變換的結果
function [STFT, f, t] = mystft(x, win, hop, nfft, fs)
	% 計算短時傅里葉變換
    % Input:
    %   x - 一維信號
    %   win - 窗函數
    %   hop - hop size,移動長度
    %   nfft - FFT points
    %   fs - 採樣率
    %
    % Output:
    %   STFT - STFT-矩陣 [T, F]
    %   f - 頻率向量
    %   t - 時間向量
    
    % 把 x 變爲列向量
    x = x(:);
    xlen = length(x);
    wlen = length(win);

    % 窗口數目 L
    L = 1+fix((xlen-wlen)/hop);
    STFT = zeros(nfft, L);
    
    % STFT
    for l = 0:L-1
        % 加窗
        xw = x(1+l*hop : wlen+l*hop).*win;
        
        % FFT計算
        X = fft(xw, nfft);
        X = fftshift(X);

        STFT(:, 1+l) = X(1:nfft);
    end
    
    % 取每個窗口中點的時間點
    t = (wlen/2:hop:wlen/2+(L-1)*hop)/fs;
    %f = (0:nfft-1)*fs/nfft;
    % 頻率 (fftshift之後的)
    f = (-nfft/2:nfft/2-1) * (fs/nfft);
    
end

3.2 使用範例

我們這裏使用 Case 1 的範例來看看 STFT 效果如何。

爲了方便可視化,這裏給出了對 STFT 變換後的可視化函數。

function PlotSTFT(T,F,S)
    % Plots STFT
    plotOpts = struct();
    plotOpts.isFsnormalized = false;
    plotOpts.cblbl = getString(message('signal:dspdata:dspdata:MagnitudedB'));
    plotOpts.title = 'Short-time Fourier Transform';
    plotOpts.threshold = max(20*log10(abs(S(:))+eps))-60;
    signalwavelet.internal.convenienceplot.plotTFR(T,F,20*log10(abs(S)+eps),plotOpts);
end

對 Case 1 中的兩種情況進行分析,代碼如下

close all; clear; clc;
fs = 1000;
t = 0:1/fs:1 - 1/fs;
% 窗口大小,推薦取 2 的冪次
wlen = 256;
% hop size 即移動步長,一般要取一個小於 wlen 的數,推薦取 2 的冪次
hop = wlen/4;
% FFT 點數,理論上應該不小於wlen,推薦取 2 的冪次
nfft = 256;

x = [10 * cos(2 * pi * 10 * t), 20 * cos(2 * pi * 20 * t),...
        30 * cos(2 * pi * 30 * t), 40 * cos(2 * pi * 40 * t)];
figure;
subplot(2, 2, 1);
plot(x);
% 隨便選的一個窗函數
win = blackman(wlen, 'periodic');

[S, f, t] = mystft(x, win, hop, nfft, fs);
subplot(2, 2, 2);
PlotSTFT(t,f,S);

x = x(end:-1:1);
subplot(2, 2, 3);
plot(x);
win = blackman(wlen, 'periodic');

[S, f, t] = mystft(x, win, hop, nfft, fs);
subplot(2, 2, 4);
PlotSTFT(t,f,S);

可以看到,在 FFT 中無法區分的頻譜圖像在 STFT 中區分就非常明顯,可以看出按照不同的時間分段,頻譜分佈的變化。

在這裏插入圖片描述

爲了更好地理解,將右上角的圖做一次三維旋轉:

在這裏插入圖片描述

可以非常清晰地看出頻率分佈隨時間的變換。注意到分界線處存在異常的高頻成分(就是 STFT 圖像中那三條豎線),這是因爲時域信號突變導致的高頻成分。

3.3 Matlab 中的實現

在老的版本中,Matlab 中 STFT 的函數名爲 spectrogram,而在 2019 版本中,引入了新的函數 stft,用法和我上面的實現的程序基本一致。

close all; clear; clc;
fs = 1000;
t = 0:1/fs:1 - 1/fs;
% 窗口大小,推薦取 2 的冪次
wlen = 256;
% hop size 即移動步長,一般要取一個小於 wlen 的數,推薦取 2 的冪次
hop = wlen/4;
% FFT 點數,理論上應該不小於wlen,推薦取 2 的冪次
nfft = 256;

x = [10 * cos(2 * pi * 10 * t), 20 * cos(2 * pi * 20 * t),...
        30 * cos(2 * pi * 30 * t), 40 * cos(2 * pi * 40 * t)];
figure;
subplot(1, 3, 1);
win = blackman(wlen, 'periodic');
[S, f, t] = mystft(x, win, hop, nfft, fs);
PlotSTFT(t,f,S);
title('My STFT');

subplot(1, 3, 2);
[S1, f1, t1] = spectrogram(x, win, wlen - hop, nfft, fs);
PlotSTFT(t1, f1, S1);
title('spectrogram');

subplot(1, 3, 3);
[S2, f2, t2] = stft(x, fs, 'Window', win, 'OverlapLength',wlen - hop,'FFTLength',nfft);
PlotSTFT(t2, f2, S2);
title('stft');

需要注意的是,我實現的時候用的參數是 hop size,而matlab提供的函數需要的參數是 overlap 這個別搞混了。結果如下。
在這裏插入圖片描述
要注意的是,spectrogram 輸出的是單邊譜,而 stft 輸出的是雙邊譜,其他區別倒不大。但是 spectrogram 還可以輸出功率譜,而 stft 就不行了。

4. STFT 的缺點

如果你仔細分析上面的內容,你會發現短時傅立葉變換也有不容忽視的缺陷。

最明顯的一個問題:窗口的寬度該設多少爲好呢?爲了闡明這個問題的影響,我們做這麼一個實驗:調整不同 wlen 的值,來看看影響。

len =  [32, 64, 128, 256];
for i = 1:4
    wlen = len(i);
    hop = wlen/4;
    nfft = wlen;
    win = blackman(wlen, 'periodic');
    [S, f, t] = mystft(x, win, hop, nfft, fs);
    subplot(2, 2, i);
    PlotSTFT(t,f,S);
    [m, n] = size(S);
    t = sprintf('Wlen = %d, S: [%d, %d]', wlen, m, n);
    title(t);
end

結果如下:
在這裏插入圖片描述

注意 S 的尺寸隨 wlen 的變換,不難發現一個事實:

  • 窗太窄,窗內的信號太短,會導致頻率分析不夠精準,頻率分辨率差,具體表現是黃色的橫線越來越寬、越來越模糊
  • 窗太寬,時域上又不夠精細,時間分辨率低,具體表現是淡藍色的豎線越來越寬、越來越模糊(還記得嗎,豎線表示交界處的突變造成的高頻干擾成分)

從定量的角度來看,STFT的時間分辨率取決於滑移寬度 HH,而頻率分辨率則取決於 FsH\frac{F_s}{H}。顯然,一方的增加必然意味着另一方的減小。這就是所謂的時頻測不準原理(跟海森堡測不準是一個性質),具體關係爲:

ΔtΔf14π \Delta t \cdot \Delta f \geqslant \frac{1}{4\pi}
在這裏插入圖片描述

另外,固定的窗口大小過於死板。對低頻信號而言,有可能連一個週期都不能覆蓋;對高頻信號而言,可能覆蓋過多週期,不能反映信號變化。

也就是說,這又是一個 Trade-Off 問題,而一個問題一旦進入 Trade-Off 模式,就開始變得玄學起來了。

爲了打破這種玄學困境,就需要一個更加強大的武器——小波變換。

至於小波變換,那就是另一個故事了。

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