상태: 초안 (2026-10-18), 실측. 숫자는 노트북(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. 직관: 같은 공, 네 가지 설명
공 하나가 떠 있습니다. 카메라는 「내 렌즈 앞 3.1 m, 약간 왼쪽 아래」라고 말합니다. 탁구대 기준으로는 「가운데에서 로봇 쪽으로 0.8 m, 높이 25 cm」입니다. 로봇 바닥 기준으로는 「앞으로 0.9 m, 위로 0.4 m」이고, 라켓 기준으로는 「면에서 3 cm 앞」입니다. 모두 같은 점입니다.
로봇 소프트웨어의 버그 가운데 상당수는 이 네 설명을 섞어 쓰는 데서 나옵니다. 그래서 좌표계(frame)마다 이름을 붙이고, 점을 옮길 때는 반드시 변환을 거치게 만듭니다. 이 시리즈는 다음 네 이름을 씁니다.
| 이름 | 원점과 축 | 누가 정하나 |
|---|---|---|
| {C} 카메라 | 렌즈 중심, z 가 보는 방향 | 카메라 설치 |
| {T} 탁구대 | 윗면 가운데, x 폭·y 길이·z 위 | ITTF 규격과 DeepMind 데이터셋 관례 |
| {B} 로봇 바닥 | 첫 관절 축 위 | 로봇 설치 |
| {E} 라켓 | 라켓 면 중심, z 가 면의 법선 | 정기구학(3편) |
2. 그림: 좌표계 사슬
- 화살표 하나가 4×4 변환 행렬 하나입니다. 카메라→탁구대는 보정(calibration)으로 한 번 구하고, 탁구대→로봇 바닥은 설치할 때 재고, 로봇 바닥→라켓은 관절 각이 바뀔 때마다 정기구학으로 다시 계산합니다.
- 표기 ᴮT_T 는 「{T} 에서 표현된 것을 {B} 에서 표현하도록 바꾸는 변환」입니다. 아래첨자와 다음 변환의 위첨자가 맞물려야 곱할 수 있으므로, 이 표기를 지키면 곱하는 순서를 틀리기 어렵습니다.
3. 계산
3.1 회전 행렬
회전 행렬(rotation matrix) R 은 3×3 이고, 열 하나하나가 회전된 좌표계의 축을 원래 좌표계에서 본 단위 벡터입니다. 그래서 항상 다음 두 성질을 만족합니다.
Rᵀ R = I (열들이 서로 수직이고 길이가 1)
det R = +1 (오른손 좌표계를 유지, -1 이면 거울상)
z 축을 중심으로 θ 만큼 돌리는 회전은 다음과 같습니다. 회전 두 개를 이어 붙이면 행렬을 곱하고, 되돌리려면 전치(transpose)만 하면 됩니다. 역행렬을 따로 계산할 필요가 없습니다.
Rz(θ) = [ cos θ −sin θ 0 ]
[ sin θ cos θ 0 ]
[ 0 0 1 ]
3.2 동차 변환
회전과 이동을 한 번에 다루려고 4×4 행렬을 씁니다. 이를 동차 변환(homogeneous transformation)이라 부릅니다. 점에는 네 번째 성분으로 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 이 되어 요와 롤을 따로 정할 수 없습니다. 두 회전축이 겹쳐서 자유도 하나가 사라진 것이고, 이를 짐벌 잠금(gimbal lock)이라 부릅니다. 정확히 90° 일 때만 문제가 되는 것이 아니라 근처에서 잡음이 크게 부풀어 오른다는 점이 실제로 더 중요합니다. 4.3 절에서 잽니다.
3.4 쿼터니언
단위 쿼터니언(unit quaternion) q = (w, x, y, z) 는 회전축 k 와 회전각 θ 로 q = (cos θ/2, k·sin θ/2) 입니다. 숫자 네 개로 회전을
나타내고, 짐벌 잠금이 없으며, 두 회전 사이를 부드럽게 보간하기 쉽습니다. 대신 q 와 −q 가 같은 회전이라는 점(이중 덮개)을 늘 기억해야
합니다. 자세 오차를 계산할 때 부호를 맞추지 않으면 가까운 두 자세가 360° 가까이 다르다고 나옵니다.
이 시리즈의 코드에서 쿼터니언 곱은 순수 파이썬 함수 하나입니다.
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 카메라에서 탁구대로: 표식점으로 자세 구하기
카메라 두 대로 삼각측량을 하면 카메라 좌표 {C} 의 3차원 점을 얻습니다. 탁구대 모서리처럼 탁구대 좌표 {T} 를 아는 점 N 개를 카메라로도 재면, 두 점 집합을 가장 잘 겹치는 R 과 p 를 구할 수 있습니다. 두 집합의 중심을 빼고 공분산 행렬을 특이값 분해(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 로 가장 가까운 회전 행렬에 되돌리면 백만 번 뒤에도 1.2×10⁻¹⁵ 였습니다.
해석은 둘입니다. 첫째, PC 의 float64 에서는 표류를 걱정할 일이 거의 없습니다. 백만 번은 1 kHz 제어로 17분입니다. 둘째, float32 를 쓰는 마이크로컨트롤러에서는 이야기가 다릅니다. 10만 번(1 kHz 로 100초)에 2.6×10⁻⁴ 은 축이 약 0.015° 비틀린 것과 비슷한 크기이고 계속 자랍니다. 모터 제어기나 IMU 펌웨어처럼 float32 로 회전을 누적하는 코드는 주기적으로 다시 정규화해야 합니다. 쿼터니언은 길이 하나만 1 로 나누면 되므로 이 일이 행렬보다 쉽습니다.
4.2 계산 시간
| 연산 | 한 번(µs) |
|---|---|
| numpy 3×3 행렬 곱 | 0.68 |
| numpy 4×4 행렬 곱 | 0.66 |
| numpy 3×3 행렬 × 벡터 | 0.60 |
| 순수 파이썬 쿼터니언 곱 | 0.55 |
| 순수 파이썬 쿼터니언으로 벡터 회전 | 1.33 |
numpy 는 3×3 이든 4×4 든 0.6~0.7 µs 로 거의 같습니다. 이 크기에서는 곱셈 자체보다 함수 호출 비용이 대부분이기 때문입니다. 그러니 파이썬에서 표현을 고를 때 속도는 기준이 되지 않습니다. 짐벌 잠금, 보간, 정규화의 쉬움 같은 성질로 고르면 됩니다.
4.3 짐벌 잠금 근처의 잡음 증폭
ZYX 자세를 만들고 0.0057°(10⁻⁴ rad) 크기의 작은 무작위 회전을 잡음으로 더한 뒤, 3.3 절의 식으로 각을 다시 뽑았습니다. 피치를 90° 에 점점 가깝게 하면서 각 2,000번씩 시도했습니다.
| 피치와 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 에 두고 탁구대 가운데를 내려다보게 했습니다. 탁구대 네 모서리, 네트 기둥 위 두 점, 가운데 선 양 끝을 기본 표식으로 쓰고, 더 많은 점이 필요하면 탁구대 위 임의의 점을 더했습니다. 카메라 좌표의 점마다 가우시안 잡음 σ 를 더하고 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 |
- 카메라 위치 오차가 탁구대 위 점의 오차보다 큽니다. 카메라는 표식점들의 중심에서 3 m 넘게 떨어져 있어 작은 회전 오차가 지렛대처럼 커지지만, 우리가 관심 있는 것은 카메라 자체가 아니라 탁구대 위의 공 위치입니다. 공은 표식점 근처를 날아다니므로 오차가 작게 남습니다.
- 점을 8개에서 32개로 늘리면 오차가 약 √4 = 2 배가 아니라 1.7배 줄었습니다. 임의로 더한 점이 탁구대 면 위에만 있어 모서리처럼 넓게 퍼진 점보다 정보가 적기 때문으로 봅니다(추정).
- 1편의 시간 예산에서 공의 위치 잡음은 6편이 다루는데, 보정 오차는 매 프레임 잡음과 달리 모든 프레임에 같은 방향으로 남는 치우침입니다. 칼만 필터는 잡음을 줄여 주지만 치우침은 줄이지 못합니다.
5. 한계
- 카메라 두 대의 삼각측량으로 이미 3차원 점을 얻었다고 가정했습니다. 카메라 한 대의 영상 좌표에서 자세를 구하는 PnP 문제, 렌즈 왜곡, 두 카메라 사이의 시간 어긋남은 다루지 않았습니다(미실측).
- 잡음을 각 축에 같은 크기로 넣었습니다. 실제 삼각측량 잡음은 카메라 시선 방향으로 훨씬 큽니다(미실측).
6. 정리
- 좌표계마다 이름을 붙이고, 변환에는 「어디에서 어디로」를 적습니다. 회전의 역은 전치, 동차 변환의 역은 전용 식입니다.
- 내부 계산은 회전 행렬이나 쿼터니언으로 합니다. 오일러 각은 피치 90° 근처에서 잡음을 수백 배로 키웁니다.
- float32 로 회전을 오래 누적하면 정규화가 필요합니다. float64 에서는 백만 번에도 10⁻¹² 수준입니다.
- 표식점 8개, 점 잡음 5 mm 면 탁구대 위 위치 오차는 수 mm 입니다. 이 오차는 모든 프레임에 똑같이 남는 치우침이라 필터로 지울 수 없습니다.
티티 한마디. 같은 공도 누가 보느냐에 따라 좌표가 달라요. 라켓을 세웠다 눕혔다 할 때는 오일러 각 말고 쿼터니언을 꼭 쥐고 있어야 해요!
참고
모든 자료는 2026-10-18 에 열람했습니다.
- Lynch, Park, Modern Robotics: Mechanics, Planning, and Control, Cambridge University Press 2017, 공개 프리프린트(2017년 5월), 3장 강체 운동.
- ITTF, Statutes 2026, 2.1.1·2.2.2(탁구대·네트 기둥 위치).
- Google DeepMind, competitive_robot_table_tennis, 좌표계 설명(README), CC-BY 4.0.