窓関数(ハニング・ハミング・ブラックマン)の定義と効果

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}$
  • メインローブ幅とサイドローブレベルにはトレードオフがある

次のステップとして、以下の記事も参考にしてください。