Sean Carnahan
Remove YOLOv7, ensure LFS for models, update Dockerfile
3c6f221
raw
history blame
9.44 kB
import cv2
import mediapipe as mp
import numpy as np
from typing import List, Dict, Tuple
class PoseAnalyzer:
# Add MediaPipe skeleton connections as a class variable
MP_CONNECTIONS = [
(11, 13), (13, 15), # Left arm
(12, 14), (14, 16), # Right arm
(11, 12), # Shoulders
(11, 23), (12, 24), # Torso sides
(23, 24), # Hips
(23, 25), (25, 27), # Left leg
(24, 26), (26, 28), # Right leg
(27, 31), (28, 32), # Ankles to feet
(15, 17), (16, 18), # Wrists to hands
(15, 19), (16, 20), # Wrists to pinky
(15, 21), (16, 22), # Wrists to index
(15, 17), (17, 19), (19, 21), # Left hand
(16, 18), (18, 20), (20, 22) # Right hand
]
def __init__(self):
# Initialize MediaPipe Pose
self.mp_pose = mp.solutions.pose
self.pose = self.mp_pose.Pose(
static_image_mode=False,
model_complexity=2, # Using the most accurate model
min_detection_confidence=0.1,
min_tracking_confidence=0.1
)
self.mp_drawing = mp.solutions.drawing_utils
# Define key angles for bodybuilding poses
self.key_angles = {
'front_double_biceps': {
'shoulder_angle': (90, 120), # Expected angle range
'elbow_angle': (80, 100),
'wrist_angle': (0, 20)
},
'side_chest': {
'shoulder_angle': (45, 75),
'elbow_angle': (90, 110),
'wrist_angle': (0, 20)
},
'back_double_biceps': {
'shoulder_angle': (90, 120),
'elbow_angle': (80, 100),
'wrist_angle': (0, 20)
}
}
def detect_pose(self, frame: np.ndarray, last_valid_landmarks=None) -> Tuple[np.ndarray, List[Dict]]:
"""
Detect pose in the given frame and return the frame with pose landmarks drawn
and the list of detected landmarks. If detection fails, reuse last valid landmarks if provided.
"""
# Convert the BGR image to RGB
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# Process the frame and detect pose
results = self.pose.process(rgb_frame)
# Draw the pose landmarks on the frame
if results.pose_landmarks:
# Draw all 33 keypoints as bright red, smaller circles, and show index
for idx, landmark in enumerate(results.pose_landmarks.landmark):
x = int(landmark.x * frame.shape[1])
y = int(landmark.y * frame.shape[0])
if landmark.visibility > 0.1: # Lowered threshold from 0.3 to 0.1
cv2.circle(frame, (x, y), 3, (0, 0, 255), -1)
cv2.putText(frame, str(idx), (x+8, y-8), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1)
# Draw skeleton lines
# Convert landmarks to pixel coordinates for easier access
landmark_points = []
for landmark in results.pose_landmarks.landmark:
landmark_points.append((int(landmark.x * frame.shape[1]), int(landmark.y * frame.shape[0]), landmark.visibility))
for pt1, pt2 in self.MP_CONNECTIONS:
if pt1 < len(landmark_points) and pt2 < len(landmark_points):
x1, y1, v1 = landmark_points[pt1]
x2, y2, v2 = landmark_points[pt2]
if v1 > 0.1 and v2 > 0.1:
cv2.line(frame, (x1, y1), (x2, y2), (0, 255, 255), 2)
# Convert landmarks to a list of dictionaries
landmarks = []
for idx, landmark in enumerate(results.pose_landmarks.landmark):
landmarks.append({
'x': landmark.x,
'y': landmark.y,
'z': landmark.z,
'visibility': landmark.visibility
})
return frame, landmarks
# If detection fails, reuse last valid landmarks if provided
if last_valid_landmarks is not None:
return frame, last_valid_landmarks
return frame, []
def calculate_angle(self, landmarks: List[Dict], joint1: int, joint2: int, joint3: int) -> float:
"""
Calculate the angle between three joints.
"""
if len(landmarks) < max(joint1, joint2, joint3):
return None
# Get the coordinates of the three joints
p1 = np.array([landmarks[joint1]['x'], landmarks[joint1]['y']])
p2 = np.array([landmarks[joint2]['x'], landmarks[joint2]['y']])
p3 = np.array([landmarks[joint3]['x'], landmarks[joint3]['y']])
# Calculate the angle
v1 = p1 - p2
v2 = p3 - p2
angle = np.degrees(np.arccos(
np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))
))
return angle
def analyze_pose(self, landmarks: List[Dict], pose_type: str) -> Dict:
"""
Analyze the pose and provide feedback based on the pose type.
Enhanced: Calculates angles for both left and right arms (shoulder, elbow, wrist) for all pose types.
"""
if not landmarks or pose_type not in self.key_angles:
return {'error': 'Invalid pose type or no landmarks detected'}
feedback = {
'pose_type': pose_type,
'angles': {},
'corrections': []
}
# Indices for MediaPipe 33 keypoints
LEFT_SHOULDER = 11
RIGHT_SHOULDER = 12
LEFT_ELBOW = 13
RIGHT_ELBOW = 14
LEFT_WRIST = 15
RIGHT_WRIST = 16
LEFT_HIP = 23
RIGHT_HIP = 24
LEFT_KNEE = 25
RIGHT_KNEE = 26
LEFT_ANKLE = 27
RIGHT_ANKLE = 28
# Calculate angles for both arms
# Shoulder angles (hip-shoulder-elbow)
l_shoulder_angle = self.calculate_angle(landmarks, LEFT_HIP, LEFT_SHOULDER, LEFT_ELBOW)
r_shoulder_angle = self.calculate_angle(landmarks, RIGHT_HIP, RIGHT_SHOULDER, RIGHT_ELBOW)
# Elbow angles (shoulder-elbow-wrist)
l_elbow_angle = self.calculate_angle(landmarks, LEFT_SHOULDER, LEFT_ELBOW, LEFT_WRIST)
r_elbow_angle = self.calculate_angle(landmarks, RIGHT_SHOULDER, RIGHT_ELBOW, RIGHT_WRIST)
# Wrist angles (elbow-wrist-hand index, if available)
# MediaPipe does not have hand index, so we can use a pseudo point (e.g., extend wrist direction)
# For now, skip wrist angle or set to None
# Leg angles (optional)
l_knee_angle = self.calculate_angle(landmarks, LEFT_HIP, LEFT_KNEE, LEFT_ANKLE)
r_knee_angle = self.calculate_angle(landmarks, RIGHT_HIP, RIGHT_KNEE, RIGHT_ANKLE)
# Add angles to feedback
if l_shoulder_angle:
feedback['angles']['L Shoulder'] = l_shoulder_angle
if not self.key_angles[pose_type]['shoulder_angle'][0] <= l_shoulder_angle <= self.key_angles[pose_type]['shoulder_angle'][1]:
feedback['corrections'].append(
f"Adjust L Shoulder to {self.key_angles[pose_type]['shoulder_angle'][0]}-{self.key_angles[pose_type]['shoulder_angle'][1]} deg"
)
if r_shoulder_angle:
feedback['angles']['R Shoulder'] = r_shoulder_angle
if not self.key_angles[pose_type]['shoulder_angle'][0] <= r_shoulder_angle <= self.key_angles[pose_type]['shoulder_angle'][1]:
feedback['corrections'].append(
f"Adjust R Shoulder to {self.key_angles[pose_type]['shoulder_angle'][0]}-{self.key_angles[pose_type]['shoulder_angle'][1]} deg"
)
if l_elbow_angle:
feedback['angles']['L Elbow'] = l_elbow_angle
if not self.key_angles[pose_type]['elbow_angle'][0] <= l_elbow_angle <= self.key_angles[pose_type]['elbow_angle'][1]:
feedback['corrections'].append(
f"Adjust L Elbow to {self.key_angles[pose_type]['elbow_angle'][0]}-{self.key_angles[pose_type]['elbow_angle'][1]} deg"
)
if r_elbow_angle:
feedback['angles']['R Elbow'] = r_elbow_angle
if not self.key_angles[pose_type]['elbow_angle'][0] <= r_elbow_angle <= self.key_angles[pose_type]['elbow_angle'][1]:
feedback['corrections'].append(
f"Adjust R Elbow to {self.key_angles[pose_type]['elbow_angle'][0]}-{self.key_angles[pose_type]['elbow_angle'][1]} deg"
)
# Optionally add knee angles
if l_knee_angle:
feedback['angles']['L Knee'] = l_knee_angle
if r_knee_angle:
feedback['angles']['R Knee'] = r_knee_angle
return feedback
def process_frame(self, frame: np.ndarray, pose_type: str = 'front_double_biceps', last_valid_landmarks=None) -> Tuple[np.ndarray, Dict, List[Dict]]:
"""
Process a single frame, detect pose, and analyze it. Returns frame, analysis, and used landmarks.
"""
# Detect pose
frame_with_pose, landmarks = self.detect_pose(frame, last_valid_landmarks=last_valid_landmarks)
# Analyze pose if landmarks are detected
analysis = self.analyze_pose(landmarks, pose_type) if landmarks else {'error': 'No pose detected'}
return frame_with_pose, analysis, landmarks