相補フィルタの理論とPython実装:ジャイロスコープと加速度計のセンサフュージョン

相補フィルタの理論をZ変換・周波数応答から解説し、IMU(ジャイロ+加速度計)のセンサフュージョンにおけるPython実装を紹介します。

相補フィルタとは

相補フィルタ(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 sGPSの低サンプルレートを補完
ロボット姿勢推定ジャイロ + 加速度計 + 磁気計0.5〜2 s9軸IMUでヨー角も推定
歩行者自律航法(PDR)ジャイロ + 加速度計 + 気圧計1〜3 s屋内測位での姿勢推定
VR/ARヘッドトラッキングジャイロ + 加速度計 + カメラ0.1〜0.5 s低遅延が重要

関連記事

参考文献

  • 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

関連ツール