PART 6 확장과 마무리 · 약 35분

21비전 그리핑 · 음성 · 에이전트

YOLO + 깊이 카메라 그리핑, reSpeaker 음성 제어, 자연어 명령 에이전트 구조를 살펴봅니다.

보고, 듣고, 알아듣는 로봇팔#

지금까지 reBot 은 우리가 숫자로 알려 준 곳으로만 움직였습니다. "x 0.35 m, y 0, z 0.20 m 로 가라" 같은 식이었지요. 실제 세상에서 쓸모 있는 로봇이 되려면 세 가지가 더 필요합니다.

능력사람으로 치면reBot 공식 예제이 장의 절
보기 — 물체가 어디 있는지 스스로 찾기눈YOLO + 깊이 카메라 그리핑 데모비전 그리핑
듣기 — 소리가 난 방향과 말의 뜻 알기귀reSpeaker Flex 음성 제어음성 제어
알아듣기 — "빨간 블록 집어 줘"를 동작 순서로 바꾸기머리임바디드 에이전트(wrc_demo)에이전트

이렇게 몸을 가지고 실제 세상에서 보고·듣고·움직이는 AI 를 임바디드 AI(Embodied AI, 체화된 AI) 라고 부릅니다. reBot-DevArm 저장소가 첫 줄에 내세우는 목표도 "임바디드 AI 를 배우는 문턱을 낮추는 것"입니다.

이 장은 세 데모를 구조 중심으로 살펴봅니다. 설치 명령은 자주 바뀌므로 핵심만 보이고, 정확한 최신 절차는 각 위키 링크를 따라가세요. 대신 가장 중요한 수학인 "픽셀 + 깊이 → 카메라 좌표 → 로봇 좌표" 변환은 Playground 에서 직접 계산해 봅니다.


비전 그리핑 — 네 단계 파이프라인#

공식 그리핑 데모는 손목에 깊이 카메라(RGB-D) 를 달고, 화면에서 물체를 찾아 집습니다. 지원 카메라는 Orbbec Gemini 2, Intel RealSense D435i, D405 이고 USB 3.0 으로 연결합니다. 손목 거치대 STEP 파일은 저장소의 hardware/camera-mounts/ 에 있고, RS·DM 이 같은 거치대를 씁니다(14장).

① YOLO 인식 cube 0.94 가운데 픽셀 (u, v) ② 깊이 읽기 깊이 영상[v][u] d = 0.560 m (카메라 → 물체 윗면) ③ 좌표 변환 xc = (u − cx)·d / fx yc = (v − cy)·d / fy p_base = R·p_cam + t (0.34, −0.08, 0.04) ④ IK → 집기 접근점 (위 8 cm) → 직선 하강 → 그리퍼 닫기 → 들어올리기 "어디에 있나?"(비전) + "어떻게 갈까?"(기구학) = 보고 집는 로봇 fx, fy, cx, cy = 카메라 내부 파라미터 (공식 데모: intrinsics.npz) R, t = 카메라가 로봇 어디에 붙어 있는지 = 외부 파라미터 (공식 데모: hand_eye.npz) 공식 데모는 상자의 짧은 축으로 그리퍼 방향을, 깊이 분위수로 집는 높이를 정합니다
비전 그리핑의 네 단계: 인식 → 깊이 → 좌표 변환 → IK 로 집기
  1. 인식 — YOLO(You Only Look Once, 한 번에 화면 전체에서 물체를 찾는 신경망)가 물체를 찾아 상자를 그립니다. 공식 데모의 기본 모델은 yoloe-26l-seg.pt 로, 열린 어휘(open vocabulary) 모델이라 "yellow banana", "water bottle" 처럼 찾을 물체 이름을 글로 적어 주면 따로 학습하지 않아도 찾습니다. 가벼운 yoloe-26s-seg.pt, 정해진 종류만 찾는 yolov8n-seg.pt 도 고를 수 있습니다.
  2. 깊이 읽기 — 깊이 카메라는 픽셀마다 "카메라에서 거기까지 몇 m" 를 알려 줍니다. 상자 안 깊이 값들의 분위수(예: 가운데 값, depth_quantile 기본 0.5)로 집을 높이를 정합니다. 한 픽셀만 읽으면 잡음에 약하기 때문입니다.
  3. 좌표 변환 — 픽셀 (u, v) 와 깊이 d 로 카메라 기준 3D 좌표를 구하고, 이것을 로봇 베이스 기준 좌표로 바꿉니다(다음 절).
  4. 집기 — IK 로 닿을 수 있는지 확인한 뒤, 물체 위 접근점(pregrasp_offset_m 0.08 m)으로 가서 → 내려가서(insertion_depth_m 만큼 더 깊이) → 그리퍼를 닫고 → 들어 올려 준비 자세로 돌아옵니다.

그리퍼를 어느 방향으로 벌릴지는 물체를 감싼 최소 넓이 직사각형(OBB, 회전된 상자) 의 짧은 축 으로 정합니다. 바나나처럼 긴 물체는 짧은 쪽을 잡아야 손가락 사이에 들어가니까요.

공식 데모를 실행하는 흐름 (실물용)

RS 위키 기준으로 정리하면 이렇습니다. 정확한 명령과 옵션은 위키의 최신판을 따르세요.

bash
git clone https://github.com/Seeed-Projects/reBot-DevArm-Grasp.git rebot_grasp
cd rebot_grasp
conda env create -f environment.yml -n rebotarm
conda activate rebotarm
# sdk/reBotArm_control_py 에서 pip install -e .  (SDK 설치)
# RS 는 CAN 을 먼저 올립니다
sudo ip link set can0 type can bitrate 1000000
sudo ip link set can0 up

python scripts/object_detection.py           # ① 카메라·인식만 확인
python scripts/ordinary_grasp_pipeline.py    # ② 팔 없이 그립 추정만 확인
python scripts/collect_handeye_eih.py        # ③ 핸드-아이 보정
python scripts/main.py --dry-run             # ④ 팔을 움직이지 않고 전체 흐름 확인
python scripts/main.py                       # ⑤ 실제 집기 (G 키로 집기)

--dry-run 처럼 "팔은 빼고 먼저 돌려 보는" 단계가 있다는 점을 눈여겨보세요. 카메라 → 인식 → 계산 → 팔, 이렇게 한 층씩 확인하는 습관이 사고를 막습니다.


픽셀 + 깊이 → 카메라 좌표 → 로봇 좌표#

1단계 — 핀홀 공식: 픽셀을 미터로#

카메라는 바늘구멍 사진기처럼 동작한다고 볼 수 있습니다(핀홀 모델). 영상 중심 (cx, cy) 에서 (u − cx) 픽셀 떨어져 보이는 점은, 거리 d 에서 실제로 이만큼 떨어져 있습니다.

text
xc = (u − cx) · d / fx      ← 카메라 오른쪽 방향 (m)
yc = (v − cy) · d / fy      ← 카메라 아래쪽 방향 (m)
zc = d                      ← 카메라가 바라보는 방향 (m)

닮은 삼각형 원리입니다. 같은 10 픽셀이라도 멀리 있을수록(d 가 클수록) 실제 거리는 더 큽니다. fx, fy(초점 거리, 픽셀 단위)와 cx, cy(영상 중심)는 카메라마다 정해진 내부 파라미터(intrinsics) 입니다. 공식 데모는 이 값을 config/calibration/<카메라>/intrinsics.npz 에 저장합니다.

2단계 — 외부 파라미터: 카메라 좌표를 로봇 좌표로#

카메라 좌표는 "카메라가 본 세상"입니다. 로봇은 베이스 좌표(앞 +X, 왼쪽 +Y, 위 +Z)로 생각하므로 한 번 더 바꿔야 합니다. 카메라가 로봇 어디에, 어느 방향으로 붙어 있는지를 나타내는 회전 R 과 위치 t 를 외부 파라미터(extrinsics) 라고 합니다.

text
p_base = R · p_cam + t

10장의 변환 행렬과 똑같은 계산입니다. 문제는 R 과 t 를 자로 재서는 정확히 알 수 없다는 것입니다. 그래서 핸드-아이 보정(hand-eye calibration) 을 합니다.

eye-in-hand (손목 카메라) 카메라 base_T_cam = base_T_ee(q) · ee_T_cam base_T_ee(q): 관절각으로 FK 를 매번 계산 ee_T_cam: 핸드-아이 보정으로 한 번 구함 공식 그리핑 데모 방식 (TSAI, ArUco 마커) eye-to-hand (고정 카메라) 카메라 base_T_cam = 고정값 팔이 움직여도 변하지 않음 에이전트 데모(wrc)의 위쪽 카메라 방식
카메라가 손목에 있으면(eye-in-hand) FK 를 한 번 더 곱하고, 고정되어 있으면(eye-to-hand) 고정 변환 하나면 됩니다.
방식카메라 위치변환reBot 예
eye-in-hand손목(팔과 함께 움직임)base_T_cam = base_T_ee(q) · ee_T_cam — 지금 관절각으로 FK 를 계산해 곱함공식 그리핑 데모 (eye_in_hand 모드)
eye-to-hand스탠드·천장에 고정base_T_cam 하나가 고정값에이전트 데모의 위쪽(overhead) 카메라

핸드-아이 보정은 어떻게 하나#

공식 데모는 ArUco 마커(검은 테두리의 QR 비슷한 정사각형 표식)를 책상에 두고, 팔을 여러 자세로 옮기며 "그때의 손목 자세(FK)"와 "카메라가 본 마커 자세"를 짝지어 모읍니다. 이 짝들로부터 손목-카메라 사이 변환을 푸는 고전 알고리즘이 TSAI 방법 입니다.

  • 자동 모드: python scripts/collect_handeye_eih.py — 미리 정한 50개 자세를 돌며 마커가 안정적으로 보일 때 기록
  • 수동 모드: --manual — 중력 보상 상태에서 손으로 팔을 옮기고 Enter 로 기록
  • 최소 5개, 15개 이상 권장. 결과는 hand_eye.npz 로 저장
  • 마커 한 변 길이(calibration.aruco.marker_length_m, 예시 0.1 m)를 실제 인쇄한 크기와 똑같이 적어야 합니다. 이 값이 틀리면 모든 거리가 같은 비율로 틀어집니다.
  • 그래도 몇 mm 씩 어긋나면 calibration.hand_eye_compensation_m 에 X·Y·Z 보정값(m)을 넣어 미세 조정합니다.
그리퍼가 늘 같은 방향으로 1~2 cm 빗나간다면

대부분 보정 문제입니다. 마커 길이 설정, 샘플 수(15개 이상), 샘플 자세가 충분히 다양한지(회전까지 섞였는지) 확인하세요. 깊이가 흔들리면 depth_quantile, 카메라 높이, 반짝이는 표면(깊이 카메라가 약함)을 의심합니다. 이 내용은 22장 트러블슈팅 표에도 있습니다.


실습 — 가상 카메라로 큐브 집기#

실물 카메라가 없어도 변환 수학은 그대로 연습할 수 있습니다. 시뮬레이터의 큐브 위치(arm.cube())를 가짜 카메라가 픽셀로 찍었다고 치고, 우리가 그 픽셀을 다시 로봇 좌표로 되돌려 arm.move_to 로 집으러 갑니다.

실습 — 카메라 좌표 → 로봇 좌표 → arm.move_to

3D 시뮬레이터 Python 탭을 열고, 보기 옵션에서 큐브 가 켜져 있는지 확인한 뒤 아래 코드를 붙여 넣어 실행하세요.

python
import math

# 가상 카메라: 책상 위 0.60 m 에서 똑바로 내려다보는 고정 카메라 (eye-to-hand)
# 영상 위쪽 = 로봇 앞(+X) 이 되도록 달았다고 가정합니다.
FX, FY, CX, CY = 600.0, 600.0, 320.0, 240.0   # 내부 파라미터 (가상의 값)
CAM = (0.30, 0.0, 0.60)                        # 로봇 좌표계에서 카메라 위치 (m)
HALF = 0.02                                    # 깊이는 큐브 '윗면'까지: 윗면 = 중심 + HALF

def robot_to_pixel(p):
    """가짜 카메라: 로봇 좌표 → (u, v, 깊이). 실제로는 YOLO + 깊이 카메라가 주는 값"""
    xc = -(p[1] - CAM[1])
    yc = -(p[0] - CAM[0])
    d = CAM[2] - (p[2] + HALF)
    return round(CX + FX * xc / d), round(CY + FY * yc / d), round(d, 3)

def pixel_to_robot(u, v, d):
    """우리가 만들 함수: (u, v, 깊이) → 로봇 좌표 (큐브 중심)"""
    xc = (u - CX) * d / FX          # ① 핀홀 공식
    yc = (v - CY) * d / FY
    zc = d
    x = CAM[0] - yc                 # ② 외부 파라미터: R = [[0,-1,0],[-1,0,0],[0,0,-1]], t = CAM
    y = CAM[1] - xc
    z = CAM[2] - zc
    return (x, y, z - HALF)         # ③ 윗면 → 중심

cube = arm.cube()
if cube is None:
    print("보기 옵션에서 큐브를 켜 주세요.")
else:
    u, v, d = robot_to_pixel(cube)
    print(f"카메라가 본 것  : 픽셀 ({u}, {v}), 깊이 {d} m")
    x, y, z = pixel_to_robot(u, v, d)
    err = math.dist((x, y, z), cube) * 1000
    print(f"계산한 로봇 좌표: ({x:.3f}, {y:.3f}, {z:.3f}) m  (실제와 {err:.1f} mm 차이)")

    arm.home()
    arm.open()
    arm.move_to(x, y, z + 0.08, t=1.5, down=True)   # 접근점: 8 cm 위
    arm.move_line(x, y, z, t=1.0)                    # 곧게 내려가기
    arm.close()
    arm.wait(0.5)
    arm.move_line(x, y, z + 0.08, t=1.0)             # 들어 올리기

확인할 것

  1. 출력의 "실제와 차이" 가 1 mm 안팎인가요? 픽셀을 정수로 반올림했기 때문에 생기는 오차입니다. 깊이 0.56 m 에서 1 픽셀은 약 0.56 / 600 ≈ 0.9 mm 입니다.
  2. pixel_to_robot 안의 d 에 + 0.01 을 더해(깊이 카메라 오차 1 cm) 다시 돌려 보세요. 어느 축이 가장 많이 틀어지나요?
  3. CAM 을 (0.31, 0.0, 0.60) 으로 바꿔 외부 파라미터가 1 cm 틀린 상황을 만들어 보세요. 단, robot_to_pixel 은 진짜 카메라이니 그대로 두고 pixel_to_robot 만 틀린 값을 쓰게 해야 합니다. 그리퍼가 늘 한쪽으로 빗나가는 것 — 이것이 핸드-아이 보정이 틀렸을 때의 모습입니다.

move_line 이 경고를 내면 그 줄을 arm.move_to(x, y, z, t=1.0, down=True) 로 바꿔 보세요.

시뮬레이터와 실물의 차이

시뮬레이터 큐브는 "그리퍼가 충분히 닫히고 TCP 가 가까우면 붙는" 단순 규칙이라 높이가 조금 틀려도 잡힙니다. 실물에서는 TCP(그리퍼 몸체 원점)와 손가락 끝 사이 거리, 물체 높이, 책상과의 충돌을 모두 따져야 해서 공식 데모에 insertion_depth_m, min_base_z_m(이보다 낮게는 내려가지 않음) 같은 값이 있습니다.


음성 제어 — reSpeaker Flex 와 소리 방향(DoA)#

두 번째 데모는 마이크 배열 reSpeaker Flex XVF3800(마이크 4개가 원형으로 배치, XIAO ESP32S3 와 짝을 이룬 제품)로 reBot B601-DM 을 움직입니다. 두 가지 모드가 있습니다.

DoA 모드 — 소리가 난 쪽으로 돌아보기#

DoA(Direction of Arrival, 도래 방향) 는 소리가 어느 방향에서 왔는지를 각도로 알려 주는 기능입니다. 마이크 4개에 소리가 도착하는 시간 차이로 계산하며, XVF3800 칩이 0~360° 값을 USB 로 컴퓨터에 보내 줍니다. 위키에 소개된 처리 방식은 이렇습니다.

  1. 최근 4개 각도를 모아 가중 평균 하고, 튀는 값을 걸러냅니다(잡음·선풍기 소리 대책).
  2. 각도가 15° 이상(--threshold) 바뀌면 팔이 그쪽으로 돌아 고개를 끄덕이고, 3초(--cooldown) 동안 쉽니다.
  3. 소리가 없으면 "숨 쉬는" 듯한 대기 동작을 합니다.

음성 모드 — 말을 명령으로#

Enter 를 누르고 있는 동안 녹음하고, 놓으면 Whisper(음성→글자)로 받아 적은 뒤 LLM(Llama-3.3-70B, Groq API 경유)이 문장을 다음과 같은 구조화된 JSON 으로 바꿉니다. 답변은 Edge-TTS 로 말해 줍니다.

json
{"action": "turn_left", "params": {}, "reply": "왼쪽으로 돌게요"}

지원 동작은 왼쪽/오른쪽 돌기, 인사(끄덕이기), 손 흔들기, 영점 복귀, 정지 정도로 단순합니다. 중요한 것은 구조입니다 — LLM 은 팔을 직접 움직이지 않고, 정해진 동작 목록 중 하나를 고를 뿐 입니다.

bash
# 실물용 — 위키 기준. 설치는 위키의 conda/uv 절차를 먼저 따르세요.
python sound_tracking_arm.py --mode doa      # 소리 방향 추적
python sound_tracking_arm.py --mode voice    # 음성 명령
API 키는 코드에 적지 마세요

위키 예제는 Groq API 키를 파이썬 파일에 붙여 넣는 방식을 안내하지만, 그 파일을 깃허브에 올리면 키가 그대로 공개됩니다. --groq-key 옵션이나 환경 변수로 넘기는 습관을 들이세요. 음성 모드는 인터넷 연결이 필요하고, 무료 계정은 요청 수 제한이 있습니다.

실습 — 소리 방향을 관절 1 각도로

DoA 각도(0~360°)를 베이스 회전(joint1) 명령으로 바꾸는 함수를 만들어 봅니다. Python 탭에서 실행하세요.

python
def doa_to_joint1(doa_deg, mic_front=0):
    """마이크 각도 → joint1 각도(도). mic_front = 로봇 정면에 해당하는 마이크 각도"""
    a = (doa_deg - mic_front + 180) % 360 - 180      # -180 ~ +180 로 접기
    if arm.model == 'RS':
        a = -a          # RS 의 joint1 축은 (0,0,-1): 양(+)의 각도가 시계 방향
    lo, hi = arm.limits()['joint1']
    return max(lo, min(hi, a))                       # 관절 한계로 자르기

arm.home()
for doa in [40, 90, 200, 330]:
    j1 = doa_to_joint1(doa)
    print(f"소리 {doa:3d}° → joint1 {j1:6.1f}°")
    arm.move_joint(1, j1, t=1.0)
    arm.move_joint(5, 20, t=0.3)    # 끄덕
    arm.move_joint(5, 0, t=0.3)
    arm.wait(0.5)
  • 소리 90°(왼쪽) 일 때 팔이 화면에서 로봇의 왼쪽(+Y) 으로 도는지 확인하세요. 모델을 RS ↔ DM 으로 바꿔도 같은 쪽으로 돌아야 합니다.
  • 200° 는 거의 뒤쪽입니다. 접은 값 −160° 가 joint1 한계(약 ±160.4°) 안에 들어가는지 출력으로 확인하세요.

임바디드 에이전트 — 말로 시키면 스스로 계획#

세 번째는 wrc_demo 입니다. "빨간 블록 집어 줘" 같은 자연어 명령을 받아 스스로 물체를 찾고, 집는 방법을 계획하고, 안전 검사를 통과한 동작만 실행합니다. B601-RS 와 위쪽에 고정한 RGB-D 카메라를 씁니다.

"빨간 블록 집어 줘" 자연어 명령 오케스트레이터 (결정) ① 반사(Reflex·정규식) → ② 습관(Habit·경험) → ③ LLM 앞 단계에서 해결되면 다음 단계는 건너뜀 LLM / VLM 로컬 Qwen3-VL 또는 클라우드 도구 호출 pick_and_place(...) 스킬 런타임 (SkillRuntime) 물체 찾기 → 그립 후보 → IK 로 걸러내기 비전 (WorldWatcher) YOLOE ≈3 Hz → BeliefStore 그립 계획 GraspGen 서비스 / OBB 대체 안전 하네스 (SafetyHarness) 50 Hz 경유점마다 승인 · 하나라도 어기면 중단(fail-closed) SafeArm → motorbridge → CAN 실제 모터 구동 기록 (trace.jsonl) 호출·키프레임 저장 위 = "생각"(느려도 됨) 아래 = "몸"(빠르고 확실해야 함)
wrc_demo 의 층 구조: 위쪽은 "생각", 아래쪽은 "몸". 그 사이에 안전 하네스가 문지기로 서 있습니다.
층구성 요소(코드 이름)하는 일
결정오케스트레이터 — 반사 → 습관 → LLM 3단자주 쓰는 명령은 정규식(agent/reflex.py)으로 즉시, 해 본 적 있는 일은 경험 기억으로, 처음 보는 일만 LLM 에게
모델기본 로컬 Qwen3-VL-2B-Instruct-AWQ-4bit(vLLM), 클라우드 프로필 anthropic·openai·minimax, 시험용 mock문장과 영상을 이해해 도구 호출을 만듦
비전WorldWatcher → BeliefStore약 3 Hz 로 YOLOE 검출, "세상에 무엇이 어디 있는지" 메모
스킬SkillRuntime — pick_and_place, grasp_object, place_at, teach_record/replay, task_done 등물체 위치 조회 → 그립 후보 → IK 로 걸러내기
안전SafetyHarness.approve()50 Hz 경유점 하나하나 검사, 어기면 즉시 중단(fail-closed)
실행SafeArm → motorbridge → SocketCAN실제 모터 구동
기록trace.jsonl + 키프레임 JPEG무엇을 왜 했는지 나중에 확인

왜 "반사 → 습관 → LLM" 순서일까#

LLM 은 똑똑하지만 느리고, 가끔 틀리고, 돈(또는 GPU)이 듭니다. "홈으로 가" 같은 명령은 정규식 한 줄로 충분하니 LLM 을 부를 이유가 없습니다. 사람도 뜨거운 것에 닿으면 생각하기 전에 손을 떼고(반사), 매일 하는 일은 습관대로 하고, 처음 겪는 일에서만 곰곰이 생각하지요.

왜 안전 하네스가 따로 있을까#

LLM 이 만든 계획을 그대로 믿지 않기 위해서입니다. 관절 한계에 너무 가까운지, 너무 낮게 내려가지 않는지 같은 규칙은 LLM 이 아니라 평범한 코드가 검사합니다. 위키는 IK 의 한계 여유(limit_margin=0.025)가 하네스의 여유(joint_margin=0.02)보다 반드시 커야 하고 둘을 함께 바꿔야 한다고 강조합니다. IK 가 만든 자세가 하네스에서 걸리지 않도록 한 겹 더 안쪽에서 계획하는 것입니다.

아직 "실물 검증 전"인 부분이 있습니다

wrc_demo 위키는 실제 로봇 동작 일부를 "검증되지 않음(Not verified)"으로 표시하고, 현장에서 그리퍼와 핸드-아이 보정을 다시 확인하라고 안내합니다. 실물에 돌릴 때는 비상정지를 손 닿는 곳에 두고, 처음엔 빈 그리퍼·느린 속도로 시작하세요.

에이전트의 뼈대를 직접 만들어 보기#

LLM 을 쓰지 않아도 구조는 흉내 낼 수 있습니다. 핵심은 세 가지입니다 — ① 명령을 JSON 같은 구조화된 값으로 바꾸기, ② 허용된 스킬 목록에서만 고르기, ③ 실행 전에 안전 검사.

python
import json

# LLM 이 돌려줬다고 치는 답 (음성 데모의 action/params/reply 형식을 흉내)
llm_answer = '{"action": "pick", "params": {"target": "cube"}, "reply": "큐브를 집을게요"}'

Z_MIN = 0.01        # 이보다 낮게는 내려가지 않음 (하네스 규칙)

def safe_target(x, y, z):
    if z < Z_MIN:
        raise ValueError(f"너무 낮습니다: z={z:.3f}")
    if arm.ik(x, y, z, down=True) is None:
        raise ValueError("IK 해가 없습니다 (닿지 않는 곳)")
    return x, y, z

def skill_pick(target):
    if target != "cube" or arm.cube() is None:
        return "그 물체가 보이지 않아요"
    x, y, z = safe_target(*arm.cube())
    arm.open()
    arm.move_to(x, y, z + 0.08, t=1.5, down=True)
    arm.move_to(x, y, z, t=1.0, down=True)
    arm.close()
    arm.move_to(x, y, z + 0.08, t=1.0, down=True)
    return "집었어요"

def skill_home():
    arm.home()
    return "홈으로 왔어요"

SKILLS = {"pick": skill_pick, "home": skill_home}   # 허용 목록

cmd = json.loads(llm_answer)
print("로봇:", cmd["reply"])
skill = SKILLS.get(cmd["action"])
if skill is None:
    print("모르는 동작이라 실행하지 않습니다:", cmd["action"])
else:
    try:
        print("결과:", skill(**cmd["params"]))
    except ValueError as e:
        print("안전 검사에서 막힘 →", e)

llm_answer 의 "pick" 을 "dance" 로 바꾸거나, Z_MIN 을 0.5 로 올려 보세요. 모르는 명령은 실행하지 않고, 위험한 목표는 막는 것을 확인할 수 있습니다. 진짜 에이전트는 이 llm_answer 자리에 LLM 호출이 들어가고, SKILLS 와 safe_target 이 훨씬 촘촘해질 뿐입니다.


정리#

  • 비전 그리핑은 ① YOLO 인식 → ② 깊이 읽기 → ③ 카메라 좌표 → 로봇 좌표 → ④ IK 로 접근·하강·집기·들기 순서입니다. 공식 데모는 열린 어휘 모델 yoloe-26l-seg.pt 와 OBB 짧은 축(그리퍼 방향), 깊이 분위수(높이)를 씁니다.
  • 핀홀 공식 xc = (u − cx)·d / fx 가 픽셀을 미터로 바꾸고, p_base = R·p_cam + t 가 카메라 좌표를 로봇 좌표로 바꿉니다. fx, fy, cx, cy 는 내부 파라미터, R, t 는 외부 파라미터입니다.
  • 외부 파라미터는 핸드-아이 보정으로 구합니다. 손목 카메라(eye-in-hand)는 현재 FK 를 한 번 더 곱합니다. 공식 데모는 ArUco 마커 + TSAI, 15개 이상 샘플을 권장합니다.
  • reSpeaker Flex 는 마이크 4개의 도착 시간 차로 소리 방향(DoA)을 구하고, 음성 모드는 Whisper → LLM → JSON 명령 구조입니다.
  • wrc_demo 에이전트는 반사 → 습관 → LLM 의 결정 층, 스킬 런타임, 그리고 모든 경유점을 검사하는 안전 하네스로 이루어집니다. LLM 은 "무엇을" 고르고, "어떻게 안전하게" 는 평범한 코드가 책임집니다.

확인 문제#

  1. 깊이 카메라가 영상 중심에서 오른쪽으로 60 픽셀 떨어진 점의 깊이를 0.5 m 로 읽었습니다. fx = 600 일 때 그 점은 카메라 중심선에서 옆으로 몇 cm 떨어져 있나요?
  2. 손목 카메라(eye-in-hand)로 본 물체를 로봇 좌표로 바꿀 때, 고정 카메라(eye-to-hand)보다 추가로 필요한 정보는 무엇인가요?
  3. 그리퍼가 물체마다 항상 같은 방향으로 약 1.5 cm 빗나갑니다. YOLO 문제와 핸드-아이 보정 문제 중 어느 쪽을 먼저 의심해야 할까요? 그 이유는?
  4. 에이전트 구조에서 LLM 이 팔 관절에 직접 각도를 보내지 않고 pick_and_place 같은 정해진 스킬만 호출하게 하는 이유를 두 가지 말해 보세요.
정답
  1. xc = 60 × 0.5 / 600 = 0.05 m, 즉 5 cm 입니다.
  2. 물체를 본 순간의 손목 자세, 즉 그때의 관절각으로 계산한 FK(base_T_ee(q))입니다. 손목-카메라 변환(ee_T_cam)은 보정으로 한 번 구해 두고, 매번 FK 와 곱합니다.
  3. 핸드-아이 보정입니다. YOLO 상자가 틀리면 물체·장면마다 오차가 들쭉날쭉하지만, 외부 파라미터가 틀리면 모든 점이 같은 변환으로 밀려서 항상 같은 방향·같은 크기로 빗나갑니다. 마커 길이 설정과 샘플 수를 먼저 확인하세요.
  4. ① 스킬 안에서 IK·관절 한계·최저 높이 같은 안전 검사를 평범한 코드로 확실히 할 수 있습니다. ② LLM 이 엉뚱한 답을 내도 허용 목록 밖의 동작은 실행되지 않습니다. (그 밖에: 스킬은 검증된 동작이라 재사용·기록이 쉽고, LLM 은 느려서 50 Hz 제어 루프를 맡을 수 없습니다.)