相補フィルタとは
相補フィルタ(Complementary Filter)は、特性の異なる2つのセンサの出力を周波数領域で最適に統合するフィルタです。一方のセンサにはハイパスフィルタを、もう一方にはローパスフィルタを適用し、両者の和が全帯域で1になるように設計されます。
最も典型的な応用はIMU(慣性計測装置)におけるジャイロスコープと加速度計のセンサフュージョンです。
| センサ | 長所 | 短所 |
|---|---|---|
| ジャイロスコープ | 短時間の角度変化に正確 | 長時間でドリフトが蓄積 |
| 加速度計 | 長時間で安定(重力基準) | 振動・加速度ノイズに敏感 |
ジャイロスコープは角速度を積分して角度を求めるため、バイアス誤差がドリフトとして蓄積します(低周波誤差)。一方、加速度計は重力ベクトルから傾斜角を算出できますが、振動や並進加速度の影響を受けやすい(高周波ノイズ)。
相補フィルタはこの特性の相補性を利用し、ジャイロからは高周波成分を、加速度計からは低周波成分を抽出して融合することで、両方の欠点を相殺します。
主な応用分野
- ドローン・マルチコプター: 姿勢推定(ピッチ・ロール角)
- ロボティクス: 二足歩行ロボットの姿勢安定化
- スマートフォン: 画面回転、AR/VRヘッドトラッキング
- 車両制御: 横滑り角の推定
理論
ハイパスフィルタとローパスフィルタの相補性
相補フィルタの核心は、ハイパスフィルタ \(H_{HPF}(z)\) とローパスフィルタ \(H_{LPF}(z)\) が以下の関係を満たすことです。
\[H_{HPF}(z) + H_{LPF}(z) = 1 \tag{1}\]この性質により、2つのセンサ信号を統合した結果が全帯域にわたって歪みなく再構成されます。
1次相補フィルタの伝達関数
1次ローパスフィルタの伝達関数を次のように定義します。
\[H_{LPF}(z) = \frac{(1-\alpha)}{1 - \alpha z^{-1}} \tag{2}\]相補条件 \((1)\) から、ハイパスフィルタは次のようになります。
\[H_{HPF}(z) = 1 - H_{LPF}(z) = \frac{1 - z^{-1}}{1 - \alpha z^{-1}} \tag{3}\]相補フィルタの出力は、ジャイロ出力 \(X_{gyro}(z)\) と加速度計出力 \(X_{accel}(z)\) に対して次のように表されます。
\[Y(z) = H_{HPF}(z) \cdot X_{gyro}(z) + H_{LPF}(z) \cdot X_{accel}(z) \tag{4}\]時間領域での更新式
式 \((4)\) を逆Z変換して時間領域に戻すと、以下の再帰的な更新式が得られます。
\[\theta[n] = \alpha \left(\theta[n-1] + \dot{\theta}_{gyro}[n] \cdot \Delta t\right) + (1 - \alpha) \cdot \theta_{accel}[n] \tag{5}\]ここで、
- \(\theta[n]\): 現在の推定角度
- \(\dot{\theta}_{gyro}[n]\): ジャイロスコープの角速度出力
- \(\theta_{accel}[n]\): 加速度計から算出した角度
- \(\Delta t\): サンプリング周期
- \(\alpha\): フィルタ係数(\(0 < \alpha < 1\))
式 \((5)\) は直感的に次のように解釈できます。
- 第1項: ジャイロの積分値に重み \(\alpha\) を乗じる(ハイパスフィルタ効果)
- 第2項: 加速度計の角度に重み \((1-\alpha)\) を乗じる(ローパスフィルタ効果)
時定数 \(\tau\) とフィルタ係数 \(\alpha\) の関係
パラメータ \(\alpha\) は時定数 \(\tau\) とサンプリング周期 \(\Delta t\) から決定されます。
\[\alpha = \frac{\tau}{\tau + \Delta t} \tag{6}\]時定数 \(\tau\) の物理的意味は次のとおりです。
- \(\tau\) が大きい(\(\alpha \to 1\)): ジャイロの寄与が大きい。短期応答に優れるが、ドリフト補正が遅い
- \(\tau\) が小さい(\(\alpha \to 0\)): 加速度計の寄与が大きい。ドリフト補正は速いが、振動ノイズに弱い
クロスオーバー周波数 \(f_c\)(LPFとHPFのゲインが等しくなる周波数)は次の関係にあります。
\[f_c = \frac{1}{2\pi\tau} \tag{7}\]周波数応答解析
相補フィルタの周波数応答を解析すると、LPFとHPFのゲインがクロスオーバー周波数 \(f_c\) で交差し、全帯域でゲインの合計が0 dBに保たれることを確認できます。
import numpy as np
import matplotlib.pyplot as plt
from scipy import signal
# --- パラメータ ---
fs = 100.0 # サンプリング周波数 [Hz]
dt = 1.0 / fs
tau = 1.0 # 時定数 [s]
alpha = tau / (tau + dt)
# --- 伝達関数の定義 ---
# LPF: H_LPF(z) = (1-alpha) / (1 - alpha*z^{-1})
b_lpf = [1 - alpha]
a_lpf = [1, -alpha]
# HPF: H_HPF(z) = (1 - z^{-1}) / (1 - alpha*z^{-1})
b_hpf = [1, -1]
a_hpf = [1, -alpha]
# --- 周波数応答 ---
w_lpf, h_lpf = signal.freqz(b_lpf, a_lpf, worN=4096, fs=fs)
w_hpf, h_hpf = signal.freqz(b_hpf, a_hpf, worN=4096, fs=fs)
# 合計の周波数応答
h_sum = h_lpf + h_hpf
fc = 1.0 / (2 * np.pi * tau) # クロスオーバー周波数
# --- プロット ---
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8))
# ゲイン特性
ax1.semilogx(w_lpf, 20 * np.log10(np.abs(h_lpf) + 1e-10),
label='LPF (Accelerometer)', linewidth=2)
ax1.semilogx(w_hpf, 20 * np.log10(np.abs(h_hpf) + 1e-10),
label='HPF (Gyroscope)', linewidth=2)
ax1.semilogx(w_lpf, 20 * np.log10(np.abs(h_sum) + 1e-10),
label='Sum (HPF + LPF)', linewidth=2, linestyle='--', color='black')
ax1.axvline(fc, color='r', linestyle=':', alpha=0.7,
label=f'$f_c$ = {fc:.3f} Hz')
ax1.axhline(-3, color='gray', linestyle=':', alpha=0.5, label='-3 dB')
ax1.set_xlabel('Frequency [Hz]')
ax1.set_ylabel('Gain [dB]')
ax1.set_title(f'Complementary Filter Frequency Response (τ={tau} s, α={alpha:.4f})')
ax1.legend()
ax1.grid(True, which='both', alpha=0.3)
ax1.set_xlim([0.001, fs / 2])
ax1.set_ylim([-40, 5])
# 位相特性
ax2.semilogx(w_lpf, np.angle(h_lpf, deg=True), label='LPF', linewidth=2)
ax2.semilogx(w_hpf, np.angle(h_hpf, deg=True), label='HPF', linewidth=2)
ax2.axvline(fc, color='r', linestyle=':', alpha=0.7)
ax2.set_xlabel('Frequency [Hz]')
ax2.set_ylabel('Phase [degrees]')
ax2.set_title('Phase Response')
ax2.legend()
ax2.grid(True, which='both', alpha=0.3)
ax2.set_xlim([0.001, fs / 2])
plt.tight_layout()
plt.savefig('complementary_frequency_response.png', dpi=150, bbox_inches='tight')
plt.show()
LPFとHPFのゲインがクロスオーバー周波数で \(-3\) dB に交差し、合計が全帯域で 0 dB(= 1)に保たれていることが確認できます。これが「相補(complementary)」の名前の由来です。
Pythonによる実装
IMUセンサのシミュレーション
実際のIMUセンサをシミュレーションし、相補フィルタの効果を検証します。
import numpy as np
import matplotlib.pyplot as plt
# --- シミュレーションパラメータ ---
fs = 100.0 # サンプリング周波数 [Hz]
dt = 1.0 / fs
T = 30.0 # シミュレーション時間 [s]
t = np.arange(0, T, dt)
N = len(t)
# --- 真の角度(テスト信号)---
# ゆっくりした正弦波運動 + ステップ変化
theta_true = (
15.0 * np.sin(2 * np.pi * 0.1 * t) + # 0.1 Hz の揺動
5.0 * np.sin(2 * np.pi * 0.5 * t) + # 0.5 Hz の揺動
10.0 * (t > 15) # t=15s でステップ変化
)
# --- 真の角速度 ---
omega_true = np.gradient(theta_true, dt)
# --- ジャイロスコープ出力(角速度 + バイアスドリフト + ノイズ)---
gyro_bias = 0.5 # バイアス [deg/s](ドリフトの原因)
gyro_noise_std = 2.0 # ノイズ標準偏差 [deg/s]
gyro_output = omega_true + gyro_bias + gyro_noise_std * np.random.randn(N)
# --- 加速度計出力(角度 + 振動ノイズ)---
accel_noise_std = 5.0 # ノイズ標準偏差 [deg]
accel_output = theta_true + accel_noise_std * np.random.randn(N)
# --- ジャイロのみによる角度推定(積分)---
theta_gyro = np.zeros(N)
theta_gyro[0] = theta_true[0]
for i in range(1, N):
theta_gyro[i] = theta_gyro[i - 1] + gyro_output[i] * dt
# --- 相補フィルタ ---
tau = 1.0 # 時定数 [s]
alpha = tau / (tau + dt)
theta_comp = np.zeros(N)
theta_comp[0] = theta_true[0]
for i in range(1, N):
# 式 (5) の実装
theta_comp[i] = alpha * (theta_comp[i - 1] + gyro_output[i] * dt) \
+ (1 - alpha) * accel_output[i]
# --- 比較プロット ---
fig, axes = plt.subplots(4, 1, figsize=(12, 14), sharex=True)
# 真の角度
axes[0].plot(t, theta_true, 'k-', linewidth=2, label='True angle')
axes[0].set_ylabel('Angle [deg]')
axes[0].set_title('True Angle')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# ジャイロのみ
axes[1].plot(t, theta_gyro, 'r-', alpha=0.8, label='Gyro only (integration)')
axes[1].plot(t, theta_true, 'k--', alpha=0.5, label='True')
axes[1].set_ylabel('Angle [deg]')
axes[1].set_title('Gyroscope Only (drift accumulation)')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
# 加速度計のみ
axes[2].plot(t, accel_output, 'b-', alpha=0.5, label='Accelerometer only')
axes[2].plot(t, theta_true, 'k--', alpha=0.5, label='True')
axes[2].set_ylabel('Angle [deg]')
axes[2].set_title('Accelerometer Only (noisy)')
axes[2].legend()
axes[2].grid(True, alpha=0.3)
# 相補フィルタ
axes[3].plot(t, theta_comp, 'g-', linewidth=2, label=f'Complementary (α={alpha:.3f})')
axes[3].plot(t, theta_true, 'k--', alpha=0.5, label='True')
axes[3].set_ylabel('Angle [deg]')
axes[3].set_xlabel('Time [s]')
axes[3].set_title('Complementary Filter')
axes[3].legend()
axes[3].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('complementary_filter_comparison.png', dpi=150, bbox_inches='tight')
plt.show()
# --- RMSE の計算 ---
rmse_gyro = np.sqrt(np.mean((theta_gyro - theta_true) ** 2))
rmse_accel = np.sqrt(np.mean((accel_output - theta_true) ** 2))
rmse_comp = np.sqrt(np.mean((theta_comp - theta_true) ** 2))
print(f"RMSE (Gyro only): {rmse_gyro:.2f} deg")
print(f"RMSE (Accelerometer only): {rmse_accel:.2f} deg")
print(f"RMSE (Complementary): {rmse_comp:.2f} deg")
ジャイロのみの推定はバイアスドリフトにより時間とともに真値から大きく乖離し、加速度計のみの推定は振動ノイズで大きく変動します。相補フィルタはこれら両方の問題を軽減し、真値に近い推定結果を得られることが確認できます。
パラメータ \(\alpha\) の影響
\(\alpha\)(時定数 \(\tau\))の値が推定精度に与える影響を分析します。
# --- 異なる α(τ)での比較 ---
tau_values = [0.1, 0.5, 1.0, 5.0, 10.0]
fig, axes = plt.subplots(2, 1, figsize=(12, 10))
for tau_val in tau_values:
alpha_val = tau_val / (tau_val + dt)
theta_est = np.zeros(N)
theta_est[0] = theta_true[0]
for i in range(1, N):
theta_est[i] = alpha_val * (theta_est[i - 1] + gyro_output[i] * dt) \
+ (1 - alpha_val) * accel_output[i]
rmse = np.sqrt(np.mean((theta_est - theta_true) ** 2))
axes[0].plot(t, theta_est, label=f'τ={tau_val}s (α={alpha_val:.4f}, RMSE={rmse:.2f}°)')
axes[0].plot(t, theta_true, 'k--', linewidth=2, alpha=0.7, label='True')
axes[0].set_ylabel('Angle [deg]')
axes[0].set_xlabel('Time [s]')
axes[0].set_title('Effect of Time Constant τ on Complementary Filter')
axes[0].legend(fontsize=8)
axes[0].grid(True, alpha=0.3)
# τ vs RMSE のプロット
tau_range = np.logspace(-2, 2, 100)
rmse_list = []
for tau_val in tau_range:
alpha_val = tau_val / (tau_val + dt)
theta_est = np.zeros(N)
theta_est[0] = theta_true[0]
for i in range(1, N):
theta_est[i] = alpha_val * (theta_est[i - 1] + gyro_output[i] * dt) \
+ (1 - alpha_val) * accel_output[i]
rmse_list.append(np.sqrt(np.mean((theta_est - theta_true) ** 2)))
axes[1].semilogx(tau_range, rmse_list, 'b-', linewidth=2)
axes[1].set_xlabel('Time Constant τ [s]')
axes[1].set_ylabel('RMSE [deg]')
axes[1].set_title('RMSE vs Time Constant τ')
axes[1].grid(True, which='both', alpha=0.3)
# 最適τの表示
optimal_idx = np.argmin(rmse_list)
optimal_tau = tau_range[optimal_idx]
optimal_rmse = rmse_list[optimal_idx]
axes[1].axvline(optimal_tau, color='r', linestyle='--',
label=f'Optimal τ={optimal_tau:.2f}s (RMSE={optimal_rmse:.2f}°)')
axes[1].legend()
plt.tight_layout()
plt.savefig('alpha_effect.png', dpi=150, bbox_inches='tight')
plt.show()
\(\alpha\) の選択指針:
| \(\tau\) の範囲 | \(\alpha\) の傾向 | 特性 |
|---|---|---|
| \(\tau < 0.5\)s | \(\alpha\) 小 | 加速度計重視。ノイズが多いが、ドリフト小 |
| \(0.5 \le \tau \le 2\)s | \(\alpha\) 中 | バランス型。多くの応用で適切 |
| \(\tau > 5\)s | \(\alpha\) 大 | ジャイロ重視。応答は良いが、ドリフト大 |
最適な \(\tau\) はセンサのノイズ特性に依存します。ジャイロのバイアスが大きい場合は \(\tau\) を小さく、加速度計のノイズが大きい場合は \(\tau\) を大きく設定します。
カルマンフィルタとの比較
相補フィルタとカルマンフィルタはどちらもセンサフュージョンに用いられますが、設計思想が異なります。
| 特性 | 相補フィルタ | カルマンフィルタ |
|---|---|---|
| パラメータ | \(\alpha\)(1つ) | \(Q, R, P\)(プロセス・観測ノイズ共分散) |
| 計算コスト | 非常に低い(加減乗のみ) | 中程度(行列演算) |
| 設計難易度 | 簡単 | モデルの定義が必要 |
| 最適性 | 最適ではない | 線形ガウスの仮定下で最適 |
| 適応性 | 固定パラメータ | ゲインが自動調整される |
| 非線形対応 | 対応不可 | EKF/UKFで対応可能 |
| リアルタイム性 | 極めて高い | 高い(組込み向け実装あり) |
| 実装行数 | 約5行 | 約30〜50行 |
選択の指針:
- 計算リソースが限られるマイコン(Arduino等)→ 相補フィルタ
- ノイズ特性が既知で最適推定が必要 → カルマンフィルタ
- プロトタイピングや教育目的 → 相補フィルタ(理解しやすい)
- 多センサ統合(GPS + IMU等) → カルマンフィルタ
実際には、1次の相補フィルタは定常カルマンフィルタの特殊ケースと見なすこともできます。カルマンフィルタのゲインが収束した定常状態では、更新式が式 \((5)\) と同形になります。
実用例
| 応用分野 | 統合センサ | 典型的な \(\tau\) | 備考 |
|---|---|---|---|
| ドローン姿勢制御 | ジャイロ + 加速度計 | 0.5〜2 s | ピッチ・ロール角の推定 |
| スマートフォン画面回転 | ジャイロ + 加速度計 | 0.2〜1 s | 素早い応答と安定性のバランス |
| 車両ヨーレート推定 | ジャイロ + GPS方位 | 1〜5 s | GPSの低サンプルレートを補完 |
| ロボット姿勢推定 | ジャイロ + 加速度計 + 磁気計 | 0.5〜2 s | 9軸IMUでヨー角も推定 |
| 歩行者自律航法(PDR) | ジャイロ + 加速度計 + 気圧計 | 1〜3 s | 屋内測位での姿勢推定 |
| VR/ARヘッドトラッキング | ジャイロ + 加速度計 + カメラ | 0.1〜0.5 s | 低遅延が重要 |
関連記事
- ローパスフィルタの設計と比較:移動平均・バターワース・チェビシェフ - 相補フィルタのLPF部分の理論的背景を解説しています。
- ハイパスフィルタの設計とPython実装 - 相補フィルタのHPF部分の理論的背景を解説しています。
- カルマンフィルタの理論とPython実装 - 相補フィルタの上位互換として位置づけられるカルマンフィルタを解説しています。
- 指数移動平均(EMA)フィルタの周波数特性 - 相補フィルタのLPF部分はEMAと等価であり、その周波数特性を解説しています。
- 適応フィルタ(LMS/RLS)の理論とPython実装 - 環境に応じてパラメータを自動調整する適応フィルタを解説しています。
参考文献
- Mahony, R., Hamel, T., & Pflimlin, J.-M. (2008). Nonlinear Complementary Filters on the Special Orthogonal Group. IEEE Transactions on Automatic Control, 53(5), 1203-1218.
- Higgins, W. T. (1975). A Comparison of Complementary and Kalman Filtering. IEEE Transactions on Aerospace and Electronic Systems, AES-11(3), 321-325.
- Arduino and MPU6050 Accelerometer and Gyroscope Tutorial
関連ツール
- DevToolBox - 開発者向け無料ツール集 - JSON整形、正規表現テスターなど85種類以上の開発者向けツール
- CalcBox - 暮らしの計算ツール - 統計計算、周波数変換など61種類以上の計算ツール