목차
메타 설명
웹캠 영상에서 손가락, 얼굴 표정, 전신 자세를 실시간으로 추적하려면 어떻게 해야 할까요? MediaPipe Tasks와 OpenCV를 이용해 손 랜드마크, 얼굴 메시, 눈 깜빡임, 포즈 랜드마크, 관절 각도, 손가락 개수, 스쿼트 횟수 측정까지 단계별로 구현합니다.
지난 시간에는 OpenCV를 이용해 이미지와 영상을 처리했습니다.
이미지를 자르고, 크기를 바꾸고, 특정 색상을 찾고, 엣지와 윤곽선을 검출했으며, 웹캠에서 움직이는 영역까지 찾아냈습니다.
그런데 OpenCV가 움직임을 발견했다고 가정해 보겠습니다.
화면 왼쪽에서
무언가 움직였다.여기까지는 알 수 있습니다.
하지만 곧 새로운 질문이 등장합니다.
움직인 것은 사람일까?
손을 들고 있는가?
손가락은 몇 개 펼쳤는가?
눈을 깜빡였는가?
팔꿈치가 몇 도로 굽혀졌는가?
스쿼트를 몇 번 했는가?
자세가 올바른가?OpenCV의 기본 영상 처리만으로 이 모든 규칙을 직접 구현하는 것은 쉽지 않습니다.
손 하나만 보더라도 상황은 복잡합니다.
손이 회전한다.
손가락이 서로 가려진다.
카메라와 거리가 달라진다.
왼손과 오른손이 다르다.
조명이 변한다.
배경과 피부색이 비슷하다.그래서 오늘은 이미 학습된 머신러닝 모델을 이용해 영상 속 사람의 주요 위치를 좌표로 찾아주는 MediaPipe를 배워 보겠습니다.
MediaPipe가 웹캠을 바라보면 화면은 더 이상 단순한 영상이 아닙니다.
손목은 여기
검지는 여기
왼쪽 눈은 여기
오른쪽 어깨는 여기
무릎은 여기
발목은 여기사람의 움직임이 좌표와 선으로 번역됩니다.
웹캠 속에 갑자기 디지털 해부학자가 출근하는 셈입니다. 🖐️🧍♀️📐
1. MediaPipe란?
MediaPipe는 이미지, 영상, 오디오 등 다양한 입력을 처리하는 머신러닝 파이프라인과 미리 학습된 모델을 애플리케이션에 적용할 수 있도록 제공하는 오픈소스 프로젝트입니다.
현재 개발자가 주로 사용하는 고수준 기능은 MediaPipe Solutions와 그 핵심 프로그래밍 인터페이스인 MediaPipe Tasks입니다.
MediaPipe Tasks는 Android, 웹, Python 등 여러 플랫폼에서 사용할 수 있는 API를 제공하며, 손·얼굴·자세 랜드마크와 객체 검출, 이미지 분류, 제스처 인식 같은 기능을 비교적 적은 코드로 적용할 수 있도록 설계되었습니다.
대표적인 비전 작업은 다음과 같습니다.
Hand Landmarker
→ 손의 주요 좌표 검출
Face Landmarker
→ 얼굴의 세밀한 좌표와 표정 정보 검출
Pose Landmarker
→ 전신 관절 좌표 검출
Gesture Recognizer
→ 손동작 종류 인식
Object Detector
→ 객체 위치와 종류 검출
Image Segmenter
→ 픽셀 단위 영역 분리MediaPipe는 사람처럼 화면을 이해하는 만능 두뇌가 아닙니다.
각 작업에 특화된 모델과 처리 파이프라인을 제공하는 전문 기술팀에 가깝습니다.
손을 찾는 직원
얼굴을 찾는 직원
자세를 분석하는 직원
동작을 분류하는 직원회사 규모는 작아 보여도 조직도가 꽤 탄탄합니다.
2. MediaPipe Tasks란?
MediaPipe Tasks는 미리 학습된 머신러닝 모델을 애플리케이션에 배포하고 실행하기 위한 고수준 API입니다.
Python에서는 다음과 같은 구조를 사용합니다.
import mediapipe as mp
BaseOptions = mp.tasks.BaseOptions
RunningMode = mp.tasks.vision.RunningMode작업에 따라 필요한 클래스를 선택합니다.
HandLandmarker = (
mp.tasks.vision.HandLandmarker
)
FaceLandmarker = (
mp.tasks.vision.FaceLandmarker
)
PoseLandmarker = (
mp.tasks.vision.PoseLandmarker
)기본 사용 흐름은 비슷합니다.
1. 모델 파일 경로 지정
2. 옵션 설정
3. 작업 객체 생성
4. 이미지 또는 프레임 전달
5. 결과 좌표 사용
6. 작업 객체 종료코드로 표현하면 다음과 같습니다.
options = TaskOptions(
base_options=BaseOptions(
model_asset_path=model_path
),
running_mode=RunningMode.VIDEO
)
with TaskClass.create_from_options(
options
) as task:
result = task.detect_for_video(
mp_image,
timestamp_ms
)한 번 구조를 이해하면 손, 얼굴, 자세 작업을 비슷한 방식으로 사용할 수 있습니다.
3. OpenCV와 MediaPipe의 역할
OpenCV와 MediaPipe는 함께 사용할 때 특히 강력합니다.
OpenCV의 역할
웹캠 열기
영상 프레임 읽기
BGR·RGB 색상 변환
화면 좌우 반전
선과 원 그리기
글자 출력
영상 저장
키보드 입력 처리MediaPipe의 역할
손 위치 찾기
손가락 관절 찾기
얼굴 특징점 찾기
표정 정보 추정
전신 관절 위치 찾기
손동작 종류 인식전체 과정은 다음과 같습니다.
웹캠
→ OpenCV가 프레임 읽기
→ BGR을 RGB로 변환
→ MediaPipe 모델 실행
→ 랜드마크 좌표 반환
→ OpenCV가 좌표와 연결선 그리기
→ 화면 출력OpenCV는 촬영 감독이고 MediaPipe는 모션 분석가입니다.
OpenCV:
“오늘 촬영분입니다.”
MediaPipe:
“손목 0번, 검지 끝 8번,
왼쪽 무릎 25번 확인했습니다.”
OpenCV:
“좋습니다. 화면에 표시하겠습니다.”4. MediaPipe Legacy Solutions 주의사항
인터넷에서 MediaPipe 예제를 검색하면 다음 코드를 자주 발견할 수 있습니다.
mp.solutions.hands
mp.solutions.pose
mp.solutions.face_mesh이는 기존 MediaPipe Legacy Solutions 방식의 예제일 가능성이 큽니다.
현재 공식 문서는 새로운 프로젝트에서 MediaPipe Tasks API를 중심으로 안내합니다. PyPI 안내에 따르면 일부 Legacy Solutions 지원은 2023년 3월 1일 종료되었고, 기존 코드와 바이너리는 별도 보증 없이 남아 있습니다.
이번 글에서는 다음과 같은 현재 Tasks API를 사용합니다.
mp.tasks.vision.HandLandmarker
mp.tasks.vision.FaceLandmarker
mp.tasks.vision.PoseLandmarker오래된 예제가 무조건 실행되지 않는다는 뜻은 아닙니다.
다만 새 프로젝트에서는 현재 API 구조를 익히는 편이 장기 유지보수에 유리합니다.
오래된 블로그:
“mp.solutions를 사용하세요.”
현재 공식 문서:
“Tasks API 회의실은 이쪽입니다.”5. MediaPipe 설치하기
OpenCV와 MediaPipe를 함께 설치합니다.
python -m pip install mediapipe opencv-python numpy가상환경을 사용하는 것이 좋습니다.
Windows
python -m venv venv
venv\Scripts\activate
python -m pip install --upgrade pip
python -m pip install mediapipe opencv-python numpymacOS 또는 Linux
python3 -m venv venv
source venv/bin/activate
python -m pip install --upgrade pip
python -m pip install mediapipe opencv-python numpy설치 확인:
import cv2
import mediapipe as mp
print(
f"OpenCV: {cv2.__version__}"
)
print(
f"MediaPipe: {mp.__version__}"
)2026년 7월 30일 기준 PyPI의 최신 MediaPipe 배포 버전은 `1.0.0`이며 2026년 7월 27일 공개되었습니다. PyPI에는 Python 3.9부터 3.12까지의 분류 정보와 Windows, Linux, Apple Silicon macOS용 휠이 표시되어 있습니다.
실제 설치 가능 여부는 운영체제, CPU 아키텍처, Python 버전에 따라 달라질 수 있습니다.
6. 설치 환경과 호환성 확인
설치에 실패했다면 먼저 Python 버전을 확인합니다.
python --version현재 Python 실행 경로:
python -c "import sys; print(sys.executable)"패키지 정보:
python -m pip show mediapipe설치 가능한 버전 확인:
python -m pip index versions mediapipeApple Silicon Mac
MediaPipe 1.0.0 PyPI 파일 목록에는 macOS 11 이상 ARM64용 휠이 포함되어 있습니다.
Apple Silicon 확인:
uname -m결과:
arm64Intel Mac
현재 최신 버전에 Intel Mac용 휠이 제공되지 않는 시점이라면 설치가 실패할 수 있습니다.
이 경우 다음을 검토합니다.
지원되는 이전 MediaPipe 버전 사용
Apple Silicon 환경 사용
Linux 또는 Windows 환경 사용
Docker 환경 검토
공식 지원 파일 업데이트 확인무작정 버전을 낮추기 전에 Python과 OpenCV를 포함한 전체 호환성을 확인해야 합니다.
7. 모델 파일 준비하기
MediaPipe Tasks는 Python 패키지 외에 각 작업과 호환되는 `.task` 모델 파일이 필요합니다.
공식 Hand Landmarker, Face Landmarker, Pose Landmarker 개발 가이드도 모델 파일을 내려받아 로컬 경로에 저장한 뒤 `model_asset_path`로 지정하도록 안내합니다.
공식 모델 페이지에서 모델을 내려받아 다음처럼 정리합니다.
mediapipe_project/
├─ models/
│ ├─ hand_landmarker.task
│ ├─ face_landmarker.task
│ ├─ pose_landmarker.task
│ └─ gesture_recognizer.task
├─ hand_tracking.py
├─ face_tracking.py
├─ pose_tracking.py
└─ squat_counter.py파일 이름은 내려받은 뒤 프로젝트에 맞게 변경해도 됩니다.
코드에서 실제 파일명과 맞춰야 합니다.
from pathlib import Path
HAND_MODEL_PATH = Path(
"models/hand_landmarker.task"
)
FACE_MODEL_PATH = Path(
"models/face_landmarker.task"
)
POSE_MODEL_PATH = Path(
"models/pose_landmarker.task"
)모델 존재 여부를 미리 확인합니다.
def validate_model(
model_path: Path
) -> None:
if not model_path.exists():
raise FileNotFoundError(
"모델 파일을 찾을 수 없습니다: "
f"{model_path.resolve()}"
)사용:
validate_model(
HAND_MODEL_PATH
)모델 파일을 준비하지 않고 코드만 실행하면 MediaPipe는 빈손으로 출근한 탐정이 됩니다.
8. MediaPipe 프로젝트 구조
조금 더 체계적으로 구성하면 다음과 같습니다.
mediapipe_tracking/
├─ models/
│ ├─ hand_landmarker.task
│ ├─ face_landmarker.task
│ └─ pose_landmarker.task
├─ common/
│ ├─ camera.py
│ ├─ drawing.py
│ ├─ geometry.py
│ └─ performance.py
├─ examples/
│ ├─ hand_tracking.py
│ ├─ finger_counter.py
│ ├─ face_tracking.py
│ ├─ blink_detector.py
│ ├─ pose_tracking.py
│ └─ squat_counter.py
├─ requirements.txt
└─ README.md`requirements.txt`:
mediapipe
opencv-python
numpy실제 운영 프로젝트에서는 검증한 버전을 고정할 수 있습니다.
mediapipe==검증한버전
opencv-python==검증한버전
numpy==검증한버전최신 버전이라는 이유만으로 운영 환경에 즉시 적용하기보다 테스트 후 업데이트하는 것이 좋습니다.
9. 랜드마크란?
랜드마크는 이미지나 영상 속 대상에서 의미 있는 주요 위치를 나타내는 좌표입니다.
손에서는 다음과 같은 위치가 랜드마크입니다.
손목
엄지 관절
검지 관절
중지 관절
약지 관절
새끼손가락 관절
각 손가락 끝자세에서는 다음 위치를 추적할 수 있습니다.
코
눈
귀
어깨
팔꿈치
손목
골반
무릎
발목
발끝MediaPipe Hand Landmarker는 손 한 개당 21개의 랜드마크를 반환합니다. Pose Landmarker는 사람 한 명당 33개의 자세 랜드마크를 반환합니다. Face Landmarker 모델은 얼굴당 478개의 3차원 랜드마크와 선택적으로 52개의 Blendshape 점수를 제공할 수 있습니다.
손:
21개
전신:
33개
얼굴:
478개얼굴 쪽 담당자는 점을 꽤 촘촘하게 찍습니다.
10. 정규화 좌표와 픽셀 좌표
MediaPipe가 반환하는 이미지 랜드마크의 `x`, `y` 값은 일반적으로 0과 1 사이로 정규화되어 있습니다.
x = 0.0
→ 이미지 왼쪽
x = 1.0
→ 이미지 오른쪽
y = 0.0
→ 이미지 위쪽
y = 1.0
→ 이미지 아래쪽손 랜드마크의 `x`, `y`는 이미지 너비와 높이에 따라 0부터 1 사이로 정규화되며, 별도의 월드 랜드마크도 제공됩니다.
픽셀 좌표로 변환하려면 이미지 크기를 곱합니다.
def normalized_to_pixel(
landmark,
image_width: int,
image_height: int
) -> tuple[int, int]:
x = int(
landmark.x
* image_width
)
y = int(
landmark.y
* image_height
)
return (
x,
y
)예:
이미지 크기:
640 × 480
랜드마크:
x=0.5, y=0.25계산:
픽셀 x:
0.5 × 640 = 320
픽셀 y:
0.25 × 480 = 120결과:
(320, 120)범위를 벗어나는 좌표에 대비해 제한할 수도 있습니다.
def normalized_to_pixel_safe(
landmark,
image_width: int,
image_height: int
) -> tuple[int, int]:
x = round(
landmark.x
* image_width
)
y = round(
landmark.y
* image_height
)
x = max(
0,
min(
image_width - 1,
x
)
)
y = max(
0,
min(
image_height - 1,
y
)
)
return (
x,
y
)11. 이미지·비디오·라이브 스트림 모드
MediaPipe 비전 작업은 세 가지 실행 모드를 지원합니다.
IMAGE
한 장의 이미지를 처리합니다.
running_mode=(
mp.tasks.vision.RunningMode.IMAGE
)실행:
result = landmarker.detect(
mp_image
)VIDEO
동영상이나 웹캠 프레임을 순서대로 동기 처리합니다.
running_mode=(
mp.tasks.vision.RunningMode.VIDEO
)실행:
result = landmarker.detect_for_video(
mp_image,
timestamp_ms
)LIVE_STREAM
콜백을 이용해 비동기 방식으로 처리합니다.
running_mode=(
mp.tasks.vision.RunningMode.LIVE_STREAM
)실행:
landmarker.detect_async(
mp_image,
timestamp_ms
)공식 API는 IMAGE, VIDEO, LIVE_STREAM 세 모드를 구분합니다. VIDEO와 LIVE_STREAM에서는 연속 프레임의 타임스탬프가 증가해야 하며, LIVE_STREAM은 처리 속도를 유지하기 위해 작업이 바쁜 경우 새 프레임을 건너뛸 수 있습니다.
이번 글의 웹캠 예제는 이해하기 쉬운 `VIDEO` 모드를 주로 사용합니다.
VIDEO 모드:
한 프레임 처리 완료
→ 다음 프레임 처리
LIVE_STREAM 모드:
프레임 전달
→ 처리는 콜백에서 완료12. OpenCV 프레임을 MediaPipe 이미지로 변환하기
OpenCV 프레임의 기본 색상 순서는 BGR입니다.
MediaPipe 비전 입력에는 RGB 형식의 `mp.Image`를 사용합니다.
import cv2
import mediapipe as mp
rgb_frame = cv2.cvtColor(
frame,
cv2.COLOR_BGR2RGB
)
mp_image = mp.Image(
image_format=mp.ImageFormat.SRGB,
data=rgb_frame
)전체 변환 흐름:
OpenCV 웹캠 프레임
BGR 배열
→ cvtColor
RGB 배열
→ mp.Image
MediaPipe 입력 이미지BGR을 RGB로 변환하지 않으면 모델 입력 색상이 달라집니다.
코드는 정상 실행되더라도 검출 성능이 흔들릴 수 있습니다.
이런 오류는 프로그램이 멈추지 않기 때문에 더 은밀합니다.
13. 손 랜드마크 이해하기
손 랜드마크 번호는 다음 구조를 가집니다.
0:
손목
1~4:
엄지손가락
5~8:
검지손가락
9~12:
중지손가락
13~16:
약지손가락
17~20:
새끼손가락각 손가락의 끝점:
엄지 끝:
4
검지 끝:
8
중지 끝:
12
약지 끝:
16
새끼손가락 끝:
20손가락 관절 구조:
검지:
5 → 6 → 7 → 8
중지:
9 → 10 → 11 → 12
약지:
13 → 14 → 15 → 16
새끼:
17 → 18 → 19 → 20손목에서 각 손가락 끝까지 연결선을 그리면 손의 뼈대와 비슷한 구조를 얻을 수 있습니다.
Hand Landmarker 결과에는 정규화된 이미지 좌표, 미터 단위 월드 좌표, 왼손·오른손 분류가 포함됩니다.
14. 손 연결선 정의하기
그림을 그리기 위한 연결 관계를 직접 정의해 보겠습니다.
HAND_CONNECTIONS = [
# 손목과 손바닥
(0, 1),
(0, 5),
(5, 9),
(9, 13),
(13, 17),
(17, 0),
# 엄지
(1, 2),
(2, 3),
(3, 4),
# 검지
(5, 6),
(6, 7),
(7, 8),
# 중지
(9, 10),
(10, 11),
(11, 12),
# 약지
(13, 14),
(14, 15),
(15, 16),
# 새끼손가락
(17, 18),
(18, 19),
(19, 20)
]랜드마크를 그리는 함수:
import cv2
import numpy as np
def draw_hand_landmarks(
frame: np.ndarray,
landmarks
) -> None:
height, width = frame.shape[:2]
pixel_points = [
normalized_to_pixel_safe(
landmark,
width,
height
)
for landmark in landmarks
]
for start_index, end_index in (
HAND_CONNECTIONS
):
cv2.line(
frame,
pixel_points[start_index],
pixel_points[end_index],
(
0,
255,
0
),
thickness=2,
lineType=cv2.LINE_AA
)
for index, point in enumerate(
pixel_points
):
color = (
(
0,
0,
255
)
if index in {
4,
8,
12,
16,
20
}
else (
255,
100,
0
)
)
cv2.circle(
frame,
point,
radius=4,
color=color,
thickness=-1,
lineType=cv2.LINE_AA
)손가락 끝은 빨간색, 나머지 관절은 파란색 계열로 표시했습니다.
컴퓨터가 손을 보자마자 관절마다 출석 번호를 붙입니다.
15. 웹캠 손 추적 프로그램
다음 코드는 웹캠에서 최대 두 개의 손을 추적합니다.
from pathlib import Path
import time
import cv2
import mediapipe as mp
import numpy as np
MODEL_PATH = Path(
"models/hand_landmarker.task"
)
HAND_CONNECTIONS = [
(0, 1),
(0, 5),
(5, 9),
(9, 13),
(13, 17),
(17, 0),
(1, 2),
(2, 3),
(3, 4),
(5, 6),
(6, 7),
(7, 8),
(9, 10),
(10, 11),
(11, 12),
(13, 14),
(14, 15),
(15, 16),
(17, 18),
(18, 19),
(19, 20)
]
def normalized_to_pixel(
landmark,
width: int,
height: int
) -> tuple[int, int]:
x = int(
landmark.x * width
)
y = int(
landmark.y * height
)
return (
max(
0,
min(
width - 1,
x
)
),
max(
0,
min(
height - 1,
y
)
)
)
def draw_hand(
frame: np.ndarray,
landmarks
) -> None:
height, width = frame.shape[:2]
points = [
normalized_to_pixel(
landmark,
width,
height
)
for landmark in landmarks
]
for start, end in HAND_CONNECTIONS:
cv2.line(
frame,
points[start],
points[end],
(
0,
255,
0
),
2,
cv2.LINE_AA
)
for point in points:
cv2.circle(
frame,
point,
4,
(
0,
120,
255
),
-1,
cv2.LINE_AA
)
def main() -> None:
if not MODEL_PATH.exists():
raise FileNotFoundError(
"손 랜드마크 모델이 없습니다: "
f"{MODEL_PATH.resolve()}"
)
BaseOptions = mp.tasks.BaseOptions
HandLandmarker = (
mp.tasks.vision.HandLandmarker
)
HandLandmarkerOptions = (
mp.tasks.vision
.HandLandmarkerOptions
)
RunningMode = (
mp.tasks.vision.RunningMode
)
options = HandLandmarkerOptions(
base_options=BaseOptions(
model_asset_path=str(
MODEL_PATH
)
),
running_mode=RunningMode.VIDEO,
num_hands=2,
min_hand_detection_confidence=0.5,
min_hand_presence_confidence=0.5,
min_tracking_confidence=0.5
)
capture = cv2.VideoCapture(
0
)
if not capture.isOpened():
raise RuntimeError(
"웹캠을 열 수 없습니다."
)
with HandLandmarker.create_from_options(
options
) as landmarker:
while True:
success, frame = (
capture.read()
)
if not success:
break
frame = cv2.flip(
frame,
1
)
rgb_frame = cv2.cvtColor(
frame,
cv2.COLOR_BGR2RGB
)
mp_image = mp.Image(
image_format=(
mp.ImageFormat.SRGB
),
data=rgb_frame
)
timestamp_ms = (
time.monotonic_ns()
// 1_000_000
)
result = (
landmarker
.detect_for_video(
mp_image,
timestamp_ms
)
)
for hand_index, landmarks in (
enumerate(
result.hand_landmarks
)
):
draw_hand(
frame,
landmarks
)
if (
hand_index
< len(result.handedness)
and result.handedness[
hand_index
]
):
category = (
result.handedness[
hand_index
][0]
)
label = (
category.category_name
)
score = category.score
wrist = (
normalized_to_pixel(
landmarks[0],
frame.shape[1],
frame.shape[0]
)
)
cv2.putText(
frame,
(
f"{label} "
f"{score:.2f}"
),
(
wrist[0],
max(
30,
wrist[1] - 20
)
),
(
cv2
.FONT_HERSHEY_SIMPLEX
),
0.7,
(
255,
255,
255
),
2,
cv2.LINE_AA
)
cv2.imshow(
"MediaPipe Hand Tracking",
frame
)
if cv2.waitKey(1) == ord(
"q"
):
break
capture.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
main()주요 설정
num_hands=2최대 두 개의 손을 검출합니다.
min_hand_detection_confidence=0.5손 검출 결과를 인정할 최소 신뢰도입니다.
min_tracking_confidence=0.5프레임 사이에서 손 추적이 성공했다고 판단할 기준입니다.
Hand Landmarker 옵션은 최대 손 개수와 검출·손 존재·추적 신뢰도 기준을 설정할 수 있습니다. 기본값은 각각 손 1개와 신뢰도 0.5입니다.
16. 왼손과 오른손 판별하기
결과의 `handedness`에는 손 방향 분류가 포함됩니다.
for hand_result in result.handedness:
if not hand_result:
continue
category = hand_result[0]
print(
category.category_name
)
print(
category.score
)결과 예:
Left
0.98또는:
Right
0.96화면을 좌우 반전했는지, 카메라 방향이 어떻게 설정되었는지에 따라 사용자가 기대하는 왼손·오른손 표시와 차이가 발생할 수 있습니다.
frame = cv2.flip(
frame,
1
)셀카 화면처럼 보이게 좌우 반전한 뒤 모델에 전달하면 표시와 손 방향 관계를 실제 환경에서 반드시 확인해야 합니다.
현실의 왼손
카메라 원본
좌우 반전 화면
모델이 처리한 영상네 요소가 한 회의실에 들어오면 방향 감각이 잠시 혼란스러울 수 있습니다.
17. 손가락이 펴졌는지 판단하기
가장 단순한 방법은 손가락 끝점과 중간 관절의 `y` 좌표를 비교하는 것입니다.
def is_index_finger_up(
landmarks
) -> bool:
tip = landmarks[8]
pip = landmarks[6]
return tip.y < pip.y이미지 좌표에서는 위쪽으로 갈수록 `y` 값이 작아집니다.
검지 끝 y < 검지 중간 관절 y
→ 검지가 위로 펼쳐졌을 가능성그러나 이 방식은 손이 회전하면 잘못될 수 있습니다.
손바닥이 위를 향함
손이 옆으로 누움
카메라를 향해 손가락을 뻗음좀 더 안정적인 판단을 위해 관절 각도를 사용할 수 있습니다.
18. 손가락 개수 세기
손가락의 관절 각도를 이용하는 간단한 휴리스틱을 만들어 보겠습니다.
먼저 세 점 사이의 각도를 계산합니다.
import math
def calculate_angle(
point_a,
point_b,
point_c
) -> float:
vector_ba = (
point_a.x - point_b.x,
point_a.y - point_b.y
)
vector_bc = (
point_c.x - point_b.x,
point_c.y - point_b.y
)
dot_product = (
vector_ba[0] * vector_bc[0]
+ vector_ba[1] * vector_bc[1]
)
length_ba = math.hypot(
vector_ba[0],
vector_ba[1]
)
length_bc = math.hypot(
vector_bc[0],
vector_bc[1]
)
denominator = (
length_ba * length_bc
)
if denominator == 0:
return 0.0
cosine_value = (
dot_product / denominator
)
cosine_value = max(
-1.0,
min(
1.0,
cosine_value
)
)
return math.degrees(
math.acos(
cosine_value
)
)손가락별 주요 관절 번호:
FINGER_JOINTS = {
"thumb": (
2,
3,
4
),
"index": (
5,
6,
8
),
"middle": (
9,
10,
12
),
"ring": (
13,
14,
16
),
"pinky": (
17,
18,
20
)
}손가락 개수 계산:
def count_extended_fingers(
landmarks
) -> tuple[int, dict[str, float]]:
angles = {}
thumb_angle = calculate_angle(
landmarks[2],
landmarks[3],
landmarks[4]
)
angles["thumb"] = thumb_angle
finger_count = (
1
if thumb_angle >= 145
else 0
)
for name, (
mcp_index,
pip_index,
tip_index
) in FINGER_JOINTS.items():
if name == "thumb":
continue
angle = calculate_angle(
landmarks[mcp_index],
landmarks[pip_index],
landmarks[tip_index]
)
angles[name] = angle
if angle >= 160:
finger_count += 1
return (
finger_count,
angles
)웹캠 코드에서 사용합니다.
finger_count, angles = (
count_extended_fingers(
landmarks
)
)
cv2.putText(
frame,
f"Fingers: {finger_count}",
(
30,
70
),
cv2.FONT_HERSHEY_SIMPLEX,
1.5,
(
0,
255,
255
),
3,
cv2.LINE_AA
)이 방식의 한계
손가락이 카메라 방향으로 향함
손이 심하게 회전함
관절이 일부 가려짐
손가락이 반쯤 굽혀짐
랜드마크가 흔들림각도 기준인 145도와 160도는 절대적인 값이 아닙니다.
카메라 환경과 원하는 동작에 맞게 조정해야 합니다.
이 코드는 의료용 손 기능 평가기가 아니라 재미있는 인터페이스를 만들기 위한 휴리스틱입니다.
19. 손가락 각도 계산하기
검지 관절의 각도를 화면에 출력해 보겠습니다.
index_angle = calculate_angle(
landmarks[5],
landmarks[6],
landmarks[8]
)검지 중간 관절 위치:
point = normalized_to_pixel(
landmarks[6],
frame.shape[1],
frame.shape[0]
)출력:
cv2.putText(
frame,
f"{index_angle:.0f}",
point,
cv2.FONT_HERSHEY_SIMPLEX,
0.6,
(
255,
255,
255
),
2,
cv2.LINE_AA
)이제 손가락을 굽히면 각도가 변합니다.
손가락을 곧게 펼침
→ 약 180도에 가까움
손가락을 굽힘
→ 각도 감소실시간으로 관절 각도가 움직이는 모습을 보면 손이 갑자기 살아 있는 각도기처럼 느껴집니다.
20. Gesture Recognizer 활용
손가락 개수와 제스처 규칙을 직접 만들 수도 있지만, MediaPipe에는 미리 학습된 Gesture Recognizer 작업도 있습니다.
Gesture Recognizer는 손 랜드마크와 함께 인식된 손동작 범주를 반환할 수 있습니다. 공식 Python 가이드는 이미지, 비디오, 라이브 스트림 모드를 지원하며 별도의 `gesture_recognizer.task` 모델 번들을 사용하도록 안내합니다.
기본 구조:
from pathlib import Path
import time
import cv2
import mediapipe as mp
MODEL_PATH = Path(
"models/gesture_recognizer.task"
)
BaseOptions = mp.tasks.BaseOptions
GestureRecognizer = (
mp.tasks.vision.GestureRecognizer
)
GestureRecognizerOptions = (
mp.tasks.vision
.GestureRecognizerOptions
)
RunningMode = (
mp.tasks.vision.RunningMode
)
options = GestureRecognizerOptions(
base_options=BaseOptions(
model_asset_path=str(
MODEL_PATH
)
),
running_mode=RunningMode.VIDEO,
num_hands=2
)프레임 인식:
result = recognizer.recognize_for_video(
mp_image,
timestamp_ms
)결과 확인:
for gesture_list in result.gestures:
if not gesture_list:
continue
gesture = gesture_list[0]
print(
gesture.category_name,
gesture.score
)모델에 따라 다음과 같은 기본 동작이 포함될 수 있습니다.
Open_Palm
Closed_Fist
Pointing_Up
Thumb_Up
Thumb_Down
Victory
ILoveYou직접 만든 손가락 개수 로직은 규칙을 세밀하게 제어하기 좋습니다.
Gesture Recognizer는 정해진 손동작을 빠르게 적용하기 좋습니다.
21. 얼굴 랜드마크 이해하기
Face Landmarker는 얼굴의 세밀한 메시 구조를 반환합니다.
모델 번들은 얼굴당 478개의 3차원 랜드마크를 추정하고, 선택적으로 52개의 얼굴 Blendshape 점수와 얼굴 변환 행렬을 반환할 수 있습니다.
랜드마크는 다음 영역에 분포합니다.
얼굴 외곽
눈썹
눈
코
입술
볼
턱
홍채 주변활용 예:
얼굴 필터
가상 안경
아바타 애니메이션
눈 깜빡임 감지
입 모양 분석
얼굴 방향 추정
표정 기반 인터페이스478개 점을 전부 화면에 그리면 얼굴 위에 디지털 별자리가 펼쳐집니다.
22. 얼굴 메시 출력하기
다음 예제는 웹캠 얼굴 랜드마크를 점으로 표시합니다.
from pathlib import Path
import time
import cv2
import mediapipe as mp
import numpy as np
MODEL_PATH = Path(
"models/face_landmarker.task"
)
def draw_face_points(
frame: np.ndarray,
face_landmarks,
step: int = 2
) -> None:
height, width = frame.shape[:2]
for index in range(
0,
len(face_landmarks),
step
):
landmark = face_landmarks[index]
x = int(
landmark.x * width
)
y = int(
landmark.y * height
)
cv2.circle(
frame,
(
x,
y
),
1,
(
0,
255,
255
),
-1,
cv2.LINE_AA
)
def main() -> None:
if not MODEL_PATH.exists():
raise FileNotFoundError(
"얼굴 모델이 없습니다: "
f"{MODEL_PATH.resolve()}"
)
BaseOptions = mp.tasks.BaseOptions
FaceLandmarker = (
mp.tasks.vision.FaceLandmarker
)
FaceLandmarkerOptions = (
mp.tasks.vision
.FaceLandmarkerOptions
)
RunningMode = (
mp.tasks.vision.RunningMode
)
options = FaceLandmarkerOptions(
base_options=BaseOptions(
model_asset_path=str(
MODEL_PATH
)
),
running_mode=RunningMode.VIDEO,
num_faces=1,
min_face_detection_confidence=0.5,
min_face_presence_confidence=0.5,
min_tracking_confidence=0.5,
output_face_blendshapes=True
)
capture = cv2.VideoCapture(
0
)
if not capture.isOpened():
raise RuntimeError(
"웹캠을 열 수 없습니다."
)
with FaceLandmarker.create_from_options(
options
) as landmarker:
while True:
success, frame = (
capture.read()
)
if not success:
break
frame = cv2.flip(
frame,
1
)
rgb_frame = cv2.cvtColor(
frame,
cv2.COLOR_BGR2RGB
)
mp_image = mp.Image(
image_format=(
mp.ImageFormat.SRGB
),
data=rgb_frame
)
timestamp_ms = (
time.monotonic_ns()
// 1_000_000
)
result = (
landmarker
.detect_for_video(
mp_image,
timestamp_ms
)
)
for face_landmarks in (
result.face_landmarks
):
draw_face_points(
frame,
face_landmarks,
step=2
)
cv2.imshow(
"Face Landmarks",
frame
)
if cv2.waitKey(1) == ord(
"q"
):
break
capture.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
main()모든 점을 표시하려면 다음과 같이 설정합니다.
step=1성능이나 화면 가독성을 고려해 일부 점만 그릴 수도 있습니다.
step=223. 얼굴 표정 Blendshape
Blendshape는 얼굴 표정의 특정 움직임이 얼마나 나타났는지를 점수로 표현한 값입니다.
예:
eyeBlinkLeft
eyeBlinkRight
jawOpen
mouthSmileLeft
mouthSmileRight
browInnerUpFace Landmarker는 옵션을 활성화하면 얼굴당 52개의 Blendshape 점수를 반환할 수 있습니다.
옵션:
output_face_blendshapes=True결과를 딕셔너리로 변환합니다.
def blendshapes_to_dict(
categories
) -> dict[str, float]:
return {
category.category_name: (
category.score
)
for category in categories
}사용:
if result.face_blendshapes:
scores = blendshapes_to_dict(
result.face_blendshapes[0]
)
print(
scores.get(
"eyeBlinkLeft",
0.0
)
)Blendshape 점수는 표정의 절대적인 의학적 측정값이 아닙니다.
0.0
→ 해당 움직임이 거의 없다고 추정
1.0에 가까움
→ 해당 움직임이 강하다고 추정조명, 얼굴 각도, 안경, 가림 등에 따라 값이 달라질 수 있습니다.
24. 눈 깜빡임 감지하기
왼쪽과 오른쪽 눈 깜빡임 점수를 사용하겠습니다.
def detect_blink(
blendshape_scores: dict[
str,
float
],
threshold: float = 0.55
) -> tuple[bool, float, float]:
left_score = blendshape_scores.get(
"eyeBlinkLeft",
0.0
)
right_score = blendshape_scores.get(
"eyeBlinkRight",
0.0
)
blink_detected = (
left_score >= threshold
and right_score >= threshold
)
return (
blink_detected,
left_score,
right_score
)프레임 처리 중 사용합니다.
if result.face_blendshapes:
scores = blendshapes_to_dict(
result.face_blendshapes[0]
)
blink, left, right = (
detect_blink(
scores
)
)
status = (
"BLINK"
if blink
else "EYES OPEN"
)
cv2.putText(
frame,
(
f"{status} "
f"L:{left:.2f} "
f"R:{right:.2f}"
),
(
20,
40
),
cv2.FONT_HERSHEY_SIMPLEX,
0.8,
(
0,
255,
255
),
2,
cv2.LINE_AA
)깜빡임 횟수 세기
눈을 감고 있는 매 프레임마다 숫자를 증가시키면 한 번의 깜빡임이 여러 번으로 계산됩니다.
상태 변화를 이용해야 합니다.
blink_count = 0
previous_blink = False루프 내부:
if blink and not previous_blink:
blink_count += 1
previous_blink = blink출력:
cv2.putText(
frame,
f"Blink count: {blink_count}",
(
20,
80
),
cv2.FONT_HERSHEY_SIMPLEX,
0.8,
(
255,
255,
255
),
2,
cv2.LINE_AA
)눈을 뜬 상태
→ 눈을 감은 상태로 변경
→ 1회 증가
→ 계속 감고 있음
→ 증가하지 않음
→ 다시 뜬 뒤 감음
→ 다음 1회 증가눈이 감긴 시간을 확인해 의도적인 눈 감기와 짧은 깜빡임을 구분할 수도 있습니다.
25. 입 벌림과 미소 감지 아이디어
Blendshape 점수를 이용해 간단한 표정 인터페이스를 만들 수 있습니다.
입 벌림
jaw_open = scores.get(
"jawOpen",
0.0
)
if jaw_open >= 0.55:
status = "MOUTH OPEN"미소
smile_left = scores.get(
"mouthSmileLeft",
0.0
)
smile_right = scores.get(
"mouthSmileRight",
0.0
)
smile_score = (
smile_left
+ smile_right
) / 2
if smile_score >= 0.5:
status = "SMILE"활용 아이디어:
미소를 지으면 사진 촬영
입을 벌리면 게임 캐릭터 점프
왼쪽 눈을 감으면 이전 화면
오른쪽 눈을 감으면 다음 화면
눈썹을 올리면 메뉴 열기그러나 표정 점수는 사람마다 다르게 나타날 수 있습니다.
처음 몇 초 동안 사용자의 평상시 점수를 측정한 뒤 개인 기준값을 설정하는 편이 더 안정적입니다.
26. 포즈 랜드마크 이해하기
Pose Landmarker는 사람의 주요 신체 위치 33개를 추적합니다.
결과에는 정규화된 이미지 좌표와 3차원 월드 좌표가 포함되며, 선택적으로 사람 영역의 세그멘테이션 마스크도 출력할 수 있습니다.
주요 번호:
0:
코
11:
왼쪽 어깨
12:
오른쪽 어깨
13:
왼쪽 팔꿈치
14:
오른쪽 팔꿈치
15:
왼쪽 손목
16:
오른쪽 손목
23:
왼쪽 골반
24:
오른쪽 골반
25:
왼쪽 무릎
26:
오른쪽 무릎
27:
왼쪽 발목
28:
오른쪽 발목
31:
왼쪽 발끝
32:
오른쪽 발끝자세 결과의 `visibility`는 해당 랜드마크가 이미지에서 보일 가능성을 나타냅니다.
if landmark.visibility < 0.5:
# 신뢰하기 어려운 좌표
...관절이 화면 밖에 있거나 다른 신체 부위에 가려졌다면 좌표를 무조건 사용하지 않는 것이 좋습니다.
27. 자세 연결선 정의하기
주요 신체 연결 관계를 정의합니다.
POSE_CONNECTIONS = [
# 얼굴과 상체
(11, 12),
# 왼팔
(11, 13),
(13, 15),
# 오른팔
(12, 14),
(14, 16),
# 몸통
(11, 23),
(12, 24),
(23, 24),
# 왼쪽 다리
(23, 25),
(25, 27),
(27, 29),
(29, 31),
(27, 31),
# 오른쪽 다리
(24, 26),
(26, 28),
(28, 30),
(30, 32),
(28, 32)
]그리기 함수:
def draw_pose(
frame,
landmarks,
visibility_threshold: float = 0.5
) -> None:
height, width = frame.shape[:2]
points = {}
for index, landmark in enumerate(
landmarks
):
visibility = (
landmark.visibility
if landmark.visibility
is not None
else 1.0
)
if visibility < (
visibility_threshold
):
continue
points[index] = (
int(
landmark.x * width
),
int(
landmark.y * height
)
)
for start, end in POSE_CONNECTIONS:
if (
start not in points
or end not in points
):
continue
cv2.line(
frame,
points[start],
points[end],
(
0,
255,
0
),
3,
cv2.LINE_AA
)
for point in points.values():
cv2.circle(
frame,
point,
5,
(
0,
100,
255
),
-1,
cv2.LINE_AA
)28. 웹캠 전신 자세 추적
from pathlib import Path
import time
import cv2
import mediapipe as mp
MODEL_PATH = Path(
"models/pose_landmarker.task"
)
POSE_CONNECTIONS = [
(11, 12),
(11, 13),
(13, 15),
(12, 14),
(14, 16),
(11, 23),
(12, 24),
(23, 24),
(23, 25),
(25, 27),
(27, 29),
(29, 31),
(27, 31),
(24, 26),
(26, 28),
(28, 30),
(30, 32),
(28, 32)
]
def draw_pose(
frame,
landmarks
) -> None:
height, width = frame.shape[:2]
points = {}
for index, landmark in enumerate(
landmarks
):
visibility = (
landmark.visibility
if landmark.visibility
is not None
else 1.0
)
if visibility < 0.5:
continue
points[index] = (
int(
landmark.x * width
),
int(
landmark.y * height
)
)
for start, end in POSE_CONNECTIONS:
if (
start not in points
or end not in points
):
continue
cv2.line(
frame,
points[start],
points[end],
(
0,
255,
0
),
3,
cv2.LINE_AA
)
for point in points.values():
cv2.circle(
frame,
point,
5,
(
0,
0,
255
),
-1,
cv2.LINE_AA
)
def main() -> None:
if not MODEL_PATH.exists():
raise FileNotFoundError(
"자세 모델이 없습니다: "
f"{MODEL_PATH.resolve()}"
)
BaseOptions = mp.tasks.BaseOptions
PoseLandmarker = (
mp.tasks.vision.PoseLandmarker
)
PoseLandmarkerOptions = (
mp.tasks.vision
.PoseLandmarkerOptions
)
RunningMode = (
mp.tasks.vision.RunningMode
)
options = PoseLandmarkerOptions(
base_options=BaseOptions(
model_asset_path=str(
MODEL_PATH
)
),
running_mode=RunningMode.VIDEO,
num_poses=1,
min_pose_detection_confidence=0.5,
min_pose_presence_confidence=0.5,
min_tracking_confidence=0.5,
output_segmentation_masks=False
)
capture = cv2.VideoCapture(
0
)
if not capture.isOpened():
raise RuntimeError(
"웹캠을 열 수 없습니다."
)
with PoseLandmarker.create_from_options(
options
) as landmarker:
while True:
success, frame = (
capture.read()
)
if not success:
break
frame = cv2.flip(
frame,
1
)
rgb_frame = cv2.cvtColor(
frame,
cv2.COLOR_BGR2RGB
)
mp_image = mp.Image(
image_format=(
mp.ImageFormat.SRGB
),
data=rgb_frame
)
timestamp_ms = (
time.monotonic_ns()
// 1_000_000
)
result = (
landmarker
.detect_for_video(
mp_image,
timestamp_ms
)
)
for pose_landmarks in (
result.pose_landmarks
):
draw_pose(
frame,
pose_landmarks
)
cv2.imshow(
"Pose Tracking",
frame
)
if cv2.waitKey(1) == ord(
"q"
):
break
capture.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
main()전신이 화면에 들어오지 않으면 무릎이나 발목 좌표의 정확도가 떨어질 수 있습니다.
카메라를 충분히 멀리 배치
전신이 화면 안에 들어오도록 조정
조명을 밝게 유지
신체와 배경 대비 확보29. 세 점으로 관절 각도 계산하기
관절 각도는 세 점으로 계산할 수 있습니다.
팔꿈치 각도:
어깨
→ 팔꿈치
→ 손목무릎 각도:
골반
→ 무릎
→ 발목가운데 점이 각도의 꼭짓점입니다.
import math
def calculate_landmark_angle(
point_a,
point_b,
point_c
) -> float:
angle_radians = (
math.atan2(
point_c.y - point_b.y,
point_c.x - point_b.x
)
- math.atan2(
point_a.y - point_b.y,
point_a.x - point_b.x
)
)
angle_degrees = abs(
math.degrees(
angle_radians
)
)
if angle_degrees > 180:
angle_degrees = (
360 - angle_degrees
)
return angle_degrees사용:
left_hip = landmarks[23]
left_knee = landmarks[25]
left_ankle = landmarks[27]
knee_angle = (
calculate_landmark_angle(
left_hip,
left_knee,
left_ankle
)
)다리를 곧게 폄
→ 약 180도
무릎을 굽힘
→ 각도 감소2차원 이미지 좌표로 계산한 각도는 실제 3차원 관절 각도와 다를 수 있습니다.
카메라가 몸의 측면에 가까울수록 스쿼트 무릎 각도 측정이 비교적 직관적입니다.
30. 팔꿈치 각도 표시하기
왼팔 기준:
left_shoulder = landmarks[11]
left_elbow = landmarks[13]
left_wrist = landmarks[15]
elbow_angle = (
calculate_landmark_angle(
left_shoulder,
left_elbow,
left_wrist
)
)팔꿈치 픽셀 좌표:
height, width = frame.shape[:2]
elbow_point = (
int(
left_elbow.x * width
),
int(
left_elbow.y * height
)
)화면에 표시:
cv2.putText(
frame,
f"{elbow_angle:.0f}",
elbow_point,
cv2.FONT_HERSHEY_SIMPLEX,
0.8,
(
255,
255,
255
),
2,
cv2.LINE_AA
)활용 예:
팔굽혀펴기 횟수
덤벨 컬 횟수
팔 스트레칭 각도
골프 스윙 분석
재활 동작 보조단, 의료 진단이나 재활 평가에는 임상적으로 검증된 장비와 전문가 판단이 필요합니다.
웹캠 기반 각도는 보조적인 피드백으로 생각해야 합니다.
31. 스쿼트 자세 판단하기
스쿼트 횟수를 세기 위해 무릎 각도를 사용해 보겠습니다.
간단한 상태:
UP:
서 있는 상태
DOWN:
무릎을 굽힌 상태판단 기준 예:
무릎 각도 160도 이상
→ UP
무릎 각도 100도 이하
→ DOWN상태 변화:
UP
→ 내려가서 DOWN
→ 다시 올라와 UP
→ 1회 증가코드:
def update_squat_state(
knee_angle: float,
current_stage: str,
squat_count: int
) -> tuple[str, int]:
if knee_angle <= 100:
current_stage = "DOWN"
elif (
knee_angle >= 160
and current_stage == "DOWN"
):
current_stage = "UP"
squat_count += 1
return (
current_stage,
squat_count
)처음 상태:
stage = "UP"
squat_count = 0각도 기준은 체형, 카메라 위치, 운동 깊이에 따라 조정해야 합니다.
32. 실전 프로젝트: AI 스쿼트 카운터
다음 프로젝트는 웹캠으로 자세를 추적하고 스쿼트 횟수를 계산합니다.
프로젝트 기능
전신 관절 추적
좌우 무릎 각도 계산
평균 무릎 각도 사용
UP·DOWN 상태 판별
스쿼트 횟수 증가
무릎 관절 각도 표시
자세 검출 실패 경고
실시간 FPS 표시전체 코드
from collections import deque
from pathlib import Path
import math
import time
import cv2
import mediapipe as mp
import numpy as np
MODEL_PATH = Path(
"models/pose_landmarker.task"
)
POSE_CONNECTIONS = [
(11, 12),
(11, 13),
(13, 15),
(12, 14),
(14, 16),
(11, 23),
(12, 24),
(23, 24),
(23, 25),
(25, 27),
(27, 29),
(29, 31),
(27, 31),
(24, 26),
(26, 28),
(28, 30),
(30, 32),
(28, 32)
]
def calculate_angle(
point_a,
point_b,
point_c
) -> float:
radians = (
math.atan2(
point_c.y - point_b.y,
point_c.x - point_b.x
)
- math.atan2(
point_a.y - point_b.y,
point_a.x - point_b.x
)
)
degrees = abs(
math.degrees(
radians
)
)
if degrees > 180:
degrees = (
360 - degrees
)
return degrees
def smooth_value(
history: deque,
value: float
) -> float:
history.append(
value
)
return sum(
history
) / len(
history
)
def landmark_is_visible(
landmark,
threshold: float = 0.55
) -> bool:
visibility = landmark.visibility
if visibility is None:
return True
return visibility >= threshold
def draw_pose(
frame: np.ndarray,
landmarks
) -> None:
height, width = frame.shape[:2]
points = {}
for index, landmark in enumerate(
landmarks
):
if not landmark_is_visible(
landmark,
0.5
):
continue
points[index] = (
int(
landmark.x * width
),
int(
landmark.y * height
)
)
for start, end in POSE_CONNECTIONS:
if (
start not in points
or end not in points
):
continue
cv2.line(
frame,
points[start],
points[end],
(
0,
255,
0
),
3,
cv2.LINE_AA
)
for point in points.values():
cv2.circle(
frame,
point,
5,
(
0,
120,
255
),
-1,
cv2.LINE_AA
)
def draw_panel(
frame: np.ndarray,
count: int,
stage: str,
knee_angle: float,
fps: float,
message: str
) -> None:
overlay = frame.copy()
cv2.rectangle(
overlay,
(
10,
10
),
(
390,
175
),
(
20,
20,
20
),
thickness=-1
)
cv2.addWeighted(
overlay,
0.65,
frame,
0.35,
0,
frame
)
cv2.putText(
frame,
f"Squats: {count}",
(
25,
50
),
cv2.FONT_HERSHEY_SIMPLEX,
1.1,
(
0,
255,
255
),
3,
cv2.LINE_AA
)
cv2.putText(
frame,
f"Stage: {stage}",
(
25,
90
),
cv2.FONT_HERSHEY_SIMPLEX,
0.8,
(
255,
255,
255
),
2,
cv2.LINE_AA
)
cv2.putText(
frame,
f"Knee: {knee_angle:.0f} deg",
(
25,
125
),
cv2.FONT_HERSHEY_SIMPLEX,
0.7,
(
255,
255,
255
),
2,
cv2.LINE_AA
)
cv2.putText(
frame,
f"FPS: {fps:.1f}",
(
25,
155
),
cv2.FONT_HERSHEY_SIMPLEX,
0.6,
(
200,
200,
200
),
2,
cv2.LINE_AA
)
cv2.putText(
frame,
message,
(
20,
frame.shape[0] - 25
),
cv2.FONT_HERSHEY_SIMPLEX,
0.7,
(
0,
255,
0
),
2,
cv2.LINE_AA
)
def main() -> None:
if not MODEL_PATH.exists():
raise FileNotFoundError(
"포즈 모델을 찾을 수 없습니다: "
f"{MODEL_PATH.resolve()}"
)
BaseOptions = mp.tasks.BaseOptions
PoseLandmarker = (
mp.tasks.vision.PoseLandmarker
)
PoseLandmarkerOptions = (
mp.tasks.vision
.PoseLandmarkerOptions
)
RunningMode = (
mp.tasks.vision.RunningMode
)
options = PoseLandmarkerOptions(
base_options=BaseOptions(
model_asset_path=str(
MODEL_PATH
)
),
running_mode=RunningMode.VIDEO,
num_poses=1,
min_pose_detection_confidence=0.6,
min_pose_presence_confidence=0.6,
min_tracking_confidence=0.6
)
capture = cv2.VideoCapture(
0
)
if not capture.isOpened():
raise RuntimeError(
"웹캠을 열 수 없습니다."
)
capture.set(
cv2.CAP_PROP_FRAME_WIDTH,
960
)
capture.set(
cv2.CAP_PROP_FRAME_HEIGHT,
720
)
squat_count = 0
stage = "UP"
knee_angle_history = deque(
maxlen=5
)
previous_time = time.perf_counter()
with PoseLandmarker.create_from_options(
options
) as landmarker:
while True:
success, frame = (
capture.read()
)
if not success:
break
frame = cv2.flip(
frame,
1
)
rgb_frame = cv2.cvtColor(
frame,
cv2.COLOR_BGR2RGB
)
mp_image = mp.Image(
image_format=(
mp.ImageFormat.SRGB
),
data=rgb_frame
)
timestamp_ms = (
time.monotonic_ns()
// 1_000_000
)
result = (
landmarker
.detect_for_video(
mp_image,
timestamp_ms
)
)
knee_angle = 0.0
message = (
"Move your full body "
"into the camera view"
)
if result.pose_landmarks:
landmarks = (
result.pose_landmarks[0]
)
required_indices = [
23,
24,
25,
26,
27,
28
]
visible = all(
landmark_is_visible(
landmarks[index],
0.55
)
for index in (
required_indices
)
)
if visible:
left_angle = (
calculate_angle(
landmarks[23],
landmarks[25],
landmarks[27]
)
)
right_angle = (
calculate_angle(
landmarks[24],
landmarks[26],
landmarks[28]
)
)
raw_knee_angle = (
left_angle
+ right_angle
) / 2
knee_angle = smooth_value(
knee_angle_history,
raw_knee_angle
)
if knee_angle <= 100:
stage = "DOWN"
message = (
"Good depth. "
"Now stand up!"
)
elif (
knee_angle >= 160
and stage == "DOWN"
):
stage = "UP"
squat_count += 1
message = (
"Rep completed!"
)
elif stage == "UP":
message = (
"Bend your knees "
"and go down"
)
else:
message = (
"Keep moving up"
)
draw_pose(
frame,
landmarks
)
height, width = (
frame.shape[:2]
)
for index, angle in [
(
25,
left_angle
),
(
26,
right_angle
)
]:
point = (
int(
landmarks[
index
].x * width
),
int(
landmarks[
index
].y * height
)
)
cv2.putText(
frame,
f"{angle:.0f}",
point,
(
cv2
.FONT_HERSHEY_SIMPLEX
),
0.7,
(
255,
255,
255
),
2,
cv2.LINE_AA
)
current_time = time.perf_counter()
elapsed = (
current_time
- previous_time
)
fps = (
1.0 / elapsed
if elapsed > 0
else 0.0
)
previous_time = current_time
draw_panel(
frame,
squat_count,
stage,
knee_angle,
fps,
message
)
cv2.imshow(
"AI Squat Counter",
frame
)
key = cv2.waitKey(1)
if key == ord("r"):
squat_count = 0
stage = "UP"
knee_angle_history.clear()
elif key == ord("q"):
break
capture.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
main()조작 방법
q:
프로그램 종료
r:
횟수 초기화좌우 무릎 평균을 사용하는 이유
한쪽 다리가 순간적으로 가려지거나 좌표가 흔들릴 수 있습니다.
raw_knee_angle = (
left_angle
+ right_angle
) / 2두 무릎을 평균 내면 특정 상황에서 값이 조금 더 안정적으로 보일 수 있습니다.
그러나 카메라가 완전히 측면에 있다면 가까운 쪽 다리만 사용하는 편이 더 적합할 수도 있습니다.
33. 잘못된 자세 경고하기
스쿼트 자세 분석에는 무릎 각도 외에도 여러 기준을 사용할 수 있습니다.
상체 기울기
어깨와 골반의 관계로 몸통 기울기를 계산할 수 있습니다.
def midpoint(
point_a,
point_b
) -> tuple[float, float]:
return (
(
point_a.x
+ point_b.x
) / 2,
(
point_a.y
+ point_b.y
) / 2
)어깨 중심과 골반 중심:
shoulder_center = midpoint(
landmarks[11],
landmarks[12]
)
hip_center = midpoint(
landmarks[23],
landmarks[24]
)수직선과의 각도:
def torso_lean_angle(
shoulder_center,
hip_center
) -> float:
dx = (
shoulder_center[0]
- hip_center[0]
)
dy = (
shoulder_center[1]
- hip_center[1]
)
return abs(
math.degrees(
math.atan2(
dx,
-dy
)
)
)경고:
lean = torso_lean_angle(
shoulder_center,
hip_center
)
if lean >= 35:
message = (
"Keep your torso "
"more upright"
)무릎 방향
정면 영상에서는 무릎과 발목의 가로 위치 차이를 비교할 수 있습니다.
left_knee_x = landmarks[25].x
left_ankle_x = landmarks[27].x
left_alignment = abs(
left_knee_x
- left_ankle_x
)하지만 단일 카메라의 2차원 좌표만으로 정확한 자세를 판정하기에는 한계가 큽니다.
카메라 위치
렌즈 왜곡
신체 비율
발 간격
운동 방식
촬영 각도따라서 화면 경고는 참고용으로 제공해야 합니다.
“의학적으로 잘못된 자세입니다.”
보다
“현재 카메라 기준에서
상체 기울기가 크게 측정되었습니다.”처럼 표현하는 편이 안전하고 정확합니다.
34. MediaPipe Holistic
얼굴, 양손, 전신 자세를 한 번에 추적하고 싶다면 Holistic Landmarker를 사용할 수 있습니다.
현재 Python Tasks API에는 `HolisticLandmarker`와 관련 옵션이 제공되며, 결과에는 얼굴, 자세, 왼손, 오른손 랜드마크가 포함될 수 있습니다. 선택적으로 얼굴 Blendshape와 세그멘테이션 마스크도 출력할 수 있습니다.
개념:
Face Landmarker
+ Left Hand Landmarker
+ Right Hand Landmarker
+ Pose Landmarker
= Holistic Landmarker활용:
전신 아바타
댄스 동작 분석
수어 인터페이스
가상 캐릭터 모션 캡처
화상회의 효과
전신 게임 컨트롤기본 구조:
BaseOptions = mp.tasks.BaseOptions
HolisticLandmarker = (
mp.tasks.vision.HolisticLandmarker
)
HolisticLandmarkerOptions = (
mp.tasks.vision
.HolisticLandmarkerOptions
)
RunningMode = (
mp.tasks.vision.RunningMode
)
options = HolisticLandmarkerOptions(
base_options=BaseOptions(
model_asset_path=(
"models/"
"holistic_landmarker.task"
)
),
running_mode=RunningMode.VIDEO,
output_face_blendshapes=True,
output_segmentation_mask=False
)여러 작업을 각각 실행하는 것보다 일관된 전신 결과를 얻기 편리할 수 있지만 계산량도 고려해야 합니다.
35. 좌표 흔들림 줄이기
랜드마크는 매 프레임 조금씩 흔들릴 수 있습니다.
실제 손:
가만히 있음
검출 좌표:
100, 101, 99, 102, 100...화면에 선을 그리면 떨림이 보일 수 있습니다.
이동평균
최근 값 여러 개의 평균을 사용합니다.
from collections import deque
x_history = deque(
maxlen=5
)
y_history = deque(
maxlen=5
)새 좌표:
x_history.append(
current_x
)
y_history.append(
current_y
)
smooth_x = sum(
x_history
) / len(
x_history
)
smooth_y = sum(
y_history
) / len(
y_history
)지수 이동평균
def exponential_smoothing(
previous_value: float,
current_value: float,
alpha: float = 0.3
) -> float:
return (
alpha * current_value
+ (
1 - alpha
) * previous_value
)alpha가 큼
→ 현재 움직임에 빠르게 반응
→ 흔들림이 더 보일 수 있음
alpha가 작음
→ 더 부드러움
→ 움직임 반응이 느림지나치게 강한 평활화를 사용하면 손을 움직인 뒤 화면 속 뼈대가 늦게 따라옵니다.
랜드마크가 월요일 아침 직원처럼 움직일 수 있습니다.
36. FPS 측정하기
FPS는 초당 처리하는 프레임 수입니다.
import time
previous_time = time.perf_counter()루프 내부:
current_time = time.perf_counter()
elapsed = (
current_time
- previous_time
)
fps = (
1.0 / elapsed
if elapsed > 0
else 0.0
)
previous_time = current_time화면 출력:
cv2.putText(
frame,
f"FPS: {fps:.1f}",
(
20,
40
),
cv2.FONT_HERSHEY_SIMPLEX,
0.8,
(
0,
255,
0
),
2,
cv2.LINE_AA
)FPS가 지나치게 출렁이면 최근 값의 평균을 사용할 수 있습니다.
fps_history = deque(
maxlen=30
)
fps_history.append(
fps
)
average_fps = sum(
fps_history
) / len(
fps_history
)37. 실시간 처리 성능 높이기
입력 해상도 줄이기
small_frame = cv2.resize(
frame,
(
640,
360
),
interpolation=cv2.INTER_AREA
)해상도가 낮아지면 처리할 픽셀 수가 감소합니다.
1920 × 1080
→ 약 207만 픽셀
640 × 360
→ 약 23만 픽셀검출 대상 수 줄이기
num_hands=1
num_faces=1
num_poses=1필요 이상의 대상을 검출하지 않습니다.
모든 랜드마크를 그리지 않기
얼굴 랜드마크 478개를 매 프레임 모두 그리면 화면 표시 비용도 증가합니다.
for index in range(
0,
len(face_landmarks),
3
):
...일부 프레임만 분석하기
frame_index += 1
if frame_index % 2 == 0:
# 두 프레임마다 한 번 분석
...다만 빠른 움직임을 놓칠 수 있습니다.
VIDEO 모드 사용
연속 프레임에서는 추적 정보를 활용할 수 있습니다.
Face Landmarker 공식 가이드에 따르면 VIDEO와 LIVE_STREAM 모드는 추적을 이용해 매 프레임마다 얼굴 검출 모델을 다시 호출하는 일을 줄여 지연 시간을 낮춥니다.
화면 표시 비용 확인
cv2.imshow()
cv2.putText()
cv2.circle()
cv2.line()모델 추론뿐 아니라 화면에 수백 개 점을 그리는 작업도 FPS에 영향을 줄 수 있습니다.
38. 멀티스레드와 LIVE_STREAM 모드
VIDEO 모드는 현재 프레임 처리가 끝날 때까지 기다립니다.
프레임 읽기
→ 모델 처리
→ 결과 출력
→ 다음 프레임LIVE_STREAM 모드는 프레임을 비동기로 전달하고 결과를 콜백으로 받습니다.
latest_result = None
def result_callback(
result,
output_image,
timestamp_ms: int
) -> None:
global latest_result
latest_result = result옵션:
options = (
mp.tasks.vision
.HandLandmarkerOptions(
base_options=(
mp.tasks.BaseOptions(
model_asset_path=str(
MODEL_PATH
)
)
),
running_mode=(
mp.tasks.vision
.RunningMode
.LIVE_STREAM
),
result_callback=(
result_callback
)
)
)프레임 전달:
landmarker.detect_async(
mp_image,
timestamp_ms
)LIVE_STREAM은 지연 시간을 줄이는 데 유리할 수 있지만 작업이 바쁜 경우 일부 입력 프레임이 무시될 수 있습니다.
다음 문제가 추가됩니다.
콜백과 화면 루프 사이의 데이터 공유
이전 결과와 현재 프레임의 시간 차이
스레드 안전성
프로그램 종료 처리
프레임 누락 허용 여부먼저 VIDEO 모드로 기능을 완성한 뒤 성능 요구가 생겼을 때 LIVE_STREAM으로 확장하는 편이 좋습니다.
39. OpenCV·MediaPipe·PyTorch 연결
세 도구를 연결하면 더욱 복잡한 시스템을 만들 수 있습니다.
OpenCV
→ 웹캠 프레임 읽기
MediaPipe
→ 손·얼굴·자세 영역 찾기
OpenCV
→ 관심 영역 잘라내기
PyTorch
→ 잘라낸 영역을 상세 분류
OpenCV
→ 결과 표시예를 들어 손 영역을 MediaPipe로 찾고 PyTorch 모델로 사용자 정의 수어 동작을 분류할 수 있습니다.
손 경계 영역 계산
def hand_bounding_box(
landmarks,
image_width: int,
image_height: int,
padding: int = 20
) -> tuple[int, int, int, int]:
x_values = [
landmark.x
for landmark in landmarks
]
y_values = [
landmark.y
for landmark in landmarks
]
x1 = int(
min(x_values)
* image_width
) - padding
y1 = int(
min(y_values)
* image_height
) - padding
x2 = int(
max(x_values)
* image_width
) + padding
y2 = int(
max(y_values)
* image_height
) + padding
x1 = max(
0,
x1
)
y1 = max(
0,
y1
)
x2 = min(
image_width,
x2
)
y2 = min(
image_height,
y2
)
return (
x1,
y1,
x2,
y2
)관심 영역 추출:
x1, y1, x2, y2 = (
hand_bounding_box(
landmarks,
frame.shape[1],
frame.shape[0]
)
)
hand_image = frame[
y1:y2,
x1:x2
]그다음 PyTorch 모델 입력 크기로 전처리합니다.
BGR에서 RGB 변환
→ 크기 조절
→ 텐서 변환
→ 정규화
→ 배치 차원 추가
→ 모델 추론MediaPipe는 어디를 볼지 알려 주고 PyTorch는 그 영역이 무엇인지 더 깊게 판단하는 구조입니다.
40. 개인정보와 영상 처리
MediaPipe Tasks 공식 안내에 따르면 입력 이미지, 영상, 텍스트 등의 작업 처리는 기기에서 수행되며 입력 데이터 자체를 Google 서버로 전송하지 않습니다. 다만 API 성능과 사용 관련 지표가 전송될 수 있고, 관련 법률에 따라 사용자 동의를 확보할 책임은 애플리케이션 개발자에게 있습니다.
온디바이스 처리라고 해서 개인정보 문제가 자동으로 사라지는 것은 아닙니다.
확인할 내용:
촬영 사실을 사용자에게 알렸는가?
촬영 동의를 받았는가?
영상을 저장하는가?
랜드마크 좌표를 저장하는가?
보관 기간은 얼마인가?
누가 데이터에 접근할 수 있는가?
얼굴 식별에 사용하는가?
외부 서버로 결과를 전송하는가?랜드마크 좌표도 사용자 행동을 나타내는 데이터가 될 수 있습니다.
얼굴 영상은 저장하지 않음
하지만
눈 깜빡임 시간
운동 횟수
자세 변화
표정 점수이런 정보도 목적과 상황에 따라 민감할 수 있습니다.
필요한 정보만 처리하고 불필요한 원본 영상을 저장하지 않는 것이 좋습니다.
41. MediaPipe 사용 시 주의점
랜드마크는 실제 관절 위치와 완전히 같지 않다
모델이 영상에서 추정한 좌표입니다.
랜드마크 좌표
≠
의료 장비 측정값2차원 각도와 3차원 각도는 다르다
카메라 방향이 달라지면 같은 자세도 각도가 다르게 보입니다.
가려짐에 취약할 수 있다
팔이 몸 뒤로 감
두 손이 겹침
긴 옷으로 관절이 가려짐
물체가 얼굴을 가림화면 밖 랜드마크를 확인한다
좌표가 0부터 1 범위를 약간 벗어날 수도 있으므로 픽셀 변환 시 제한하는 것이 안전합니다.
visibility와 confidence를 활용한다
신뢰도가 낮은 좌표로 각도를 계산하면 값이 크게 튈 수 있습니다.
임계값을 환경에 맞게 조정한다
눈 깜빡임 0.55
손가락 각도 160도
스쿼트 하단 100도
스쿼트 상단 160도이 값은 예제 기준일 뿐입니다.
상태 전환을 사용한다
동작 횟수는 매 프레임 조건만 확인하면 중복 계산됩니다.
DOWN으로 전환
→ UP으로 전환
→ 1회 증가실제 사용자로 테스트한다
신체 비율, 옷, 카메라 위치, 조명에 따라 결과가 달라집니다.
안전 관련 판단을 자동화하지 않는다
의료 진단
산업 안전 판정
법 집행
신원 인증높은 정확성과 검증이 요구되는 분야에서는 단순한 예제 코드를 그대로 사용하면 안 됩니다.
42. 자주 발생하는 오류
오류 1. mediapipe 모듈을 찾을 수 없음
ModuleNotFoundError:
No module named 'mediapipe'설치:
python -m pip install mediapipe확인:
python -m pip show mediapipe현재 Python:
python -c "import sys; print(sys.executable)"오류 2. 설치 가능한 버전이 없음
No matching distribution found
for mediapipe확인:
Python 버전
운영체제
CPU 아키텍처
32비트·64비트
Intel Mac·Apple Siliconpython --versionpython -c "import platform; print(platform.machine())"현재 환경과 호환되는 MediaPipe 휠이 존재해야 합니다.
오류 3. 모델 파일을 찾을 수 없음
Unable to open file
또는
ValueError:
ExternalFile must specify
at least one of ...확인:
from pathlib import Path
model_path = Path(
"models/hand_landmarker.task"
)
print(
model_path.resolve()
)
print(
model_path.exists()
)오류 4. 잘못된 모델을 사용함
Face Landmarker에 Hand Landmarker 모델을 전달할 수 없습니다.
HandLandmarker
→ hand_landmarker.task
FaceLandmarker
→ face_landmarker.task
PoseLandmarker
→ pose_landmarker.task모델 번들은 작업의 입력과 출력 규격에 맞아야 합니다.
오류 5. 타임스탬프가 증가하지 않음
VIDEO와 LIVE_STREAM 모드에서는 연속된 타임스탬프가 증가해야 합니다.
안전한 방식:
timestamp_ms = (
time.monotonic_ns()
// 1_000_000
)잘못된 방식:
timestamp_ms = 0매 프레임 같은 값을 전달하면 오류가 발생할 수 있습니다.
오류 6. BGR 이미지를 그대로 전달함
변환:
rgb_frame = cv2.cvtColor(
frame,
cv2.COLOR_BGR2RGB
)MediaPipe 이미지:
mp_image = mp.Image(
image_format=mp.ImageFormat.SRGB,
data=rgb_frame
)오류 7. 웹캠을 열 수 없음
카메라가 다른 프로그램에서 사용 중
카메라 권한이 꺼짐
장치 번호가 다름
원격 서버에 웹캠이 없음장치 확인:
for camera_index in range(5):
capture = cv2.VideoCapture(
camera_index
)
print(
camera_index,
capture.isOpened()
)
capture.release()오류 8. 랜드마크가 검출되지 않음
확인:
대상이 화면 안에 충분히 크게 보이는가?
조명이 밝은가?
손이나 몸이 심하게 가려졌는가?
신뢰도 기준이 너무 높은가?
올바른 모델을 사용했는가?
RGB 변환을 했는가?신뢰도를 일시적으로 낮춰 비교할 수 있습니다.
min_hand_detection_confidence=0.4지나치게 낮추면 오검출이 증가할 수 있습니다.
오류 9. 화면은 나오지만 매우 느림
대응:
카메라 해상도 낮추기
검출 대상 수 줄이기
얼굴 점 일부만 그리기
불필요한 화면 창 줄이기
프레임 일부만 분석하기
VIDEO 추적 모드 사용오류 10. 손가락 개수가 틀림
원인:
손이 옆으로 누움
엄지 방향 판단 오류
손가락 일부가 가려짐
각도 기준이 맞지 않음
손이 너무 작게 보임각 손가락 각도를 화면에 출력해 기준값을 조정합니다.
print(
angles
)오류 11. 스쿼트 횟수가 여러 번 증가함
상태 전환을 확인합니다.
잘못된 방식:
if knee_angle >= 160:
squat_count += 1올바른 방식:
if knee_angle <= 100:
stage = "DOWN"
elif (
knee_angle >= 160
and stage == "DOWN"
):
stage = "UP"
squat_count += 1오류 12. 각도가 크게 튐
대응:
visibility 확인
이동평균 적용
신뢰도 낮은 프레임 건너뛰기
카메라 고정
더 밝은 조명 사용오류 13. Face Blendshape 결과가 비어 있음
옵션을 확인합니다.
output_face_blendshapes=True또한 얼굴이 정상적으로 검출되어야 합니다.
if result.face_blendshapes:
...빈 결과에 바로 `[0]`으로 접근하면 오류가 발생합니다.
오류 14. LIVE_STREAM 결과가 모든 프레임에 나오지 않음
LIVE_STREAM 모드는 지연 시간을 줄이기 위해 처리 중 새 입력 프레임을 건너뛸 수 있습니다.
모든 프레임 결과가 반드시 필요하다면 VIDEO 모드를 검토합니다.
43. 연습 문제
문제 1
웹캠 프레임을 BGR에서 RGB로 변환한 뒤 `mp.Image`로 만드세요.
문제 2
정규화 좌표 `x=0.25`, `y=0.5`를 800×600 이미지의 픽셀 좌표로 변환하세요.
정답:
x:
0.25 × 800 = 200
y:
0.5 × 600 = 300문제 3
Hand Landmarker를 VIDEO 모드로 생성하고 최대 손 개수를 2개로 설정하세요.
문제 4
검지 끝 8번과 검지 중간 관절 6번의 위치를 화면에 서로 다른 색상으로 표시하세요.
문제 5
검지, 중지, 약지, 새끼손가락 중 펼쳐진 손가락 개수를 계산하세요.
문제 6
Gesture Recognizer 결과에서 가장 높은 점수의 제스처 이름을 출력하세요.
문제 7
Face Landmarker의 얼굴 랜드마크를 두 점 간격으로 화면에 표시하세요.
문제 8
`eyeBlinkLeft`와 `eyeBlinkRight` 점수가 모두 0.6 이상일 때 `BLINK`를 표시하세요.
문제 9
눈을 감은 상태가 유지되는 동안 한 번만 깜빡임 횟수가 증가하도록 상태 전환을 구현하세요.
문제 10
왼쪽 어깨 11번, 팔꿈치 13번, 손목 15번으로 팔꿈치 각도를 계산하세요.
문제 11
왼쪽 골반 23번, 무릎 25번, 발목 27번으로 무릎 각도를 계산하세요.
문제 12
무릎 각도가 95도 이하이면 `DOWN`, 165도 이상으로 돌아오면 스쿼트 횟수를 증가시키세요.
문제 13
최근 무릎 각도 7개의 이동평균을 계산하세요.
문제 14
카메라 영상의 평균 FPS를 최근 30프레임 기준으로 표시하세요.
문제 15
OpenCV, MediaPipe, PyTorch의 역할을 각각 설명하세요.
44. 핵심 요약
설치
python -m pip install \
mediapipe \
opencv-python \
numpy모델 경로
from pathlib import Path
MODEL_PATH = Path(
"models/hand_landmarker.task"
)OpenCV 프레임 변환
rgb_frame = cv2.cvtColor(
frame,
cv2.COLOR_BGR2RGB
)
mp_image = mp.Image(
image_format=mp.ImageFormat.SRGB,
data=rgb_frame
)타임스탬프
timestamp_ms = (
time.monotonic_ns()
// 1_000_000
)Hand Landmarker
options = (
mp.tasks.vision
.HandLandmarkerOptions(
base_options=(
mp.tasks.BaseOptions(
model_asset_path=str(
MODEL_PATH
)
)
),
running_mode=(
mp.tasks.vision
.RunningMode
.VIDEO
),
num_hands=2
)
)손 프레임 처리
result = (
landmarker
.detect_for_video(
mp_image,
timestamp_ms
)
)정규화 좌표 변환
pixel_x = int(
landmark.x * width
)
pixel_y = int(
landmark.y * height
)Face Landmarker
options = (
mp.tasks.vision
.FaceLandmarkerOptions(
base_options=(
mp.tasks.BaseOptions(
model_asset_path=str(
FACE_MODEL_PATH
)
)
),
running_mode=(
mp.tasks.vision
.RunningMode
.VIDEO
),
output_face_blendshapes=True
)
)눈 깜빡임 점수
left_blink = scores.get(
"eyeBlinkLeft",
0.0
)
right_blink = scores.get(
"eyeBlinkRight",
0.0
)Pose Landmarker
options = (
mp.tasks.vision
.PoseLandmarkerOptions(
base_options=(
mp.tasks.BaseOptions(
model_asset_path=str(
POSE_MODEL_PATH
)
)
),
running_mode=(
mp.tasks.vision
.RunningMode
.VIDEO
),
num_poses=1
)
)관절 각도
knee_angle = calculate_angle(
landmarks[23],
landmarks[25],
landmarks[27]
)스쿼트 상태 전환
if knee_angle <= 100:
stage = "DOWN"
elif (
knee_angle >= 160
and stage == "DOWN"
):
stage = "UP"
squat_count += 145. 마무리
이번 시간에는 MediaPipe를 이용해 웹캠 속 사람의 얼굴, 손, 전신 자세를 실시간으로 추적하는 방법을 알아보았습니다.
MediaPipe의 핵심은 영상 속 사람을 다음처럼 변환하는 것입니다.
사람의 손
→ 21개 랜드마크
사람의 얼굴
→ 478개 랜드마크
→ 얼굴 표정 점수
사람의 전신
→ 33개 랜드마크
→ 3차원 월드 좌표이 좌표를 이용하면 다양한 기능을 만들 수 있습니다.
손가락 개수 세기
손동작으로 화면 제어
눈 깜빡임 횟수 측정
미소 촬영 버튼
가상 얼굴 필터
팔꿈치 각도 측정
스쿼트 횟수 계산
운동 자세 피드백
전신 아바타 제어전체 처리 과정도 다시 정리해 보겠습니다.
OpenCV가 웹캠 프레임 읽기
→ BGR을 RGB로 변환
→ MediaPipe 이미지 생성
→ 랜드마크 모델 실행
→ 정규화 좌표 반환
→ 픽셀 좌표로 변환
→ 각도와 거리 계산
→ 상태와 동작 판단
→ OpenCV로 결과 표시MediaPipe가 좌표를 찾아준다고 모든 문제가 자동으로 해결되는 것은 아닙니다.
어떤 좌표를 사용할 것인가?
신뢰도가 낮을 때 어떻게 할 것인가?
어떤 각도에서 동작으로 판단할 것인가?
좌표 흔들림을 어떻게 줄일 것인가?
상태 전환은 어떻게 관리할 것인가?
잘못된 자세를 어떤 표현으로 알려줄 것인가?이런 판단은 개발자가 직접 설계해야 합니다.
MediaPipe는 손목이 어디 있는지 알려줍니다.
하지만 그 손목 움직임이 인사인지, 운동인지, 게임 명령인지는 애플리케이션이 결정해야 합니다.
MediaPipe:
“검지 끝은 8번 좌표입니다.”
개발자:
“좋아. 검지를 올리면
프레젠테이션 다음 장으로 넘기자.”
사용자:
“코를 긁었을 뿐인데
슬라이드가 세 장 넘어갔어요.”그래서 실시간 동작 인터페이스에는 단일 프레임 조건보다 다음 요소가 중요합니다.
여러 프레임의 연속성
상태 변화
최소 유지 시간
사용자별 기준값
오검출 방지
동작 취소 규칙컴퓨터 비전의 재미는 화면에서 점을 찾는 데서 끝나지 않습니다.
그 점들이 시간에 따라 어떻게 움직이는지 분석해 의미 있는 동작으로 바꾸는 순간부터 진짜 인터페이스가 시작됩니다. 🖐️✨
이제 웹캠은 단순히 영상을 촬영하는 장치가 아닙니다.
손짓을 읽고, 표정을 감지하고, 자세를 계산하는 실시간 센서가 되었습니다.
다음 편 예고
[Python 완전정복 시리즈 #33] Streamlit 완벽 이해하기 | Python 코드만으로 데이터·AI 웹앱을 만드는 가장 쉬운 방법
다음 시간에는 지금까지 만든 Python 기능을 브라우저에서 실행할 수 있는 웹 애플리케이션으로 변신시킵니다.
텍스트와 버튼 출력, 파일 업로드, 데이터프레임 표시, 차트 시각화, 세션 상태, 캐시, 사이드바, 멀티페이지 앱, 머신러닝 모델 연결까지 Streamlit을 이용해 실습해 보겠습니다.
#Python #파이썬 #Python강좌 #파이썬기초 #MediaPipe #미디어파이프 #OpenCV #컴퓨터비전 #ComputerVision #손추적 #손가락인식 #제스처인식 #얼굴랜드마크 #FaceLandmarker #HandLandmarker #PoseLandmarker #포즈추적 #자세분석 #관절각도 #스쿼트카운터 #웹캠 #실시간영상처리 #Blendshape #눈깜빡임 #운동자세분석 #AI비전 #코딩공부 #프로그래밍
