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 판입니다. 랜드마크 번호와 좌표 뜻은 두 인터페이스가 같습니다.
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.4)로만 판정하면, 손가락이 그 근처에 있을 때 집기 → 놓기 → 집기 가 1초에 몇 번씩 바뀌어 말을 엉뚱한 데 떨어뜨립니다. 손 체스는 다음 장치들로 동작을 매끄럽게 이어 줍니다.
| 문제 | 해결 장치 | 손 체스에서의 값 |
|---|---|---|
| 경계에서 집기/놓기가 깜빡임 | 히스테리시스 — 집는 기준과 놓는 기준을 다르게 | 집기 0.32 미만, 놓기 0.47 초과 (민감도 슬라이더로 조절) |
| 한 프레임 잘못 인식해도 바로 반응 | 디바운스 — 같은 판정이 연속으로 나와야 바꿈 | 2 프레임 연속 |
| 커서가 덜덜 떨림 | One Euro 필터 — 천천히 움직일 땐 많이 거르고, 빨리 움직일 땐 덜 거름 | 최소 차단 주파수 1.2 Hz |
| 말 가장자리를 집으면 놓침 | 관용 집기 — 커서에서 가장 가까운 흰 말을 잡음 | 칸 크기의 0.75 배 안 |
| 칸 경계에 걸쳐 놓기 애매함 | 자석 — 둘 수 있는 칸 가운데 가까이 가면 말이 살짝 끌려감 | 칸 크기의 0.45 배 안 |
| 둘 수 없는 칸에 놓음 | 말이 원래 자리로 미끄러져 돌아감 | 0.26 초 |
| 손이 잠깐 화면 밖으로 나감 | 바로 떨어뜨리지 않고 잠시 기다림 | 0.7 초 지나면 제자리로 |
| 손이 카메라 가장자리에서 잘림 | 카메라 화면의 가운데 부분만 체스판 전체에 대응 | 좌우 16%, 위아래 12% 여백 (점선 상자) |
말을 집으면 그림자가 생기고 조금 커져 "들어 올린" 느낌을 주고, 갈 수 있는 칸에는 점이 찍힙니다. 말이 손 아래 칸이 둘 수 있는 칸이면 초록, 아니면 빨강으로 칠해져 놓기 전에 결과를 미리 알 수 있습니다.
- 손 체스를 열고 [카메라 켜기] 를 누른 뒤 카메라 권한을 허용합니다.
- 오른쪽 카메라 화면의 점선 상자 안에서 손을 움직여, 체스판 위의 동그라미 커서를 흰 폰 위에 올립니다.
- 엄지와 검지를 붙여 집고(막대가 초록색), 붙인 채 두 칸 앞으로 옮긴 뒤 손가락을 뗍니다.
- 컴퓨터가 검은 말을 들어 옮길 때까지 기다렸다가 계속 둡니다.
- 일부러 둘 수 없는 칸에 놓아 보고, 말이 제자리로 돌아오는지 확인합니다.
- 잘 안 집히면 집기 민감도를 올리고, 손을 펴고 있는데도 집히면 내려서 내 손에 맞는 값을 찾습니다.
컴퓨터는 어떻게 두나#
컴퓨터 쪽은 두 부분으로 되어 있습니다.
- 규칙 검사 — 말마다 갈 수 있는 칸을 만들고, 그 수를 둔 뒤 내 킹이 공격받으면 뺍니다. 캐슬링, 앙파상, 폰 승진(자동으로 퀸)까지 처리하고, 둘 수가 없을 때 체크면 체크메이트, 아니면 스테일메이트 입니다.
- 수 고르기 — 말의 가치(폰 100, 나이트 320, 비숍 330, 룩 500, 퀸 900)와 말의 위치 점수를 더해 판을 평가하고, 몇 수 앞까지 알파-베타 탐색으로 읽습니다. 수준이 높을수록 더 깊이 읽습니다(쉬움 1수, 보통 3수, 어려움 4수 앞).
이 계산은 Web Worker(브라우저의 별도 일꾼 스레드)에서 돌기 때문에, 컴퓨터가 생각하는 동안에도 손 추적과 화면이 멈추지 않습니다. 생각하는 것과 보는 것을 나누는 이 구조는 실제 로봇 프로그램에서도 똑같이 씁니다. 카메라 처리와 모터 제어 루프가 서로 기다리지 않게 하는 것이죠.
reBot 이 둔다면 — 칸 이름을 로봇 좌표로#
이제 진짜 체스판을 reBot 앞에 놓았다고 생각해 봅시다. 로봇은 체스판 위쪽(8랭크 쪽) 에 앉아 사용자를 바라봅니다. 그러면 로봇의 x 축(앞)은 사용자 쪽, y 축(왼쪽)은 h 파일 쪽입니다.
판 크기와 위치는 공식 관절 한계 안에서 64칸 모두에 그리퍼를 아래로 향해 닿도록 정했습니다. 강좌의 기구학으로 RS·DM 두 모델 모두 확인한 값입니다.
| 상수 | 값 | 뜻 |
|---|---|---|
X0 | 0.16 m | 로봇에서 체스판 앞 가장자리까지 |
SQ | 0.028 m | 한 칸 크기 (판 전체 22.4 cm) |
Z | 0.03 m | 말을 집는 높이 |
LIFT | 0.06 m | 집은 뒤 들어 올리는 높이 |
GRAVE | (0.25, 0.17) | 잡힌 말을 내려놓는 곳 (판 오른쪽 바깥) |
Python Playground 에서 칸 이름을 좌표로 바꾸는 함수를 만들고, 22장의 R1 처럼 움직이기 전에 모든 칸을 arm.ik(..., down=True) 로 검사합니다.
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랭크 쪽 모서리)부터 닿지 않게 됩니다.
컴퓨터의 수를 reBot 으로 다시 두기#
손 체스 오른쪽 아래 [4. reBot 이 둔다면] 에는 컴퓨터가 수를 둘 때마다 로봇이 할 일이 좌표와 함께 나옵니다. 상대 말을 잡는 수라면 잡힌 말을 먼저 판 밖으로 치우고 내 말을 옮기며, 캐슬링이면 킹과 룩을 차례로 옮깁니다.
[Playground 코드 복사] 를 누르면 지금까지 컴퓨터가 둔 수를 모두 담은 코드가 복사됩니다. 생김새는 다음과 같습니다.
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()- 손 체스에서 몇 수를 둔 뒤 [Playground 코드 복사] 를 누릅니다.
- Python Playground 에 붙여 넣고 실행해, reBot 이 컴퓨터의 수를 차례로 두는 모습을 봅니다.
- 잡는 수가 나왔을 때 로봇이 먼저 판 밖(GRAVE)으로 말을 치우는지 확인합니다.
- 모델을 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로 그대로 로봇 동작이 됩니다. 움직이기 전에 모든 칸이 닿는지 먼저 검사하는 습관을 잊지 마세요.