剛体の姿勢表現(回転行列・オイラー角・クォータニオン)を解説

剛体の姿勢表現として、以下の3つの方法が代表的です。

  • 回転行列(Direction Cosine Matrix, DCM)
  • オイラー角(Euler Angles)
  • クォータニオン(Quaternion, 四元数)

それぞれに長所と短所があり、用途に応じて使い分けられます。特に宇宙機の姿勢制御では、クォータニオンが広く使用されています。

本記事の内容

  • 回転行列の定義と性質
  • オイラー角とジンバルロック問題
  • クォータニオンの数学的構造
  • 各表現の相互変換
  • Pythonでの実装

回転行列(DCM)

定義

回転行列 $\bm{R} \in \text{SO}(3)$ は $3 \times 3$ の直交行列で、以下の性質を満たします。

$$ \bm{R}^T \bm{R} = \bm{R} \bm{R}^T = \bm{I}_3, \quad \det(\bm{R}) = 1 $$

回転行列は、基準座標系から機体座標系への変換を表します。

$$ \bm{r}_{\text{body}} = \bm{R} \bm{r}_{\text{ref}} $$

基本回転行列

各軸周りの回転行列は以下の通りです。

$x$ 軸周り(角度 $\phi$):

$$ \bm{R}_x(\phi) = \begin{pmatrix} 1 & 0 & 0 \\ 0 & \cos\phi & \sin\phi \\ 0 & -\sin\phi & \cos\phi \end{pmatrix} $$

$y$ 軸周り(角度 $\theta$):

$$ \bm{R}_y(\theta) = \begin{pmatrix} \cos\theta & 0 & -\sin\theta \\ 0 & 1 & 0 \\ \sin\theta & 0 & \cos\theta \end{pmatrix} $$

$z$ 軸周り(角度 $\psi$):

$$ \bm{R}_z(\psi) = \begin{pmatrix} \cos\psi & \sin\psi & 0 \\ -\sin\psi & \cos\psi & 0 \\ 0 & 0 & 1 \end{pmatrix} $$

長所と短所

  • 長所: 数学的に厳密、組み合わせが容易(行列積)
  • 短所: 9パラメータ + 6拘束条件、数値計算で直交性が崩れやすい

オイラー角

定義

3つの角度で姿勢を表現します。回転順序(3-2-1, ZYXなど)によって定義が異なります。

ZYX(航空宇宙で標準的)の場合、

$$ \bm{R} = \bm{R}_x(\phi) \bm{R}_y(\theta) \bm{R}_z(\psi) $$

  • $\psi$: ヨー角($z$ 軸周り)
  • $\theta$: ピッチ角($y$ 軸周り)
  • $\phi$: ロール角($x$ 軸周り)

ジンバルロック

オイラー角の重大な欠点がジンバルロック(Gimbal Lock)です。ピッチ角 $\theta = \pm 90°$ のとき、ロール軸とヨー軸が平行になり、1自由度を失います。

$$ \theta = 90° \implies \bm{R} = \begin{pmatrix} 0 & \sin(\phi-\psi) & \cos(\phi-\psi) \\ 0 & \cos(\phi-\psi) & -\sin(\phi-\psi) \\ -1 & 0 & 0 \end{pmatrix} $$

$\phi$ と $\psi$ が独立に決まらず、$\phi – \psi$ のみが決定されます。

クォータニオン

定義

クォータニオン $\bm{q}$ は4つのパラメータで姿勢を表現します。

$$ \bm{q} = q_0 + q_1 \bm{i} + q_2 \bm{j} + q_3 \bm{k} = \begin{pmatrix} q_0 \\ q_1 \\ q_2 \\ q_3 \end{pmatrix} $$

ここで $q_0$ はスカラー部、$(q_1, q_2, q_3)$ はベクトル部です。単位クォータニオンの条件は、

$$ |\bm{q}|^2 = q_0^2 + q_1^2 + q_2^2 + q_3^2 = 1 $$

軸-角度表現との関係

回転軸 $\hat{\bm{n}} = (n_x, n_y, n_z)$ 周りに角度 $\theta$ だけ回転するクォータニオンは、

$$ \bm{q} = \begin{pmatrix} \cos(\theta/2) \\ n_x \sin(\theta/2) \\ n_y \sin(\theta/2) \\ n_z \sin(\theta/2) \end{pmatrix} $$

クォータニオンによる回転

ベクトル $\bm{v}$ のクォータニオン $\bm{q}$ による回転は、

$$ \bm{v}’ = \bm{q} \otimes \bm{v} \otimes \bm{q}^* $$

ここで $\bm{q}^* = (q_0, -q_1, -q_2, -q_3)$ は共役クォータニオン、$\otimes$ はクォータニオン積です。

クォータニオンから回転行列への変換

$$ \bm{R} = \begin{pmatrix} 1-2(q_2^2+q_3^2) & 2(q_1q_2+q_0q_3) & 2(q_1q_3-q_0q_2) \\ 2(q_1q_2-q_0q_3) & 1-2(q_1^2+q_3^2) & 2(q_2q_3+q_0q_1) \\ 2(q_1q_3+q_0q_2) & 2(q_2q_3-q_0q_1) & 1-2(q_1^2+q_2^2) \end{pmatrix} $$

長所と短所

  • 長所: ジンバルロックなし、4パラメータのみ、補間が容易
  • 短所: 直感的に理解しにくい

Pythonでの実装

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

def euler_to_dcm(phi, theta, psi):
    """オイラー角(ZYX)から回転行列へ変換"""
    Rx = np.array([[1, 0, 0],
                   [0, np.cos(phi), np.sin(phi)],
                   [0, -np.sin(phi), np.cos(phi)]])
    Ry = np.array([[np.cos(theta), 0, -np.sin(theta)],
                   [0, 1, 0],
                   [np.sin(theta), 0, np.cos(theta)]])
    Rz = np.array([[np.cos(psi), np.sin(psi), 0],
                   [-np.sin(psi), np.cos(psi), 0],
                   [0, 0, 1]])
    return Rx @ Ry @ Rz

def euler_to_quaternion(phi, theta, psi):
    """オイラー角からクォータニオンへ変換"""
    cp = np.cos(phi / 2)
    sp = np.sin(phi / 2)
    ct = np.cos(theta / 2)
    st = np.sin(theta / 2)
    cy = np.cos(psi / 2)
    sy = np.sin(psi / 2)

    q0 = cp * ct * cy + sp * st * sy
    q1 = sp * ct * cy - cp * st * sy
    q2 = cp * st * cy + sp * ct * sy
    q3 = cp * ct * sy - sp * st * cy
    return np.array([q0, q1, q2, q3])

def quaternion_to_dcm(q):
    """クォータニオンから回転行列へ変換"""
    q0, q1, q2, q3 = q
    R = np.array([
        [1-2*(q2**2+q3**2), 2*(q1*q2+q0*q3), 2*(q1*q3-q0*q2)],
        [2*(q1*q2-q0*q3), 1-2*(q1**2+q3**2), 2*(q2*q3+q0*q1)],
        [2*(q1*q3+q0*q2), 2*(q2*q3-q0*q1), 1-2*(q1**2+q2**2)]
    ])
    return R

def quaternion_multiply(q1, q2):
    """クォータニオン積"""
    a0, a1, a2, a3 = q1
    b0, b1, b2, b3 = q2
    return np.array([
        a0*b0 - a1*b1 - a2*b2 - a3*b3,
        a0*b1 + a1*b0 + a2*b3 - a3*b2,
        a0*b2 - a1*b3 + a2*b0 + a3*b1,
        a0*b3 + a1*b2 - a2*b1 + a3*b0
    ])

# 変換の検証
phi = np.radians(30)    # ロール
theta = np.radians(45)  # ピッチ
psi = np.radians(60)    # ヨー

R_euler = euler_to_dcm(phi, theta, psi)
q = euler_to_quaternion(phi, theta, psi)
R_quat = quaternion_to_dcm(q)

print("=== 変換の検証 ===")
print(f"オイラー角: phi={np.degrees(phi):.1f}, theta={np.degrees(theta):.1f}, psi={np.degrees(psi):.1f}")
print(f"クォータニオン: {q}")
print(f"回転行列の差(ノルム): {np.linalg.norm(R_euler - R_quat):.2e}")

# ジンバルロックの可視化
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# ピッチ角による回転行列の条件数
pitch_angles = np.linspace(-89, 89, 1000)
cond_numbers = []
for th in pitch_angles:
    R = euler_to_dcm(0, np.radians(th), 0)
    cond_numbers.append(np.linalg.cond(R))

axes[0].plot(pitch_angles, cond_numbers, 'b-', linewidth=1)
axes[0].set_xlabel('Pitch Angle [deg]')
axes[0].set_ylabel('Condition Number')
axes[0].set_title('DCM Condition Number vs Pitch')
axes[0].grid(True)

# クォータニオンの球面線形補間(SLERP)
def slerp(q1, q2, t):
    """球面線形補間"""
    dot = np.dot(q1, q2)
    if dot < 0:
        q2 = -q2
        dot = -dot
    if dot > 0.9995:
        return q1 + t * (q2 - q1)
    theta_0 = np.arccos(dot)
    sin_theta = np.sin(theta_0)
    s1 = np.sin((1 - t) * theta_0) / sin_theta
    s2 = np.sin(t * theta_0) / sin_theta
    return s1 * q1 + s2 * q2

q_start = euler_to_quaternion(0, 0, 0)
q_end = euler_to_quaternion(np.radians(90), np.radians(45), np.radians(30))

t_values = np.linspace(0, 1, 50)
euler_angles = []
for t in t_values:
    q_interp = slerp(q_start, q_end, t)
    R = quaternion_to_dcm(q_interp)
    theta_interp = np.arcsin(-R[2, 0])
    phi_interp = np.arctan2(R[2, 1], R[2, 2])
    psi_interp = np.arctan2(R[1, 0], R[0, 0])
    euler_angles.append([np.degrees(phi_interp),
                         np.degrees(theta_interp),
                         np.degrees(psi_interp)])

euler_angles = np.array(euler_angles)
axes[1].plot(t_values, euler_angles[:, 0], 'r-', label='Roll')
axes[1].plot(t_values, euler_angles[:, 1], 'g-', label='Pitch')
axes[1].plot(t_values, euler_angles[:, 2], 'b-', label='Yaw')
axes[1].set_xlabel('Interpolation Parameter t')
axes[1].set_ylabel('Euler Angle [deg]')
axes[1].set_title('SLERP Interpolation')
axes[1].legend()
axes[1].grid(True)

plt.tight_layout()
plt.show()

まとめ

本記事では、剛体の姿勢表現方法について解説しました。

  • 回転行列は $3\times3$ の直交行列で厳密だが、9パラメータが必要
  • オイラー角は3パラメータで直感的だが、ジンバルロック問題がある
  • クォータニオンは4パラメータでジンバルロックがなく、宇宙機制御で広く使われる
  • SLERP(球面線形補間)により、クォータニオンで滑らかな姿勢遷移を実現できる
  • 各表現は相互に変換可能であり、用途に応じて使い分ける

次のステップとして、以下の記事も参考にしてください。