時間周波数解析の基礎の基礎, FFT, STFT, 窓関数などについて

信号処理

最近, 時間周波数解析を行う機会があったので, その理論や役割などをまとめてみる.

今回対象とする信号

今回, 例として用いる信号は, 時間に伴い周波数が1Hzから10Hzに, 線形に増加するchrip信号である.
この信号は次のように示される.

\begin{gather}
w(t)=\sin(\frac{1}{2}\alpha t^2+w_ot+\phi_0) \tag{1}
\end{gather}

ここで, $w_0$は初期角周波数を表し, $\phi_0$は初期位相角を表す.
$\alpha$は変調周波数の時間あたりの周波数変化を表し, 次式で表現する.
\begin{gather}
\alpha = \frac{w_f-w_0}{T}=\frac{2\pi(f_1-f_0)}{T} \tag{2}
\end{gather}
ここで, $T$は, 変調する時間間隔を表し, ここでは10 sとする.
$f_0$は1 Hz, $f_1$は10 Hzとする.

図1 今回扱うチャープ信号

プログラム
import numpy as np
import matplotlib.pyplot as plt
from scipy import signal

t1= 10
Ts = 0.01
fs = 1/Ts
t = np.arange(0,t1+Ts,Ts)

f0=1.00
f1=10

y = signal.chirp(t,f0=f0,t1=t1, f1=f1,method="linear")

FONTSIZE = 18
LINEWIDTH = 1

plt.figure()
plt.plot(t, y, linewidth=LINEWIDTH)
plt.grid(True)
plt.xlabel("Time, s", fontsize=FONTSIZE)
plt.ylabel("Amplitude", fontsize=FONTSIZE)
plt.xticks(fontsize=FONTSIZE)
plt.yticks(fontsize=FONTSIZE)

フーリエ変換

図1に示した信号をFFT解析した結果を図2 に示す.
FFTにおいては時間情報が消え, 周波数と強度のみの関係が得られる.
図2より, 0と10Hz周辺の周波数が強く, それ以外の周波数が低くなっていることがわかる.

理想的には図3のような矩形状になることであるが, FFTは有限長の信号に対し処理を行うため今回のようにリップルと呼ばれるスペクトルリーケージ(漏れ)が生じる.

図2 FFTした結果

プログラム
"""


ここでは、チャープ信号をFFTする
"""

import numpy as np
import matplotlib.pyplot as plt
from scipy import signal

t1= 10
Ts = 0.01
fs = 1/Ts
t = np.arange(0,t1+Ts,Ts)

f0=1.00
f1=10

y = signal.chirp(t,f0=f0,t1=t1, f1=f1,method="linear")

Y = np.fft.rfft(y)                     #rは実数のみを表す
f_range = np.fft.rfftfreq(len(y), d=Ts)  # frequency axis
Amp = np.abs(Y)

FONTSIZE = 18
LINEWIDTH = 1

# ---- Fig1: time signal ----
plt.figure()
plt.plot(t, y, linewidth=LINEWIDTH)
plt.grid(True)
plt.xlabel("Time, s", fontsize=FONTSIZE)
plt.ylabel("Amplitude", fontsize=FONTSIZE)
plt.xticks(fontsize=FONTSIZE)
plt.yticks(fontsize=FONTSIZE)


plt.figure()
plt.plot(f_range, Amp, linewidth=LINEWIDTH)
plt.grid(True)
plt.xlabel("Frequency, Hz", fontsize=FONTSIZE)
plt.ylabel("Amplitude", fontsize=FONTSIZE)
plt.xticks(fontsize=FONTSIZE)
plt.yticks(fontsize=FONTSIZE)


plt.show()

図3 FFT結果とリップルについて

短時間フーリエ変換ー矩形窓を使った場合

図1のような信号に対して, 矩形窓を使った短時間フーリエ変換を行う.
シフト幅は1と固定し, 窓幅は256とする.

スペクトログラム

矩形窓を用いたSTFTによるスペクトログラムを図4に示す.
10Hzまで線形に周波数が上がっていることが確認できる.
しかしそれ以外でも縦方向に緑の線(-40dBくらい)も確認できる.

図4 チャープ信号を窓幅256の矩形窓でSTFTした結果のスペクトログラム

プログラム
"""

ここでは、チャープ信号を矩形窓を使って短時間フーリエ変換を行う
"""

import numpy as np
import matplotlib.pyplot as plt
from scipy import signal

t1= 10
Ts = 0.01
fs = 1/Ts
t = np.arange(0,t1+Ts,Ts)

f0=1.00
f1=10

y = signal.chirp(t,f0=f0,t1=t1, f1=f1,method="linear")

Y = np.fft.rfft(y)                   
f_range = np.fft.rfftfreq(len(y), d=Ts)  # frequency axis
Amp = np.abs(Y)


win_size = 256
inc = 1
nfft = 2048
noverlap = win_size-inc

# Rectangle window
win_rec = signal.windows.boxcar(win_size)
"""
detrend str or function or False, optional
Specifies how to detrend each segment. If detrend is a string, it is passed as the type argument to the detrend function. If it is a function, it takes a segment and returns a detrended segment. If detrend is False, no detrending is done. Defaults to False.

return_onesidedbool, optional
If True, return a one-sided spectrum for real data. If False return a two-sided spectrum. Defaults to True, but for complex data, a two-sided spectrum is always returned.

boundarystr or None, optional
Specifies whether the input signal is extended at both ends, and how to generate the new values, in order to center the first windowed segment on the first input point. This has the benefit of enabling reconstruction of the first input point when the employed window function starts at zero. Valid options are ['even', 'odd', 'constant', 'zeros', None]. Defaults to ‘zeros’, for zero padding extension. I.e. [1, 2, 3, 4] is extended to [0, 1, 2, 3, 4, 0] for nperseg=3.

paddedbool, optional
Specifies whether the input signal is zero-padded at the end to make the signal fit exactly into an integer number of window segments, so that all of the signal is included in the output. Defaults to True. Padding occurs after boundary extension, if boundary is not None, and padded is True, as is the default.


"""
f,tt,zxx = signal.stft(y,fs=fs,window=win_rec,nperseg=win_size,noverlap=noverlap,nfft=nfft,boundary=None)



FONTSIZE = 18
LINEWIDTH = 1

# ---- Fig1: time signal ----
plt.figure()
plt.plot(t, y, linewidth=LINEWIDTH)
plt.grid(True)
plt.xlabel("Time, s", fontsize=FONTSIZE)
plt.ylabel("Amplitude", fontsize=FONTSIZE)
plt.xticks(fontsize=FONTSIZE)
plt.yticks(fontsize=FONTSIZE)


plt.figure()
plt.plot(f_range, Amp, linewidth=LINEWIDTH)
plt.grid(True)
plt.xlabel("Frequency, Hz", fontsize=FONTSIZE)
plt.ylabel("Amplitude", fontsize=FONTSIZE)
plt.xticks(fontsize=FONTSIZE)
plt.yticks(fontsize=FONTSIZE)


# ---- Fig3: Spectrogram (Rectangle) ----
plt.figure()
plt.pcolormesh(tt, f, 20 * np.log10(np.abs(zxx) + 1e-12), shading="auto")
cb = plt.colorbar()
cb.set_label("Magnitude, dB", fontsize=FONTSIZE)
plt.xlabel("Time, s", fontsize=FONTSIZE)
plt.ylabel("Frequency, Hz", fontsize=FONTSIZE)
plt.xticks(fontsize=FONTSIZE)
plt.yticks(fontsize=FONTSIZE)

plt.show()

振動周波数の出力

図4において, 各時刻における最大エネルギーを示す周波数が, その時刻における振動周波数になる.
図4のスペクトログラムは時間と周波数とエネルギーの3次元のデータである.
そこから時間軸方向にそって, 最もエネルギーの大きい周波数を選びプロットした図が図5である.

図5 窓幅256の矩形窓を用いた場合の振動周波数とチャープ信号の比較

プログラム
"""
ここでは、STFT結果から振動周波数を得る
"""

import numpy as np
import matplotlib.pyplot as plt
from scipy import signal

t1= 10
Ts = 0.01
fs = 1/Ts
t = np.arange(0,t1+Ts,Ts)
t_ref = np.arange(0,10,0.01)
y_ref = 9/10*t_ref+1
f0=1.00
f1=10

y = signal.chirp(t,f0=f0,t1=t1, f1=f1,method="linear")

Y = np.fft.rfft(y)                     
f_range = np.fft.rfftfreq(len(y), d=Ts)  
Amp = np.abs(Y)


win_size = 256
inc = 1
nfft = 2048
noverlap = win_size-inc

# Rectangle window
win_rec = signal.windows.boxcar(win_size)

f,tt,zxx = signal.stft(y,fs=fs,window=win_rec,nperseg=win_size,noverlap=noverlap,nfft=nfft,boundary=None)
"""zxx.shape = (周波数ビン数, 時間フレーム数)"""
P_rec = np.abs(zxx) ** 2
kmax_rec = np.argmax(P_rec, axis=0)   # 各時間フレームごとに 最大周波数ビンを求めている
f_peak_rec = f[kmax_rec]


FONTSIZE = 18
LINEWIDTH = 1

plt.figure()
plt.plot(tt,f_peak_rec,label="peak frequency")
plt.plot(t_ref,y_ref,label="chirp")
plt.grid()
plt.legend()
plt.xlabel("Time, s",fontsize=FONTSIZE)
plt.ylabel("Frequency, Hz",fontsize=FONTSIZE)
plt.xticks(fontsize=FONTSIZE)
plt.yticks(fontsize=FONTSIZE)



plt.show()

短時間フーリエ変換ーブラックマン窓を使った場合

次に矩形窓の代わりに, ブラックマン窓を用いた場合のスペクトログラムと振動周波数の比較結果を次に示す.

プログラム

プログラムとしては、

win_rec = signal.windows.boxcar(win_size)

win_rec = signal.windows.blackman(win_size)

と変えるだけ.

スペクトログラム

条件は窓の種類以外は矩形窓と同様である.
窓幅は256, シフト幅は1でSTFTを行う.STFTの結果得られるスペクトログラムを図6に示す.
矩形窓を用いたず4と比較すると, 緑っぽい線(-40dB)が少なくなり, 青い線(-150dB)が多くなったことが確認できる.この違いについては次説の「窓関数による違い」で述べる.

図7 チャープ信号を窓幅256のブラックマン窓でSTFTした結果のスペクトログラム

振動周波数の出力

図7の結果に対し, 振動周波数とチャープ信号を比較した結果を図8に示す.
矩形窓を用いた場合の図5と比較するとチャープ信号をよりよく表現できていることがわかる.

図8 窓幅256のブラックマン窓を用いた場合の振動周波数とチャープ信号の比較

窓関数による違いは?

窓関数による違いを考察する.矩形窓からブラックマン窓に変えたことで, スペクトログラムは図4から図7に変わった.図4では高周波帯域に-20dB程度の周波数が確認できるが, 図7ではその傾向がなくなり, -150dBほどまで下がっている.

この差は,窓関数の違いによるものである. 矩形窓を用いて信号を切り出す場合,左右の境界部分に段差成分が発生することで,切り出す前の信号には含まれない高周波な周波数成分が含まれることがある. 図4における高周波帯域の周波数が, それに該当する. この問題は,矩形波をフーリエ変換したsinc関数にある. sinc関数は低周波帯域で振幅が高くなり(メインローブ),高周波帯域では振幅が小さくなり徐々に減衰していく(サイドローブ). そのため,矩形窓を使った場合, 元の信号を遮断しているため, 高周波な偽の振幅が現れる

また, 矩形窓を使ったスペクトログラムは, ブラックマン窓を用いたスペクトログラムと比べて,周波数
分解能が高い. これは, 図9に示すように, メインローブが矩形窓のほうが短いためより鋭く分解することができる.

図9 窓による違い

さらに,スペクトルの時間・周波数・振幅の関係を直感的に理解するため,図10, 図11に矩形窓およびブラックマン窓を用いた3次元スペクトログラムを示す.3次元表示では,矩形窓において, 高周波帯域に振幅成分(サイドローブ)が広がっている様子や,ブラックマン窓においてサイドローブが抑圧されている様子が,2次元表示よりも明瞭に確認できる(窓幅が小さい方が違いが明瞭であるため, 窓幅64の結果も図12, 図13に示す.).
まとめると,矩形窓は周波数を鋭く分解するのに適しているが, 窓端の急峻な不連続性によりサイドローブが大きく,本来の信号には含まれない高周波成分がスペクトログラム上に現れやすいという欠点を持つ.一方でブラックマン窓はメインローブが広いため周波数分解能は低下するものの,サイドローブを大幅に抑圧し,スペクトルリークを大きく低減できるという利点がある.このトレードオフ関係を理解した上で,解析目的に応じて両者を適切に使い分ける必要がある.

図10 窓による違い(窓幅256の矩形窓を用いた3次元スペクトログラム)
図11 窓による違い(窓幅256のブラックマン窓を用いた3次元スペクトログラム)

図12 窓による違い(窓幅64の矩形窓を用いた3次元スペクトログラム)

図13 窓による違い(窓幅64のブラックマン窓を用いた3次元スペクトログラム)

窓幅の違いは?

今までは, 窓幅を256としてSTFTを行った. 違いを検証するために, 矩形窓とブラックマン窓の双方で, 窓幅を256の4分の1である64とした場合のスペクトログラムを図14と図15にそれぞれ示す.
加えて, チャープ信号と振動周波数の比較をそれぞれ図16, 図17に示す.
ブラックマン窓を用いた場合の図7と図8を比較すると, 線形に増加する振幅の幅が広くなり, 周波数分解能が下がったことが読み取れる. これは,短い区間でフーリエ変換しているため,長い周期の信号は読み取れなくなったためである.
(仮に5秒の周期の信号があったとき, 4秒までを区切りフーリエ変換すると, その信号を読み取れない. 一方で6秒までを区切りフーリエ変換すると, その信号は読み取れる. したがって, 時間方向のデータは多いほうが, 周波数分解能は上がる.)

一方で,ブラックマン窓を用いた振動周波数の比較結果である,図8, 図17を比べると, 信号の最初と最後の周波数を計算できていることがわかる. これは, 窓が狭くなったため, 信号を切り出す回数が増え,より細かくFFTができるようになったためである.

図14 チャープ信号を窓幅64の矩形窓でSTFTした結果のスペクトログラム
図15 チャープ信号を窓幅64のブラックマン窓でSTFTした結果のスペクトログラム
図16 窓幅64の矩形窓を用いた場合の振動周波数とチャープ信号の比較

図17 窓幅64のブラックマン窓を用いた場合の振動周波数とチャープ信号の比較

周波数と窓長の関係

今までは, 1Hzから10Hzまで10秒で線形に増加する信号を見てきた.
この変調周波数を, 10Hzから50Hzに増加させてSTFTを行う.

窓幅256点のブラックマン窓を用いてSTFTを行った. スペクトログラムの結果を図18に示す.
変調周波数が10Hzのときの図7と比較すると, 周波数のピークがぼやけており,視覚的には周波数分解能が下がっているように見える.
そこで窓幅を4分の1にした結果を図19に示す. 窓幅を小さくしたことで, 1フレーム内での周波数の変動が低下し,グラフ内が視覚的に見えやすくなったことがわかる.

図18 変調周波数が50Hzのときのスペクトログラム(ブラックマン窓使用/窓幅264)

図19 変調周波数が50Hzのときのスペクトログラム(ブラックマン窓使用/窓幅64)

一般に, 周波数が大きい場合は窓の幅を大きくし, 周波数が小さい場合は窓の幅も小さくするほうが, 周波数分解能, 時間分解能の両方で優れる.
窓の大きさを周波数によって変える手法がウェーブレット変換であるが, これはまた次の機会に.

コメント

タイトルとURLをコピーしました