이화여대 캡스톤디자인 졸업프로젝트로써 "시선추적, 제스처인식, 생성형 AI 기반 종합 발표 트레이닝 웹서비스, SPitching"을 개발 중이다.
그 중 핵심 기능인 제스처인식 기능에 대해 설명하고자 한다.
여러 제스처/모션인식 기술 중 초기 환경세팅이 쉬운 Google Mediapipe 기술을 선택했다.
더 구체적으로는 주요 신체 위치를 식별하고, 자세를 분석하고, 움직임을 분류할 수 있는 Mediapipe Pose와
손의 주요 지점을 찾고 시각적 효과를 렌더링할 수 있는 Mediapipe Hands 기술을 선택하여 둘을 적절히 혼합하였다.
우리의 서비스는 대부분의 경우에 앉은 상태에서 노트북을 앞에 두고 발표연습을 하는 것이기 때문에 23번부터 32번까지의 하반신 랜드마크는 제외하고 0번부터 22번에 해당하는 상반신, 얼굴, 손과 관련된 랜드마크만 이용한다.
손 랜드마크 모델 번들은 손바닥 감지 모델과 손 랜드마크 감지 모델을 포함한다.
손바닥 감지 모델은 입력 이미지 내에서 손을 찾고, 손 랜드마크 감지 모델은 손바닥 감지 모델에서 정의한 잘린 손 이미지에서 특정 손 랜드마크를 식별한다.
손바닥 감지 모델을 실행하는 데 시간이 많이 걸리기 때문에 라이브 스트림 실행 모드에서 Hand Landmarker는 한 프레임에서 손 랜드마크 모델에 의해 정의된 바운딩 박스를 사용하여 후속 프레임의 손 영역을 현지화한다.
Hand Landmarker는 손 랜드마크 모델이 더 이상 손의 존재를 식별하지 못하거나 프레임 내에서 손을 추적하지 못하는 경우에만 손바닥 감지 모델을 다시 트리거한다.
이렇게 하면 Hand Landmarker가 손바닥 감지 모델을 트리거하는 횟수가 줄어든다.
먼저 발표자가 자주 사용하는 제스처를 supervised learning으로 학습을 시키고 미리 라벨링을 해줘야 한다.
3가지의 기본 제스처를 정의하였다.
1) 손을 얼굴에 갖다댐 (손을 턱에 갖다댐, 손으로 입을 막음, 손으로 머리를 긁음 등)
2) 오픈핸드 제스처 (무언가를 제시하거나 포인팅)
3) 과도한 손동작 (손을 너무 자주 움직임)
아래 코드는 OpenCV를 이용해 실시간 비디오 스트리밍을 처리하고, Mediapipe Pose와 Hands를 사용하여 각 제스처를 인식하는 코드이다. 특정 제스처가 감지되면 그 제스처를 카운팅하여 화면에 결과를 표시한다.
import cv2
import mediapipe as mp
import numpy as np
# MediaPipe 설정
mp_pose = mp.solutions.pose
mp_hands = mp.solutions.hands
mp_drawing = mp.solutions.drawing_utils
# 손과 포즈 추적기 초기화
pose = mp_pose.Pose(min_detection_confidence=0.5, min_tracking_confidence=0.5)
hands = mp_hands.Hands(min_detection_confidence=0.5, min_tracking_confidence=0.5)
# 제스처 카운트 초기화
gestures_count = {'hand_to_face': 0, 'open_hand_gesture': 0, 'excessive_hand_movement': 0}
# 얼굴 근처 좌표 설정 (입술, 턱 근처)
FACE_THRESH = 0.15 # 손목과 얼굴(입술, 턱) 사이의 거리 임계값
# 과도한 손동작을 감지하기 위한 움직임 범위 설정
EXCESSIVE_HAND_MOVEMENT_THRESH = 0.1 # 손목의 x, y 변화가 일정 범위를 초과할 때 과도한 손동작으로 인식
# 화면에서 제스처 감지하기 위한 함수
def detect_gestures(pose_landmarks, hand_landmarks):
global gestures_count
# 얼굴 근처로 손을 갖다대는 제스처 감지
for hand in hand_landmarks:
for lm in hand.landmarks:
# 손목 좌표 추출
wrist_x, wrist_y = lm.x, lm.y
# 얼굴 근처로 손을 갖다대는 제스처: 얼굴의 위치를 임의로 화면 중앙(0.5, 0.5)으로 설정
face_distance = np.sqrt((wrist_x - 0.5) ** 2 + (wrist_y - 0.5) ** 2)
if face_distance < FACE_THRESH:
gestures_count['hand_to_face'] += 1 # 손이 얼굴 근처에 있을 때
# 오픈핸드 제스처 감지
for hand in hand_landmarks:
# 손바닥을 펼쳤는지 확인하기 위해, 손가락 끝과 손목 위치 간의 거리를 이용
thumb_tip = hand.landmarks[mp_hands.HandLandmark.THUMB_TIP]
index_tip = hand.landmarks[mp_hands.HandLandmark.INDEX_FINGER_TIP]
middle_tip = hand.landmarks[mp_hands.HandLandmark.MIDDLE_FINGER_TIP]
ring_tip = hand.landmarks[mp_hands.HandLandmark.RING_FINGER_TIP]
pinky_tip = hand.landmarks[mp_hands.HandLandmark.PINKY_TIP]
# 손바닥을 펼쳤는지 확인 (손가락 끝과 손목 간의 상대적 거리로 판단)
if (index_tip.y < thumb_tip.y and middle_tip.y < thumb_tip.y and
ring_tip.y < thumb_tip.y and pinky_tip.y < thumb_tip.y): # 손바닥이 펼쳐져 있을 때
gestures_count['open_hand_gesture'] += 1 # 오픈핸드 제스처 감지
# 과도한 손동작 감지
if len(hand_landmarks) > 0:
prev_wrist_x, prev_wrist_y = None, None
for hand in hand_landmarks:
for lm in hand.landmarks:
wrist_x, wrist_y = lm.x, lm.y
if prev_wrist_x is not None:
# 손목 위치 변화량 계산
wrist_move_distance = np.sqrt((wrist_x - prev_wrist_x) ** 2 + (wrist_y - prev_wrist_y) ** 2)
if wrist_move_distance > EXCESSIVE_HAND_MOVEMENT_THRESH: # 변화가 큰 경우
gestures_count['excessive_hand_movement'] += 1
prev_wrist_x, prev_wrist_y = wrist_x, wrist_y
# 비디오 캡처
cap = cv2.VideoCapture(0)
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
# RGB로 변환 (MediaPipe는 RGB 형식 필요)
rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# Pose와 Hands 추적
pose_results = pose.process(rgb_image)
hands_results = hands.process(rgb_image)
# 포즈와 손 랜드마크가 감지되면 제스처 분석
if pose_results.pose_landmarks and hands_results.multi_hand_landmarks:
# 포즈 랜드마크
pose_landmarks = pose_results.pose_landmarks
# 손 랜드마크
hand_landmarks = hands_results.multi_hand_landmarks
# 제스처 분석
detect_gestures(pose_landmarks, hand_landmarks)
# 랜드마크 그리기
frame = cv2.cvtColor(rgb_image, cv2.COLOR_RGB2BGR)
mp_drawing.draw_landmarks(frame, pose_results.pose_landmarks, mp_pose.POSE_CONNECTIONS)
if hands_results.multi_hand_landmarks:
for hand_landmark in hands_results.multi_hand_landmarks:
mp_drawing.draw_landmarks(frame, hand_landmark, mp_hands.HAND_CONNECTIONS)
# 제스처 카운트 출력
cv2.putText(frame, f"Hand to Face: {gestures_count['hand_to_face']}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
cv2.putText(frame, f"Open Hand Gesture: {gestures_count['open_hand_gesture']}", (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
cv2.putText(frame, f"Excessive Hand Movement: {gestures_count['excessive_hand_movement']}", (10, 90), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
# 화면에 결과 보여주기
cv2.imshow("Gesture Detection", frame)
# 'q' 키를 눌러 종료
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()