粗大メモ置き場

個人用,たまーに来訪者を意識する雑記メモ

3次元回転のオイラー角をそのまま補間したら駄目とは言うが実際どれくらい変わるのか?(Python)

はじめに

3次元回転にはオイラー角、回転行列、クォータニオン、ロドリゲスの回転公式と多岐に渡る表現方法があり、時と場合によってそれらを使い分ける必要があります。

中でも任意の2姿勢を補間する際、「オイラー角を始点と終端で補間するのは悪手で、代わりにLERPやSLERPを使うと良い」というのは教科書などに載っていて皆知っていると思いますが、一体どれほど悪くなるのかについて具体的に示した資料が見つけられなかったので自分でやってみました。

下準備

本題に入る前に下記について作成したものを載せておきます。

  • python/matplotlibを用いた回転角の図示
  • scipyを用いた3D回転の形式変換

matplotlibによる回転角の図示

入力した角度が三次元でどのような見た目になるかについて知りたかったので簡単にmatplotlibで表示できるclassを作りました。

回転順序は 1-2-3(x-y-z)の軸の順に回転する(R = RzRyRx)としました。

# 使い方
plane1  = Plane3D()
plane1.rotate_points_123euler([10,20,20]) # degreeでroll pitch yaw角を入れる。
plane1.show_as_mesh()

こんな感じで回転された平面が図示されます。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import scipy
from scipy.spatial.transform import Rotation as R

class Plane3D:
    """
    3次元平面の描画用クラス
    """
    def __init__(self, side_num=10):
        
        assert type(side_num) is int
        self.init_attitude(side_num)
        
        
    def show_as_mesh(self):
        # Create the figure
        fig = plt.figure()
        # Add an axes
        ax = fig.add_subplot(111,projection='3d')
        # plot the surface
        ax.plot_surface(self.__X, self.__Y, self.__Z)
        return ax
    
    def getXYZ(self):
        """
        保持している図示用の点群の座標を吐き出す関数
        """
        return self.__X, self.__Y, self.__Z
        
    def rotate_points_123euler(self,rpy):
        """
        degree単位回転
        1-2-3(x-y-z)の軸の順に回転する。すなわち R = RzRyRxを掛け算する。
        
        """
        R3d = self.euler2Robj(rpy)
        self.normal_vector = R3d.dot(self.normal_vector)
        self.calc_Z()
        
    def euler2Robj(self,rpy):
        """
        degree単位回転行列の取得
        1-2-3(x-y-z)の軸の順に回転する R = RzRyRxを返す
        """
        R3d = R.from_euler('zyx',[rpy[2],rpy[1],rpy[0]],degrees=True).as_matrix()
        return R3d

    def set_points_123euler(self,rpy):
        """
        1-2-3(x-y-z)の軸の順のEuler角に姿勢を固定する
        """
        self.normal_vector = np.array([0.,0.,1.0]).reshape(-1,1)
        self.rotate_points_123euler(rpy)
        
        
    def init_attitude(self,side_num):
        
        # ベースとなるGrid
        self.__X,  self.__Y = np.mgrid[-side_num:side_num+1,-side_num:side_num+1]
        # 法線ベクトル(縦)
        self.normal_vector = np.array([0.,0.,1.0]).reshape(-1,1)
        # 高さ計算
        dlen = side_num*2+1
        self.__Z = np.zeros((dlen,dlen))
            
    def calc_Z(self):
        if self.normal_vector[2,0] != 0:
            Z_ = - self.__X * self.normal_vector[0,0] - self.__Y * self.normal_vector[1,0]
            self.__Z = Z_ /self.normal_vector[2,0]
        else:
            print("Plane is Vertical!!")

ScipyのRotationの使い方について

ScipyのRotationにて1-2-3(x-y-z)の軸の順に回転する R = RzRyRxを返すには下記の書き方をする必要があります。この辺はパッケージによって違うので注意です。

個人的にはr-p-yの順に引数を与えたいと思ったのですが関数と仕様ケースが合わないのでLambda関数などを用いてごまかしています。

# RzRyRxの掛け算(Yaw-Pitch-Roll)
R.from_euler('zyx',[y,p,r]).as_matrix()

ScipyのSLERPの使い方について

ScipyにはSLERP(球面補間)のライブラリが用意されています。 一見すると使い方がわかりにくかったのでこちらもメモしておきます。

from scipy.spatial.transform import Slerp

rpy2ypr = lambda x: [x[2],x[1],x[0]]

# slerp objectを生成
key_rots =R.from_euler('zyx', [rpy2ypr(rpy0), rpy2ypr(rpyn)], degrees = True) # 始点と終点
key_times = [0, 1] # 補間点を設定
slerp = Slerp(key_times, key_rots) # オブジェクト作成

# 0-1の間の補間点における角度をオイラー角で取得
slerp(0.4).as_euler('zyx',degrees=True)

オイラー角補間とSLERP(球面補間)の比較

ここではr-p-yが(0,0,0)と(10,20,30)または(20,40,60)の間で補間をしてみた際の違いを図示します。

SLERPで補間した結果をオイラー角に再度変換し直したものをオイラー角の線形補間と比較すると下図のようになります。

(0,0,0)から(10,20,30)におけるSLERPとオイラー角線形補間の差分

(0,0,0)から(20,40,60)におけるSLERPとオイラー角線形補間の差分

2点間の距離?が離れれば離れるほど差が顕著になるのがわかると思います。

このグラフを見て、結構違うなとなるかあんまり差がないなとなるかはアプリケーション次第だと思います。何か指標のようなものを知っている人がいたら教えて下さい。

ちなみに、もっとも差があるであろう補間の中央付近での3次元形状の違いを図示すると下記の様になります。

(0,0,0)から(20,40,60)の中央点におけるSLERPとオイラー角線形補間の差分の図示

うーん、個人的にはあんまり変わらない気が... 角速度が重要なケースならよりSLERPの重要性が際立ちそうなものですが、以外と動かしてみたらバレないのではというのが筆者の所感です。 なにかCriticalなケースがあれば教えて下さい。

雑談

誤差状態カルマンフィルタ構築に向けたクォータニオンキネマティクス、という良さげな和文テキストが最近公開されました。ちょっと遊んで見ようかなと思っています。

www.flight.t.u-tokyo.ac.jp