Spaces:
Running
on
T4
Running
on
T4
Sean Carnahan
Patch for Hugging Face Spaces: fix matplotlib config, check .gitignore, prep for model file inclusion
f20fe1f
import cv2 | |
import numpy as np | |
import tensorflow as tf | |
import tensorflow_hub as hub | |
from typing import List, Dict, Tuple | |
class MoveNetAnalyzer: | |
KEYPOINT_DICT = { | |
'nose': 0, | |
'left_eye': 1, | |
'right_eye': 2, | |
'left_ear': 3, | |
'right_ear': 4, | |
'left_shoulder': 5, | |
'right_shoulder': 6, | |
'left_elbow': 7, | |
'right_elbow': 8, | |
'left_wrist': 9, | |
'right_wrist': 10, | |
'left_hip': 11, | |
'right_hip': 12, | |
'left_knee': 13, | |
'right_knee': 14, | |
'left_ankle': 15, | |
'right_ankle': 16 | |
} | |
def __init__(self, model_name="lightning"): | |
# Initialize MoveNet model | |
if model_name == "lightning": | |
self.model = hub.load("https://tfhub.dev/google/movenet/singlepose/lightning/4") | |
self.input_size = 192 | |
else: # thunder | |
self.model = hub.load("https://tfhub.dev/google/movenet/singlepose/thunder/4") | |
self.input_size = 256 | |
self.movenet = self.model.signatures['serving_default'] | |
# 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. | |
""" | |
# Resize and pad the image to keep aspect ratio | |
img = frame.copy() | |
img = tf.image.resize_with_pad(tf.expand_dims(img, axis=0), self.input_size, self.input_size) | |
img = tf.cast(img, dtype=tf.int32) | |
# Detection | |
results = self.movenet(img) | |
keypoints = results['output_0'].numpy() # Shape [1, 1, 17, 3] | |
# Draw the pose landmarks on the frame | |
if keypoints[0, 0, 0, 2] > 0.1: # Lowered confidence threshold for the nose | |
# Convert keypoints to image coordinates | |
y, x, c = frame.shape | |
shaped = np.squeeze(keypoints) # Shape [17, 3] | |
# Draw keypoints | |
for kp in shaped: | |
ky, kx, kp_conf = kp | |
if kp_conf > 0.1: | |
# Convert to image coordinates | |
x_coord = int(kx * x) | |
y_coord = int(ky * y) | |
cv2.circle(frame, (x_coord, y_coord), 6, (0, 255, 0), -1) | |
# Convert landmarks to a list of dictionaries | |
landmarks = [] | |
for kp in shaped: | |
landmarks.append({ | |
'x': float(kp[1]), | |
'y': float(kp[0]), | |
'visibility': float(kp[2]) | |
}) | |
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. | |
""" | |
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': [] | |
} | |
pose_rules = self.key_angles[pose_type] | |
if pose_type == 'front_double_biceps': | |
# Example: Left Shoulder - Elbow - Wrist for elbow angle | |
# Example: Left Hip - Shoulder - Elbow for shoulder angle (arm abduction) | |
# Note: These are examples, actual biomechanical definitions can be complex. | |
# We'll stick to the previous definition for front_double_biceps shoulder angle for now. | |
# Shoulder angle: right_hip - right_shoulder - right_elbow (can also use left) | |
# Elbow angle: right_shoulder - right_elbow - right_wrist (can also use left) | |
# Wrist angle (simplistic): right_elbow - right_wrist - a point slightly above wrist (not easily done without more points) | |
# Using right side for front_double_biceps as an example, consistent with a typical bodybuilding pose display | |
# Shoulder Angle (approximating arm abduction/flexion relative to torso) | |
# Using Right Hip, Right Shoulder, Right Elbow | |
rs = self.KEYPOINT_DICT['right_shoulder'] | |
re = self.KEYPOINT_DICT['right_elbow'] | |
rh = self.KEYPOINT_DICT['right_hip'] | |
rw = self.KEYPOINT_DICT['right_wrist'] | |
shoulder_angle = self.calculate_angle(landmarks, rh, rs, re) | |
if shoulder_angle is not None: | |
feedback['angles']['R Shoulder'] = shoulder_angle | |
if not (pose_rules['shoulder_angle'][0] <= shoulder_angle <= pose_rules['shoulder_angle'][1]): | |
feedback['corrections'].append( | |
f"Adjust R Shoulder to {pose_rules['shoulder_angle'][0]}-{pose_rules['shoulder_angle'][1]} deg" | |
) | |
elbow_angle = self.calculate_angle(landmarks, rs, re, rw) | |
if elbow_angle is not None: | |
feedback['angles']['R Elbow'] = elbow_angle | |
if not (pose_rules['elbow_angle'][0] <= elbow_angle <= pose_rules['elbow_angle'][1]): | |
feedback['corrections'].append( | |
f"Adjust R Elbow to {pose_rules['elbow_angle'][0]}-{pose_rules['elbow_angle'][1]} deg" | |
) | |
# Wrist angle is hard to define meaningfully with current keypoints for this pose, skipping for now. | |
elif pose_type == 'side_chest': | |
# Assuming side chest often displays left side to judges | |
ls = self.KEYPOINT_DICT['left_shoulder'] | |
le = self.KEYPOINT_DICT['left_elbow'] | |
lw = self.KEYPOINT_DICT['left_wrist'] | |
lh = self.KEYPOINT_DICT['left_hip'] # For shoulder angle relative to torso | |
# Shoulder angle (e.g. arm flexion/extension in sagittal plane for the front arm) | |
# For side chest, the front arm's shoulder angle relative to the torso (hip-shoulder-elbow) | |
shoulder_angle = self.calculate_angle(landmarks, lh, ls, le) | |
if shoulder_angle is not None: | |
feedback['angles']['L Shoulder'] = shoulder_angle | |
if not (pose_rules['shoulder_angle'][0] <= shoulder_angle <= pose_rules['shoulder_angle'][1]): | |
feedback['corrections'].append( | |
f"Adjust L Shoulder to {pose_rules['shoulder_angle'][0]}-{pose_rules['shoulder_angle'][1]} deg" | |
) | |
elbow_angle = self.calculate_angle(landmarks, ls, le, lw) | |
if elbow_angle is not None: | |
feedback['angles']['L Elbow'] = elbow_angle | |
if not (pose_rules['elbow_angle'][0] <= elbow_angle <= pose_rules['elbow_angle'][1]): | |
feedback['corrections'].append( | |
f"Adjust L Elbow to {pose_rules['elbow_angle'][0]}-{pose_rules['elbow_angle'][1]} deg" | |
) | |
# Wrist angle for side chest is also nuanced, skipping detailed check for now. | |
elif pose_type == 'back_double_biceps': | |
# Similar to front, but from back. We can calculate for both arms or pick one. | |
# Let's do right side for consistency with front_double_biceps example. | |
rs = self.KEYPOINT_DICT['right_shoulder'] | |
re = self.KEYPOINT_DICT['right_elbow'] | |
rh = self.KEYPOINT_DICT['right_hip'] | |
rw = self.KEYPOINT_DICT['right_wrist'] | |
shoulder_angle = self.calculate_angle(landmarks, rh, rs, re) | |
if shoulder_angle is not None: | |
feedback['angles']['R Shoulder'] = shoulder_angle | |
if not (pose_rules['shoulder_angle'][0] <= shoulder_angle <= pose_rules['shoulder_angle'][1]): | |
feedback['corrections'].append( | |
f"Adjust R Shoulder to {pose_rules['shoulder_angle'][0]}-{pose_rules['shoulder_angle'][1]} deg" | |
) | |
elbow_angle = self.calculate_angle(landmarks, rs, re, rw) | |
if elbow_angle is not None: | |
feedback['angles']['R Elbow'] = elbow_angle | |
if not (pose_rules['elbow_angle'][0] <= elbow_angle <= pose_rules['elbow_angle'][1]): | |
feedback['corrections'].append( | |
f"Adjust R Elbow to {pose_rules['elbow_angle'][0]}-{pose_rules['elbow_angle'][1]} deg" | |
) | |
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 | |
def classify_pose(self, landmarks: List[Dict]) -> str: | |
""" | |
Classify the pose based on keypoint positions and angles. | |
Returns one of: 'front_double_biceps', 'side_chest', 'back_double_biceps'. | |
""" | |
if not landmarks or len(landmarks) < 17: | |
return 'front_double_biceps' # Default/fallback | |
# Calculate angles for both arms | |
# Right side | |
rs = self.KEYPOINT_DICT['right_shoulder'] | |
re = self.KEYPOINT_DICT['right_elbow'] | |
rh = self.KEYPOINT_DICT['right_hip'] | |
rw = self.KEYPOINT_DICT['right_wrist'] | |
# Left side | |
ls = self.KEYPOINT_DICT['left_shoulder'] | |
le = self.KEYPOINT_DICT['left_elbow'] | |
lh = self.KEYPOINT_DICT['left_hip'] | |
lw = self.KEYPOINT_DICT['left_wrist'] | |
# Shoulder angles | |
r_shoulder_angle = self.calculate_angle(landmarks, rh, rs, re) | |
l_shoulder_angle = self.calculate_angle(landmarks, lh, ls, le) | |
# Elbow angles | |
r_elbow_angle = self.calculate_angle(landmarks, rs, re, rw) | |
l_elbow_angle = self.calculate_angle(landmarks, ls, le, lw) | |
# Heuristic rules: | |
# - Front double biceps: both arms raised, elbows bent, both shoulders abducted | |
# - Side chest: one arm across chest (elbow in front of body), other arm flexed | |
# - Back double biceps: both arms raised, elbows bent, but person is facing away (shoulders/hips x order reversed) | |
# Use x-coordinates to estimate facing direction | |
# If right shoulder x < left shoulder x, assume facing front; else, facing back | |
facing_front = landmarks[rs]['x'] < landmarks[ls]['x'] | |
# Count how many arms are "up" (shoulder angle in expected range) | |
arms_up = 0 | |
if r_shoulder_angle and 80 < r_shoulder_angle < 150: | |
arms_up += 1 | |
if l_shoulder_angle and 80 < l_shoulder_angle < 150: | |
arms_up += 1 | |
elbows_bent = 0 | |
if r_elbow_angle and 60 < r_elbow_angle < 130: | |
elbows_bent += 1 | |
if l_elbow_angle and 60 < l_elbow_angle < 130: | |
elbows_bent += 1 | |
# Side chest: one arm's elbow is much closer to the body's midline (x of elbow near x of nose) | |
nose_x = landmarks[self.KEYPOINT_DICT['nose']]['x'] | |
le_x = landmarks[le]['x'] | |
re_x = landmarks[re]['x'] | |
side_chest_like = (abs(le_x - nose_x) < 0.08 or abs(re_x - nose_x) < 0.08) | |
if arms_up == 2 and elbows_bent == 2: | |
if facing_front: | |
return 'front_double_biceps' | |
else: | |
return 'back_double_biceps' | |
elif side_chest_like: | |
return 'side_chest' | |
else: | |
# Default/fallback | |
return 'front_double_biceps' |