File size: 14,829 Bytes
c636b75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
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.
        Handles pose_types not in self.key_angles by providing a note.
        """
        feedback = {
            'pose_type': pose_type,
            'angles': {},
            'corrections': [],
            'notes': []  # Initialize notes field
        }

        if not landmarks:
            # If no landmarks, it's a more fundamental issue than just pose_type.
            # The process_frame method already handles this by passing {'error': 'No pose detected'}
            # from self.analyze_pose if landmarks is empty.
            # However, to be safe, if this method is called directly with no landmarks:
            feedback['error'] = 'No landmarks provided for analysis'
            return feedback

        if pose_type not in self.key_angles:
            feedback['notes'].append(f"No specific angle checks defined for pose: {pose_type}")
            # Still return the feedback structure, but angles and corrections will be empty.
            # The 'error' field will not be set here, allowing app.py to distinguish this case.
            return feedback
        
        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]):
                    # Debug print before forming correction string
                    print(f"[MOVENET_DEBUG_CORRECTION] pose_type: {pose_type}, rule_key: 'shoulder_angle', rules_for_angle: {pose_rules.get('shoulder_angle')}")
                    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"
                    )
        
        # Clear notes if pose_type was valid and processed, unless specific notes were added by pose logic
        if not feedback['notes']: # Only clear if no specific notes were added during pose rule processing
            feedback.pop('notes', None)
            
        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'