PART 4 텔레오퍼레이션과 모방학습 · 약 30분

14손목 카메라와 시각화

UVC32·D405·D435i·Gemini 2 손목 카메라 마운트와 URDF 조립체, 카메라 좌표 변환을 익힙니다.

카메라가 모방학습의 절반입니다#

정책(16장)은 사람처럼 눈으로 보고 손을 움직입니다. 관절 각도만으로는 "블록이 어디 있는지" 를 알 수 없으니, 카메라 영상이 입력의 절반을 차지합니다. 보통 두 종류를 함께 씁니다.

카메라붙이는 곳잘 보이는 것약한 것
손목 카메라 (eye-in-hand)그리퍼 바로 위, 팔과 함께 움직임집기 직전의 근접 장면, 손가락과 물체의 상대 위치전체 배치, 팔 자세
고정 카메라 (eye-to-hand)별도 스탠드·삼각대작업대 전체, 물체의 대략 위치가림(팔이 가리면 안 보임), 미세 정렬
좋은 카메라 배치의 판별 기준

Seeed 위키의 데이터 수집 팁에 이런 말이 있습니다. "화면의 카메라 영상만 보고 내가 직접 그 작업을 할 수 있어야 한다." 손목 영상과 고정 영상만 보고도 집을 수 있다면 정책도 배울 수 있습니다.

이 장에서는 reBot 용 손목 카메라 마운트 4종, 카메라를 URDF 에 붙이는 방법, 카메라 좌표와 로봇 좌표를 오가는 변환(내부·외부 파라미터, 핸드-아이)을 배웁니다.


손목 카메라 마운트 4종#

reBot 저장소의 hardware/camera-mounts 에는 B601-RS 와 B601-DM 이 함께 쓰는 손목 카메라 마운트 STEP 파일이 있습니다. README 는 대표 4종을 다음처럼 정리합니다.

카메라마운트 STEP 파일종류특징 (출처)
32×32 UVC 보드 카메라UVC32_mount.step일반 RGB (USB UVC)가장 작고 가벼움. 안착면 구멍 간격 28×28 mm
RealSense D405 / Orbbec Gemini 305D405_305_Mount.step근거리 스테레오 깊이Seeed 위키: D405 는 7~50 cm 근거리 정밀 작업용
RealSense D435iD435_Gemini2_Mount.step깊이 + RGB + IMU중·근거리, 3D 재구성·SLAM 에도 사용
Orbbec Gemini 2D435_Gemini2_Mount.step (D435i 와 공용)깊이 + RGBOrbbec SDK, LeRobot orbbec 타입

카탈로그에는 이 밖에 D455f_Mount.step 도 있고, 손목이 아닌 고정 카메라용 스탠드(V1: 3D 프린팅 몸체 + 어댑터 4종, V2: 알루미늄 프로파일 조립체)도 있습니다. 카탈로그 README 는 "치수·체결 부품·출력 설정·호환 검증은 제공하지 않으니 출력 전에 CAD 형상을 실제 카메라와 대조하라" 고 적고 있습니다.

어떤 카메라를 고를까 — 입문자 기준
  • 처음 모방학습 → UVC 같은 일반 RGB 카메라 + 고정 카메라 1대로 충분합니다. LeRobot 의 ACT·SmolVLA 예제는 RGB 영상만 씁니다.
  • 집기 직전 거리 정보가 필요 → 근거리에 강한 D405 가 손목에 잘 맞습니다.
  • 깊이로 물체 위치를 계산하는 비전 그리핑(21장) → D405·D435i·Gemini 2 같은 RGB-D.
로봇 팔 (RS / DM 공식 URDF) base_link link1 … link5 link6 RS: gripper_end (z 0.16621) DM: end_link (z 0.15539) RS: 같은 이름에 합침 / DM: 고정 조인트 추가 카메라 조립체 (Camera/urdf/*.urdf) camera_mount_link (브래킷) camera_bottom_screw_frame camera_link → camera_color_frame → camera_color_optical_frame X 오른쪽 Y 아래 Z 시선(화면 밖으로) 광학 프레임의 축 약속
손목 카메라 조립체는 그리퍼 몸체 프레임(RS gripper_end / DM end_link) 아래에 고정 조인트로 매달립니다.

카메라 URDF 조립체#

마운트를 실제로 달았다면, 로봇 모델(URDF)에도 같은 카메라를 달아야 RViz·MuJoCo·좌표 변환이 맞습니다. Rebot_Arm_description/Camera 폴더가 이를 위한 4종 카메라 조립체 URDF 를 제공합니다.

text
Camera/urdf/
├── uvc32.urdf
├── d405.urdf
├── d435i.urdf
└── gemini2.urdf

D405 조립체의 링크 사슬은 다음과 같습니다(파일에서 확인한 이름).

text
gripper_end            ← 비어 있는 자리표시(placeholder) 루트
 └─ gripper_to_mount   (fixed) → camera_mount_link        ← 마운트(브래킷)
     └─ camera_joint   (fixed) → camera_bottom_screw_frame ← 카메라 바닥 나사 기준
         └─ camera_link
             ├─ camera_color_frame → camera_color_optical_frame
             ├─ camera_depth_frame → camera_depth_optical_frame
             └─ camera_infra1/2_frame → ..._optical_frame

RS 와 DM 에 붙이는 방법이 다릅니다#

네 URDF 는 모두 **빈 gripper_end 링크를 뿌리**로 삼습니다. README 의 안내는 이렇습니다.

모델그리퍼 몸체 링크합치는 방법
B601-RSgripper_end (link6 에서 z 0.16621 m)카메라 URDF 의 **빈 gripper_end 를 지우고** 나머지 링크·조인트·재질을 RS URDF 에 합칩니다. 이름이 같아서 그대로 이어 붙습니다.
B601-DMend_link (link6 에서 z 0.15539 m)카메라 루트 링크를 남겨 두고, **고정 조인트로 end_link 에 연결**합니다. 그 조인트의 origin 에 실제 장착 위치에 맞는 변환을 넣어야 합니다.

DM 쪽 연결은 예를 들면 이런 모양입니다.

xml
<!-- DM 용 예시: origin 값은 실제 장착 위치에 맞게 정해야 합니다 (0 은 자리표시) -->
<joint name="end_to_camera_root" type="fixed">
  <parent link="end_link"/>
  <child link="gripper_end"/>   <!-- 카메라 조립체의 루트 링크 -->
  <origin xyz="0 0 0" rpy="0 0 0"/>
</joint>
조립체는 한 번에 하나만

네 조립체는 camera_link 같은 링크 이름을 서로 공유합니다. README 는 한 번에 하나만 골라 쓰라고 합니다. 또 메시 경로가 urdf/ 기준 상대 경로이므로 Camera/ 폴더를 통째로 복사해야 하고, ROS 에서는 package:// 경로로 바꿔야 합니다. D435i 는 Collada(.dae) 메시를 쓰므로 로더가 이를 지원해야 합니다.

장착 파라미터 표#

README 의 장착 파라미터입니다(단위: 미터, 라디안). 브래킷은 조립체 루트(gripper_end) 기준, 카메라는 브래킷 링크 기준입니다.

카메라브래킷 XYZ브래킷 RPY카메라 XYZ카메라 RPY
D405−0.1201, 0.0003, 0.05071.5827, 0.0024, 1.5515−0.0003, 0.0443, −0.0093−0.0225, −1.0448, −1.5460
D435i−0.1201, 0.0003, 0.04501.5827, 0.0024, 1.5515−0.0003, 0.0445, −0.00950.0126, −1.0489, −1.5981
Gemini 2−0.1201, 0.0003, 0.04501.5827, 0.0024, 1.55150.0249, 0.0487, 0.00900.0042, −1.0502, −1.5718

읽는 법: 그리퍼 몸체 원점의 접근 방향이 +X 이므로, 브래킷 X 가 −0.12 m 라는 것은 마운트가 TCP 에서 손목 쪽으로 약 12 cm 뒤에 있다는 뜻입니다. 카메라 RPY 의 약 −1.05 rad(약 −60°) 기울기는 카메라가 그리퍼 끝을 비스듬히 내려다보도록 고개를 숙인 각도입니다. (UVC 는 CAD 에서 생성한 값이며 표에 별도로 있습니다.)

실습 — URDF 뷰어로 그리퍼 몸체 프레임 확인
  1. URDF 뷰어에서 RS 모델을 열고 링크 트리에서 gripper_end 를 찾습니다. 조인트 축·프레임 표시를 켜면 +X(빨강)가 그리퍼가 뻗는 방향입니다.
  2. DM 모델로 바꿔 end_link 를 찾아 같은 방향인지 비교합니다.
  3. 저장소에서 Camera/urdf/d405.urdf 를 내려받아 뷰어에 드래그 앤 드롭 해 보세요. 메시가 없으면 형상은 비어 보이지만, 링크 트리에서 위 사슬 구조를 확인할 수 있습니다.

카메라 좌표계와 광학 프레임#

카메라 링크가 왜 camera_color_frame 과 camera_color_optical_frame 두 개나 있을까요? ROS 관례(REP 103) 때문입니다.

프레임XYZ쓰임
camera_color_frame (일반 몸체 프레임)앞왼쪽위로봇 쪽 좌표와 같은 약속
camera_color_optical_frame (광학 프레임)오른쪽아래앞(시선)이미지 처리·점군의 약속

D405 URDF 의 두 프레임 사이 조인트에는 rpy="-1.5708 0 -1.5708" 이 들어 있습니다. 축 이름을 바꿔 붙이는 회전일 뿐 위치는 같습니다. 깊이 카메라가 주는 3D 점(X, Y, Z)은 광학 프레임 기준이라는 것만 기억하세요. Z 가 곧 "카메라에서 얼마나 멀리" 입니다.


내부 파라미터 — 픽셀과 3D 점#

카메라는 3D 세상을 2D 픽셀로 찍습니다. 가장 단순한 핀홀(바늘구멍) 모델에서 광학 프레임의 점 (X, Y, Z) 는 다음 픽셀 (u, v) 에 찍힙니다.

text
u = fx · X / Z + cx
v = fy · Y / Z + cy
  • fx, fy — 초점 거리(픽셀 단위). 클수록 확대(좁은 화각)
  • cx, cy — 주점. 보통 영상 중심 근처(640×480 이면 약 320, 240)

이 네 값(과 렌즈 왜곡 계수)을 내부 파라미터(intrinsics) 라고 합니다. RealSense·Orbbec 같은 깊이 카메라는 공장 캘리브레이션 값을 SDK 로 읽을 수 있고, 일반 UVC 카메라는 체커보드 등으로 직접 구합니다.

카메라 중심 Z (시선) 이미지 평면 (거리 f) (u, v) (X, Y, Z) X 깊이 Z (깊이 카메라가 측정) 닮은 삼각형: u − cx = fx · X / Z
핀홀 모델: 깊이 Z 를 알면 픽셀 (u, v) 를 거꾸로 3D 점으로 되돌릴 수 있습니다.

깊이 카메라의 진짜 힘은 거꾸로 가는 계산입니다. 픽셀 (u, v) 에서 깊이 Z 를 읽으면,

python
# 예시 값입니다 (실제 값은 카메라 SDK 나 캘리브레이션으로 구합니다)
fx, fy, cx, cy = 600.0, 600.0, 320.0, 240.0
u, v, Z = 400, 300, 0.25          # 픽셀 (400, 300) 의 깊이가 0.25 m

X = (u - cx) * Z / fx
Y = (v - cy) * Z / fy
print(f"광학 프레임 점: X={X:.3f}  Y={Y:.3f}  Z={Z:.3f} m")
# → X=0.033 (오른쪽)  Y=0.025 (아래)  Z=0.250 (앞)

외부 파라미터와 핸드-아이#

이제 이 점을 로봇 베이스 좌표로 바꿔야 팔을 그곳으로 보낼 수 있습니다. 손목 카메라라면 변환이 사슬로 이어집니다.

text
p_base = T_base←tcp(q) · T_tcp←cam · p_cam
          ───────────    ──────────
          정기구학(10장)   외부 파라미터(카메라가 손목 어디에 붙었나)
  • T_base←tcp(q) 는 관절각 q 로 매 순간 계산되는 정기구학입니다(10장).
  • T_tcp←cam 은 카메라가 그리퍼에 어떻게 붙어 있는지 — 외부 파라미터(extrinsics) 입니다. 손목 카메라는 팔과 함께 움직이므로 이 값이 고정입니다. 이것이 eye-in-hand 의 특징입니다.

외부 파라미터는 두 가지 방법으로 얻습니다.

  1. URDF 값 — 위 장착 파라미터 표처럼 CAD 에서 나온 이론값. 시각화·대략적 계산에 충분합니다.
  2. 핸드-아이 캘리브레이션 — 실제 조립 오차까지 반영한 측정값. Seeed 의 비전 그리핑 데모는 ArUco 마커를 놓고 팔을 여러 자세로 움직이며 마커를 찍은 뒤, TSAI 방식 Eye-in-Hand 로 이 변환을 계산합니다(scripts/collect_handeye_eih.py, 자동 모드 50개 자세, 최소 5개·권장 15개 이상 샘플). 결과는 intrinsics.npz, hand_eye.npz 로 저장됩니다. 자세한 실습은 21장에서 합니다.
마운트를 다시 조이면 캘리브레이션도 다시

핸드-아이 결과는 "그 나사 조임 상태" 의 측정값입니다. 마운트를 풀었다 다시 달거나 카메라가 살짝 돌아가면 수 mm~수 cm 오차가 생깁니다. 모방학습 데이터 수집에서도 카메라를 움직이면 이전 데이터와 다른 시점이 되어 정책 성능이 떨어집니다.

실습 — Playground 로 카메라 점을 베이스 좌표로 바꾸기

시뮬레이터 Python 탭에서 실행해 보세요. 계산을 단순하게 하려고 "카메라가 TCP 위치에 있고 그리퍼 접근 방향(+X)을 바라본다" 고 가정합니다.

python
import math

arm.move_to(0.40, 0.0, 0.10, t=1.5, down=True)   # 그리퍼를 아래로
x, y, z, r, p, yw = arm.tcp_pose()                 # 미터·도
print("TCP:", round(x, 3), round(y, 3), round(z, 3), " rpy:", round(r), round(p), round(yw))

def rpy_to_R(roll, pitch, yaw):                    # URDF 관례 R = Rz·Ry·Rx (도 입력)
    a, b, c = [math.radians(v) for v in (roll, pitch, yaw)]
    ca, sa, cb, sb, cc, sc = math.cos(a), math.sin(a), math.cos(b), math.sin(b), math.cos(c), math.sin(c)
    return [[cc*cb, cc*sb*sa - sc*ca, cc*sb*ca + sc*sa],
            [sc*cb, sc*sb*sa + cc*ca, sc*sb*ca - cc*sa],
            [-sb,   cb*sa,            cb*ca]]

R = rpy_to_R(r, p, yw)
approach = [R[0][0], R[1][0], R[2][0]]             # TCP 의 +X 축 = 접근 방향
print("접근 방향:", [round(v, 2) for v in approach])  # 아래(0, 0, -1)에 가까워야 정상

d = 0.07                                            # 카메라가 앞쪽 0.07 m 에서 물체를 봤다면
target = [x + d*approach[0], y + d*approach[1], z + d*approach[2]]
print("물체의 베이스 좌표:", [round(v, 3) for v in target])
arm.move_to(target[0], target[1], target[2] + 0.05, t=1.5, down=True)  # 5 cm 위로 접근

접근 방향이 (0, 0, −1) 근처로 나오면 회전 관례가 맞는 것입니다. d 를 바꿔 가며 팔이 어디로 내려가는지 보세요.


시뮬레이터 카메라 패널과 LeRobot 카메라 설정#

실습 — 가상 손목 · 스탠드 카메라
  1. 시뮬레이터의 뷰 옵션에서 카메라 패널을 켭니다. 손목(wrist)과 스탠드(top) 가상 카메라 창이 나타납니다.
  2. 뷰 옵션에서 큐브를 켜고, 조인트 탭의 슬라이더로 팔을 큐브 위로 가져갑니다. 손목 화면에서 큐브가 점점 커지고, 스탠드 화면에서는 팔이 큐브를 가리기 시작하는 것을 확인하세요.
  3. joint6(손목 회전)만 돌려 보세요. 손목 영상은 통째로 돌지만 스탠드 영상은 거의 그대로입니다. 두 카메라가 서로 다른 정보를 준다는 뜻입니다.
  4. "스탠드 화면만 보고 큐브를 집을 수 있나?", "손목 화면만 보고는?" 을 각각 시도해 보세요. 둘 다 있어야 쉬운 이유를 몸으로 느낄 수 있습니다.

MuJoCo 기반 온라인 데모(README 소개)에서도 B601-RS 의 D405 손목 카메라와 상단 카메라 영상을 브라우저로 볼 수 있습니다.

실물에서는 LeRobot 의 --robot.cameras 에 카메라를 적습니다. Seeed 위키 기준으로 일반 USB 카메라는 opencv 타입, 깊이 카메라는 Seeed LeRobot 포크의 DepthCameraSupport 브랜치에서 realsense_d405_color · realsense_d405_depth · orbbec_color 같은 타입을 씁니다. 카메라 찾기는 다음 명령입니다.

bash
lerobot-find-cameras opencv      # 일반 USB 카메라 (인덱스 확인)
lerobot-find-cameras realsense   # RealSense (일련번호 확인)
lerobot-find-cameras orbbec      # Orbbec
위키 권장 사항이유
해상도 640×480 유지안정성과 실시간성의 균형
깊이 카메라 3대 이상이면 fps 15전체 안정성
USB 카메라는 허브 없이 직접 연결영상 전송 속도 확보
depth_alpha 로 깊이 영상 표시 배율 조정위키 예시 0.2(D435i·Gemini 2), 0.03(D405)

전체 명령은 15장에서 녹화와 함께 다룹니다.


정리#

  • 손목 카메라(eye-in-hand)는 근접 장면을, 고정 카메라(eye-to-hand)는 전체 배치를 봅니다. 둘을 함께 쓰는 것이 기본입니다.
  • 공식 손목 마운트는 RS·DM 공용 4종: UVC32, D405/Gemini 305, D435i/Gemini 2(공용 마운트) — STEP 파일로 제공됩니다.
  • 카메라 URDF 4종은 빈 gripper_end 루트를 가집니다. RS 는 자리표시를 지우고 기존 gripper_end 에 합치고, DM 은 end_link 에 고정 조인트(실제 장착 변환)로 연결합니다. 한 번에 하나만 씁니다.
  • 내부 파라미터(fx, fy, cx, cy)는 픽셀 ↔ 3D 점, 외부 파라미터는 카메라 ↔ 그리퍼 관계입니다. p_base = T_base←tcp(q) · T_tcp←cam · p_cam.
  • 정확한 외부 파라미터는 핸드-아이 캘리브레이션(ArUco + TSAI Eye-in-Hand)으로 구하며, 마운트를 다시 달면 다시 해야 합니다.

확인 문제#

  1. 손목 카메라와 고정 카메라가 각각 잘 보는 것과 약한 것을 하나씩 적어 보세요.
  2. D435i 와 Gemini 2 손목 카메라는 어떤 마운트 파일을 공유하나요?
  3. 카메라 URDF 조립체를 B601-DM 에 붙일 때 RS 와 달리 무엇을 해야 하나요?
  4. fx = fy = 600, cx = 320, cy = 240 인 카메라에서 픽셀 (320, 240) 의 깊이가 0.3 m 입니다. 광학 프레임 좌표는?
  5. eye-in-hand 구성에서 T_tcp←cam 이 "고정값" 인 이유와, 이 값이 틀어지는 대표적인 상황은?
정답
  1. 손목 카메라: 집기 직전의 근접 장면에 강하고, 전체 배치·팔 자세는 약합니다. 고정 카메라: 작업대 전체·물체 대략 위치에 강하고, 팔이 가리면 안 보이고 미세 정렬에 약합니다.
  2. D435_Gemini2_Mount.step 입니다.
  3. 카메라 루트 링크를 남겨 두고 **고정 조인트로 end_link 에 연결**하며, 그 조인트 origin 에 실제 장착 위치에 맞는 변환을 넣어야 합니다(RS 는 빈 gripper_end 를 지우고 기존 gripper_end 에 바로 합칩니다).
  4. 영상 중심이므로 X = 0, Y = 0, Z = 0.3 m — 시선 방향으로 정면 30 cm 입니다.
  5. 카메라가 그리퍼에 나사로 고정되어 팔과 함께 움직이므로 둘 사이 관계는 변하지 않습니다. 마운트를 풀었다 다시 달거나 충돌로 카메라가 돌아가면 틀어지므로 핸드-아이 캘리브레이션을 다시 해야 합니다.