FFTで有限長の信号を周波数解析するとき、信号を有限区間で切り取る操作は矩形窓を掛けることと等価です。この切り取りにより、本来存在しない周波数成分が現れるスペクトルリーケージ(spectral leakage)が発生します。窓関数(window function)は、このリーケージを抑制するためのテクニックです。
窓関数の選択は、周波数分析の精度に直接影響します。メインローブの幅とサイドローブの大きさにはトレードオフがあり、解析目的に応じた窓関数を選ぶ必要があります。
本記事の内容
- スペクトルリーケージの発生メカニズム
- 矩形窓・ハニング窓・ハミング窓・ブラックマン窓の定義
- メインローブとサイドローブの特性比較
- Pythonでの窓関数の効果の可視化
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
スペクトルリーケージとは
有限長信号と矩形窓
FFTは有限長 $N$ 点の信号を入力とします。無限に続く信号から有限長を切り出す操作は、矩形窓関数 $w_{\text{rect}}[n]$ を掛けることに等しいです:
$$ w_{\text{rect}}[n] = \begin{cases} 1 & (0 \leq n \leq N-1) \\ 0 & (\text{otherwise}) \end{cases} $$
切り出された信号 $x_w[n] = x[n] \cdot w[n]$ のスペクトルは、元の信号のスペクトル $X(f)$ と窓関数のスペクトル $W(f)$ の畳み込みになります:
$$ X_w(f) = X(f) * W(f) $$
リーケージの発生
矩形窓のフーリエ変換はディリクレ核(sinc関数に似た形)であり、大きなサイドローブを持ちます。この畳み込みにより、元の信号には存在しない周波数にエネルギーが「漏れ出す」のがスペクトルリーケージです。
特に、信号の周波数がFFTの周波数ビン($k \cdot f_s / N$, $k = 0, 1, \dots$)にちょうど乗らない場合にリーケージが顕著になります。
窓関数の定義
窓関数は、信号の両端を滑らかにゼロに近づけることでリーケージを抑制します。以下に代表的な窓関数を定義します。$n = 0, 1, \dots, N-1$ とします。
矩形窓(Rectangular window)
$$ \boxed{w_{\text{rect}}[n] = 1} $$
窓を掛けないことと同義です。メインローブは最も狭いですが、サイドローブが大きく($-13\,\text{dB}$)リーケージが顕著です。
ハニング窓(Hann window)
$$ \boxed{w_{\text{hann}}[n] = 0.5 – 0.5 \cos\left(\frac{2\pi n}{N-1}\right)} $$
余弦関数を用いて両端を滑らかにゼロにします。サイドローブは $-31\,\text{dB}$ まで減衰し、汎用的な窓関数として最も広く使われます。名前はオーストリアの気象学者ユリウス・フォン・ハンに由来します。
ハミング窓(Hamming window)
$$ \boxed{w_{\text{hamming}}[n] = 0.54 – 0.46 \cos\left(\frac{2\pi n}{N-1}\right)} $$
ハニング窓の係数を調整し、第1サイドローブを最小化したものです。サイドローブは $-43\,\text{dB}$ まで減衰します。ただし、両端は完全にはゼロになりません($w[0] = w[N-1] = 0.08$)。
ブラックマン窓(Blackman window)
$$ \boxed{w_{\text{blackman}}[n] = 0.42 – 0.5 \cos\left(\frac{2\pi n}{N-1}\right) + 0.08 \cos\left(\frac{4\pi n}{N-1}\right)} $$
3項の余弦和で構成されます。サイドローブは $-58\,\text{dB}$ と大きく減衰しますが、メインローブは最も広くなります。
メインローブとサイドローブ
窓関数のスペクトル特性は2つの指標で評価されます:
| 特性 | 意味 |
|---|---|
| メインローブ幅 | 周波数分解能に対応。狭いほど近接した2つの周波数を区別しやすい |
| サイドローブレベル | リーケージの大きさ。低いほどリーケージが少ない |
これらにはトレードオフがあります:
| 窓関数 | メインローブ幅(ビン数) | 最大サイドローブ | サイドローブ減衰率 |
|---|---|---|---|
| 矩形窓 | 2 | $-13\,\text{dB}$ | $-6\,\text{dB/oct}$ |
| ハニング窓 | 4 | $-31\,\text{dB}$ | $-18\,\text{dB/oct}$ |
| ハミング窓 | 4 | $-43\,\text{dB}$ | $-6\,\text{dB/oct}$ |
| ブラックマン窓 | 6 | $-58\,\text{dB}$ | $-18\,\text{dB/oct}$ |
数学的背景:窓関数と畳み込み
窓関数の効果を数学的に整理します。時間領域での乗算は周波数領域での畳み込みに対応します:
$$ x_w[n] = x[n] \cdot w[n] \quad \longleftrightarrow \quad X_w(f) = X(f) * W(f) $$
理想的な窓関数のスペクトル $W(f)$ はデルタ関数 $\delta(f)$ であるべきですが、有限長の窓では不可能です。現実にはメインローブの幅とサイドローブの高さを調整することで、用途に合った特性を実現します。
一般的な余弦窓は以下の形で統一的に表せます:
$$ w[n] = \sum_{p=0}^{P} (-1)^p \alpha_p \cos\left(\frac{2\pi p n}{N-1}\right) $$
$P = 0$ で矩形窓、$P = 1$ でハニング窓・ハミング窓、$P = 2$ でブラックマン窓になります。
Pythonでの可視化
import numpy as np
import matplotlib.pyplot as plt
N = 64 # 窓の長さ
n = np.arange(N)
# 窓関数の定義
windows = {
'Rectangular': np.ones(N),
'Hann': 0.5 - 0.5 * np.cos(2 * np.pi * n / (N - 1)),
'Hamming': 0.54 - 0.46 * np.cos(2 * np.pi * n / (N - 1)),
'Blackman': 0.42 - 0.5 * np.cos(2 * np.pi * n / (N - 1))
+ 0.08 * np.cos(4 * np.pi * n / (N - 1)),
}
colors = ['gray', 'blue', 'red', 'green']
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# (1) 窓関数の時間波形
for (name, w), c in zip(windows.items(), colors):
axes[0, 0].plot(n, w, lw=2, label=name, color=c)
axes[0, 0].set_xlabel('Sample $n$')
axes[0, 0].set_ylabel('$w[n]$')
axes[0, 0].set_title('Window functions (time domain)')
axes[0, 0].legend()
axes[0, 0].grid(True, alpha=0.3)
# (2) 窓関数の周波数応答(dB)
N_fft = 4096
for (name, w), c in zip(windows.items(), colors):
W = np.fft.fft(w, N_fft)
W_shifted = np.fft.fftshift(W)
W_dB = 20 * np.log10(np.abs(W_shifted) / np.max(np.abs(W_shifted)) + 1e-15)
freq = np.arange(N_fft) - N_fft // 2
axes[0, 1].plot(freq[:N_fft//8+1], W_dB[N_fft//2:N_fft//2+N_fft//8+1],
lw=1.5, label=name, color=c)
axes[0, 1].set_xlabel('Frequency bin')
axes[0, 1].set_ylabel('Magnitude [dB]')
axes[0, 1].set_title('Frequency response of window functions')
axes[0, 1].set_ylim(-120, 5)
axes[0, 1].legend()
axes[0, 1].grid(True, alpha=0.3)
# (3) スペクトルリーケージの比較
fs = 1000 # サンプリング周波数
N_sig = 256
t = np.arange(N_sig) / fs
f_signal = 50.5 * fs / N_sig # ビンの中間の周波数(リーケージが最大)
x = np.sin(2 * np.pi * f_signal * t)
for (name, _), c in zip(windows.items(), colors):
if name == 'Rectangular':
w_sig = np.ones(N_sig)
elif name == 'Hann':
w_sig = 0.5 - 0.5 * np.cos(2 * np.pi * np.arange(N_sig) / (N_sig - 1))
elif name == 'Hamming':
w_sig = 0.54 - 0.46 * np.cos(2 * np.pi * np.arange(N_sig) / (N_sig - 1))
else:
w_sig = (0.42 - 0.5 * np.cos(2 * np.pi * np.arange(N_sig) / (N_sig - 1))
+ 0.08 * np.cos(4 * np.pi * np.arange(N_sig) / (N_sig - 1)))
X = np.fft.fft(x * w_sig, 4 * N_sig)
X_dB = 20 * np.log10(np.abs(X[:2*N_sig]) / np.max(np.abs(X)) + 1e-15)
freq_axis = np.arange(2 * N_sig) * fs / (4 * N_sig)
axes[1, 0].plot(freq_axis, X_dB, lw=1.2, label=name, color=c, alpha=0.8)
axes[1, 0].set_xlabel('Frequency [Hz]')
axes[1, 0].set_ylabel('Magnitude [dB]')
axes[1, 0].set_title(f'Spectral leakage (f = {f_signal:.1f} Hz)')
axes[1, 0].set_xlim(0, fs / 4)
axes[1, 0].set_ylim(-100, 5)
axes[1, 0].legend()
axes[1, 0].grid(True, alpha=0.3)
# (4) 2つの近接した周波数の分離能力
f1, f2 = 100, 108 # 近接した2周波数
x2 = np.sin(2 * np.pi * f1 * t) + 0.5 * np.sin(2 * np.pi * f2 * t)
for (name, _), c in zip(windows.items(), colors):
if name == 'Rectangular':
w_sig = np.ones(N_sig)
elif name == 'Hann':
w_sig = 0.5 - 0.5 * np.cos(2 * np.pi * np.arange(N_sig) / (N_sig - 1))
elif name == 'Hamming':
w_sig = 0.54 - 0.46 * np.cos(2 * np.pi * np.arange(N_sig) / (N_sig - 1))
else:
w_sig = (0.42 - 0.5 * np.cos(2 * np.pi * np.arange(N_sig) / (N_sig - 1))
+ 0.08 * np.cos(4 * np.pi * np.arange(N_sig) / (N_sig - 1)))
X2 = np.fft.fft(x2 * w_sig, 4 * N_sig)
X2_dB = 20 * np.log10(np.abs(X2[:2*N_sig]) / np.max(np.abs(X2)) + 1e-15)
freq_axis = np.arange(2 * N_sig) * fs / (4 * N_sig)
axes[1, 1].plot(freq_axis, X2_dB, lw=1.2, label=name, color=c, alpha=0.8)
axes[1, 1].set_xlabel('Frequency [Hz]')
axes[1, 1].set_ylabel('Magnitude [dB]')
axes[1, 1].set_title(f'Frequency resolution ({f1} Hz + {f2} Hz)')
axes[1, 1].set_xlim(50, 170)
axes[1, 1].set_ylim(-80, 5)
axes[1, 1].axvline(f1, color='k', ls=':', alpha=0.3)
axes[1, 1].axvline(f2, color='k', ls=':', alpha=0.3)
axes[1, 1].legend()
axes[1, 1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
可視化から、以下のことが確認できます:
- 矩形窓はメインローブが最も狭いが、サイドローブが大きくリーケージが目立つ
- ブラックマン窓はサイドローブが最も小さいが、メインローブが広く周波数分解能が低い
- ハニング窓・ハミング窓はバランスの取れた特性を持つ
窓関数の選び方
用途に応じた窓関数の選択指針をまとめます。
| 用途 | 推奨窓関数 | 理由 |
|---|---|---|
| 汎用的なスペクトル解析 | ハニング窓 | バランスが良い |
| 近接周波数の分離 | 矩形窓 | メインローブが最も狭い |
| 弱い信号の検出 | ブラックマン窓 | サイドローブが最も低い |
| 音声処理 | ハミング窓 | 不連続のないスペクトル |
まとめ
本記事では、窓関数の定義とスペクトルリーケージへの効果を解説しました。
- スペクトルリーケージ: 有限長切り取り(矩形窓)により本来ない周波数成分が現れる
- ハニング窓: $0.5 – 0.5\cos(2\pi n/(N-1))$、汎用的
- ハミング窓: $0.54 – 0.46\cos(2\pi n/(N-1))$、第1サイドローブ最小
- ブラックマン窓: 3項余弦和、サイドローブ $-58\,\text{dB}$
- メインローブ幅とサイドローブレベルにはトレードオフがある
次のステップとして、以下の記事も参考にしてください。