728x90

사람을 인식하고 해당 신체의 모든 부위 얼굴, 팔, 다리, 몸통의 움직임을 추적하는

MediaPipe 라이브러리를 사용하여 팔 운동 트레이닝 로직(AI 이두컬 트레이너)을 만들어보자.

 

 

1. MediaPipe, OpenCV 설치

!pip install mediapipe opencv-python
  • 이번 작업에서 가장 중요한 두 가지 라이브러리를 설치한다.

 

2. 해당 라이브러리 임포트

import cv2 # openCV
import mediapipe as mp
import numpy as np
mp_drawing = mp.solutions.drawing_utils
mp_pose = mp.solutions.pose
  • mp_drawings: 신체를 인식하여 여러 부위를 감지 및 추적하는 것을 시각화 할 때 사용하는 유틸리티
  • mp_pose: 실제 신체를 감지함과 동시에 감지 및 추적된 신체 부위의 위치 등을 추출해준다.

 

3. 감지 및 추적 될 신체 부위의 라벨링 값 확인

mp_pose.POSE_CONNECTIONS 
  • MediaPipe의 자체 알고리즘이 예측 결과로 신체에 그림을 그리기 위해 설정한 라벨링 값을 출력해보자.
frozenset({(0, 1),
           (0, 4),
           (1, 2),
           (2, 3),
           (3, 7),
           (4, 5),
           (5, 6),
           (6, 8),
           (9, 10),
           (11, 12),
           (11, 13),
           (11, 23),
           (12, 14),
           (12, 24),
           (13, 15),
           (14, 16),
           (15, 17),
           (15, 19),
           (15, 21),
           (16, 18),
           (16, 20),
           (16, 22),
           (17, 19),
           (18, 20),
           (23, 24),
           (23, 25),
           (24, 26),
           (25, 27),
           (26, 28),
           (27, 29),
           (27, 31),
           (28, 30),
           (28, 32),
           (29, 31),
           (30, 32)})
  • 출력해서 나온 결과를 아래 그림과 비교하면 각 숫자가 어떤 역할을 하는지 알 수 있다. 예를 들어, 코(0)는 눈(1, 4)와 연결된 것을 확인해 볼 수 있다.

 

4. 카메라 내 신체를 감지하여 그림을 그려보기

cap = cv2.VideoCapture(0) # 웹캠 불러오기
## 미디어파이프 인스턴스 설정 (신뢰도는 0.5, 연속 프레임 신뢰도 0.5)
with mp_pose.Pose(min_detection_confidence=0.5, min_tracking_confidence=0.5) as pose: 
    # 미디어파이프를 효율적으로 사용하기 위해 with as 구문 사용

    while cap.isOpened():
        ret, frame = cap.read()
        
        # 이미지 다시 칠하기: 미디어 파이프에 전달하기 위해 BGR -> RGB로 변경
        image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        image.flags.writeable = False # 이미지 픽셀 값 수정 막기
				# pose.process()의 인자로 이미지를 넘겨주면 해당 이미지의 픽셀값이 수정 될 수도 있음
      
        # 신체를 감지함, 이미지 속의 신체 부위의 위치를 감지 및 움직임을 추적
        results = pose.process(image)
    
        # Recolor back to BGR
        # 이미지를 다시 RGB 에서 BGR 로 변경
        image.flags.writeable = True # 이미지 픽셀 값 수정 허용
        image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
        
        # Render detections
        # 이미지나 상에 감지된 랜드마크를 그린다
        mp_drawing.draw_landmarks(image, results.pose_landmarks, mp_pose.POSE_CONNECTIONS,
                                mp_drawing.DrawingSpec(color=(245,117,66), thickness=2, circle_radius=2), 
                                mp_drawing.DrawingSpec(color=(245,66,230), thickness=2, circle_radius=2) 
                                 )     
        # mp_drawing.draw_landmarks(이미지, 이미지의 감지된 신체부위 위치 및 관절, 연결할 랜드마크 선들, 선 스타일)
        
        cv2.imshow('Mediapipe Feed', image)

        if cv2.waitKey(10) & 0xFF == ord('q'):
            break

    cap.release()
    cv2.destroyAllWindows()
  • 요약 하자면 카메라를 불러와서 각 프레임 단위로 신체를 인식하고 여러 신체 부위를 감지 및 탐지하여 해당 이미지(프레임)에 관절라인, 즉 그림을 그린다.
  • min_detection_confidence:신체가 감지될 때 필요한 최소 신뢰도 설정.
  • min_tracking_confidence: 신체가 한번 감지 된 후 연속 프레임에서 해당 신체를 추적 할 때 필요한 최소 신뢰도 설정
  • MediaPipe의 신체 감지 알고리즘이 특정 부위를 50% 이상의 신뢰도(정확도)로 측정 되었을 때 해당 부위를 유효하게 간주함. 이 값을 너무 낮게 설정하면 잘못된 감지가 증가할 수 있으며, 너무 높게 설정하면 유효한 포즈를 놓칠 수 있다.
  • results = pose.process(image): 해당 프레임을 가져와 실제로 신체 감지 및 추적이 이루어진다. result 변수에는 감지 및 추적에 대해 다양한 정보가 들어간다.
  • mp_drawing.draw_landmarks(이미지, 이미지의 감지된 신체부위 위치 및 관절, 연결할 랜드마크 선들, 선 스타일): 프레임에 본격적으로 그림을 그린다.

 

5. 왼쪽 어깨, 팔꿈치, 팔목에 대한 좌표 추출

with mp_pose.Pose(min_detection_confidence=0.5, min_tracking_confidence=0.5) as pose:
    while cap.isOpened():
        ret, frame = cap.read()
        
        results = pose.process(image)

        try:
            landmarks = results.pose_landmarks.landmark
						# 각 신체부위에 대한 좌표 추출
            print(landmarks)
        except:
            pass
# 왼쪽 어깨 x,y 좌표
print(landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].x)
print(landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].y)
0.7106555700302124
0.8390206694602966
  • 팔을 굽혔다 폈다 하는 각도를 계산하기 위해서는 어깨, 팔꿈치, 팔목 이 3가지의 좌표가 필요하다.
shoulder = [landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].x,landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].y]
elbow = [landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].x,landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].y]
wrist = [landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].x,landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].y]
[0.7106555700302124, 0.8390206694602966],
 [0.781584620475769, 1.1789385080337524],
 [0.796806812286377, 1.4664262533187866]

 

6. 팔 각도 계산

def calculate_angle(a,b,c):
    # 각 값을 받아 넘파이 배열로 변환
    a = np.array(a) # First
    b = np.array(b) # Mid
    c = np.array(c) # End
    
    # 세 좌표를 가지고 각도로 변환하는 코드
    radians = np.arctan2(c[1]-b[1], c[0]-b[0]) - np.arctan2(a[1]-b[1], a[0]-b[0])
    angle = np.abs(radians*180.0/np.pi)
    
    # 180도가 넘으면 360에서 뺀 값을 계산한다.
    if angle > 180.0:
        angle = 360-angle
        
    return angle
  • 해당 함수에 어깨, 팔꿈치, 손목의 좌표를 넣어보자.
calculate_angle(shoulder, elbow, wrist)
156.64536266620797

 

7. 팔 운동 카운트 기능 삽입

shoulder = [landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].x,landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].y]
            elbow = [landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].x,landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].y]
            wrist = [landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].x,landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].y]
            # 왼쪽 어깨, 팔꿈치, 팔목 등 감지된 랜드마크의 좌표 값들을 계산
            
            # 해당 좌표들에 대해 각도를 계산
            angle = calculate_angle(shoulder, elbow, wrist)
            
            # 이미지에 각도를 팔꿈치 위치에 표시한다.
            # cv2.putText(): 이미지 위에 텍스트를 넣는 함수
            cv2.putText(image, str(angle), 
                           tuple(np.multiply(elbow, [640, 480]).astype(int)), 
                           cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2, cv2.LINE_AA
                                )
            
            # 만약 각도가 160도가 넘으면, down 상태
            # 각도가 30도 보다 작고 down이면 count 증가, up 상태
            if angle > 160:
                stage = "down"
            if angle < 30 and stage =='down':
                stage="up"
                counter +=1
                print(counter)
  • 카메라의 프레임마다 팔의 각도를 가져오는 angle 변수를 가지고 if문을 작성하여 카운트를 올려주는 코드를 작성한다.
  • 각도가 160도라면 팔을 구부리지 않고 쭉 펴져 있는 상태이기 때문에 운동 횟수를 늘리지 않고, 30도처럼 팔을 다 굽힌 상태라면 횟수를 늘린다.

 

※ 전체 코드

def calculate_angle(a,b,c):
    # 각 값을 받아 넘파이 배열로 변환
    a = np.array(a) # First
    b = np.array(b) # Mid
    c = np.array(c) # End
    
    # 세 좌표를 가지고 각도로 변환하는 코드
    radians = np.arctan2(c[1]-b[1], c[0]-b[0]) - np.arctan2(a[1]-b[1], a[0]-b[0])
    angle = np.abs(radians*180.0/np.pi)
    
    # 180도가 넘으면 360에서 뺀 값을 계산한다.
    if angle >180.0:
        angle = 360-angle
        
    return angle





cap = cv2.VideoCapture(0)

# Curl counter variables
counter = 0 
stage = None

## Setup mediapipe instance
with mp_pose.Pose(min_detection_confidence=0.5, min_tracking_confidence=0.5) as pose:
    while cap.isOpened():
        ret, frame = cap.read()
        
        # Recolor image to RGB
        image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        image.flags.writeable = False
      
        # Make detection
        results = pose.process(image)
    
        # Recolor back to BGR
        image.flags.writeable = True
        image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
        
        # Extract landmarks
        try:
            landmarks = results.pose_landmarks.landmark
            
            # Get coordinates
            shoulder = [landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].x,landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].y]
            elbow = [landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].x,landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].y]
            wrist = [landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].x,landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].y]
            # 왼쪽 어깨, 팔꿈치, 팔목 등 감지된 랜드마크의 좌표 값들을 계산
            # x,y,z까지 총 3개의 값이 있 는데 2차원의 값만 얻음
            
            # Calculate angle
            # 해당 좌표들에 대해 각도를 계산
            angle = calculate_angle(shoulder, elbow, wrist)
            
            # Visualize angle
            # 이미지에 각도를 팔꿈치 위치에 표시한다.
            # cv2.putText(): 이미지 위에 텍스트를 넣는 함수
            cv2.putText(image, str(angle), 
                           tuple(np.multiply(elbow, [640, 480]).astype(int)), 
                           cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2, cv2.LINE_AA
                                )
            
            # Curl counter logic
            # 만약 각도가 160도가 넘으면, down 상태
            # 각도가 30도 보다 작고 down이면 count 증가, up 상태
            if angle > 160:
                stage = "down"
            if angle < 30 and stage =='down':
                stage="up"
                counter +=1
                print(counter)
                       
        except:
            pass
        
        # Render curl counter
        # Setup status box
        cv2.rectangle(image, (0,0), (225,73), (245,117,16), -1)
        
        # Rep data
        cv2.putText(image, 'REPS', (15,12), 
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,0,0), 1, cv2.LINE_AA)
        cv2.putText(image, str(counter), 
                    (10,60), 
                    cv2.FONT_HERSHEY_SIMPLEX, 2, (255,255,255), 2, cv2.LINE_AA)
        
        # Stage data
        cv2.putText(image, 'STAGE', (65,12), 
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,0,0), 1, cv2.LINE_AA)
        cv2.putText(image, stage, 
                    (60,60), 
                    cv2.FONT_HERSHEY_SIMPLEX, 2, (255,255,255), 2, cv2.LINE_AA)
        
        
        # Render detections
        mp_drawing.draw_landmarks(image, results.pose_landmarks, mp_pose.POSE_CONNECTIONS,
                                mp_drawing.DrawingSpec(color=(245,117,66), thickness=2, circle_radius=2), 
                                mp_drawing.DrawingSpec(color=(245,66,230), thickness=2, circle_radius=2) 
                                 )               
        
        cv2.imshow('Mediapipe Feed', image)

        if cv2.waitKey(10) & 0xFF == ord('q'):
            break

    cap.release()
    cv2.destroyAllWindows()

 

출처1: https://giveme-happyending.tistory.com/202

 

mediapipe 사용하여 팔굽히기 모션 인식하기

공부에 참고한 링크 https://youtu.be/06TE_U21FK4 개발환경 운영체제: Window 10 64 bit 개발언어: Python 3.11 개발 툴: Jupyter Lab 추가 패키지: mediapipe, opencv-python 라이브러리 & 모듈 설치 MediaPipe, opencv-python 설치

giveme-happyending.tistory.com

출처2: https://www.youtube.com/watch?v=06TE_U21FK4&t=642s

 

728x90