バンドパスフィルタの設計とPython実装

バンドパスフィルタの理論(通過帯域・阻止帯域・帯域幅・中心周波数)をZ変換と周波数応答から解説し、scipy.signalを用いたIIR/FIR設計とPythonによる可視化コードを紹介します。

バンドパスフィルタとは

バンドパスフィルタ(帯域通過フィルタ)は、特定の周波数帯域(通過帯域)の信号を通過させ、それ以外の周波数(阻止帯域)を減衰させるフィルタです。

ラジオの選局、音声処理の特定周波数強調、生体信号(心拍・脳波)のノイズ除去など、幅広い分野で活用されています。

主要パラメータ

パラメータ説明
\(f_l\)下側遮断周波数(-3dB点)
\(f_h\)上側遮断周波数(-3dB点)
\(f_0 = \sqrt{f_l f_h}\)中心周波数(幾何平均)
\(BW = f_h - f_l\)帯域幅(Bandwidth)
\(Q = f_0 / BW\)Q値(鋭さの指標)

Q値が大きいほど通過帯域が狭く(鋭い選択性)、小さいほど広帯域になります。

周波数応答の導出

ローパス→バンドパス変換

バンドパスフィルタは、ローパスフィルタ(LPF)のプロトタイプから周波数変換によって設計できます。

アナログ領域でのローパス→バンドパス変換は、複素変数 \(s\) を以下のように置き換えます。

\[ s \rightarrow \frac{s^2 + \omega_l \omega_h}{s(\omega_h - \omega_l)} \tag{1}\]

ここで \(\omega_l = 2\pi f_l\)、\(\omega_h = 2\pi f_h\) です。

1次のバターワースLPFの伝達関数 \(H_{LP}(s) = \frac{\omega_c}{s + \omega_c}\) に変換 \((1)\) を適用すると、2次のバンドパスフィルタが得られます。

\[ H\_{BP}(s) = \frac{BW \cdot s}{s^2 + BW \cdot s + \omega_0^2} \tag{2}\]

ここで \(BW = \omega_h - \omega_l\)、\(\omega_0 = \sqrt{\omega_l \omega_h}\) です。

離散時間バンドパスフィルタ(双一次変換)

式 \((2)\) を双一次変換(Bilinear Transform)でデジタル領域に変換します。

\[s = \frac{2}{T} \cdot \frac{1 - z^{-1}}{1 + z^{-1}} \tag{3}\]

これを式 \((2)\) に代入すると、デジタルバンドパスフィルタの伝達関数 \(H(z)\) が得られます。

\[ H(z) = \frac{b_0 + b_1 z^{-1} + b_2 z^{-2}}{1 + a_1 z^{-1} + a_2 z^{-2}} \tag{4}\]

実用上は scipy.signal がこの計算を自動的に行ってくれます。

Pythonによる実装

scipy.signalを用いたIIRバンドパスフィルタ

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

# --- パラメータ設定 ---
fs = 1000.0          # サンプリング周波数 [Hz]
f_low = 50.0         # 下側遮断周波数 [Hz]
f_high = 150.0       # 上側遮断周波数 [Hz]
order = 4            # フィルタ次数

# --- バターワース バンドパスフィルタの設計 ---
nyq = fs / 2.0       # ナイキスト周波数
low = f_low / nyq
high = f_high / nyq

b, a = signal.butter(order, [low, high], btype='bandpass')

# --- 周波数応答の計算 ---
w, h = signal.freqz(b, a, worN=8000, fs=fs)

# --- プロット ---
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8))

# ゲイン特性 (dB)
ax1.semilogx(w, 20 * np.log10(np.abs(h) + 1e-10))
ax1.axvline(f_low, color='r', linestyle='--', label=f'$f_l$ = {f_low} Hz')
ax1.axvline(f_high, color='g', linestyle='--', label=f'$f_h$ = {f_high} Hz')
ax1.axhline(-3, color='gray', linestyle=':', label='-3 dB')
ax1.set_xlabel('Frequency [Hz]')
ax1.set_ylabel('Gain [dB]')
ax1.set_title(f'Butterworth Bandpass Filter (order={order})')
ax1.legend()
ax1.grid(True, which='both', alpha=0.3)
ax1.set_xlim([1, nyq])
ax1.set_ylim([-80, 5])

# 位相特性
ax2.semilogx(w, np.angle(h, deg=True))
ax2.axvline(f_low, color='r', linestyle='--')
ax2.axvline(f_high, color='g', linestyle='--')
ax2.set_xlabel('Frequency [Hz]')
ax2.set_ylabel('Phase [degrees]')
ax2.set_title('Phase Response')
ax2.grid(True, which='both', alpha=0.3)
ax2.set_xlim([1, nyq])

plt.tight_layout()
plt.savefig('bandpass_response.png', dpi=150, bbox_inches='tight')
plt.show()

FIRバンドパスフィルタの設計

IIRフィルタは位相が非線形ですが、FIRフィルタは線形位相を実現できます。音声処理など位相の保存が重要な場面で有用です。

from scipy.signal import firwin, freqz

# --- FIR バンドパスフィルタ ---
numtaps = 101        # フィルタ係数の数(奇数推奨)
fir_bpf = firwin(
    numtaps,
    [f_low, f_high],
    pass_zero=False,  # バンドパス指定
    fs=fs,
    window='hamming'
)

w_fir, h_fir = freqz(fir_bpf, worN=8000, fs=fs)

# IIRとFIRの比較プロット
plt.figure(figsize=(10, 5))
plt.semilogx(w, 20 * np.log10(np.abs(h) + 1e-10), label='IIR Butterworth (order=4)', linewidth=2)
plt.semilogx(w_fir, 20 * np.log10(np.abs(h_fir) + 1e-10), label=f'FIR Hamming (taps={numtaps})', linewidth=2, linestyle='--')
plt.axvline(f_low, color='r', linestyle=':', alpha=0.7, label=f'$f_l$ = {f_low} Hz')
plt.axvline(f_high, color='g', linestyle=':', alpha=0.7, label=f'$f_h$ = {f_high} Hz')
plt.axhline(-3, color='gray', linestyle=':', alpha=0.7)
plt.xlabel('Frequency [Hz]')
plt.ylabel('Gain [dB]')
plt.title('Bandpass Filter Comparison: IIR vs FIR')
plt.legend()
plt.grid(True, which='both', alpha=0.3)
plt.xlim([1, nyq])
plt.ylim([-80, 5])
plt.tight_layout()
plt.show()

信号への適用例

実際のノイズ混じり信号にバンドパスフィルタを適用します。

# --- テスト信号の生成 ---
t = np.linspace(0, 1.0, int(fs), endpoint=False)  # 1秒分

# 目的信号: 100 Hz のサイン波
signal_pure = np.sin(2 * np.pi * 100 * t)

# ノイズ: 10 Hz の低周波 + 300 Hz の高周波 + ホワイトノイズ
noise = (
    0.5 * np.sin(2 * np.pi * 10 * t) +
    0.5 * np.sin(2 * np.pi * 300 * t) +
    0.3 * np.random.randn(len(t))
)

x_noisy = signal_pure + noise

# --- フィルタ適用 ---
# lfilter: 因果的フィルタ(リアルタイム処理向け、位相遅れあり)
y_lfilter = signal.lfilter(b, a, x_noisy)

# filtfilt: ゼロ位相フィルタ(オフライン処理向け、位相遅れなし)
y_filtfilt = signal.filtfilt(b, a, x_noisy)

# --- 比較プロット ---
fig, axes = plt.subplots(3, 1, figsize=(12, 8), sharex=True)

axes[0].plot(t[:200], x_noisy[:200], alpha=0.8, label='Noisy signal')
axes[0].set_ylabel('Amplitude')
axes[0].set_title('Input Signal (noisy)')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

axes[1].plot(t[:200], y_lfilter[:200], color='orange', label='lfilter (causal)')
axes[1].plot(t[:200], signal_pure[:200], color='gray', linestyle='--', alpha=0.6, label='True signal')
axes[1].set_ylabel('Amplitude')
axes[1].set_title('After lfilter (with phase delay)')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

axes[2].plot(t[:200], y_filtfilt[:200], color='green', label='filtfilt (zero-phase)')
axes[2].plot(t[:200], signal_pure[:200], color='gray', linestyle='--', alpha=0.6, label='True signal')
axes[2].set_ylabel('Amplitude')
axes[2].set_xlabel('Time [s]')
axes[2].set_title('After filtfilt (zero-phase)')
axes[2].legend()
axes[2].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('bandpass_filtering_result.png', dpi=150, bbox_inches='tight')
plt.show()

IIR vs FIR 設計の比較

特性IIR(バターワース等)FIR
位相特性非線形(位相歪みあり)線形位相が実現可能
フィルタ係数数少ない(\(2N+1\)程度)多い(数十〜数百)
計算コスト低い高い(タップ数依存)
安定性設計によっては不安定常に安定
遮断特性低次でも急峻タップ数増加で改善
遅延非定数定数遅延(\((N-1)/2\)サンプル)
適した用途リアルタイム処理音声・画像処理、位相重視

設計指針:

  • リアルタイムで計算コストを抑えたい → IIR(バターワース/チェビシェフ)
  • 位相の線形性が重要(音声・医療信号) → FIR(Hamming/Kaiser窓)
  • オフライン処理で位相遅れを完全除去 → filtfilt(IIR/FIR共通)

次数と帯域幅の影響

# 次数の違いによる周波数応答の比較
fig, ax = plt.subplots(figsize=(10, 6))

for ord_n in [2, 4, 6, 8]:
    b_n, a_n = signal.butter(ord_n, [low, high], btype='bandpass')
    w_n, h_n = signal.freqz(b_n, a_n, worN=8000, fs=fs)
    ax.semilogx(w_n, 20 * np.log10(np.abs(h_n) + 1e-10), label=f'order={ord_n}')

ax.axhline(-3, color='gray', linestyle=':', label='-3 dB')
ax.axvline(f_low, color='r', linestyle='--', alpha=0.5)
ax.axvline(f_high, color='g', linestyle='--', alpha=0.5)
ax.set_xlabel('Frequency [Hz]')
ax.set_ylabel('Gain [dB]')
ax.set_title('Effect of Filter Order on Bandpass Response')
ax.legend()
ax.grid(True, which='both', alpha=0.3)
ax.set_xlim([1, nyq])
ax.set_ylim([-80, 5])
plt.tight_layout()
plt.show()

次数を上げると遮断特性が急峻になりますが、IIRフィルタでは次数が高くなるほど位相遅れと数値的不安定性のリスクが増すため、次数8以下が実用的な目安です。

関連記事

参考文献


関連ツール