SatooRu's Profile

全ての記事梶研の進捗日常の記録制作物一覧

梶研 [カルマンフィルタ序章]

2023年10月3日

thumbnail

カルマンフィルタ序章

出席率

  • 3年セミナー:??%

スケジュール

短期的な予定

  • 端末姿勢推定
    • データを収集
    • グラフを作成
    • 静止状態の判別
    • 端末の姿勢を推定
    • 加速度を世界座標に変換
    • カルマンフィルタを用いた推定

長期的な予定

  • 9月中旬まで 端末姿勢推定(加速度, 角速度)
  • 10月中 端末姿勢推定(カルマンフィルタ)
  • 11月中 Kotlin(とRust)の勉強 「いつでもセンシングアプリ」

進捗報告

データをとる

回転を滑らかにしたデータを取る

机に置く(z軸正が上)縦向きに立てる(y軸正が上)横向きに立てる(x軸正が上)机に置く(z軸正が上)

元データ

加速度

acc.png (73.9 kB) norm.png (68.0 kB)

白: 静止時
灰: 動作時

角速度, 角度

gyro.png (45.5 kB) rotate.png (56.2 kB)

姿勢推定結果

pos.png (49.3 kB)

うまくいっているように見えるが、角速度のメリットが分かりづらい

加速度だけを使用した姿勢推定

pos_from_acc.png (61.5 kB)

世界座標系に変換した加速度

word_coordinates.png (50.5 kB)

加速度を大きくする

小走りしながらスマホを立てて戻す

加速度だけを使用した姿勢推定

run_accpos.png (67.0 kB)

相補フィルターを用いた姿勢推定

run_pos.png (43.5 kB)

加速度だけではノイズが大きいが、
相補フィルターではノイズを抑えることができている

カルマンフィルタ

もっと調べた

Youtube

ChatGPT に生成させたサンプルコード

生成したコード
1import numpy as np 2import pandas as pd 3 4# カルマンフィルタの初期化 5def initialize_filter(): 6 # 初期状態推定値 (クォータニオン) 7 x = np.array([1.0, 0.0, 0.0, 0.0]) # 初期の姿勢は単位クォータニオン(姿勢なし) 8 9 # 初期推定誤差共分散行列 10 P = np.eye(4) 11 12 # プロセスノイズの共分散行列 (システムノイズ) 13 Q = np.eye(4) * 0.01 # 適切な値を設定する必要があります 14 15 # 観測ノイズの共分散行列 16 R = np.eye(3) * 0.1 # 適切な値を設定する必要があります 17 18 return x, P, Q, R 19 20# クォータニオンを角速度に変換する関数 21def quaternion_to_rotation_rate(q, gyro_data): 22 q_conj = np.array([q[0], -q[1], -q[2], -q[3]]) 23 omega = 2.0 * R.from_quat(q_conj).inv().as_rotvec() * gyro_data 24 return omega 25 26# カルマンフィルタの更新ステップ 27def update_filter(x, P, Q, R, accel_data, gyro_data, dt): 28 # 予測ステップ 29 A = np.eye(4) + 0.5 * dt * np.array([ 30 [0, -gyro_data[0], -gyro_data[1], -gyro_data[2]], 31 [gyro_data[0], 0, gyro_data[2], -gyro_data[1]], 32 [gyro_data[1], -gyro_data[2], 0, gyro_data[0]], 33 [gyro_data[2], gyro_data[1], -gyro_data[0], 0] 34 ]) 35 x_pred = A.dot(x) 36 P_pred = A.dot(P).dot(A.T) + Q 37 38 # カルマンゲインの計算 39 H = np.eye(3) 40 K = P_pred.dot(H.T).dot(np.linalg.inv(H.dot(P_pred).dot(H.T) + R)) 41 42 # 角速度をクォータニオンに変換 43 omega = quaternion_to_rotation_rate(x, gyro_data) 44 45 # 観測更新 46 x_updated = x_pred + K.dot(accel_data - H.dot(x_pred)) 47 P_updated = (np.eye(4) - K.dot(H)).dot(P_pred) 48 49 return x_updated, P_updated 50 51# メインプログラム 52if __name__ == "__main__": 53 # データをDataFrameから読み込む (仮のデータ) 54 data = pd.read_csv('sensor_data.csv') # ファイル名は実際のファイルに合わせて変更 55 56 # カルマンフィルタの初期化 57 x, P, Q, R = initialize_filter() 58 59 # サンプリング時間 60 dt = data['timestamp'].diff().mean() 61 62 # データフレームをイテレートし、カルマンフィルタを更新 63 for index, row in data.iterrows(): 64 accel_data = np.array([row['accel_x'], row['accel_y'], row['accel_z']]) 65 gyro_data = np.array([row['gyro_x'], row['gyro_y'], row['gyro_z']]) 66 x, P = update_filter(x, P, Q, R, accel_data, gyro_data, dt) 67 68 # 推定された姿勢をクォータニオンからオイラー角に変換 69 euler_angles = R.from_quat(x).as_euler('xyz') 70 71 print(f"推定されたロール角度: {euler_angles[0]} rad") 72 print(f"推定されたピッチ角度: {euler_angles[1]} rad") 73 print(f"推定されたヨー角度: {euler_angles[2]} rad")
整形, デバッグ, コメントを追加したコード
1import numpy as np 2import pandas as pd 3import matplotlib.pyplot as plt 4from scipy.spatial.transform import Rotation 5 6 7def initialize_filter(): 8 """ 9 カルマンフィルターを初期化する 10 P, Q, R は適切な値を設定する必要がある 11 """ 12 13 # 初期状態推定値 (クォータニオン) 14 # 初期の姿勢は単位クォータニオン(姿勢なし) 15 x = np.array([1.0, 0.0, 0.0, 0.0]) 16 17 # 初期推定誤差共分散行列 18 P = np.eye(4) 19 20 # プロセスノイズの共分散行列 21 Q = np.eye(4) * 0.01 22 23 # 観測ノイズの共分散行列 24 R = np.eye(3) * 0.1 25 26 return x, P, Q, R 27 28 29def quaternion_to_rotation_rate(q, gyro_data): 30 """ 31 クォータニオンを角速度に変換する 32 33 Args: 34 q (np.array): クォータニオン 35 gyro_data (np.array): ジャイロセンサーの値 36 """ 37 38 # クォータニオンの共役を計算 39 # ax + bi (iは虚数)の共役は ax - bi みたいな感じ 40 q_conj = np.array([q[0], -q[1], -q[2], -q[3]]) 41 42 # クォータニオンを回転ベクトルに変換 43 # 2倍するのは、回転ベクトルの長さが角速度になるようにするため 44 # 逆回転のためにinv()を使う 45 # inv で逆行列を求める 46 # as_rotvec で回転ベクトルに変換 47 # gyro_data で回転ベクトルの長さを決める 48 omega = 2.0 * Rotation.from_quat(q_conj).inv().as_rotvec() * gyro_data 49 return omega 50 51 52def update_filter(x, P, Q, R, accel_data, gyro_data, dt): 53 """ 54 カルマンフィルターを更新する 55 56 Args: 57 x (np.array): 状態推定値 58 P (np.array): 推定誤差共分散行列 59 Q (np.array): プロセスノイズの共分散行列 60 R (np.array): 観測ノイズの共分散行列 61 accel_data (np.array): 加速度センサーの値 62 gyro_data (np.array): ジャイロセンサーの値 63 dt (float): サンプリング時間 64 """ 65 66 # 予測ステップ 67 x_gyro = gyro_data[0] 68 y_gyro = gyro_data[1] 69 z_gyro = gyro_data[2] 70 71 # 角速度を表す行列 72 omega = np.array([ 73 [0 , -x_gyro, -y_gyro, -z_gyro], 74 [x_gyro, 0, z_gyro, -y_gyro], 75 [y_gyro, -z_gyro, 0, x_gyro], 76 [z_gyro, y_gyro, -x_gyro, 0] 77 ]) 78 79 # np.eye(4) は 4x4 の単位行列を生成する 80 # 0.5 はサンプリング時間の半分 81 # dt はサンプリング時間 82 A = np.eye(4) + (0.5 * dt * omega) 83 del omega 84 85 # 角速度xサンプリング時間 を掛けることで予測値を求める 86 x_pred = A.dot(x) 87 P_pred = A.dot(P).dot(A.T) + Q 88 89 # カルマンゲインの計算 90 # 3x4のゼロ行列を作成 91 H = np.zeros((3, 4)) 92 93 # H行列の適切な要素に値を設定 94 H[0, 1] = 1 # x方向の加速度とx軸回りの姿勢の関係 95 H[1, 2] = 1 # y方向の加速度とy軸回りの姿勢の関係 96 H[2, 3] = 1 # z方向の加速度とz軸回りの姿勢の関係 97 98 # カルマンゲインを求める 99 K = P_pred.dot(H.T).dot(np.linalg.inv(H.dot(P_pred).dot(H.T) + R)) 100 101 # 角速度をクォータニオンに変換 102 omega = quaternion_to_rotation_rate(x, gyro_data) 103 104 # 観測更新 105 # 状態推定値(x_pred) と カルマンゲイン(K) を足す 106 x_updated = x_pred + K.dot(accel_data - H.dot(x_pred)) 107 P_updated = (np.eye(4) - K.dot(H)).dot(P_pred) 108 109 return x_updated, P_updated 110 111 112# メインプログラム 113if __name__ == "__main__": 114 # データをDataFrameから読み込む (Accelerometer.csv と Gyroscope.csv に変更) 115 file_path = "./logs/run_kururu/" 116 accel_data = pd.read_csv(f'{file_path}Accelerometer.csv') 117 gyro_data = pd.read_csv(f'{file_path}Gyroscope.csv') 118 119 # カルマンフィルターの初期化 120 x, P, Q, R = initialize_filter() 121 122 # サンプリング時間 123 dt = accel_data['Time (s)'].diff().mean() 124 125 # データフレームをイテレートし、カルマンフィルターを更新 126 length = accel_data.shape[0] if accel_data.shape[0] < gyro_data.shape[0] else gyro_data.shape[0] 127 128 datum = [] 129 for index in range(length - 1): 130 accel_data_point = np.array([ 131 accel_data.at[index, 'Acceleration x (m/s^2)'], 132 accel_data.at[index, 'Acceleration y (m/s^2)'], 133 accel_data.at[index, 'Acceleration z (m/s^2)'] 134 ]) 135 gyro_data_point = np.array([ 136 gyro_data.at[index, 'Gyroscope x (rad/s)'], 137 gyro_data.at[index, 'Gyroscope y (rad/s)'], 138 gyro_data.at[index, 'Gyroscope z (rad/s)'] 139 ]) 140 x, P = update_filter(x, P, Q, R, accel_data_point, gyro_data_point, dt) 141 142 # 推定された姿勢をクォータニオンからオイラー角に変換 143 euler_angles = Rotation.from_quat(x).as_euler('xyz') 144 datum.append([euler_angles[0], euler_angles[1], euler_angles[2]]) 145 146 # datumをプロット 147 datum = np.array(datum) 148 plt.plot(datum[:, 0], label='roll') 149 plt.plot(datum[:, 1], label='pitch') 150 plt.plot(datum[:, 2], label='yaw') 151 plt.legend() 152 plt.show()

カルマンフィルタのイメージ

1フレーム前の状態と観測値から、現在の状態を推定する
うまく計算してだされた値(カルマンゲイン)と推定値を使って、現在の状態を更新する

カルマンフィルタの流れ

  1. 初期化
    • 初期状態推定値を設定
      • 姿勢なし: [0, 0, 0, 0]
    • 初期推定誤差共分散行列を設定
    • プロセスノイズの共分散行列を設定
    • 観測ノイズの共分散行列を設定
  2. 予測する
    • 前回の状態推定値 と 角速度xサンプリング周波数 を使って計算する
  3. カルマンゲインを計算する
    • 予測推定誤差 と 観測ノイズの共分散行列 を使って計算する
  4. 観測を更新する
    • 姿勢(x) を更新する
      • 推定値(x_pred) と カルマンゲイン(K) を足す
    • 推定誤差(P) を更新する
      • カルマンゲイン と 予測推定誤差を使って計算する
  5. (1フレーム進んで2へ戻る)

クォータニオン

クォータニオンは 回転を表す4次元のベクトル.
複素数を4次元に拡張したもの.

複素数 i に加えて j も追加したイメージ

数列: 1次元(線)
複素数: 2次元(平面)
クォータニオン?: 3次元(空間)

クォータニオン 参考元

状態推定値

端末の状態(姿勢)の クォータニオン

初期推定誤差

推定値と実際の値との間に生じる誤差

なぜそれを求めれるのかがわからない

プロセスノイズ

システムやプロセスの挙動や状態に影響を与える外部からのランダムな要因や摂動のこと
ex) 摩擦や風の影響、車両の制御システムの誤差など

行列でどう表現しているのかがわからない

観測ノイズ

センサで計測した値に生じる誤差

行列でどう表現しているのかがわからない

カルマンフィルタを用いた姿勢推定結果

kalman_sample.png (46.1 kB)

前回: 全くわからない.
今回: 基本は分かった気になれた.
次回: コードを書いてみる(予定)

進路関係

なし

余談

お月見どろぼう

otukimi.jpg (3.1 MB) okasi.jpg (5.2 MB)

近所の子供達がいろんな家を回ってお菓子を盗む(合法)イベント.
伝統が残っていて嬉しい

mocopi で遊んでみた

思ったより手軽で精度も高かった.
データを取り出したりして遊んでみたい.

旅行の必需品