웹캠 한 대로 Aero Hand Open(TetherIA의 오픈소스 힘줄 구동 로봇손)을 조작하고, 그 조작 데이터를 모아 관측(손 랜드마크) → 행동(7채널 관절 명령) 을 직접 회귀하는 모방학습(Behavior Cloning) 정책을 학습시킵니다.
카메라 ─ MediaPipe 21 랜드마크 ─ landmarks.py (63차원 정규자세 특징)
│
┌──────────────┴───────────────┐
│ mapping.py (해석적 63→7 매핑) │ ← expert
└──────────────┬───────────────┘
│ collect_teleop.py 로 (obs, action) 수집
▼
train_teleop_bc.py MLP 63→128→128→7
│
▼
teleop_bc_play.py 정책이 관절을 직접 구동
핵심은 "사람의 손 모양을 로봇이 이해하는 신호로 어떻게 바꿀 것인가" 입니다. 규칙 기반 매핑(expert)을 먼저 세우고, 그 입출력을 데이터로 모아 신경망이 같은 함수를 내도록 증류(distill)합니다.
학습된 정책이 카메라 속 손 모양을 실시간으로 따라 실물 Aero Hand 를 움직입니다.
| 파일 | 단계 | 내용 | 로봇 |
|---|---|---|---|
landmarks.py |
— | 랜드마크 → 63차원 정규자세 특징 (BC 관측) | 불필요 |
mapping.py |
— | 랜드마크 → 7채널 해석적 매핑 (BC expert) + 캘리브레이션 | 불필요 |
config.py |
— | 포트 해석, 채널 정의, 안전 파라미터 | — |
poses.py |
— | 정규화값 → actuation 변환 (손별 안전범위 재매핑) | — |
hand_mirror.py |
캘리브 | 규칙 기반 실시간 미러링 + --calibrate (범위 관측) |
캘리브만 불필요 |
collect_teleop.py |
1 | 7채널 연속 명령 수집 → data/teleop_demos.npz |
불필요 |
train_teleop_bc.py |
2 | 연속 회귀 BC 학습 → teleop_policy.pt |
불필요 |
teleop_bc_play.py |
3 | 학습된 정책이 관절을 직접 구동 (--compare 로 수식 대조) |
시연만 |
수집도 학습도 로봇 없이 됩니다. expert 라벨이 사람 손 랜드마크에서 계산되지 로봇 상태에서 오는 게 아니기 때문입니다. 카메라만 있으면 데이터를 모으고 정책을 학습할 수 있고, 실물은 마지막 재생 단계에서만 필요합니다.
MediaPipe 는 두 가지 랜드마크를 줍니다. 초기 버전은 화면 정규화 좌표(hand_landmarks)를 썼는데, 여기엔 구조적 문제가 있습니다.
- x 는 프레임 가로폭, y 는 세로폭 기준입니다. 16:9 카메라에서 x 가 1.78배 눌리므로, 두 점 사이 거리나 각도를 계산하는 순간 이미 왜곡되어 있습니다.
- z 는 또 다른 스케일입니다. xyz 를 섞은 3D 연산이 원리적으로 성립하지 않습니다.
그래서 전부 hand_world_landmarks(미터 단위 실제 3D)로 옮겼습니다. 그 위에 정규 자세(canonical pose)로 정렬합니다.
원점 = 손목
y축 = 손목 → 중지 MCP
z축 = 손바닥 법선 (검지 MCP × 소지 MCP)
x축 = y × z
스케일 = 손목→중지 MCP 거리로 나눔
손의 위치·방향·크기가 제거되고 "손모양"만 남습니다. 평행이동·회전·스케일 변환에 대해 최대 오차 1e-7(float32 노이즈 수준)로 불변인 것을 확인했습니다. BC 가 포즈당 수백 프레임으로도 버티는 이유입니다. (landmarks.py)
expert 는 63차원 정규자세를 7채널 구동값으로 바꾸는 해석적 수식입니다(mapping.py). 네 손가락은 관절각 코사인으로 단순하지만, 엄지는 관절 구조가 달라 별도 처리가 필요한데 초기 구현에 네 가지 결함이 겹쳐 있었습니다.
① 엄지에 4손가락 공식을 그대로 적용 — 엄지는 MCP-IP-TIP 이 굽혀도 거의 일직선이라 각도 변화폭이 작아, 로봇 엄지가 거의 움직이지 않았습니다.
② 독립 자유도 두 개를 하나로 낭비 — ch1 과 ch2 에 같은 값을 스케일만 바꿔 넣어 상관계수가 1.0 이었습니다.
③ 벌림이 굽힘과 간섭 — 벌림을 엄지 TIP 과 소지 MCP 사이 거리로 쟀습니다. 벌리지 않고 굽히기만 해도 TIP 이 손바닥으로 들어오면서 벌림값이 함께 떨어졌습니다.
④ 스케일 기준이 불안정 — 벌림 범위가 상수로 하드코딩되어 있었습니다.
CMC→MCP 방향을 손바닥 평면 기준으로 분해합니다.
평면 안쪽 각도 → ch0 벌림 (엄지가 검지에서 멀어지는 정도)
평면 바깥 각도 → ch1 굽힘 (엄지가 손바닥을 가로지르는 정도)
MCP·IP 관절각 평균 → ch2 텐던 (엄지 자체 말림)
ch2 는 손목 쪽 두 신호와 다른 관절을 보므로 ③이 해소됩니다. 범위는 하드코딩 대신 실행 시 캘리브레이션으로 잡습니다.
엄지 벌림 자세를 고정한 채 말단만 말면서 288프레임 측정:
| 지표 | 정지 시(노이즈) | 말 때 | 노이즈 대비 |
|---|---|---|---|
| 기존 벌림 (std) | 0.022 | 0.176 | 7.9배 |
| 새 벌림 ch0 (std) | 0.017 | 0.068 | 4.0배 |
| 새 텐던 ch2 (std) | 0.0015 | 0.031 | 20.9배 |
기존 공식은 굽힘 성분까지 벌림에 섞어 7.9배로 부풀렸습니다. 새 ch0 은 랜드마크 1(CMC)·2(MCP)만 읽어 두 점을 고정한 합성 테스트에서 변동이 정확히 0.000000 — 실제 손에서 남는 4배는 MediaPipe 가 추정한 엄지 뿌리 관절이 실제로 움직인 것이고, 그건 로봇이 따라가는 게 맞습니다.
원시 신호 범위는 사람마다·카메라 거리마다 다릅니다. hand_mirror.py --no-robot --calibrate 로 채널별 가동범위를 관측해 calibration.json 에 저장합니다. 타이머로 끊지 않고 사용자가 끝낼 때까지 돌아가며, 채널별 확보 폭을 실시간 막대로 보여줍니다.
고정 시간(8초)으로 했을 때 손가락 굽힘 최대가 0.13 에 그쳤습니다 — 거의 펴진 상태로, 주먹이 범위에 들어오지 못했습니다. 이 좁은 범위를 0~1 로 확대하면 손이 조금만 움직여도 로봇이 전 범위를 휘두르는 위험한 매핑이 됩니다. 그래서 채널별 최소 확보 폭을 강제합니다.
캘리브레이션은 범위는 흡수하지만 방향은 흡수하지 못합니다. 채널이 사람 손과 반대로 움직이면
calibration.json의invert를 켜면 됩니다.
![]() |
![]() |
collect_teleop.py 는 매 프레임 (관측 63차원, expert 행동 7채널) 쌍을 모읍니다. SPACE 로 녹화 시작/일시정지, u 로 마지막 50프레임 취소, q 로 저장 후 종료.
3개 포즈만 찍으면 그 3점만 재현하는 정책이 됩니다. 상태공간을 골고루 훑어야 합니다.
- 손가락 하나씩 개별로 굽혔다 펴기, 여러 손가락 조합
- 엄지 벌림/모음, 엄지 말림
- 가위바위보 같은 실제 쓸 자세, 그 사이 중간 자세들
- 손 위치·각도·카메라 거리도 바꿔가며
저장 시 채널별 커버리지(min/max/폭/표준편차)를 출력합니다. 어떤 채널의 폭이 좁으면 그 관절을 덜 움직인 것이고, 학습된 정책도 그 채널을 제대로 못 냅니다.
obs : 63차원 정규자세 랜드마크
action : 7채널 연속값 0~1
model : MLP 63 → 128 → 128 → 7, 출력 sigmoid, 손실 MSE
expert : mapping.py 의 해석적 매핑
정책이 관절 명령을 직접 냅니다(룩업 없음). 출력이 정의상 0~1 이라 sigmoid 로 범위를 보장해 실물에 그대로 보낼 수 있습니다. clamp 로 자르면 범위 밖에서 기울기가 죽지만 sigmoid 는 그렇지 않습니다.
expert 가 해석적 수식이므로 신경망은 손으로 짠 함수를 증류(distill)합니다. BC 골격(관측 → expert 행동의 지도학습)은 지키지만 "사람 시연에서 새 기술을 배웠다"는 아닙니다. 평가는 accuracy 가 아니라 채널별 MAE 로 봅니다 — 전체 MSE 하나만 보면 어느 채널이 망가졌는지 안 보이고, 실물에서는 그 손가락만 안 따라오는 형태로 드러납니다.
학습은 마지막 epoch 가 아니라 검증 최고 시점을 저장합니다. 연속 프레임이 near-duplicate 라 학습 손실은 계속 내려가는데 검증 손실은 도로 올라가는 구간이 생깁니다.
상태: 실데이터 학습 완료 — 2390 샘플 수집, 전체 MAE 0.0269, 실물 미러링 구동 확인.
힘줄 구동 하드웨어는 급격한 신호에 취약하고, 손이 한 대뿐이면 텐던 손상은 복구가 안 됩니다.
- EMA 필터 — 프레임 간 노이즈 완화
- Slew limit — 프레임 간 변화율 제한. 첫 구동은
--slew 0.05 - SPACE 토크 해제 — 즉시 전 서보 토크 차단 (긴급 정지처럼)
- 엄지 손별 안전범위 재매핑 — 엄지 벌림/굽힘(ch0/ch1)은 개체·펌웨어(좌/우)마다 편안한 방향이 반대이고, 반대쪽 끝은 하드스톱에 눌려 서보가 수 A 를 뽑으며 탑니다. 연결된 손을 판별해
config.THUMB_SAFE_RANGE로 스톨존을 아예 안 건드리게 재매핑합니다.
학습된 출력은 수식보다 튈 수 있고 그 값이 그대로 관절로 가므로, 정책 재생(teleop_bc_play.py)에서 EMA·slew 가 더 중요합니다.
uv venv --python 3.12 .venv
uv pip install -r requirements.txt
# MediaPipe 손 랜드마크 모델 (별도 다운로드, .gitignore 로 제외됨)
curl -o hand_landmarker.task \
https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task# 0. 캘리브레이션 (로봇 불필요)
.venv/bin/python hand_mirror.py --no-robot --calibrate
# 1. 데이터 수집 (로봇 불필요)
.venv/bin/python collect_teleop.py
# 2. 학습 (로봇 불필요)
.venv/bin/python train_teleop_bc.py
# 3. 재생 — 정책이 관절을 직접 구동
.venv/bin/python teleop_bc_play.py --no-robot --compare # 수식 vs 정책 (로봇 없이)
.venv/bin/python teleop_bc_play.py --slew 0.05 # 첫 실물 구동q 종료, SPACE 토크 토글. 포트는 SDK 의 리눅스 자동 탐지(/dev/serial/by-id/)에 위임합니다. 필요하면 AERO_HAND_PORT 환경변수로 지정합니다.
- TetherIA Aero Hand Open — 오픈소스 힘줄 구동 로봇손
- SDK:
uv pip install "aero-open-sdk @ git+https://github.com/TetherIA/aero-hand-open.git#subdirectory=sdk"
- SDK:
- MediaPipe Hand Landmarker 모델 — 공식 배포처
- Python 3.12+, PyTorch (CPU 로 충분)
엄지는 관절이 4개(cmc_abd, cmc_flex, mcp, ip)인데 액추에이터는 3개입니다. ch2(thumb_tendon)가 MCP 와 IP 를 함께 당기는 언더액추에이티드 구조라, 엄지 말단 굽힘은 ch2 가 지배합니다.
| ch | 이름 | 구동 방식 | 동작 |
|---|---|---|---|
| 0 | thumb_cmc_abd |
링키지 바 | 엄지가 검지에서 멀어짐/모임 |
| 1 | thumb_cmc_flex |
CMC Flex Cable | 엄지가 손바닥을 가로지름 |
| 2 | thumb_tendon |
Thumb Pull Cable | 엄지 자체 말림 (MCP+IP) |
| 3~6 | 각 손가락 | Pull Cable | 굽힘 |
규칙 기반 텔레옵 전체(가위바위보 3-class BC 포함)와 하드웨어 조립·트러블슈팅은 상위 프로젝트 aero-hand-mediapipe-teleop 에 있습니다.


