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

FFTで有限長の信号を周波数解析するとき、信号を有限区間で切り取る操作は矩形窓を掛けることと等価です。この切り取りにより、本来存在しない周波数成分が現れるスペクトルリーケージ(spectral leakage)が発生します。窓関数(window function)は、このリーケージを抑制するためのテクニックです。

窓関数の選択は、周波数分析の精度に直接影響します。メインローブの幅とサイドローブの大きさにはトレードオフがあり、解析目的に応じた窓関数を選ぶ必要があります。

本記事の内容

  • スペクトルリーケージの発生メカニズム
  • 矩形窓・ハニング窓・ハミング窓・ブラックマン窓の定義
  • メインローブとサイドローブの特性比較
  • Pythonでの窓関数の効果の可視化

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

スペクトルリーケージとは

スペクトルリーケージの発生概念

まず全体像をつかみましょう。図のように、無限に続く信号(左上)を有限区間で切り取る(右上)操作は、矩形窓を掛けることに相当します。その結果、理想なら1本の鋭い線になるはずのスペクトル(左下)が、周囲に漏れ広がってしまいます(右下)。この漏れこそがスペクトルリーケージです。なぜ漏れるのか、その正体を順に見ていきましょう。

有限長信号と矩形窓

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関数に似た形)であり、大きなサイドローブを持ちます。この畳み込みにより、元の信号には存在しない周波数にエネルギーが「漏れ出す」のがスペクトルリーケージです。

矩形窓のディリクレ核

矩形窓のスペクトルを実際に描くと図の通りです。中央の高いメインローブの脇に、$-13\,\text{dB}$ という比較的大きなサイドローブが連なっています。畳み込みのとき、このサイドローブを通じて本来ない周波数へエネルギーが漏れます。サイドローブが高い窓ほどリーケージが大きい——これが窓選びの出発点です。

特に、信号の周波数が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}$ と大きく減衰しますが、メインローブは最も広くなります。

窓関数の時間波形

4つの窓の「形」を見比べましょう。矩形窓(灰)は両端で急にスパッと切れますが、ハニング・ハミング・ブラックマンは両端を滑らかにゼロへ近づけています。この両端の滑らかさこそがリーケージ抑制の鍵です。急な切れ目(不連続)が高い周波数成分を生み、それが漏れの原因になるためです。

各窓の係数にも意味があります。ハニング窓 $0.5 – 0.5\cos(\cdot)$ の係数は、両端をちょうどゼロにするよう選ばれています($\cos$ が両端で $1$ になるため)。一方ハミング窓 $0.54 – 0.46\cos(\cdot)$ は、あえて両端を完全にはゼロにしない($0.08$ 残す)代わりに、第1サイドローブを打ち消すよう係数を最適化したものです。「わずかな不連続を許す代わりにサイドローブを下げる」この設計が、ハミング窓の個性です。ブラックマン窓はさらに2倍角の $\cos$ 項を加えることで、サイドローブをいっそう急峻に下げています。

メインローブとサイドローブ

メインローブとサイドローブ

窓の性能は、スペクトルの2つの部分で測ります。図のように、中央の大きな山がメインローブ(幅が狭いほど周波数分解能が高い)、その脇に連なる小さな山がサイドローブ(低いほどリーケージが小さい)です。良い窓とは、この2つをバランス良く抑えた窓のことです。

窓関数のスペクトル特性は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}$

窓関数の周波数応答(dB)

4窓の周波数応答を重ねると、トレードオフが一目でわかります。矩形窓(灰)はメインローブが狭い代わりにサイドローブが高く、ブラックマン窓(緑)はサイドローブが低い代わりにメインローブが広い。ハニング・ハミングはその中間にあります。

メインローブ幅とサイドローブのトレードオフ

このトレードオフを1枚にまとめたのが上の図です。左上(矩形窓)に行くほど分解能は高いがリーケージが大きく、右下(ブラックマン窓)に行くほどリーケージは小さいがメインローブが広く分解能が落ちます。「周波数分解能」と「リーケージ抑制」は両立できないため、何を優先するかで窓を選ぶことになります。

数学的背景:窓関数と畳み込み

時間乗算は周波数畳み込み

窓関数がなぜ効くのかは、「時間領域の乗算=周波数領域の畳み込み」という関係で理解できます。図のように、信号に窓を掛けると、そのスペクトルは窓のスペクトル $W(f)$ と畳み込まれます。$W(f)$ が鋭い(理想的にはデルタ関数)ほど元のスペクトルを忠実に保てるので、窓を設計するとは「なるべく鋭い $W(f)$ を作る」営みなのだ、と捉えられます。

窓関数の効果を数学的に整理します。時間領域での乗算は周波数領域での畳み込みに対応します:

$$ 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$ でブラックマン窓になります。

ちなみに矩形窓のサイドローブが $-13\,\text{dB}$ になるのは、そのスペクトルが $\text{sinc}$ 関数($\sin(\pi f)/(\pi f)$)の形をとるためです。sinc関数の第1サイドローブのピークは主極のおよそ $0.217$ 倍で、これをデシベルに直すと $20\log_{10}(0.217) \approx -13\,\text{dB}$ になります。窓のサイドローブ特性は、このように窓の形のフーリエ変換から数学的に決まるのです。

実用上の評価指標

メインローブ幅とサイドローブのほかに、現場では次の指標もよく使われます。

指標 意味
等価雑音帯域幅(ENBW) 窓を通したとき雑音をどれだけ通すかを、等価な矩形帯域の幅で表したもの。スペクトルから雑音電力を正しく見積もるのに必要
コヒーレントゲイン 窓を掛けると信号振幅が目減りする(両端をゼロにするため)。その目減りを補正する係数。例えばハニング窓は平均 0.5 なので振幅が半分になる
スキャロップロス 信号周波数がビンの中間に来たとき、ピーク値がどれだけ下がるか。メインローブが平坦な窓ほど小さい

スペクトルから「正しい振幅・電力」を読み取りたいときは、これらの補正が欠かせません。窓を掛けたまま生の値を読むと、振幅を過小評価してしまうので注意が必要です。

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()

スペクトルリーケージ比較

ビンの中間にある周波数(リーケージが最大になる条件)で比べると、窓の差が歴然です。矩形窓は裾が高く漏れが大きいのに対し、ブラックマン窓は裾がぐっと下がり、漏れが抑えられています。弱い信号を拾いたいとき、この裾の低さが効いてきます。

近接2周波数の分離能力

逆に、近接した2つの周波数(100 Hz と 108 Hz)を分離する能力では結果が逆転します。メインローブの狭い矩形窓は2つの山をはっきり分離できますが、メインローブの広いブラックマン窓では2つがつぶれて1つに見えてしまいます。「リーケージに強い窓」と「分解能の高い窓」は別物だと、ここでも確認できます。

可視化から、以下のことが確認できます:

  • 矩形窓はメインローブが最も狭いが、サイドローブが大きくリーケージが目立つ
  • ブラックマン窓はサイドローブが最も小さいが、メインローブが広く周波数分解能が低い
  • ハニング窓・ハミング窓はバランスの取れた特性を持つ

窓関数の選び方

窓関数の選び方

結局どれを選べばよいのか、用途別の指針が図です。近接した周波数を分けたいなら矩形窓、汎用的な解析ならハニング窓、弱い信号を拾いたいならブラックマン窓——「何を優先するか」で最適な窓が決まります。万能の窓は存在せず、目的に合わせて選ぶのが正解です。

用途に応じた窓関数の選択指針をまとめます。

用途 推奨窓関数 理由
汎用的なスペクトル解析 ハニング窓 バランスが良い
近接周波数の分離 矩形窓 メインローブが最も狭い
弱い信号の検出 ブラックマン窓 サイドローブが最も低い
音声処理 ハミング窓 不連続のないスペクトル

短時間フーリエ変換と窓のオーバーラップ

実際の信号解析では、長い信号を短い区間に区切って次々とFFTする短時間フーリエ変換(STFT)がよく使われます。このとき各区間に窓を掛けますが、窓は両端をゼロにするため、区間の境目付近の情報が弱くなってしまいます。

そこで区間をオーバーラップ(重ね合わせ)させ、ある区間で弱まる部分を隣の区間が補います。ハニング窓は 50% オーバーラップでちょうど全体が一定の重みになる(足し合わせると平坦になる)性質があり、STFTの定番として使われます。窓関数は単独で使うだけでなく、こうした時間–周波数解析の土台にもなっているのです。

まとめ

本記事では、窓関数の定義とスペクトルリーケージへの効果を解説しました。

  • スペクトルリーケージ: 有限長切り取り(矩形窓)により本来ない周波数成分が現れる
  • ハニング窓: $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}$
  • メインローブ幅とサイドローブレベルにはトレードオフがある

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