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}$ |

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つの周波数(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}$
- メインローブ幅とサイドローブレベルにはトレードオフがある
次のステップとして、以下の記事も参考にしてください。