File size: 5,473 Bytes
c6abe34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import math
import numpy as np
from typing import Dict, List, Any, Optional

class SkillDiagnosticService:
    """
    Analyzes basketball skills (shooting, dribbling) using pose data.
    Implements the 4-phase shot diagnostic logic: DIP, SET, RELEASE, FINISH.
    """
    
    def __init__(self):
        # Ideal thresholds for coaching feedback
        self.THRESHOLDS = {
            'MIN_ELBOW_EXTENSION': 165.0,  # Degrees
            'IDEAL_KNEE_DIP': 125.0,       # Degrees
            'MIN_ARC_ANGLE': 40.0,         # Degrees
            'MAX_ELBOW_FLARE': 15.0        # Degrees from shoulder-wrist line
        }

    def analyze_shooting_session(self, shots: List[Dict], pose_tracks: List[Dict]) -> List[Dict]:
        """
        Analyzes a full session of shots and provides diagnostic feedback.
        """
        diagnostics = []
        for shot in shots:
            diagnostic = self.analyze_single_shot(shot, pose_tracks)
            diagnostics.append(diagnostic)
        return diagnostics

    def analyze_single_shot(self, shot: Dict, pose_tracks: List[Dict]) -> Dict:
        """
        Diagnoses a single shot attempt.
        """
        release_frame = shot.get('release_frame')
        outcome = shot.get('outcome')
        
        # 1. Identify Keyframes (Dip, Set, Release, Finish)
        keyframes = self._extract_keyframes(shot, pose_tracks)
        
        # 2. Calculate Biometrics
        biometrics = self._calculate_biometrics(keyframes)
        
        # 3. Detect Faults based on biometrics & outcome
        faults = self._detect_faults(biometrics, outcome)
        
        # 4. Synthesize AI Coaching Feedback
        feedback = self._generate_feedback(faults, outcome)
        
        return {
            'shot_id': shot.get('id'),
            'outcome': outcome,
            'keyframes': keyframes,
            'biometrics': biometrics,
            'faults': faults,
            'feedback': feedback
        }

    def _extract_keyframes(self, shot: Dict, pose_tracks: List[Dict]) -> Dict:
        """Extracts the 4 critical frames for a shot."""
        # Simple heuristic implementation - in production this would use AI classification
        release_idx = shot.get('release_frame', 0)
        
        # Heuristics for phases (assuming 30fps)
        return {
            'dip': self._get_pose(pose_tracks, release_idx - 20),
            'set': self._get_pose(pose_tracks, release_idx - 5),
            'release': self._get_pose(pose_tracks, release_idx),
            'finish': self._get_pose(pose_tracks, release_idx + 15)
        }

    def _calculate_biometrics(self, keyframes: Dict) -> Dict:
        """Calculates angles and positions for keyframes."""
        metrics = {}
        
        # Elbow Extension at Release
        release_pose = keyframes.get('release')
        if release_pose:
            metrics['elbow_extension'] = self._calculate_angle(
                release_pose[5], release_pose[7], release_pose[9]
            ) # shoulder-elbow-wrist (left or right depends on player, using dummy for now)

        # Knee Dip
        dip_pose = keyframes.get('dip')
        if dip_pose:
            metrics['knee_dip'] = self._calculate_angle(
                dip_pose[11], dip_pose[13], dip_pose[15]
            ) # hip-knee-ankle
            
        return metrics

    def _detect_faults(self, biometrics: Dict, outcome: str) -> List[str]:
        """Identifies technical errors."""
        faults = []
        if outcome == 'missed':
            ext = biometrics.get('elbow_extension')
            if ext and ext < self.THRESHOLDS['MIN_ELBOW_EXTENSION']:
                faults.append('SHORT_ARM')
                
            dip = biometrics.get('knee_dip')
            if dip and dip > 140:
                faults.append('STIFF_LEGS')
                
        return faults

    def _generate_feedback(self, faults: List[str], outcome: str) -> str:
        """Creates human-readable coaching tips."""
        if outcome == 'made' and not faults:
            return "Great shot! Form is consistent. Keep holding that follow-through."
        
        tips = []
        if 'SHORT_ARM' in faults:
            tips.append("Extend your shooting arm fully. You're losing arc by releasing too early.")
        if 'STIFF_LEGS' in faults:
            tips.append("Dip lower into your legs. Your power should come from the ground up.")
            
        if not tips and outcome == 'missed':
            return "Good form, just a bit off. Keep practicing the same motion."
            
        return " ".join(tips)

    def _get_pose(self, pose_tracks: List[Dict], frame_idx: int) -> Optional[List]:
        if 0 <= frame_idx < len(pose_tracks):
            # Return first player pose found in that frame for now
            poses = pose_tracks[frame_idx]
            if poses:
                first_id = list(poses.keys())[0]
                return poses[first_id].get('keypoints')
        return None

    @staticmethod
    def _calculate_angle(p1, p2, p3):
        if not p1 or not p2 or not p3: return None
        try:
            a = np.array(p1[:2])
            b = np.array(p2[:2])
            c = np.array(p3[:2])
            
            ba = a - b
            bc = c - b
            
            cosine_angle = np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc))
            angle = np.arccos(cosine_angle)
            return np.degrees(angle)
        except:
            return None