TT Lab
ブログ

한국어 English 日本語 中文

はじめる
ブログ

ブログ

座標系と変換:回転行列、同次変換、オイラー角とクォータニオン、カメラから卓球台へ

状態:ドラフト(2026-10-18)、実測。 数値はノートPC(Apple M1 Max、macOS 26.5)のPython 3.9.6・numpy 2.0.2で直接計算しました。 時間は7回繰り返した際の中央値(初回は除外)です。生データとコード:作業ディレクトリ measure/robot-arm-basics/の m02_frames.py, m02b_float32.py, out/m02_frames.json。実際のカメラで補正を行った実験は行っていません(未実測)。参考文献は2026-10-18に閲覧しました。 作成規則:実測データを基本とし、測定できなかったものは「未実測」、確認できなかったものは「未確認」と表記します。

目次

1. 直感:同じボール、4つの説明

ボールが1つ浮いています。カメラは「自分のレンズの前3.1 m、少し左下」と言います。卓球台を基準にすると「中央からロボット側に0.8 m、 高さ25 cm」です。ロボットベースを基準にすると「前方0.9 m、上方0.4 m」であり、ラケットを基準にすると「面から3 cm前方」です。すべて同じ点です。

ロボットのソフトウェアにおけるバグの多くは、これら4つの説明を混同して使用することから生じます。そのため、座標系(frame)ごとに名前を付け、点を移動させる際には必ず変換を経由させるようにします。本シリーズでは次の4つの名称を使用します。

名前 原点と軸 誰が決めるか
{C} カメラ レンズ中心、zが視線方向 カメラの設置
{T} 卓球台 上面中央、xが幅・yが長さ・zが上向き ITTF規格とDeepMindデータセットの慣例
{B} ロボットベース 第1関節軸の上 ロボットの設置
{E} ラケット ラケット面中心、zが面の法線 順運動学(第3部)

2. 図:座標系のチェーン

좌표계 사슬: 카메라가 본 공을 라켓 좌표로 카메라 {C} 공 위치를 잼 탁구대 {T} 원점: 윗면 가운데 로봇 바닥 {B} 팔이 서 있는 곳 라켓 {E} 정기구학 결과 보정으로 한 번 구함 설치할 때 잼 관절각 q 로 매번 동차 변환 T = [ R p ; 0 0 0 1 ] (4×4) 탁구대 좌표의 공 = ᵀT_C · (카메라 좌표의 공) 바닥 좌표의 공 = ᴮT_T · ᵀT_C · p_C 사슬은 오른쪽부터 곱하고, 거꾸로 가려면 역변환.
図:カメラから卓球台、ロボットベース、ラケットへと続く変換チェーン

3. 計算

3.1 回転行列

回転行列(rotation matrix)Rは3×3であり、 列の1つ1つが回転した座標系の軸を元の座標系から見た単位ベクトルです。そのため常に次の2つの性質を満たします。

Rᵀ R = I        (열들이 서로 수직이고 길이가 1)
det R = +1      (오른손 좌표계를 유지, -1 이면 거울상)

z軸を中心にθだけ回す回転は次のようになります。2つの回転をつなげるときは行列を掛け、元に戻すときは転置(transpose)するだけです。逆行列を個別に計算する必要はありません。

Rz(θ) = [ cos θ  −sin θ  0 ]
        [ sin θ   cos θ  0 ]
        [   0       0    1 ]

3.2 同次変換

回転と平行移動を一度に扱うために4×4行列を使用します。これを同次変換(homogeneous transformation)と呼びます。点には第4成分として1を付加します。

T = [ R  p ]        점 변환:   [x']   = T [x]
    [ 0  1 ]                   [1 ]       [1]

역변환:  T⁻¹ = [ Rᵀ  −Rᵀ p ]
               [ 0     1   ]

逆変換も通常の逆行列ではなく上記式で計算します。数値的に一段と安全かつ高速です。

3.3 オイラー角

人間が読みやすい姿勢表現はオイラー角(Euler angles)です。ロボットや航空分野で一般的なZYX順(ヨー yaw、ピッチ pitch、ロール roll)は R = Rz(요)·Ry(피치)·Rx(롤) です。回転行列から逆向きに取り出すときは次の式を使用します(r_ijはi行j列)。

피치 = asin(−r31)
요   = atan2(r21, r11)
롤   = atan2(r32, r33)

ピッチが±90°になると、r11、r21、r32、r33がすべて0になり、ヨーとロールを個別に決定できなくなります。2つの回転軸が重なって自由度が1つ失われた状態であり、これをジンバルロック(gimbal lock)と呼びます。正確に90°のときのみ問題になるのではなく、 その近傍でノイズが大きく膨れ上がるという点が実際にはより重要です。4.3節で測定します。

3.4 クォータニオン

単位クォータニオン(unit quaternion)q = (w, x, y, z)は、回転軸kと回転角θにより q = (cos θ/2, k·sin θ/2) です。4つの数値で回転を表し、ジンバルロックがなく、2つの回転の間を滑らかに補間しやすいという利点があります。その代わり、qと−qが同じ回転を指すという点(二重被覆)を常に覚えておく必要があります。姿勢誤差を計算する際に符号を合わせないと、近い2つの姿勢が360°近く離れているという結果になってしまいます。

本シリーズのコードにおいて、クォータニオンの掛け算は純粋なPythonの関数1つで実装されています。

def qmul(a, b):
    aw, ax, ay, az = a
    bw, bx, by, bz = b
    return (aw*bw - ax*bx - ay*by - az*bz,
            aw*bx + ax*bw + ay*bz - az*by,
            aw*by - ax*bz + ay*bw + az*bx,
            aw*bz + ax*by - ay*bx + az*bw)

3.5 カメラから卓球台へ:マーカー点からの姿勢推定

2台のカメラで三角測量を行うと、カメラ座標系{C}における3次元点が得られます。卓球台のコーナーのように卓球台座標系{T}が既知である点N個をカメラでも測定すれば、2つの点集合を最もよく重ね合わせるRとpを求めることができます。2つの集合の中心を引いた後に共分散行列を特異値分解(SVD)する方法であり、一般にKabschアルゴリズムと呼ばれます。

def kabsch(P, Q):                       # Q ≈ R·P + t 가 되는 R, t (P, Q 는 N×3)
    pc, qc = P.mean(0), Q.mean(0)
    U, _, Vt = np.linalg.svd((P - pc).T @ (Q - qc))
    d = np.sign(np.linalg.det(Vt.T @ U.T))      # 거울상(det = -1) 방지
    R = Vt.T @ np.diag([1, 1, d]) @ U.T
    return R, qc - R @ pc

d で符号を修正する行が抜けていると、点がほぼ同一平面上にある場合(卓球台の上面がまさにそうです)に鏡像行列が生成されてしまう可能性があります。

4. 実測

4.1 回転を長く掛け続けると直交性が崩れるのか

ジャイロの積分のように小さな回転(0.06〜0.6°)を継続的に掛け合わせていき、RᵀRが単位行列からどれほど乖離するかを測定しました。同じ回転の列をクォータニオンでも掛け合わせました。

掛け合わせた回数 float64 行列 ‖RᵀR − I‖ float64 クォータニオン |‖q‖ − 1| float32 行列 float32 クォータニオン
1,000 - - 7.8×10⁻⁶ 2.8×10⁻⁶
10,000 - - 2.9×10⁻⁵ 5.6×10⁻⁶
100,000 4.7×10⁻¹³ 2.5×10⁻¹⁴ 2.6×10⁻⁴ 8.6×10⁻⁵
1,000,000 4.4×10⁻¹² 3.1×10⁻¹⁴ (未測定) (未測定)

float64行列を1,000回ごとにSVDで最も近い回転行列に修正すると、100万回後でも1.2×10⁻¹⁵でした。

解釈は2つあります。第1に、 PCのfloat64ではドリフトを心配する必要がほとんどありません。 100万回は1 kHzの制御であれば17分に相当します。第2に、 float32を使用するマイクロコントローラーでは話が異なります。 10万回(1 kHzで100秒)での2.6×10⁻⁴は、軸が約0.015°ねじれた程度の大いさに相当し、増え続けます。モーターコントローラーやIMUのファームウェアのようにfloat32で回転を累積するコードは、定期的に再正規化を行う必要があります。クォータニオンは長さを1で割るだけでよいため、この作業を行列よりも簡単に行えます。

4.2 計算時間

演算 1回あたり(µs)
numpy 3×3 行列積 0.68
numpy 4×4 行列積 0.66
numpy 3×3 行列 × ベクトル 0.60
純粋なPythonによるクォータニオン積 0.55
純粋なPythonによるクォータニオンでのベクトル回転 1.33

numpyは3×3であっても4×4であっても0.6〜0.7 µsでほぼ同等です。このサイズでは掛け算そのものよりも関数呼び出しのオーバーヘッドが大半を占めるためです。したがって、Pythonで表現を選択する際、速度は判断基準になりません。ジンバルロック、補間、正規化の容易さといった特性に基づいて選べば問題ありません。

4.3 ジンバルロック付近のノイズ増幅

ZYX姿勢を作成し、0.0057°(10⁻⁴ rad)の大きさの小さなランダム回転をノイズとして加算した後、3.3節の式を用いて角度を再抽出しました。ピッチを90°に次第に近づけながら、それぞれ2,000回ずつ試行しました。

피치가 90°에 가까울수록 오일러 각이 흔들린다 30 10 3 1 0.3 0.1 0.03 0.01 0.1 1 10 피치와 90° 사이 간격(도, 로그) 요·롤 오차(도, 로그) 넣은 자세 잡음 0.0057° 중앙값(실선) 95백분위(점선) ZYX 오일러 각, 각 점 2,000번 시도. 잡음은 그대로인데 뽑아낸 각의 오차가 커진다.
図:ピッチが90度に近づくにつれてヨーとロール角の誤差が拡大する様子
ピッチと90°の差 ヨー・ロール誤差の中央値 95パーセンタイル
30° 0.007° 0.011°
10° 0.017° 0.032°
3° 0.058° 0.10°
1° 0.17° 0.31°
0.3° 0.54° 1.0°
0.1° 1.6° 3.1°
0.03° 5.4° 10.5°

傾きが対数軸上でほぼ1であるため、誤差はおおむね「ノイズ ÷ 90°までに残された角度(ラジアン)」の割合で増大します。卓球のラケットは面をほぼ垂直に立てたり寝かせたりするため、ピッチ90°付近を頻繁に通過します。 ラケットの姿勢をオイラー角のまま保存したりフィルタリングしたりせず、内部表現としては回転行列やクォータニオンを使用し、人間に表示するときのみオイラー角に変換するほうが安全です。

4.4 マーカー点によるカメラ姿勢の推定

カメラをロボット側エンドラインの後方2 m、高さ1.6 mの位置に設置し、卓球台の中央を見下ろすように配置しました。卓球台の4つのコーナー、ネットポストの上の2点、センターラインの両端を基本マーカーとして使用し、さらに多くの点が必要な場合は卓球台上の任意の点を追加しました。カメラ座標系における各点にガウスノイズσを加算し、3.5節の関数を用いて姿勢を求めました。各条件で500回試行した際の中央値です。

点数 σ 回転誤差 カメラ位置誤差 反対側エンドライン上の点の誤差(中央値/95パーセンタイル)
4 2 mm 0.073° 3.7 mm 1.9 / 3.6 mm
4 5 mm 0.20° 9.4 mm 4.9 / 8.5 mm
8 2 mm 0.061° 2.9 mm 1.4 / 2.6 mm
8 5 mm 0.15° 7.2 mm 3.6 / 6.9 mm
8 10 mm 0.31° 15.1 mm 7.1 / 13.7 mm
32 5 mm 0.10° 4.7 mm 2.1 / 3.8 mm

5.制限事項

6. まとめ

卓球の一言。 同じボールでも、誰が見るかによって座標が変わります。ラケットを立てたり寝かせたりするときは、オイラー角ではなくクォータニオンをしっかり握っておかなければなりません!

参考

すべての資料は2026-10-18に閲覧しました。

ログインするといいねできます

コメント

まだコメントはありません。

ログインするとコメントできます