FFTの振幅が合わない原因と補正方法:Hann窓・片側スペクトル・ゼロパディングをPythonで検証

FFTのピークが元信号の振幅と一致しない原因を切り分けます。窓のコヒーレントゲイン補正、片側スペクトルのDC・ナイキスト例外、奇数長FFT、ゼロパディング時の正規化をNumPyとSciPyの実行コードで検証します。

振幅2の波なのに、FFTのピークが2にならない

計測波形にHann窓を掛けてFFTを描いたところ、ピークが半分になった。ゼロパディングすると、さらに小さくなった。このような症状では、信号の異常を疑う前に正規化・片側化・窓補正を確認します。

この記事では「孤立した正弦波のピーク振幅を、元の物理単位で読む」処理に絞ります。入力が電圧なら縦軸もVです。FFTのアルゴリズムや窓の比較は、 FFTの基礎 と 窓関数・PSD推定 を参照してください。

最初に振幅の意味を固定します。\(x[n]=A\cos(2\pi f_0 n/f_s+\phi)\) の \(A\) はピーク振幅です。ナイキスト周波数より十分低い通常の正弦波のRMS値は \(A/\sqrt{2}\) 、ピーク間振幅は \(2A\) 。以下のコードはRMSやPSDを返しません。

何を補正するのか

観測サンプル数を \(N\) 、ゼロパディング後のFFT長を \(M\geq N\) 、窓を \(w[n]\) とします。NumPyの norm="backward" は順変換をスケーリングしないため、計算するのは次の和です。

\[ X_w[k]=\sum_{n=0}^{N-1}x[n]w[n]e^{-j2\pi kn/M}. \]

矩形窓で、周波数が \(k_0f_s/N\) に一致する正弦波を解析すると、通常の正周波数ビンには振幅の半分に対応する \(NA/2\) が現れます。負周波数側にも共役の成分があるためです。片側表示では、両側の寄与を合わせて \(2|X[k_0]|/N\) と読みます。

窓を掛けると、ビン中心の成分は窓の平均値に応じて弱まります。この平均値がコヒーレントゲインです。

\[ G_c=\frac{1}{N}\sum_{n=0}^{N-1}w[n],\qquad \widehat A[k]=\frac{2|X_w[k]|}{NG_c}=\frac{2|X_w[k]|}{\sum_n w[n]}. \]

これは孤立したビン中心の正弦波を想定する補正です。正負の窓付きローブが重なるDC・ナイキスト近傍や、近接した別の信号がある場合、常に正確な振幅になるわけではありません。

Hann窓を機械的に「2倍」しない

scipy.signal.windows.hann(N, sym=False) はスペクトル解析向けの周期窓で、\(N>1\) なら総和は \(N/2\) です。sym=True はフィルタ設計向けの対称窓で、同じ長さでも総和が異なります。

窓の名前から係数を決め打ちするより、実際に使った窓の総和を計算する方が確実です。SciPy公式の Hann窓の仕様 でも、sym による用途の違いが示されています。

DC・ナイキスト・奇数長FFTの例外

rfft は実数入力の非負周波数側だけを返します。DCは負側の対になる別ビンを持たないので、2倍しません。

ビン片側振幅への係数理由
DC(\(k=0\) )1単独の成分
通常の正周波数2負周波数の共役成分をまとめる
\(M\) が偶数の最終ビン1\(f_s/2\) のナイキスト成分
\(M\) が奇数の最終ビン2\(f_s/2\) 未満の通常の正周波数

判定に使うのは**元のサンプル数 \(N\) ではなくFFT長 \(M\) **です。例えば奇数長の記録を偶数長にパディングすれば、出力の最後はナイキストビンになります。 NumPyの rfft 仕様 にも偶数・奇数長の区別があります。

ナイキストの検証には \(A(-1)^n\) を使います。\(f_s/2\) の正弦波は位相によってサンプル値が変わり、位相0のsinはすべて0になるため、標本から任意位相の連続波の振幅を復元することはできません。

再利用できる振幅スペクトル関数

次のコードはPython、NumPy、SciPy、Matplotlibを使います。2つのコードブロックを順に同じファイルへ貼り付けると、検証と図の保存まで実行できます。

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


def amplitude_spectrum(x, fs, window=None, nfft=None):
    x = np.asarray(x)
    if x.ndim != 1 or x.size < 2 or np.iscomplexobj(x):
        raise ValueError("x must be a real one-dimensional record")
    x = x.astype(float)
    n = len(x)
    if not np.isfinite(fs) or fs <= 0 or not np.all(np.isfinite(x)):
        raise ValueError("finite samples and positive fs are required")
    m = n if nfft is None else int(nfft)
    if m < n or (nfft is not None and m != nfft):
        raise ValueError("nfft must be an integer >= len(x)")
    w = np.ones(n) if window is None else np.asarray(window, dtype=float)
    if w.shape != x.shape or not np.all(np.isfinite(w)) or w.sum() <= 0:
        raise ValueError("window must match x and have a positive sum")
    z = np.fft.rfft(x * w, n=m, norm="backward")
    a = np.abs(z) / w.sum()
    if m % 2 == 0:
        a[1:-1] *= 2
    else:
        a[1:] *= 2
    f = np.fft.rfftfreq(m, d=1 / fs)
    return f, a


fs, amplitude = 1024.0, 2.0
for n in (1024, 1023):
    k = 37
    t = np.arange(n) / fs
    x = amplitude * np.cos(2 * np.pi * (k * fs / n) * t + 0.3)
    for w in (np.ones(n), hann(n, sym=False)):
        for m in (n, 4 * n):
            f, a = amplitude_spectrum(x, fs, w, m)
            np.testing.assert_allclose(a[k * (m // n)], amplitude, atol=1e-10)
    # DC is not doubled, for either record length.
    _, dc = amplitude_spectrum(np.full(n, 1.25), fs)
    np.testing.assert_allclose(dc[0], 1.25, atol=1e-12)
    print(f"N={n}: rectangular/Hann and padded amplitude checks passed")

# An even FFT has a Nyquist bin; an odd FFT has an ordinary final bin.
n = 1024
_, a = amplitude_spectrum(0.7 * (-1.0) ** np.arange(n), fs)
np.testing.assert_allclose(a[-1], 0.7, atol=1e-12)
n = 1023
k = n // 2
_, a = amplitude_spectrum(0.7 * np.cos(2 * np.pi * k * np.arange(n) / n), fs)
np.testing.assert_allclose(a[-1], 0.7, atol=1e-10)

テストでは、矩形窓と周期Hann窓、偶数・奇数のサンプル数、4倍パディングを組み合わせても、選んだビン中心の波のピークが2になることを確認します。最後の2件は、偶数FFTのナイキストを2倍しないことと、奇数FFTの最終ビンを2倍することの検証です。

この関数はDCを自動で除去しません。平均を引く場合は x - x.mean() を窓を掛ける前に渡し、DC値を測定する用途と区別してください。低周波信号では平均除去自体が推定に影響するので、常に前処理する規則にはしません。

ゼロパディングとビン間のピーク

\(N=256\) 、\(f_s=256\) Hzの1秒分の記録では、パディングなしの周波数間隔は1 Hzです。10.5 Hzの正弦波は10 Hzと11 Hzの間にあり、どちらのビンもピーク中心を通りません。

コヒーレントゲインを補正しても、このスキャロッピング損失は残ります。FFT長を2048へ増やすと周波数の表示間隔は0.125 Hzになり、10.5 Hzを直接評価できます。

# Off-bin sinusoid: compare native and padded frequency grids.
n, fs, f0, amplitude = 256, 256.0, 10.5, 2.0
t = np.arange(n) / fs
x = amplitude * np.cos(2 * np.pi * f0 * t + 0.3)
w = hann(n, sym=False)
fig, ax = plt.subplots(figsize=(8, 4.5))
for m, label, marker in ((n, "256-point FFT", "o"), (8 * n, "2048-point FFT", None)):
    f, a = amplitude_spectrum(x, fs, w, m)
    ax.plot(f, a, marker=marker, label=label)
    band = (f > 5) & (f < 16)
    idx = np.flatnonzero(band)[np.argmax(a[band])]
    print(f"M={m}: peak={a[idx]:.6f} at {f[idx]:.3f} Hz")
ax.axhline(amplitude, color="black", linestyle="--", label="True peak amplitude = 2")
ax.set(xlim=(6, 15), ylim=(0, 2.2), xlabel="Frequency [Hz]", ylabel="Peak amplitude",
       title="Same 1-second record, different FFT grids (periodic Hann)")
ax.grid(alpha=0.3)
ax.legend()
fig.tight_layout()
fig.savefig("amplitude_padding.png", dpi=170)
plt.close(fig)

同じ観測波形を256点と2048点のFFTで評価したHann窓付き振幅スペクトル。粗いビンはピーク中心を外し、ゼロパディングは同じローブを細かく描く。

この例ではパディング前の最大値は約1.698、後は約2.000になります。元の記録を増やしたのではなく、同じ窓付き信号のスペクトルを細かい周波数点で評価した結果です。任意の信号周波数がパディング後のビン中心に一致するわけではありません。

分母を m や「パディング後の長さ×元窓の平均値」に置き換えると、8倍パディングでは振幅を8分の1にしてしまいます。分母は元の256サンプルに掛けた窓の総和のままです。

表示間隔と物理的な分離能力を分ける

  • FFTの周波数点の間隔は \(f_s/M\) 。
  • 観測時間は \(T=N/f_s\) で、パディングしても変わりません。
  • 近接した周波数を見分ける能力は、観測時間・窓のローブ幅・信号強度・雑音などに依存します。

ゼロパディングはピークの読み取りを助けますが、近接した2つの波を必ず分離できるようにはしません。分離が目的なら、まず長い記録を取得できるか検討します。パディング前からのビン中心条件と、パディング後の表示ビンが細かいことは別です。

ピークを読んではいけないケース

症状・目的対応
孤立した正弦波、ビン中心窓の総和で補正し、片側化の例外を守る
孤立した正弦波、ビン間パディング・ピーク補間、必要なら周波数を推定して正弦波フィット
近接した複数の波単一ビンの最大値を各波の振幅と扱わず、観測時間と分離条件を確認
広帯域雑音振幅の最大値ではなくPSDと帯域積分を検討
信号が時間とともに変化全区間FFTのピークを一定振幅と解釈せず、区間解析を検討

PSDでは窓の二乗和による別の正規化を使います。ここでの窓の総和をそのまま流用しないでください。また、norm="forward" や "ortho" へ変更するとFFT自体の倍率が変わるため、このコードの分母をそのまま使えません。

よくある質問

FFTの振幅は何で割ればよいですか?

NumPyの標準正規化で実数信号を解析する場合、窓付きFFTの絶対値を元の窓の総和で割ります。片側表示ではDCと偶数FFT長のナイキストビンを除いて2倍します。孤立したビン中心の正弦波に対する振幅補正であり、任意のピークが正確になる保証はありません。

ゼロパディングで振幅補正の分母は変わりますか?

変わりません。分母は観測したサンプルに掛けた窓の総和です。ゼロを追加したFFT長で割ると、振幅が過小になります。

Hann窓で補正しても振幅が小さいのはなぜですか?

信号周波数がFFTビンの中心からずれると、ピークをビン間で取り逃すスキャロッピング損失が残ります。コヒーレントゲイン補正だけではこの損失や近接信号の干渉を取り除けません。

関連記事

参考文献