一回の回転を直交する軸での二回の回転に分ける
一回の回転を直交する軸での二回の回転に分割する方法です. 座標系の設定に使うために導出しました. C++による実装も公開しています.
モチベーション
ロボットを歩行させるため,足を置く位置を地面に固定された座標系map
上で計画するつもりです.
そのためにロボットの座標系base_link
の「前方」がmap
ではどの向きなのかの情報が必要です.
ロボットには IMU が搭載されており,鉛直上向きを z 軸とする適当な初期位置odom
を基準にbase_link
の姿勢が推定されます.
重力加速度の向きからbase_link
の傾きは容易にわかりますが,鉛直軸まわりの方向は定まりずらい状況です.
そこで,map
とodom
の間の,z 軸のまわりの回転については,
ロボットを起動した後に「ロボットの前方がmap
の x 軸の方向と合うようにodom
の向きを変更しなさい」という指令をしたいと考えています.
これを実現するために,odom
からbase_link
に至る回転を
- z 軸まわりの回転
- z 軸と直交な軸のまわりの回転
の 2 回の回転に分解すると良いと考えました.
表記に関する約束
三次元での回転はクォータニオンで表現し,太字で\(\boldsymbol{q}\)のように書くことにします. 回転の合成については,\(\boldsymbol{q}\)による回転の後に続けて\(\boldsymbol{p}\)によって回転させると\(\boldsymbol{r}\)によって回転させたのと同じ結果を得られることを
$$ \boldsymbol{r}=\boldsymbol{p}\boldsymbol{q} $$
と書くことにします. ベクトルは矢印を使った\(\vec{v}\)で表記し,クォータニオンと混同しないようにします. クォータニオン\(\boldsymbol{q}\)によってベクトル\(\vec{v}\)を回転させる操作は, クォータニオンからベクトルにする操作を\(\mathrm{vect}\),ベクトルからクォータニオンにする操作を\(\mathrm{quat}\)として丁寧に書くと
$$ \mathrm{vect}\left(\boldsymbol{q}\cdot \mathrm{quat}\left(\vec{v}\right) \cdot\bar{\boldsymbol{q}}\right) $$
のようになりますが,単に
$$ \boldsymbol{q}\vec{v} $$
と書くことにします.
解決する問題
回転を表すクォータニオン\(\boldsymbol{r}\)と一回目の回転の軸の向きを表す単位ベクトル\(\vec{e}_q\)を入力とし, 2 つの回転を表すクォータニオン\(\boldsymbol{p}, \boldsymbol{q}\)を求めます. 2 回の回転\(\boldsymbol{p}, \boldsymbol{q}\)は \(\boldsymbol{q}\)による回転の後に続けて\(\boldsymbol{p}\)によって回転させると\(\boldsymbol{r}\)によって回転させたのと同じ結果を得られることと, \(\boldsymbol{p}, \boldsymbol{q}\)の回転軸は直交する(または少なくとも一方は無回転である)ことの 2 つの条件を満たします.
解法
回転軸は回転によって変化しないので,\(\vec{e}_q=\boldsymbol{q}\vec{e}_q\)です. 任意のベクトル\(\vec{v}\)で\(\boldsymbol{r}\vec{v}=\boldsymbol{p}\boldsymbol{q}\vec{v}\)が成り立ちます. ここに\(\vec{e}_q\)を代入すると
$$ \boldsymbol{r}\vec{e}_q = \boldsymbol{p}\boldsymbol{q}\vec{e}_q = \boldsymbol{p}\vec{e}_q $$
となります. 2 回目の回転\(\boldsymbol{p}\)は\(\vec{e}_q\)を\(\boldsymbol{r}\vec{e}_q\)に動かすことが確認できました.
回転軸に垂直なベクトルは,回転後も回転軸に垂直です. 2 つの回転\(\boldsymbol{p}, \boldsymbol{q}\)の軸は直交(\(\vec{e}_p\perp\vec{e}_q\))していることから, \(\vec{e}_p\perp\boldsymbol{p}\vec{e}_q\)です. \(\boldsymbol{r}\vec{e}_q = \boldsymbol{p}\vec{e}_q\) だったので
$$ \vec{e}_p\perp\boldsymbol{r}\vec{e}_q $$
が導けます.
2 回目の回転\(\boldsymbol{p}\)は\(\vec{e}_q\)を\(\boldsymbol{r}\vec{e}_q\)に動かすことと, 回転軸\(\vec{e}_p\)は 2 つのベクトル\(\vec{e}_q\)と\(\boldsymbol{r}\vec{e}_q\)の両方に垂直であることから, ほとんどの場合,\(\boldsymbol{p}\)の回転軸\(\vec{e}_p\)は
$$ \vec{e}_p=\frac{\vec{e}_q\times\boldsymbol{r}\vec{e}_q}{\left|\vec{e}_q\times\boldsymbol{r}\vec{e}_q\right|} $$
で,回転角\(\theta_p\)は
$$ \theta_p=\cos^{-1}{\left(\vec{e}_q\cdot\boldsymbol{r}\vec{e}_q\right)} $$
です. 以上から\(\boldsymbol{p}\)が求まるので,\(\boldsymbol{q}\)は
$$ \boldsymbol{q}=\bar{\boldsymbol{p}}\boldsymbol{r} $$
で得られます.
例外は\(\left|\vec{e}_q\times\boldsymbol{r}\vec{e}_q\right|=0\)の場合です. \(\left|\vec{e}_q\right|=\left|\boldsymbol{r}\vec{e}_q\right|=1\)だから,この問題が起こるのは, \(\vec{e}_q=\boldsymbol{r}\vec{e}_q\)の場合と\(\vec{e}_q=-\boldsymbol{r}\vec{e}_q\)の場合に限られます.
\(\vec{e}_q=\boldsymbol{r}\vec{e}_q\)の場合,\(\boldsymbol{r}\)によって\(\vec{e}_q\)が変化しないことから, \(\vec{e}_q\)が回転軸だと分かります. そこで,\(\boldsymbol{q}=\boldsymbol{r}\)として, \(\boldsymbol{p}\)は無回転を表すクォータニオン\(w=1, x=y=z=0\)にすれば求める解になります.
\(\vec{e}_q=-\boldsymbol{r}\vec{e}_q\)の場合,\(\boldsymbol{r}\)によって\(\vec{e}_q\)と垂直な軸周りに 180 度回転していることがわかります. そこで,\(\boldsymbol{p}=\boldsymbol{r}\)とし, \(\boldsymbol{q}\)は無回転を表すクォータニオン\(w=1, x=y=z=0\)とすれば解が得られます.
以上の方法を C++で実装しました. Eigen というライブラリを使用しました. Actat/rotation-decompositionで テストに使ったコードとともに公開しています.
#include <eigen3/Eigen/Geometry>
std::tuple<Eigen::Quaterniond, Eigen::Quaterniond> decompose_rotation(
Eigen::Quaterniond r,
Eigen::Vector3d e_q) {
e_q.normalize();
r.normalize();
Eigen::Vector3d re_q = r * e_q;
if (re_q.isApprox(e_q)) {
return std::forward_as_tuple(Eigen::Quaterniond(1, 0, 0, 0), r);
} else if (re_q.isApprox(-1 * e_q)) {
return std::forward_as_tuple(r, Eigen::Quaterniond(1, 0, 0, 0));
} else {
Eigen::Vector3d e_p = (e_q.cross(re_q)).normalized();
double theta_p = std::acos(e_q.dot(re_q));
auto p = Eigen::Quaterniond(
std::cos(theta_p / 2), std::sin(theta_p / 2) * e_p.x(),
std::sin(theta_p / 2) * e_p.y(), std::sin(theta_p / 2) * e_p.z());
Eigen::Quaterniond q = p.conjugate() * r;
return std::forward_as_tuple(p, q);
}
}
テスト条件
クォータニオンとベクトルの組を入力し,条件を満たすクォータニオンの組が返されることを確認しました.
入力
- 無回転を表すクォータニオンを入力する場合
- クォータニオンの回転軸と平行な回転軸を入力する場合
- クォータニオンの回転軸と垂直な回転軸を入力する場合
- ランダムなクォータニオンと回転軸の組を入力する場合
出力が満たすべき条件
- クォータニオンそれぞれが回転を表すクォータニオン(大きさが 1)であること
- 2 回の回転が入力を分解したものであること(\(\boldsymbol{r}=\boldsymbol{p}\boldsymbol{q}\)であること)
- 2 つのクォータニオンの回転軸が直交すること(\(\vec{e}_p\cdot\vec{e}_q=0\)であること)
感想
同じ計算をしている人が見つからなかったので,モチベーションに誤りがあるかもしれません. こわいなぁ.