File size: 9,079 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
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
"""
Transition Effort Engine - Analyzes player effort during transition phases.

Tracks player speeds during the 3 seconds after possession changes and
classifies effort as sprint, jog, or walk.
"""
from typing import Dict, Any, List
import numpy as np
from .base import BaseAnalyticsModule


class TransitionEffortEngine(BaseAnalyticsModule):
    """Analyzes player effort during offensive/defensive transitions."""
    
    def __init__(
        self,
        sprint_threshold_mps: float = 5.5,
        jog_threshold_mps: float = 3.0,
        transition_duration_seconds: float = 3.0,
    ):
        """
        Initialize transition effort engine.
        
        Args:
            sprint_threshold_mps: Speed threshold for sprint classification
            jog_threshold_mps: Speed threshold for jog classification
            transition_duration_seconds: How long to track after possession change
        """
        super().__init__("transition_effort")
        self.sprint_threshold = sprint_threshold_mps
        self.jog_threshold = jog_threshold_mps
        self.transition_duration = transition_duration_seconds
    
    def process(
        self,
        video_frames: List[Any],
        player_tracks: List[Dict],
        ball_tracks: List[Dict],
        tactical_positions: List[Dict],
        player_assignment: List[Dict],
        ball_possession: List[int],
        events: List[Dict],
        shots: List[Dict],
        court_keypoints: List[Dict],
        speeds: List[Dict],
        video_path: str,
        fps: float,
        **kwargs
    ) -> Dict[str, Any]:
        """
        Analyze transition effort for all possession changes.
        
        Returns:
            Dictionary with transition effort metrics
        """
        transition_efforts = []
        
        # Detect possession changes
        possession_changes = self._detect_possession_changes(
            ball_possession,
            player_assignment
        )
        
        transition_window_frames = int(self.transition_duration * fps)
        
        for change in possession_changes:
            frame = change["frame"]
            old_team = change["old_team"]
            new_team = change["new_team"]
            
            # Analyze effort for both teams
            # Team gaining possession: offense transition
            # Team losing possession: defense transition
            
            end_frame = min(frame + transition_window_frames, len(speeds))
            
            for analyze_frame in range(frame, end_frame):
                if analyze_frame >= len(player_assignment) or analyze_frame >= len(speeds):
                    break
                
                assignment = player_assignment[analyze_frame]
                frame_speeds = speeds[analyze_frame]
                
                for player_id, team_id in assignment.items():
                    if player_id not in frame_speeds:
                        continue
                    
                    speed = frame_speeds[player_id]
                    
                    # Classify effort
                    if speed >= self.sprint_threshold:
                        effort_type = "sprint"
                        effort_score = 100
                    elif speed >= self.jog_threshold:
                        effort_type = "jog"
                        effort_score = 60
                    else:
                        effort_type = "walk"
                        effort_score = 20
                    
                    # Determine transition type
                    if team_id == new_team:
                        transition_type = "defense_to_offense"
                    else:
                        transition_type = "offense_to_defense"
                    
                    transition_efforts.append({
                        "possession_change_frame": frame,
                        "player_track_id": player_id,
                        "team_id": team_id,
                        "transition_type": transition_type,
                        "effort_type": effort_type,
                        "max_speed_mps": float(speed),
                        "effort_score": effort_score,
                        "frame": analyze_frame,
                        "timestamp": self._get_frame_time(analyze_frame, fps)
                    })
        
        # Aggregate by possession change
        aggregated_efforts = []
        for change in possession_changes:
            frame = change["frame"]
            change_efforts = [e for e in transition_efforts if e["possession_change_frame"] == frame]
            
            if change_efforts:
                # Group by player
                player_efforts = {}
                for effort in change_efforts:
                    pid = effort["player_track_id"]
                    if pid not in player_efforts:
                        player_efforts[pid] = []
                    player_efforts[pid].append(effort)
                
                # Calculate max speed and avg effort for each player
                for pid, efforts in player_efforts.items():
                    max_speed = max(e["max_speed_mps"] for e in efforts)
                    avg_speed = np.mean([e["max_speed_mps"] for e in efforts])
                    avg_effort_score = np.mean([e["effort_score"] for e in efforts])
                    
                    # Determine overall effort type based on max speed
                    if max_speed >= self.sprint_threshold:
                        overall_effort = "sprint"
                    elif max_speed >= self.jog_threshold:
                        overall_effort = "jog"
                    else:
                        overall_effort = "walk"
                    
                    aggregated_efforts.append({
                        "possession_change_frame": frame,
                        "player_track_id": pid,
                        "team_id": efforts[0]["team_id"],
                        "transition_type": efforts[0]["transition_type"],
                        "effort_type": overall_effort,
                        "max_speed_mps": float(max_speed),
                        "avg_speed_mps": float(avg_speed),
                        "effort_score": float(avg_effort_score),
                        "duration_seconds": self.transition_duration
                    })
        
        # Calculate summary statistics
        if aggregated_efforts:
            sprint_count = sum(1 for e in aggregated_efforts if e["effort_type"] == "sprint")
            jog_count = sum(1 for e in aggregated_efforts if e["effort_type"] == "jog")
            walk_count = sum(1 for e in aggregated_efforts if e["effort_type"] == "walk")
            total = len(aggregated_efforts)
            
            summary = {
                "total_transition_events": total,
                "sprint_count": sprint_count,
                "jog_count": jog_count,
                "walk_count": walk_count,
                "sprint_rate": (sprint_count / total * 100) if total > 0 else 0,
                "avg_effort_score": float(np.mean([e["effort_score"] for e in aggregated_efforts])),
                "avg_max_speed_mps": float(np.mean([e["max_speed_mps"] for e in aggregated_efforts])),
            }
        else:
            summary = {
                "total_transition_events": 0,
                "sprint_count": 0,
                "jog_count": 0,
                "walk_count": 0,
                "sprint_rate": 0,
                "avg_effort_score": 0,
                "avg_max_speed_mps": 0,
            }
        
        return {
            "transition_efforts": aggregated_efforts,
            "summary": summary,
            "status": "success"
        }
    
    def _detect_possession_changes(
        self,
        ball_possession: List[int],
        player_assignment: List[Dict]
    ) -> List[Dict]:
        """
        Detect frames where possession changes between teams.
        
        Args:
            ball_possession: Per-frame possession (track_id or -1)
            player_assignment: Per-frame team assignments
        
        Returns:
            List of possession change events
        """
        changes = []
        prev_team = None
        
        for frame_idx in range(len(ball_possession)):
            if frame_idx >= len(player_assignment):
                break
            
            possession_player = ball_possession[frame_idx]
            if possession_player == -1:
                continue
            
            assignment = player_assignment[frame_idx]
            if possession_player not in assignment:
                continue
            
            current_team = assignment[possession_player]
            
            if prev_team is not None and current_team != prev_team:
                changes.append({
                    "frame": frame_idx,
                    "old_team": prev_team,
                    "new_team": current_team
                })
            
            prev_team = current_team
        
        return changes