사람을 인식하고 해당 신체의 모든 부위 얼굴, 팔, 다리, 몸통의 움직임을 추적하는
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