영상 속 사람의 자세를 자동으로 분석해주는 인공지능 프로젝트를 해봤어요!
MediaPipe + OpenCV를 활용해서 좋은 자세와 나쁜 자세를 실시간으로 분류하고
시간 추적 + 경고 메시지 출력까지 구현했어요!


마이크로 프로젝트 목표

  • 📸 영상 프레임마다 사람의 신체 포인트(랜드마크) 추출
  • 📐 어깨·귀·골반 위치 기반으로 자세의 기울기와 정렬 상태 계산
  • ✅ 좋은 자세는 초록색 표시, ❌ 나쁜 자세는 빨간색 표시
  • ⏱️ 좋은/나쁜 자세 유지 시간을 실시간 출력
  • 🚨 나쁜 자세가 3초 이상 지속되면 경고 메시지 출력

사용한 라이브러리와 도구

도구/라이브러리설명
Google Colab클라우드 기반 실습 환경
OpenCV컴퓨터 비전 라이브러리 (영상 처리용)
MediaPipeGoogle의 포즈/손/얼굴 추적 AI 라이브러리
MoviePyColab에서 영상 재생하기 위한 유틸
math수학 계산 함수 (거리, 각도 등)

핵심 개념 설명

프레임(Frame) 기반 처리

  • 영상은 수많은 이미지(프레임)로 구성돼 있어요.
  • 매 프레임마다 사람의 자세를 분석해서 실시간처럼 보이게 해요.

랜드마크(Landmark)란?

  • 사람의 신체에서 특정 포인트(관절, 얼굴, 귀 등)를 자동 추출한 좌표값이에요.
  • MediaPipe Pose는 33개의 랜드마크를 제공해요.

자세 정렬 확인 방법

  • 어깨 좌우 간 거리(offset)를 비교해서 정렬 상태를 확인해요.
  • 거리가 너무 좁으면 몸이 틀어진 걸로 판단 가능해요.

각도 계산: 기울기 판별

  • 귀-어깨, 골반-어깨를 각각 연결해 각도를 구해요.
  • 이걸로 목의 숙임 정도몸통의 기울기를 파악할 수 있어요.
  • 계산 공식은 피타고라스 + 코사인 법칙을 응용해요.

좋은 자세 vs 나쁜 자세

  • 목 각도 < 40도 && 몸통 각도 < 10도 → 좋은 자세!
  • 하나라도 기준 이상이면 나쁜 자세로 판단.

프레임 기반 시간 추적

good_time = (1/fps) * good_frame
bad_time = (1/fps) * bad_frame
  • 초당 프레임 수(fps)를 기준으로 현재까지 좋은/나쁜 자세가 얼마나 지속됐는지 계산해요.

경고 시스템

  • bad_time이 3초를 넘으면 중앙에 "Warning!!!!"을 크게 출력!

💻 전체 코드(Python)

코드는 언제나 각 코드가 무슨 기능을 하는지 주석도 최대한 풍부하게 달아두는 걸 추천해요 :)

import cv2
import mediapipe as mp
import math
from moviepy.editor import VideoFileClip

# 프레임 카운터 초기화
good_frame = 0
bad_frame = 0

# 폰트 설정
font = cv2.FONT_HERSHEY_SIMPLEX

# 색상 설정
blue = (255,127,0)
red = (50,50,255)
green = (127,255,0)
dark_blue = (127,20,0)
light_green = (127,233,100)
yellow = (0,255,255)
pink = (255,0,255)

# Mediapipe Pose 초기화
mp_pose = mp.solutions.pose
pose = mp_pose.Pose()

# 거리 계산 함수
def findDistance(x1, y1, x2, y2):
    # 두 점 사이의 거리 계산
    return math.sqrt((x2-x1)**2 + (y2-y1)**2)

# 각도 계산 함수
def findAngle(x1, y1, x2, y2):
    # 두 벡터 사이 각도를 라디안으로 구한 뒤 도로 변환
    cos_value = ((x2-x1)*(-x1) + (y2-y1)*(-y1)) / (
        math.sqrt((x2-x1)**2 + (y2-y1)**2) * math.sqrt(x1**2 + y1**2)
    )
    return math.degrees(math.acos(cos_value))

# 1차 결과: 어깨 거리 + 기울기 표시
cap = cv2.VideoCapture('./data/101_pose.mp4')  # 비디오 읽어오기

# 영상 크기
w = int(cap.get(3))
h = int(cap.get(4))
fps = int(cap.get(5))
# 코덱 설정
codec = cv2.VideoWriter_fourcc(*"MP4V")

# 녹화파일 설정
out = cv2.VideoWriter('./p001_result04.mp4', codec, fps, (w, h))

# 시작 표시 남겨주기
print('시작 q(≧▽≦q)')

while cap.isOpened():
    # (1) 프레임 이미지 읽어오기
    ret, frame = cap.read()
    if not ret:
        print('종료 ヾ(≧▽≦*)o')
        break

    fps = cap.get(cv2.CAP_PROP_FPS)  # 초당 프레임 수 가져오기
    h, w, _ = frame.shape  # 높이, 너비

    # (2) RGB 변환
    frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    # (3) 포즈 검출
    keypoint = pose.process(frame)
    # 다시 BGR로
    frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)

    # (4) 포즈 랜드마크 좌표 저장
    lm = keypoint.pose_landmarks
    lmPose = mp_pose.PoseLandmark

    l_shldr_x = int(lm.landmark[lmPose.LEFT_SHOULDER].x * w)
    l_shldr_y = int(lm.landmark[lmPose.LEFT_SHOULDER].y * h)
    r_shldr_x = int(lm.landmark[lmPose.RIGHT_SHOULDER].x * w)
    r_shldr_y = int(lm.landmark[lmPose.RIGHT_SHOULDER].y * h)
    l_ear_x    = int(lm.landmark[lmPose.LEFT_EAR].x      * w)
    l_ear_y    = int(lm.landmark[lmPose.LEFT_EAR].y      * h)
    l_hip_x    = int(lm.landmark[lmPose.LEFT_HIP].x      * w)
    l_hip_y    = int(lm.landmark[lmPose.LEFT_HIP].y      * h)

    # (5) 어깨 거리 계산
    offset = findDistance(l_shldr_x, l_shldr_y, r_shldr_x, r_shldr_y)
    # (6) 거리 출력
    if offset < 100:
        cv2.putText(frame, f"{int(offset)} Aligned", (w-150,30), font, 0.9, green, 2)
    else:
        cv2.putText(frame, f"{int(offset)} Not Aligned", (w-150,30), font, 0.9, red, 2)

    # (7) 랜드마크 점 찍기
    cv2.circle(frame, (l_shldr_x, l_shldr_y), 7, yellow, -1)
    cv2.circle(frame, (r_shldr_x, r_shldr_y), 7, pink,   -1)
    cv2.circle(frame, (l_ear_x,    l_ear_y),    7, yellow, -1)
    cv2.circle(frame, (l_hip_x,    l_hip_y),    7, yellow, -1)

    # (8) 목/몸통 기울기 계산
    neck_inclination  = findAngle(l_shldr_x, l_shldr_y, l_ear_x, l_ear_y)
    torso_inclination = findAngle(l_hip_x,    l_hip_y, l_shldr_x, l_shldr_y)

    # (9) 좋은/나쁜 자세 판단
    angle_text = f"Neck : {int(neck_inclination)}  Torso : {int(torso_inclination)}"
    if neck_inclination < 40 and torso_inclination < 10:
        bad_frame = 0
        good_frame += 1

        cv2.putText(frame, angle_text, (10,30), font, 0.9, green, 2)
        cv2.putText(frame, str(int(neck_inclination)), (l_shldr_x+10, l_shldr_y), font, 0.9, green, 2)
        cv2.putText(frame, str(int(torso_inclination)), (l_hip_x+10, l_hip_y), font, 0.9, green, 2)
        cv2.line(frame, (l_shldr_x, l_shldr_y), (l_ear_x, l_ear_y), green, 4)
        cv2.line(frame, (l_hip_x,    l_hip_y),    (l_shldr_x, l_shldr_y), green, 4)
    else:
        bad_frame += 1
        good_frame = 0

        cv2.putText(frame, angle_text, (10,30), font, 0.9, red, 2)
        cv2.putText(frame, str(int(neck_inclination)), (l_shldr_x+10, l_shldr_y), font, 0.9, red, 2)
        cv2.putText(frame, str(int(torso_inclination)), (l_hip_x+10, l_hip_y), font, 0.9, red, 2)
        cv2.line(frame, (l_shldr_x, l_shldr_y), (l_ear_x, l_ear_y), red, 4)
        cv2.line(frame, (l_hip_x,    l_hip_y),    (l_shldr_x, l_shldr_y), red, 4)

    out.write(frame)

out.release()
cap.release()

# 결과 미리보기
VideoFileClip('./p001_result04.mp4').ipython_display(width=600)


# 2차 결과:자세 유지 시간 + 경고 표시
cap = cv2.VideoCapture('./data/101_pose.mp4')

w   = int(cap.get(3))
h   = int(cap.get(4))
fps = int(cap.get(5))
codec = cv2.VideoWriter_fourcc(*"MP4V")
out   = cv2.VideoWriter('./p001_result05.mp4', codec, fps, (w,h))

print('시작 q(≧▽≦q)')

while cap.isOpened():
    ret, frame = cap.read()
    if not ret:
        print('종료 ヾ(≧▽≦*)o')
        break

    fps = cap.get(cv2.CAP_PROP_FPS)
    h, w, _ = frame.shape

    frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    keypoint = pose.process(frame)
    frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)

    lm = keypoint.pose_landmarks
    lmPose = mp_pose.PoseLandmark

    l_shldr_x = int(lm.landmark[lmPose.LEFT_SHOULDER].x * w)
    l_shldr_y = int(lm.landmark[lmPose.LEFT_SHOULDER].y * h)
    r_shldr_x = int(lm.landmark[lmPose.RIGHT_SHOULDER].x * w)
    r_shldr_y = int(lm.landmark[lmPose.RIGHT_SHOULDER].y * h)
    l_ear_x    = int(lm.landmark[lmPose.LEFT_EAR].x      * w)
    l_ear_y    = int(lm.landmark[lmPose.LEFT_EAR].y      * h)
    l_hip_x    = int(lm.landmark[lmPose.LEFT_HIP].x      * w)
    l_hip_y    = int(lm.landmark[lmPose.LEFT_HIP].y      * h)

    offset = findDistance(l_shldr_x, l_shldr_y, r_shldr_x, r_shldr_y)
    if offset < 100:
        cv2.putText(frame, f"{int(offset)} Aligned", (w-150,30), font, 0.9, green, 2)
    else:
        cv2.putText(frame, f"{int(offset)} Not Aligned", (w-150,30), font, 0.9, red, 2)

    cv2.circle(frame, (l_shldr_x, l_shldr_y), 7, yellow, -1)
    cv2.circle(frame, (r_shldr_x, r_shldr_y), 7, pink,   -1)
    cv2.circle(frame, (l_ear_x,    l_ear_y),    7, yellow, -1)
    cv2.circle(frame, (l_hip_x,    l_hip_y),    7, yellow, -1)

    neck_inclination  = findAngle(l_shldr_x, l_shldr_y, l_ear_x, l_ear_y)
    torso_inclination = findAngle(l_hip_x,    l_hip_y, l_shldr_x, l_shldr_y)

    angle_text = f"Neck : {int(neck_inclination)}  Torso : {int(torso_inclination)}"
    if neck_inclination < 40 and torso_inclination < 10:
        bad_frame = 0
        good_frame += 1
    else:
        bad_frame += 1
        good_frame = 0

    # (10) 좋은/나쁜 자세 시간 계산
    good_time = (1/fps) * good_frame
    bad_time  = (1/fps) * bad_frame

    # (11) 유지 시간 표시
    if good_time > 0:
        cv2.putText(frame, f"Good Pose Time : {round(good_time,1)}s", (10,h-20), font, 0.9, green, 2)
    else:
        cv2.putText(frame, f"Bad Pose Time : {round(bad_time,1)}s", (10,h-20), font, 0.9, red, 2)

    # (12) 나쁜 자세 3초 이상 경고
    if bad_time > 3:
        cv2.putText(frame, 'Warning!!!!', (int(w/2-300), int(h/2)), font, 3, red, 10)

    out.write(frame)

out.release()
cap.release()

# 최종 결과 미리보기
VideoFileClip('./p001_result05.mp4').ipython_display(width=600)

결과 예시

  • 어깨 간 거리가 좁고, 목과 몸통 각도가 작으면 Aligned + Good Pose Time
  • 목이 앞으로 빠지거나 몸통이 기울면 Not Aligned + Bad Pose Time
  • 나쁜 자세가 3초 이상이면 화면 중앙에 붉은 Warning!!!! 메시지 출력됨

이런 점이 좋아요

  • 눈으로 자세 등이 어떤지 보인다는 점이 좋았어요
  • 자세라는 주제는 실생활에 연결되니까 더 몰입도 높았어요!
  • MediaPipe가 복잡한 딥러닝 없이도 고급 기능을 제공해줘서 초보자도 접근하기 좋았죠

확장 가능성

  • 📱 모바일 앱화: 자세 교정 피드백 앱
  • 🧘 헬스/요가 자세 분석 서비스
  • 📈 자세 유지 기록 DB화 및 통계 시각화

마무리

MediaPipe와 OpenCV를 활용해서
영상 속 사람의 자세를 분석하고 실시간 피드백을 주는 코드를 짜봤어요.
처음엔 어렵게 느껴졌지만, 하나하나 구현해보니 논리 흐름이 꽤 직관적이더라고요!

자세 교정, 영상 분석, AI 피드백 시스템 같은 주제에 관심 있다면
꼭 한 번 실습해보길 추천할게요! (❁´◡`❁)


작성일 : 2025.06.23
작성자 : 발라

profile
능숙한 바이브코딩을 할 수 있게 됨을 꿈꾸며

0개의 댓글