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

24프로젝트 — 손으로 두는 체스 (MediaPipe)

MediaPipe 손 인식으로 2D 체스판의 말을 집어 옮기며 컴퓨터와 두고, 컴퓨터의 수를 reBot 좌표와 Playground 동작으로 바꿉니다.

이 장에서 만드는 것#

웹캠 앞에서 엄지와 검지로 체스 말을 집어 옮기는 체스 게임을 만듭니다. 화면의 2D 체스판에서 위쪽(검은 말)은 컴퓨터, 아래쪽(흰 말)은 사용자입니다. 손은 구글의 오픈소스 손 인식 라이브러리 MediaPipe Hands 가 찾아 줍니다. 그리고 컴퓨터가 둔 수를 reBot 이 실제 체스판에서 옮긴다면 어떤 좌표로 움직여야 하는지 계산해, Python Playground 에서 reBot 으로 다시 둬 봅니다.

단계하는 일이어지는 장
손 보기카메라 영상에서 손 관절 21개의 위치를 찾는다21장 비전
손짓 읽기엄지·검지 거리로 집기 / 놓기 를 판정한다이 장
판 위에 두기손 위치를 체스판 칸으로 바꾸고, 규칙에 맞는 수인지 검사한다이 장
로봇에게 시키기칸 이름을 로봇 좌표로 바꾸고 집기·놓기 동작을 만든다11장 IK, 22장 픽앤플레이스

손 체스 열기

준비물

웹캠이 달린 노트북이나 USB 웹캠, 그리고 인터넷 연결(손 인식 모델을 처음 한 번 내려받습니다)이 필요합니다. 카메라 영상은 브라우저 안에서만 처리되고 어디로도 보내지 않습니다. 카메라가 없어도 마우스나 터치로 말을 끌어 똑같이 둘 수 있습니다.


MediaPipe 가 보는 손#

MediaPipe Hands 는 사진 한 장에서 손을 찾아 관절 21개(랜드마크) 의 위치를 알려 줍니다. 위치는 화면 크기와 상관없이 0~1 로 정규화한 값입니다. 왼쪽 위가 (0, 0), 오른쪽 아래가 (1, 1) 입니다.

번호이름이 장에서 쓰는 곳
0손목 (WRIST)손 크기 재기
4엄지 끝 (THUMB_TIP)집기 판정, 커서 위치
8검지 끝 (INDEX_FINGER_TIP)집기 판정, 커서 위치
9가운뎃손가락 뿌리 (MIDDLE_FINGER_MCP)손 크기 재기

실제 PC 에서 Python 으로 해 보면 다음과 같습니다(pip install mediapipe opencv-python). 아래 코드는 오래 쓰여 온 mp.solutions.hands 인터페이스를 씁니다. 설치한 버전에서 이 이름이 없다고 나오면 MediaPipe 문서의 새 인터페이스(Tasks API 의 HandLandmarker)를 쓰면 되는데, 브라우저 도구가 바로 그 HandLandmarker 의 JavaScript 판입니다. 랜드마크 번호와 좌표 뜻은 두 인터페이스가 같습니다.

python
import cv2, math
import mediapipe as mp

hands = mp.solutions.hands.Hands(max_num_hands=1, min_detection_confidence=0.6)
cap = cv2.VideoCapture(0)
pinch = False

while True:
    ok, frame = cap.read()
    if not ok:
        break
    frame = cv2.flip(frame, 1)                       # 거울처럼 좌우 반전
    res = hands.process(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
    if res.multi_hand_landmarks:
        lm = res.multi_hand_landmarks[0].landmark
        d = math.dist((lm[4].x, lm[4].y), (lm[8].x, lm[8].y))     # 엄지 끝 ↔ 검지 끝
        size = math.dist((lm[0].x, lm[0].y), (lm[9].x, lm[9].y))  # 손목 ↔ 가운뎃손가락 뿌리
        ratio = d / size
        if not pinch and ratio < 0.32:
            pinch = True
            print("집기")
        elif pinch and ratio > 0.47:
            pinch = False
            print("놓기")
    cv2.imshow("hand", frame)
    if cv2.waitKey(1) & 0xFF == 27:                  # Esc 로 종료
        break

cap.release()
cv2.destroyAllWindows()
왜 손 크기로 나누나요?

손을 카메라에 가까이 대면 모든 거리가 커지고, 멀리 두면 작아집니다. 엄지-검지 거리만 보면 같은 손짓도 거리에 따라 판정이 달라집니다. 손목 ↔ 가운뎃손가락 뿌리 길이로 나눈 비율을 쓰면 카메라와의 거리에 상관없이 같은 기준이 됩니다.


집기 · 옮기기 · 놓기를 자연스럽게#

① 겨누기 손을 펴고 말 위로 ② 집기 엄지·검지 붙임 ③ 옮기기 붙인 채 이동 ④ 놓기 손가락을 뗌 둘 수 있는 칸 → 수 확정 · 둘 수 없는 칸 → 말이 제자리로 비율 < 0.32 가 두 프레임 연속 비율 > 0.47 이 두 프레임 연속 그 사이는 상태 유지
손 체스의 네 단계. 집기와 놓기의 기준값을 다르게 두어 경계에서 깜빡이지 않게 합니다.

손 인식 값은 매 프레임 조금씩 흔들립니다. 기준값 하나(예: 0.4)로만 판정하면, 손가락이 그 근처에 있을 때 집기 → 놓기 → 집기 가 1초에 몇 번씩 바뀌어 말을 엉뚱한 데 떨어뜨립니다. 손 체스는 다음 장치들로 동작을 매끄럽게 이어 줍니다.

문제해결 장치손 체스에서의 값
경계에서 집기/놓기가 깜빡임히스테리시스 — 집는 기준과 놓는 기준을 다르게집기 0.32 미만, 놓기 0.47 초과 (민감도 슬라이더로 조절)
한 프레임 잘못 인식해도 바로 반응디바운스 — 같은 판정이 연속으로 나와야 바꿈2 프레임 연속
커서가 덜덜 떨림One Euro 필터 — 천천히 움직일 땐 많이 거르고, 빨리 움직일 땐 덜 거름최소 차단 주파수 1.2 Hz
말 가장자리를 집으면 놓침관용 집기 — 커서에서 가장 가까운 흰 말을 잡음칸 크기의 0.75 배 안
칸 경계에 걸쳐 놓기 애매함자석 — 둘 수 있는 칸 가운데 가까이 가면 말이 살짝 끌려감칸 크기의 0.45 배 안
둘 수 없는 칸에 놓음말이 원래 자리로 미끄러져 돌아감0.26 초
손이 잠깐 화면 밖으로 나감바로 떨어뜨리지 않고 잠시 기다림0.7 초 지나면 제자리로
손이 카메라 가장자리에서 잘림카메라 화면의 가운데 부분만 체스판 전체에 대응좌우 16%, 위아래 12% 여백 (점선 상자)

말을 집으면 그림자가 생기고 조금 커져 "들어 올린" 느낌을 주고, 갈 수 있는 칸에는 점이 찍힙니다. 말이 손 아래 칸이 둘 수 있는 칸이면 초록, 아니면 빨강으로 칠해져 놓기 전에 결과를 미리 알 수 있습니다.

실습 1 — 손으로 한 판 두기
  1. 손 체스를 열고 [카메라 켜기] 를 누른 뒤 카메라 권한을 허용합니다.
  2. 오른쪽 카메라 화면의 점선 상자 안에서 손을 움직여, 체스판 위의 동그라미 커서를 흰 폰 위에 올립니다.
  3. 엄지와 검지를 붙여 집고(막대가 초록색), 붙인 채 두 칸 앞으로 옮긴 뒤 손가락을 뗍니다.
  4. 컴퓨터가 검은 말을 들어 옮길 때까지 기다렸다가 계속 둡니다.
  5. 일부러 둘 수 없는 칸에 놓아 보고, 말이 제자리로 돌아오는지 확인합니다.
  6. 잘 안 집히면 집기 민감도를 올리고, 손을 펴고 있는데도 집히면 내려서 내 손에 맞는 값을 찾습니다.

컴퓨터는 어떻게 두나#

컴퓨터 쪽은 두 부분으로 되어 있습니다.

  • 규칙 검사 — 말마다 갈 수 있는 칸을 만들고, 그 수를 둔 뒤 내 킹이 공격받으면 뺍니다. 캐슬링, 앙파상, 폰 승진(자동으로 퀸)까지 처리하고, 둘 수가 없을 때 체크면 체크메이트, 아니면 스테일메이트 입니다.
  • 수 고르기 — 말의 가치(폰 100, 나이트 320, 비숍 330, 룩 500, 퀸 900)와 말의 위치 점수를 더해 판을 평가하고, 몇 수 앞까지 알파-베타 탐색으로 읽습니다. 수준이 높을수록 더 깊이 읽습니다(쉬움 1수, 보통 3수, 어려움 4수 앞).

이 계산은 Web Worker(브라우저의 별도 일꾼 스레드)에서 돌기 때문에, 컴퓨터가 생각하는 동안에도 손 추적과 화면이 멈추지 않습니다. 생각하는 것과 보는 것을 나누는 이 구조는 실제 로봇 프로그램에서도 똑같이 씁니다. 카메라 처리와 모터 제어 루프가 서로 기다리지 않게 하는 것이죠.


reBot 이 둔다면 — 칸 이름을 로봇 좌표로#

이제 진짜 체스판을 reBot 앞에 놓았다고 생각해 봅시다. 로봇은 체스판 위쪽(8랭크 쪽) 에 앉아 사용자를 바라봅니다. 그러면 로봇의 x 축(앞)은 사용자 쪽, y 축(왼쪽)은 h 파일 쪽입니다.

reBot x y 8 1 a h GRAVE (0.25, 0.17) 사용자 칸 이름 → 로봇 (x, y) row = 8 − 랭크 (8랭크가 로봇 쪽 row 0) col = a~h → 0~7 x = X0 + (row + 0.5) × SQ y = (col − 3.5) × SQ X0 = 0.16 m (체스판 앞 가장자리) SQ = 0.028 m (한 칸, 판 전체 22.4 cm) 예) e7 → row 1, col 4 → (0.202, 0.014) 집는 높이 z = 0.03 m, 들어 올리는 높이 +0.06 m
로봇 베이스 기준 체스판 배치. 칸 이름(예: e7)을 행·열 번호로 바꾼 뒤 칸 크기를 곱하면 로봇 좌표가 됩니다.

판 크기와 위치는 공식 관절 한계 안에서 64칸 모두에 그리퍼를 아래로 향해 닿도록 정했습니다. 강좌의 기구학으로 RS·DM 두 모델 모두 확인한 값입니다.

상수값뜻
X00.16 m로봇에서 체스판 앞 가장자리까지
SQ0.028 m한 칸 크기 (판 전체 22.4 cm)
Z0.03 m말을 집는 높이
LIFT0.06 m집은 뒤 들어 올리는 높이
GRAVE(0.25, 0.17)잡힌 말을 내려놓는 곳 (판 오른쪽 바깥)
실습 2 — 64칸 모두 닿는지 검사하기

Python Playground 에서 칸 이름을 좌표로 바꾸는 함수를 만들고, 22장의 R1 처럼 움직이기 전에 모든 칸을 arm.ik(..., down=True) 로 검사합니다.

python
X0, SQ = 0.16, 0.028
Z, LIFT = 0.03, 0.06

def square_to_xy(name):
    col = "abcdefgh".index(name[0])
    row = 8 - int(name[1])              # 8랭크(로봇 쪽) = 0
    return X0 + (row + 0.5) * SQ, (col - 3.5) * SQ

print("e7 →", square_to_xy("e7"))

bad = []
for f in "abcdefgh":
    for r in range(1, 9):
        x, y = square_to_xy(f + str(r))
        if arm.ik(x, y, Z, down=True) is None or arm.ik(x, y, Z + LIFT, down=True) is None:
            bad.append(f + str(r))
print("닿지 않는 칸:", bad if bad else "없음")

e7 → (0.202, 0.014) 와 닿지 않는 칸: 없음 이 나오면 성공입니다. SQ 를 0.04 로 키워 다시 실행해 보세요. 판이 커지면 먼 칸(1랭크 쪽 모서리)부터 닿지 않게 됩니다.

Python Playground 열기

컴퓨터의 수를 reBot 으로 다시 두기#

손 체스 오른쪽 아래 [4. reBot 이 둔다면] 에는 컴퓨터가 수를 둘 때마다 로봇이 할 일이 좌표와 함께 나옵니다. 상대 말을 잡는 수라면 잡힌 말을 먼저 판 밖으로 치우고 내 말을 옮기며, 캐슬링이면 킹과 룩을 차례로 옮깁니다.

[Playground 코드 복사] 를 누르면 지금까지 컴퓨터가 둔 수를 모두 담은 코드가 복사됩니다. 생김새는 다음과 같습니다.

python
def pick(x, y):
    arm.open()
    arm.move_to(x, y, Z + LIFT, t=1.0, down=True)   # 말 위로
    arm.move_line(x, y, Z, t=0.6)                    # 곧게 내려가
    arm.close()                                      # 집고
    arm.move_line(x, y, Z + LIFT, t=0.6)             # 곧게 올라옴

def place(x, y):
    arm.move_to(x, y, Z + LIFT, t=1.0, down=True)
    arm.move_line(x, y, Z, t=0.6)
    arm.open()                                       # 놓고
    arm.move_line(x, y, Z + LIFT, t=0.6)

def robot_move(src, dst=None):
    pick(*square_to_xy(src))
    place(*(square_to_xy(dst) if dst else GRAVE))    # dst 가 없으면 판 밖으로
    print(src, "→", dst or "밖")

arm.home()
# e5
robot_move("e7", "e5")
# Nc6
robot_move("b8", "c6")
arm.home()
실습 3 — 한 판을 로봇으로 재생하기
  1. 손 체스에서 몇 수를 둔 뒤 [Playground 코드 복사] 를 누릅니다.
  2. Python Playground 에 붙여 넣고 실행해, reBot 이 컴퓨터의 수를 차례로 두는 모습을 봅니다.
  3. 잡는 수가 나왔을 때 로봇이 먼저 판 밖(GRAVE)으로 말을 치우는지 확인합니다.
  4. 모델을 RS ↔ DM 으로 바꿔 같은 코드가 둘 다에서 동작하는지 봅니다.
실물로 옮길 때

시뮬레이터에는 체스 말이 없고 충돌·중력도 없습니다. 실제 체스판에서 하려면 ① 체스판을 로봇에 고정하고 X0 를 자로 재서 맞추고, ② 말 높이에 맞춰 Z 를 정하고(높은 킹·퀸은 위쪽을 집도록 따로), ③ 모든 t 를 2~3배로 늘리고, ④ 사람 손이 판 위에 있을 때는 로봇이 움직이지 않도록 하세요. 손 체스의 손 인식을 그대로 쓰면 "손이 화면에 보이는 동안 로봇 정지" 같은 안전 규칙을 쉽게 넣을 수 있습니다.


확장 과제#

과제난이도힌트
E1. 손가락 수로 수준 고르기★★펴진 손가락 개수 세기: 손가락 끝(8, 12, 16, 20)이 그 마디(6, 10, 14, 18)보다 위(y 가 작음)면 펴진 것
E2. 손 모양으로 무르기★★주먹(모든 손가락 접힘)을 1초 유지하면 [한 수 무르기]
E3. 키 큰 말은 높이 집기★★robot_move 에 말 종류를 넘겨 킹·퀸은 Z + 0.02 에서 집기
E4. 치운 말 줄 세우기★★★잡힌 말마다 GRAVE 의 x 를 한 칸씩 늘려 판 옆에 나란히 놓기
E5. 가장 짧은 이동 순서★★★잡는 수에서 "치우기 → 옮기기" 사이 빈손 이동 거리를 math.dist 로 재고, 줄일 방법 찾기
E6. 카메라로 판 읽기★★★★21장의 카메라 좌표 변환으로, 사람이 실제 판에서 둔 수를 카메라가 알아보게 하기

정리#

  • MediaPipe Hands 는 손 관절 21개의 위치를 0~1 정규화 좌표로 알려 줍니다. 엄지 끝(4)과 검지 끝(8)만으로 집기·놓기를 만들 수 있습니다.
  • 엄지-검지 거리를 손 크기로 나눈 비율을 쓰면 카메라와의 거리에 상관없이 같은 판정이 됩니다.
  • 히스테리시스 + 디바운스 + One Euro 필터가 흔들리는 인식 값을 매끄러운 집기 → 옮기기 → 놓기 로 바꿔 줍니다.
  • 말이 따라오고, 갈 칸이 미리 보이고, 잘못 놓으면 제자리로 돌아오는 즉각적인 피드백이 손 조작을 자연스럽게 만듭니다.
  • 칸 이름은 x = X0 + (row + 0.5) × SQ, y = (col − 3.5) × SQ 로 로봇 좌표가 되고, 22장의 pick / place 로 그대로 로봇 동작이 됩니다. 움직이기 전에 모든 칸이 닿는지 먼저 검사하는 습관을 잊지 마세요.