はじめに
心電図(ECG)や脳波(EEG)、加速度センサなどの計測では、50Hz(日本・欧州)や60Hz(米国)の電源ノイズが信号に混入する問題が頻繁に発生します。このノイズは特定の周波数に集中しているため、単純なローパスフィルタでは対処できないことがあります。たとえば心電図のR波は高周波成分を含むため、ローパスフィルタでノイズを除去しようとすると信号自体も劣化してしまいます。
**ノッチフィルタ(Notch Filter)**は、特定の周波数帯域のみを選択的に除去し、それ以外の周波数成分をそのまま通過させるフィルタです。本記事では、IIR型ノッチフィルタの理論的背景を解説し、SciPyを使ったPython実装で電源ノイズの除去を実践します。
フィルタの基礎知識についてはFIRフィルタとIIRフィルタの比較、周波数解析の基礎については高速フーリエ変換(FFT)の仕組みとPython実装を参照してください。
ノッチフィルタの基本概念
ノッチフィルタは**帯域除去フィルタ(Band-Reject Filter)**の極端なケースで、非常に狭い周波数帯域だけを除去します。周波数応答は対象周波数において急峻な「ノッチ(切り込み)」を持ち、それ以外の帯域ではほぼ平坦なゲインを維持します。
ノッチフィルタの設計において重要なパラメータは以下の2つです。
- ノッチ周波数 \(f_0\): 除去対象の中心周波数(例: 50Hz、60Hz)
- 品質係数 \(Q\): ノッチの鋭さを制御するパラメータ
\(Q\) はノッチ周波数と帯域幅の比で定義されます。
\[Q = \frac{f_0}{\Delta f} \tag{1}\]ここで \(\Delta f\) は \(-3\) dB帯域幅です。\(Q\) が大きいほどノッチが狭くなり、対象周波数のみをピンポイントで除去できます。
IIR型ノッチフィルタの伝達関数
2次IIR型ノッチフィルタの伝達関数は以下のように表されます。
\[H(z) = \frac{1 - 2\cos(\omega_0)z^{-1} + z^{-2}}{1 - 2r\cos(\omega_0)z^{-1} + r^2 z^{-2}} \tag{2}\]ここで \(\omega_0 = 2\pi f_0 / f_s\) は正規化角周波数、\(r\) は極の半径(\(0 < r < 1\))です。
分子(零点)の役割
分子の零点は単位円上の \(z = e^{\pm j\omega_0}\) に配置されます。零点が単位円上にあるため、周波数 \(\omega_0\) における伝達関数のゲインは正確にゼロになります。これがノッチフィルタの完全な除去特性の由来です。
分母(極)の役割
分母の極は \(z = r \cdot e^{\pm j\omega_0}\) に位置し、単位円の内側(半径 \(r < 1\))に配置されます。極は零点と同じ角度に配置されますが、半径が \(r\) であるため単位円から離れています。\(r\) が1に近いほど極が零点に近づき、ノッチの影響範囲が狭くなります(高い \(Q\))。
零点・極の配置と品質係数
極の半径 \(r\) と品質係数 \(Q\) の関係は近似的に以下のように表されます。
\[Q \approx \frac{\omega_0}{2(1-r)} \tag{3}\]したがって帯域幅は次のようになります。
\[\Delta f = \frac{f_0}{Q} \approx \frac{2(1-r) \cdot f_s}{2\pi} \tag{4}\]以下のコードで、零点・極の配置を可視化します。
import numpy as np
import matplotlib.pyplot as plt
from scipy import signal
# --- パラメータ ---
fs = 1000 # サンプリング周波数 [Hz]
f0 = 50 # ノッチ周波数 [Hz]
Q = 30 # 品質係数
# --- ノッチフィルタの設計 ---
b, a = signal.iirnotch(f0, Q, fs)
# --- 零点・極の計算 ---
zeros, poles, _ = signal.tf2zpk(b, a)
# --- 零点・極配置図のプロット ---
fig, ax = plt.subplots(figsize=(6, 6))
# 単位円を描画
theta = np.linspace(0, 2 * np.pi, 200)
ax.plot(np.cos(theta), np.sin(theta), 'k-', linewidth=0.5)
# 零点と極をプロット
ax.scatter(zeros.real, zeros.imag, marker='o', s=100,
facecolors='none', edgecolors='blue', linewidths=2, label='Zeros')
ax.scatter(poles.real, poles.imag, marker='x', s=100,
color='red', linewidths=2, label='Poles')
ax.set_xlabel('Real')
ax.set_ylabel('Imaginary')
ax.set_title(f'Pole-Zero Plot (f0={f0} Hz, Q={Q})')
ax.set_aspect('equal')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# 極の半径を表示
print(f"極の半径 r = {np.abs(poles[0]):.6f}")
print(f"零点の半径 = {np.abs(zeros[0]):.6f}")
このプロットでは、零点(○)が単位円上に、極(×)が単位円の内側に配置されていることが確認できます。両者がほぼ同じ角度にあるため、ノッチ周波数以外のゲインはほぼ1(0 dB)に保たれます。
scipy.signal.iirnotch による設計
SciPyの scipy.signal.iirnotch を使えば、ノッチフィルタの設計が簡潔に行えます。
from scipy.signal import iirnotch
# パラメータ
f0 = 50 # ノッチ周波数 [Hz]
Q = 30 # 品質係数
fs = 1000 # サンプリング周波数 [Hz]
# フィルタ係数の計算
b, a = iirnotch(f0, Q, fs)
f0: 除去したい周波数 [Hz]Q: 品質係数(大きいほど狭いノッチ)fs: サンプリング周波数 [Hz]- 戻り値: 分子係数
b、分母係数a(2次IIRフィルタ)
以下のコードで、異なる \(Q\) 値における周波数応答を比較します。
import numpy as np
import matplotlib.pyplot as plt
from scipy import signal
fs = 1000
f0 = 50
fig, ax = plt.subplots(figsize=(10, 6))
for Q in [5, 15, 30, 60]:
b, a = signal.iirnotch(f0, Q, fs)
w, h = signal.freqz(b, a, worN=4096, fs=fs)
ax.plot(w, 20 * np.log10(np.maximum(np.abs(h), 1e-12)),
label=f'Q = {Q}')
ax.set_xlabel('Frequency [Hz]')
ax.set_ylabel('Magnitude [dB]')
ax.set_title('Notch Filter Frequency Response (f0 = 50 Hz)')
ax.set_xlim(0, 200)
ax.set_ylim(-60, 5)
ax.axvline(f0, color='gray', linestyle='--', alpha=0.5, label=f'f0 = {f0} Hz')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
\(Q\) が大きいほどノッチが狭く急峻になり、対象周波数のみをピンポイントで除去できることがわかります。
実践:電源ノイズ除去のPython実装
実際の電源ノイズ除去を想定した完全なコード例を示します。50Hzの基本波に加え、100Hz・150Hzの高調波も同時に除去します。
import numpy as np
import matplotlib.pyplot as plt
from scipy import signal
# --- テスト信号の生成 ---
np.random.seed(42)
fs = 1000 # サンプリング周波数 [Hz]
T = 2.0 # 信号長 [s]
t = np.arange(0, T, 1/fs)
N = len(t)
# 有用信号: 10Hzと30Hzの正弦波
useful_signal = np.sin(2 * np.pi * 10 * t) + 0.5 * np.sin(2 * np.pi * 30 * t)
# 電源ノイズ: 50Hz基本波 + 100Hz, 150Hzの高調波
power_noise = (0.8 * np.sin(2 * np.pi * 50 * t)
+ 0.4 * np.sin(2 * np.pi * 100 * t)
+ 0.2 * np.sin(2 * np.pi * 150 * t))
# ランダムノイズ
random_noise = 0.3 * np.random.randn(N)
# 観測信号
observed = useful_signal + power_noise + random_noise
# --- カスケード接続のノッチフィルタ設計 ---
Q = 30
notch_freqs = [50, 100, 150] # 基本波 + 高調波
# 各ノッチ周波数のフィルタ係数をSOS形式で結合
sos_list = []
for f_notch in notch_freqs:
b, a = signal.iirnotch(f_notch, Q, fs)
sos = signal.tf2sos(b, a)
sos_list.append(sos[0])
sos_cascade = np.array(sos_list)
# --- フィルタ適用(ゼロ位相フィルタリング) ---
filtered = signal.sosfiltfilt(sos_cascade, observed)
# --- 周波数スペクトルの計算 ---
freqs = np.fft.rfftfreq(N, 1/fs)
spectrum_before = np.abs(np.fft.rfft(observed)) / N
spectrum_after = np.abs(np.fft.rfft(filtered)) / N
# --- プロット ---
fig, axes = plt.subplots(3, 1, figsize=(10, 10))
# (a) 時間領域: フィルタ適用前
axes[0].plot(t, observed, alpha=0.7, label='Observed')
axes[0].plot(t, useful_signal, 'k--', alpha=0.5, label='True signal')
axes[0].set_xlabel('Time [s]')
axes[0].set_ylabel('Amplitude')
axes[0].set_title('Before Notch Filtering')
axes[0].set_xlim(0, 0.5)
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# (b) 時間領域: フィルタ適用後
axes[1].plot(t, filtered, alpha=0.7, label='Filtered')
axes[1].plot(t, useful_signal, 'k--', alpha=0.5, label='True signal')
axes[1].set_xlabel('Time [s]')
axes[1].set_ylabel('Amplitude')
axes[1].set_title('After Notch Filtering (50, 100, 150 Hz)')
axes[1].set_xlim(0, 0.5)
axes[1].legend()
axes[1].grid(True, alpha=0.3)
# (c) 周波数領域: 前後比較
axes[2].plot(freqs, 20 * np.log10(np.maximum(spectrum_before, 1e-12)),
alpha=0.7, label='Before')
axes[2].plot(freqs, 20 * np.log10(np.maximum(spectrum_after, 1e-12)),
alpha=0.7, label='After')
for f_notch in notch_freqs:
axes[2].axvline(f_notch, color='red', linestyle='--', alpha=0.3)
axes[2].set_xlabel('Frequency [Hz]')
axes[2].set_ylabel('Magnitude [dB]')
axes[2].set_title('Frequency Spectrum: Before vs After')
axes[2].set_xlim(0, 200)
axes[2].legend()
axes[2].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
時間領域では、フィルタ適用後の信号が元の有用信号(10Hz + 30Hz)に近づいていることが確認できます。周波数領域では、50Hz・100Hz・150Hzの各ピークが除去されている一方、10Hzと30Hzの成分はそのまま保存されています。
設計上の注意点
品質係数Qの選択
- Q が大きすぎる場合: ノッチが極端に狭くなり、わずかな周波数変動にも対応できません。また、過渡応答でリンギングが生じやすくなります。
- Q が小さすぎる場合: ノッチが広くなり、対象周波数付近の有用な信号成分まで除去してしまいます。
実用的には \(Q = 20 \sim 50\) の範囲が電源ノイズ除去に適しています。
ゼロ位相フィルタリングと因果フィルタリング
- オフライン処理:
scipy.signal.sosfiltfilt(ゼロ位相)を使用します。順方向・逆方向の2回フィルタリングにより位相歪みがゼロになります。 - リアルタイム処理:
scipy.signal.sosfilt(因果フィルタ)を使用します。未来のデータを参照できないため位相遅れが生じますが、逐次処理が可能です。
安定性
IIR型ノッチフィルタは、極の半径 \(r < 1\) であれば常に安定です。scipy.signal.iirnotch で設計したフィルタは自動的にこの条件を満たすため、安定性を個別に検証する必要はありません。
ノッチフィルタ vs バンドストップフィルタ
ノッチフィルタは帯域除去フィルタの特殊なケースですが、両者は使い分けが必要です。
| 特性 | ノッチフィルタ | バンドストップフィルタ |
|---|---|---|
| 除去帯域 | 非常に狭い(1つの周波数) | 比較的広い帯域 |
| 次数 | 2次で実現可能 | 高次数が必要な場合あり |
| 用途 | 電源ノイズ、特定の干渉信号除去 | 周波数帯域全体の除去 |
| 設計 | iirnotch | butter + btype='bandstop' |
以下に両者の周波数応答を比較するコードを示します。
import numpy as np
import matplotlib.pyplot as plt
from scipy import signal
fs = 1000
f0 = 50
# ノッチフィルタ(Q=30)
b_notch, a_notch = signal.iirnotch(f0, Q=30, fs=fs)
w_notch, h_notch = signal.freqz(b_notch, a_notch, worN=4096, fs=fs)
# バンドストップフィルタ(バターワース4次、40-60Hz除去)
sos_bs = signal.butter(4, [40, 60], btype='bandstop', fs=fs, output='sos')
w_bs, h_bs = signal.sosfreqz(sos_bs, worN=4096, fs=fs)
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(w_notch, 20 * np.log10(np.maximum(np.abs(h_notch), 1e-12)),
label='Notch (Q=30)')
ax.plot(w_bs, 20 * np.log10(np.maximum(np.abs(h_bs), 1e-12)),
label='Bandstop Butterworth (40-60 Hz)')
ax.set_xlabel('Frequency [Hz]')
ax.set_ylabel('Magnitude [dB]')
ax.set_title('Notch Filter vs Bandstop Filter')
ax.set_xlim(0, 200)
ax.set_ylim(-60, 5)
ax.axvline(f0, color='gray', linestyle='--', alpha=0.5)
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
ノッチフィルタは50Hzのみをピンポイントで除去するのに対し、バンドストップフィルタは40〜60Hzの帯域全体を抑制します。除去したい周波数が明確な場合はノッチフィルタ、ある帯域を広く除去したい場合はバンドストップフィルタが適しています。
まとめ
- ノッチフィルタは特定の周波数のみを除去する帯域除去フィルタの特殊なケースで、電源ノイズ除去に最適である
- 2次IIR型の伝達関数は、単位円上の零点でノッチ周波数を完全に除去し、内側の極でノッチ幅を制御する
- 品質係数 \(Q\) が大きいほどノッチが狭くなり、\(Q = 20 \sim 50\) が電源ノイズ除去の実用的な範囲である
scipy.signal.iirnotchで簡潔に設計でき、高調波(100Hz、150Hz等)にはカスケード接続で対応できる- オフライン処理では
sosfiltfilt(ゼロ位相)、リアルタイム処理ではsosfilt(因果フィルタ)を使い分ける
関連記事
- 高速フーリエ変換(FFT)の仕組みとPython実装 - ノッチフィルタの効果をFFTで確認するための基礎を解説しています。
- FIRフィルタとIIRフィルタの比較 - ノッチフィルタが属するIIRフィルタの特性を解説しています。
- バターワースフィルタの設計とPython実装 - ローパス/ハイパス/バンドパスフィルタの設計手法を解説しています。
- 指数移動平均(EMA)フィルタの周波数特性 - フィルタの伝達関数と周波数応答の基礎を解説しています。
- ローパスフィルタの設計と比較 - 各種ローパスフィルタの周波数応答を比較しています。
- 窓関数とパワースペクトル密度(PSD)の理論とPython実装 - ノッチフィルタ適用前後のPSD解析に有用な手法を解説しています。
参考文献
- Oppenheim, A. V., & Schafer, R. W. (2009). Discrete-Time Signal Processing (3rd ed.). Prentice Hall.
- Haykin, S., & Van Veen, B. (2002). Signals and Systems (2nd ed.). Wiley.
- SciPy signal.iirnotch documentation
- SciPy Signal Processing documentation