안녕하세요. 푸딩샘입니다. 
행복한 겨울방학이 지나고, 새 학기 준비로 어느 때보다 더 분주한 3월을 보내고 계신가요?
저는 지난 겨울방학 동안 고등학생들과 함께 진행할 프로젝트 주제를 탐색하던 중, 카메라 기반 손동작 인식과 아두이노 제어를 결합한 ‘핸드이노(Handino)’를 알게 되었습니다.
단순 입력뿐만 아니라, 카메라가 사람의 손을 인식하고 각 손가락 마디의 좌푯값을 추출한 뒤, 이를 그대로 서보모터 각도로 변환하여 로봇 손이 동일하게 움직이도록 구현하는 구조입니다. 제가 수업 시간에 활용한 언어는 파이썬과 엔트리입니다. 학생 수준에 따라 텍스트 기반 프로그래밍과 블록 기반 프로그래밍을 병행하여 수업을 진행하였습니다.
교구 한 대에 약 28만원 정도 되는 고가의 교구라, 비용 측면에서는 부담이 있지만, 단순 체험형 교구가 아니라 알고리즘 설계, 데이터 처리, 하드웨어 제어를 통합적으로 다룰 수 있는 프로젝트형 수업 도구라는 점에서 도입의 의미가 있다고 판단하고 학교 내에서 10대 정도 구매해 수업을 진행했습니다.
아래 영상은 파이썬 기반 주피터 노트북 환경에서 손동작 인식과 아두이노 제어를 연동하여 구현한 핸드이노 동작 사례입니다.
수업은 조립 단계에서 시작하여, 이후 블록 기반 프로그래밍 언어, 마지막으로 텍스트 기반 프로그래밍 언어로 확장하는 순서로 진행하였습니다.
먼저 하드웨어를 직접 조립하며 장치의 구조와 동작 원리를 이해하도록 한 뒤, 블록 코딩을 통해 제어 흐름을 시각적으로 익히게 하였습니다. 이후 텍스트 기반 프로그래밍으로 확장하여 좌표 데이터 처리와 세밀한 제어까지 구현하도록 구성하였습니다.
블록 기반 코딩은 중학교 수준에서 알고리즘의 흐름을 이해하는 데 적합하며, 텍스트 기반 코딩은 고등학교 단계에서 파이썬 심화 수업으로 활용할 수 있습니다.
핸드이노 조립
핸드이노 조립 과정은 유튜브에 상세한 설명 영상이 다수 게시되어 있어, 이를 참고하여 단계적으로 진행할 수 있습니다. 핸드이노 조립 과정은 약 3-4차시 내외의 시간이 필요합니다.
아래 링크를 참고해 주세요. 수업을 운영해 보니, 조립 과정에서 목공용 장갑이나 두꺼운 장갑을 준비하는 것이 필요했습니다. 작은 부품을 고정하거나 나사를 조일 때 손을 보호할 수 있어, 학생들이 보다 안전하게 활동을 진행할 수 있었습니다.
중학교 블록 기반 프로그래밍 수업 ( 엔트리 )
핸드이노와 엔트리를 연동하기 위해서는 하드웨어 연결뿐 아니라 전용 펌웨어 설치 과정이 필요합니다.
핸드이노을 선택한 뒤, 먼저 드라이버를 설치하고 이후 펌웨어를 설치하는 순서로 진행하면 됩니다.
저는 인공지능 블록을 활용하여 비디오 감지와 손 감지 기능을 적용하고, 학생들이 직접 인공지능 모델을 새롭게 학습시키는 방식으로 수업을 설계하였습니다.
구체적으로는 카메라를 통해 입력되는 영상에서 손의 형태와 움직임을 인식하도록 설정한 뒤, 다양한 수어 동작을 데이터로 수집하여 학습 모델을 제작하였습니다. 이후 학습된 모델이 특정 수어를 판별하면, 그 결괏값을 아두이노로 전송하여 핸드이노가 동일한 손동작을 구현하도록 프로그램을 설계하였습니다.
손 인식 명령어와 관련된 설명 사이트 : AI 블록 > 손 인식 — Entry Docs
명령어 구조가 비교적 단순하여 기본 사용 방법만 안내해도 학생들이 빠르게 프로그램을 구성할 수 있었습니다. 블록을 연결하는 수준을 넘어 반복문과 선택문을 적용하도록 확장하자, 핸드이노의 동작이 훨씬 자연스럽고 정교하게 구현되었습니다. 단순 모방 단계에서 벗어나 조건에 따라 다른 동작을 실행하거나, 일정 시간 동안 반복 동작을 수행하도록 설계하는 등 제어 구조의 완성도가 높아졌습니다.
수업 분위기 또한 달라졌습니다. 손동작이 실제 로봇 손으로 그대로 구현되는 과정을 확인하면서 학생들의 집중도가 눈에 띄게 높아졌고, 자신이 작성한 코드가 즉각적인 물리적 결과로 나타난다는 점에서 높은 몰입을 보였습니다. 특히 오류를 발견하고 원인을 추적하여 동작을 보완하는 과정 자체를 하나의 도전 과제로 받아들이는 태도가 인상적이었습니다.
기본 기능을 익힌 이후에는 단순한 제스처 재현을 넘어 ‘컴퓨터와 가위바위보’ 게임을 구현하는 방향으로 과제를 제시했습니다. 학생들은 손동작 인식 결과를 바탕으로 누가 이겼는지 판단하는 조건문을 만들고, 컴퓨터의 동작은 무작위로 선택되도록 구성하였습니다. 또한 결과에 따라 핸드이노가 서로 다른 동작을 수행하도록 프로그램을 설계하였습니다.
고등학교 텍스트 기반 프로그래밍 언어( 파이썬 )
파이썬을 활용하여 핸드이노를 제어하는 원리는 다음과 같은 구조로 설명할 수 있습니다.
먼저 아두이노 IDE에서 핸드이노를 제어할 수 있는 펌웨어를 업로드합니다. 이 펌웨어는 외부에서 전달되는 데이터를 해석하여 각 서보모터를 지정된 각도로 회전시키는 역할을 합니다. 즉, 아두이노는 시리얼 통신을 통해 전달받은 값을 실제 하드웨어 동작으로 변환하는 제어 장치로 동작합니다.
이후 주피터 노트북 환경에서 파이썬 코드를 작성합니다. 파이썬에서는 카메라 영상을 처리하여 손의 관절 좌표를 추출하고, 이를 각도 값으로 변환한 뒤 시리얼 통신을 통해 아두이노로 전송합니다.
정리하면,
카메라 입력 → 파이썬에서 손 좌표 분석 및 각도 계산 → 시리얼 통신으로 데이터 전송 → 아두이노 펌웨어가 서보모터 제어 순으로 동작합니다.
아두이노에서 5개의 서보모터를 순차적으로 제어하여 손가락이 하나씩 펴지는 동작을 구현한 프로그램
파이썬이나 시리얼 통신과의 연동 없이, IDE 환경에서 작성 → 컴파일 → 업로드의 절차만으로 동작시킬 수 있습니다.
대부분의 일반 서보모터는 약 0°에서 180° 범위 내에서 회전하도록 설계되어 있습니다. 따라서 실습에서는 0°를 손가락이 접힌 상태, 164°를 손가락이 충분히 펴진 상태로 기준값을 설정한 뒤 프로그램을 구성하면 안정적으로 동작을 구현할 수 있습니다.
#include <Servo.h>
Servo servo_1;
Servo servo_2;
Servo servo_3;
Servo servo_4;
Servo servo_5;
int CLOSE = 0; // 접힘
int OPEN = 164; // 펴짐
void setup() {
servo_1.attach(12);
servo_2.attach(10);
servo_3.attach(9);
servo_4.attach(6);
servo_5.attach(2);
}
void loop() {
servo_1.write(CLOSE);
servo_2.write(CLOSE);
servo_3.write(CLOSE);
servo_4.write(CLOSE);
servo_5.write(CLOSE);
delay(2000);
servo_1.write(OPEN);
servo_2.write(CLOSE);
servo_3.write(CLOSE);
servo_4.write(CLOSE);
servo_5.write(CLOSE);
delay(2000);
servo_1.write(OPEN);
servo_2.write(OPEN);
servo_3.write(CLOSE);
servo_4.write(CLOSE);
servo_5.write(CLOSE);
delay(2000);
servo_1.write(OPEN);
servo_2.write(OPEN);
servo_3.write(OPEN);
servo_4.write(CLOSE);
servo_5.write(CLOSE);
delay(2000);
servo_1.write(OPEN);
servo_2.write(OPEN);
servo_3.write(OPEN);
servo_4.write(OPEN);
servo_5.write(CLOSE);
delay(2000);
servo_1.write(OPEN);
servo_2.write(OPEN);
servo_3.write(OPEN);
servo_4.write(OPEN);
servo_5.write(OPEN);
delay(3000);
}
시리얼 통신으로 전달되는 문자 명령에 따라 가위 바위 보 따라하는 프로그램
IDE에 제작하는 프로그램은 아래와 같습니다.
r : 다섯 손가락 모두 펴기 (서보 각도 164)
p : 가위 모양 만들기 (검지·중지 펴기, 나머지 접기)
s : 주먹 모양 만들기 (다섯 손가락 모두 접기, 서보 각도 0)
n : 손이 없거나 인식 불가 시 기본 상태로 복귀 (다섯 손가락 펴기)}
#include <Servo.h>
Servo servo_1;
Servo servo_2;
Servo servo_3;
Servo servo_4;
Servo servo_5;
void setup() {
Serial.begin(9600);
servo_1.attach(12);
servo_2.attach(10);
servo_3.attach(9);
servo_4.attach(6);
servo_5.attach(2);
delay(500);
// 초기 상태 : 모두 펴짐
servo_1.write(164);
servo_2.write(164);
servo_3.write(164);
servo_4.write(164);
servo_5.write(164);
delay(5000);
}
void loop() {
while (Serial.available() > 0) {
char data = Serial.read();
if (data == 'r') { // 보
servo_1.write(164);
servo_2.write(164);
servo_3.write(164);
servo_4.write(164);
servo_5.write(164);
}
else if (data == 'p') { // 가위
servo_1.write(0);
servo_2.write(164);
servo_3.write(164);
servo_4.write(0);
servo_5.write(0);
}
else if (data == 's') { // 바위
servo_1.write(0);
servo_2.write(0);
servo_3.write(0);
servo_4.write(0);
servo_5.write(0);
}
else if (data == 'n') {
servo_1.write(164);
servo_2.write(164);
servo_3.write(164);
servo_4.write(164);
servo_5.write(164);
}
}
}
카메라로 인식한 손동작을 분석한 뒤(가위바위보), 핸드이노가 동일한 움직임을 재현하도록 하는 프로그램
카메라 인식 이후 손동작을 구체적으로 판단하기 위해서는, 영상 데이터를 처리하고 결과를 분류하는 파이썬 프로그램을 작성해야 합니다. 아래 그림과 같이 손가락 각 마디를 인식하고, 해당 관절의 좌표 값을 화면에 시각화하여 확인할 수 있습니다.
주피터 초기 환경 설정 과정이 다소 복잡하지만, 아나콘다를 설치한 뒤 필요한 패키지를 추가로 설치하면 실행 환경을 구성할 수 있습니다.
1. conda create --name AI python=3.8.15
명령어를 입력하면, ‘AI’라는 이름의 가상환경이 생성되며 해당 환경에 파이썬 3.8.15 버전이 설치됩니다.
이와 같이 가상환경을 별도로 구성하면 인공지능 프로젝트에 필요한 라이브러리를 독립적으로 관리할 수 있어, 버전 충돌을 방지하고 안정적인 개발 환경을 유지할 수 있습니다.
2. conda activate AI
이 명령을 실행하면 현재 작업 환경이 기본(base) 환경에서 ‘AI’ 환경으로 전환되며, 해당 가상환경에 설치된 파이썬 버전과 라이브러리들이 적용됩니다. 이후 설치하거나 실행하는 모든 패키지와 프로그램은 이 ‘AI’ 환경 안에서 동작하게 됩니다.
3.
pip install opencv-python
4.
pip install pyserial
5.
pip install pyglet
6.
python -m pip install "mediapipe==0.10.11“
7.
pip install numpy
8.
pip install imutils
설치 완료 후 jupyter notebook를 하게 되면 주피터가 자동으로 켜집니다.
IDE - 프로그램
import cv2
import mediapipe as mp
import time
import serial
ser = serial.Serial('COM3', 9600)
mp_drawing=mp.solutions.drawing_utils
mp_hands = mp.solutions.hands
cap=cv2.VideoCapture(0, cv2.CAP_DSHOW)
def rps_landmarks(lm):
def up(tip, pip):
return lm[tip].y<lm[pip].y
index=up(8,6)
middle=up(12,10)
ring=up(16,14)
pinky=up(20,18)
cnt=sum([index,middle,ring,pinky])
if cnt==0:
return "ROCK"
if index and middle and (not ring) and (not pinky):
return "SCISSORS"
if cnt>=3:
return "PAPER"
return "UNKNOWN"
last_sent = None
last_time = 0
with mp_hands.Hands(
max_num_hands=1,
min_detection_confidence=0.6,
min_tracking_confidence=0.6) as hands:
while cap.isOpened():
success, image=cap.read()
if not success:
continue
image=cv2.flip(image,1)
image_rgb= cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
result=hands.process(image_rgb)
gesture = "NO_HAND"
send_char = b'n'
if result.multi_hand_landmarks:
hand_landmarks=result.multi_hand_landmarks[0]
gesture=rps_landmarks(hand_landmarks.landmark)
if gesture == "ROCK":
send_char = b's'
elif gesture == "PAPER":
send_char = b'r'
elif gesture == "SCISSORS":
send_char = b'p'
else:
send_char = b'n'
mp_drawing.draw_landmarks(image, hand_landmarks, mp_hands.HAND_CONNECTIONS)
cv2.putText(image,gesture,(10,35),cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0,255,0),2)
now = time.time()
if (send_char != last_sent) and (now - last_time > 0.15):
ser.write(send_char)
last_sent = send_char
last_time = now
cv2.imshow("image",image)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
주피터 프로그램
import cv2
import mediapipe as mp
import time
import serial
ser = serial.Serial('COM3', 9600)
mp_drawing=mp.solutions.drawing_utils
mp_hands = mp.solutions.hands
cap=cv2.VideoCapture(0, cv2.CAP_DSHOW)
def rps_landmarks(lm):
def up(tip, pip):
return lm[tip].y<lm[pip].y
index=up(8,6)
middle=up(12,10)
ring=up(16,14)
pinky=up(20,18)
cnt=sum([index,middle,ring,pinky])
if cnt==0:
return "ROCK"
if index and middle and (not ring) and (not pinky):
return "SCISSORS"
if cnt>=3:
return "PAPER"
return "UNKNOWN"
last_sent = None
last_time = 0
with mp_hands.Hands(
max_num_hands=1,
min_detection_confidence=0.6,
min_tracking_confidence=0.6) as hands:
while cap.isOpened():
success, image=cap.read()
if not success:
continue
image=cv2.flip(image,1)
image_rgb= cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
result=hands.process(image_rgb)
gesture = "NO_HAND"
send_char = b'n'
if result.multi_hand_landmarks:
hand_landmarks=result.multi_hand_landmarks[0]
gesture=rps_landmarks(hand_landmarks.landmark)
if gesture == "ROCK":
send_char = b's'
elif gesture == "PAPER":
send_char = b'r'
elif gesture == "SCISSORS":
send_char = b'p'
else:
send_char = b'n'
mp_drawing.draw_landmarks(image, hand_landmarks, mp_hands.HAND_CONNECTIONS)
cv2.putText(image,gesture,(10,35),cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0,255,0),2)
now = time.time()
if (send_char != last_sent) and (now - last_time > 0.15):
ser.write(send_char)
last_sent = send_char
last_time = now
cv2.imshow("image",image)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
위에 제시한 프로그램들은 인터넷에 공개된 다양한 자료를 분석하고, 실제 수업 환경에 맞게 수정‧보완하는 과정을 거쳐 재구성한 것입니다. 단순히 예제를 실행하는 수준을 넘어서, 학생들의 수준과 학교 환경에 맞도록 구조를 정리하고 단계화하여 적용해 보았습니다.
핸드이노를 활용한 이 프로젝트 수업 사례가 인공지능과 피지컬 컴퓨팅을 연계하고자 하는 선생님들께 하나의 참고 자료가 되기를 바랍니다.
.png&blockId=31194e49-32bd-809e-b61a-e128331be857)








