Simon9 commited on
Commit
d31a3f8
ยท
verified ยท
1 Parent(s): 3d11bd1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +259 -119
app.py CHANGED
@@ -1,6 +1,6 @@
1
  import os
2
  from collections import deque, defaultdict
3
- from typing import List, Tuple, Dict
4
  from io import BytesIO
5
  import base64
6
 
@@ -62,6 +62,35 @@ EMBEDDINGS_PROCESSOR = AutoProcessor.from_pretrained(SIGLIP_MODEL_PATH, token=HF
62
  # ==============================================
63
  CONFIG = SoccerPitchConfiguration()
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  # ==============================================
66
  # PLAYER PERFORMANCE TRACKING
67
  # ==============================================
@@ -70,10 +99,10 @@ class PlayerPerformanceTracker:
70
 
71
  def __init__(self, pitch_config):
72
  self.config = pitch_config
73
- self.player_positions = defaultdict(list) # tracker_id -> list of (x, y, frame)
74
- self.player_velocities = defaultdict(list) # tracker_id -> list of velocities
75
- self.player_distances = defaultdict(float) # tracker_id -> total distance
76
- self.player_team = {} # tracker_id -> team_id
77
  self.player_stats = defaultdict(lambda: {
78
  'frames_visible': 0,
79
  'avg_velocity': 0,
@@ -92,22 +121,18 @@ class PlayerPerformanceTracker:
92
  self.player_positions[tracker_id].append((position[0], position[1], frame))
93
  self.player_stats[tracker_id]['frames_visible'] += 1
94
 
95
- # Calculate velocity if we have previous position
96
  if len(self.player_positions[tracker_id]) > 1:
97
  prev_pos = np.array(self.player_positions[tracker_id][-2][:2])
98
  curr_pos = np.array(position)
99
  distance = np.linalg.norm(curr_pos - prev_pos)
100
  self.player_distances[tracker_id] += distance
101
 
102
- # Velocity (assuming 30 fps)
103
- velocity = distance * 30 # cm/s
104
  self.player_velocities[tracker_id].append(velocity)
105
 
106
- # Update velocity stats
107
  if velocity > self.player_stats[tracker_id]['max_velocity']:
108
  self.player_stats[tracker_id]['max_velocity'] = velocity
109
 
110
- # Track position zones (thirds of the pitch)
111
  pitch_length = self.config.length
112
  if position[0] < pitch_length / 3:
113
  self.player_stats[tracker_id]['time_in_defensive_third'] += 1
@@ -124,7 +149,7 @@ class PlayerPerformanceTracker:
124
  stats['avg_velocity'] = np.mean(self.player_velocities[tracker_id])
125
 
126
  stats['total_distance'] = self.player_distances[tracker_id]
127
- stats['total_distance_meters'] = self.player_distances[tracker_id] / 100 # Convert to meters
128
  stats['team_id'] = self.player_team.get(tracker_id, -1)
129
 
130
  return stats
@@ -136,7 +161,6 @@ class PlayerPerformanceTracker:
136
 
137
  positions = np.array([(x, y) for x, y, _ in self.player_positions[tracker_id]])
138
 
139
- # Create 2D histogram
140
  pitch_length = self.config.length
141
  pitch_width = self.config.width
142
 
@@ -146,10 +170,9 @@ class PlayerPerformanceTracker:
146
  range=[[0, pitch_length], [0, pitch_width]]
147
  )
148
 
149
- # Apply Gaussian smoothing for better visualization
150
  heatmap = gaussian_filter(heatmap, sigma=3)
151
 
152
- return heatmap.T # Transpose for correct orientation
153
 
154
  def get_all_players_by_team(self) -> Dict[int, List[int]]:
155
  """Get all player IDs grouped by team"""
@@ -210,28 +233,22 @@ def create_player_heatmap_visualization(performance_tracker: PlayerPerformanceTr
210
  pitch = draw_pitch(CONFIG)
211
  heatmap = performance_tracker.generate_heatmap(tracker_id, resolution=150)
212
 
213
- # Normalize heatmap
214
  if heatmap.max() > 0:
215
  heatmap = heatmap / heatmap.max()
216
 
217
- # Create colored heatmap
218
- scale = 0.1 # Same scale as pitch
219
  padding = 50
220
 
221
  pitch_height, pitch_width = pitch.shape[:2]
222
  heatmap_resized = cv2.resize(heatmap, (pitch_width - 2*padding, pitch_height - 2*padding))
223
 
224
- # Apply colormap (red = high activity, blue = low activity)
225
  heatmap_colored = cv2.applyColorMap((heatmap_resized * 255).astype(np.uint8), cv2.COLORMAP_JET)
226
 
227
- # Create overlay
228
  overlay = pitch.copy()
229
  overlay[padding:pitch_height-padding, padding:pitch_width-padding] = heatmap_colored
230
 
231
- # Blend with pitch
232
  result = cv2.addWeighted(pitch, 0.6, overlay, 0.4, 0)
233
 
234
- # Add stats text
235
  stats = performance_tracker.get_player_stats(tracker_id)
236
  team_color = "Blue" if stats['team_id'] == 0 else "Pink"
237
 
@@ -284,28 +301,24 @@ def create_team_comparison_plot(performance_tracker: PlayerPerformanceTracker) -
284
 
285
  player_labels = [f"#{pid}" for pid in player_ids]
286
 
287
- # Distance covered
288
  fig.add_trace(
289
  go.Bar(x=player_labels, y=distances, name=team_names[team_id],
290
  marker_color=colors[team_id], showlegend=True),
291
  row=1, col=1
292
  )
293
 
294
- # Average speed
295
  fig.add_trace(
296
  go.Bar(x=player_labels, y=avg_speeds, name=team_names[team_id],
297
  marker_color=colors[team_id], showlegend=False),
298
  row=1, col=2
299
  )
300
 
301
- # Max speed
302
  fig.add_trace(
303
  go.Bar(x=player_labels, y=max_speeds, name=team_names[team_id],
304
  marker_color=colors[team_id], showlegend=False),
305
  row=2, col=1
306
  )
307
 
308
- # Attacking third time
309
  fig.add_trace(
310
  go.Bar(x=player_labels, y=attacking_time, name=team_names[team_id],
311
  marker_color=colors[team_id], showlegend=False),
@@ -336,7 +349,6 @@ def create_combined_heatmaps(performance_tracker: PlayerPerformanceTracker) -> n
336
  if team_id not in teams:
337
  continue
338
 
339
- # Combine all players from this team
340
  combined_heatmap = np.zeros((150, 150))
341
  for pid in teams[team_id]:
342
  player_heatmap = performance_tracker.generate_heatmap(pid, resolution=150)
@@ -345,7 +357,6 @@ def create_combined_heatmaps(performance_tracker: PlayerPerformanceTracker) -> n
345
  if combined_heatmap.max() > 0:
346
  combined_heatmap = combined_heatmap / combined_heatmap.max()
347
 
348
- # Create visualization
349
  pitch = draw_pitch(CONFIG)
350
  padding = 50
351
  pitch_height, pitch_width = pitch.shape[:2]
@@ -377,6 +388,7 @@ def create_combined_heatmaps(performance_tracker: PlayerPerformanceTracker) -> n
377
  # HELPER FUNCTIONS
378
  # ==============================================
379
  def resolve_goalkeepers_team_id(players: sv.Detections, goalkeepers: sv.Detections) -> np.ndarray:
 
380
  if len(goalkeepers) == 0 or len(players) == 0:
381
  return np.array([])
382
  goalkeepers_xy = goalkeepers.get_anchors_coordinates(sv.Position.BOTTOM_CENTER)
@@ -389,18 +401,12 @@ def resolve_goalkeepers_team_id(players: sv.Detections, goalkeepers: sv.Detectio
389
  ])
390
 
391
 
392
- def pil_image_to_data_uri(image: Image.Image) -> str:
393
- buffered = BytesIO()
394
- image.save(buffered, format="PNG")
395
- img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
396
- return f"data:image/png;base64,{img_str}"
397
-
398
-
399
  def create_game_style_radar(pitch_ball_xy, pitch_players_xy, players_class_id,
400
  pitch_referees_xy, ball_path=None):
401
  """Create game-style radar view with ball trail effect"""
402
  annotated_frame = draw_pitch(CONFIG)
403
 
 
404
  if ball_path is not None and len(ball_path) > 0:
405
  valid_path = [coords for coords in ball_path if len(coords) > 0]
406
  if len(valid_path) > 1:
@@ -417,6 +423,7 @@ def create_game_style_radar(pitch_ball_xy, pitch_players_xy, players_class_id,
417
  pitch=annotated_frame
418
  )
419
 
 
420
  if len(pitch_ball_xy) > 0:
421
  annotated_frame = draw_points_on_pitch(
422
  CONFIG, pitch_ball_xy,
@@ -426,6 +433,7 @@ def create_game_style_radar(pitch_ball_xy, pitch_players_xy, players_class_id,
426
  pitch=annotated_frame
427
  )
428
 
 
429
  for team_id, color_hex in zip([0, 1], ["00BFFF", "FF1493"]):
430
  mask = players_class_id == team_id
431
  if np.any(mask):
@@ -437,6 +445,7 @@ def create_game_style_radar(pitch_ball_xy, pitch_players_xy, players_class_id,
437
  pitch=annotated_frame
438
  )
439
 
 
440
  if len(pitch_referees_xy) > 0:
441
  annotated_frame = draw_points_on_pitch(
442
  CONFIG, pitch_referees_xy,
@@ -454,7 +463,7 @@ def create_game_style_radar(pitch_ball_xy, pitch_players_xy, players_class_id,
454
  # ==============================================
455
  def analyze_football_video(video_path: str, progress=gr.Progress()) -> Tuple:
456
  """
457
- Complete football analysis with performance tracking and heatmaps
458
  """
459
  if not video_path:
460
  return None, None, None, None, None, "โŒ Please upload a video file."
@@ -462,13 +471,17 @@ def analyze_football_video(video_path: str, progress=gr.Progress()) -> Tuple:
462
  try:
463
  progress(0, desc="๐Ÿ”ง Initializing...")
464
 
 
465
  BALL_ID, GOALKEEPER_ID, PLAYER_ID, REFEREE_ID = 0, 1, 2, 3
466
- STRIDE = 30
467
- MAXLEN = 5
 
468
 
 
469
  tracking_manager = PlayerTrackingManager(max_history=10)
470
  performance_tracker = PlayerPerformanceTracker(CONFIG)
471
 
 
472
  ellipse_annotator = sv.EllipseAnnotator(
473
  color=sv.ColorPalette.from_hex(['#00BFFF', '#FF1493', '#FFD700']),
474
  thickness=2
@@ -476,7 +489,8 @@ def analyze_football_video(video_path: str, progress=gr.Progress()) -> Tuple:
476
  label_annotator = sv.LabelAnnotator(
477
  color=sv.ColorPalette.from_hex(['#00BFFF', '#FF1493', '#FFD700']),
478
  text_color=sv.Color.from_hex('#FFFFFF'),
479
- text_thickness=2
 
480
  )
481
  triangle_annotator = sv.TriangleAnnotator(
482
  color=sv.Color.from_hex('#FFD700'),
@@ -484,6 +498,7 @@ def analyze_football_video(video_path: str, progress=gr.Progress()) -> Tuple:
484
  height=17
485
  )
486
 
 
487
  tracker = sv.ByteTrack(
488
  track_activation_threshold=0.4,
489
  lost_track_buffer=60,
@@ -492,6 +507,7 @@ def analyze_football_video(video_path: str, progress=gr.Progress()) -> Tuple:
492
  )
493
  tracker.reset()
494
 
 
495
  cap = cv2.VideoCapture(video_path)
496
  if not cap.isOpened():
497
  return None, None, None, None, None, f"โŒ Failed to open video: {video_path}"
@@ -506,10 +522,13 @@ def analyze_football_video(video_path: str, progress=gr.Progress()) -> Tuple:
506
  output_path = "/tmp/annotated_football.mp4"
507
  out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
508
 
509
- # Collect player crops
510
- progress(0.05, desc="๐Ÿƒ Collecting player samples...")
 
 
511
  player_crops = []
512
  frame_count = 0
 
513
  while frame_count < min(total_frames, 300):
514
  ret, frame = cap.read()
515
  if not ret:
@@ -518,6 +537,7 @@ def analyze_football_video(video_path: str, progress=gr.Progress()) -> Tuple:
518
  if frame_count % STRIDE == 0:
519
  result = CLIENT.infer(frame, model_id=PLAYER_DETECTION_MODEL_ID)
520
  detections = sv.Detections.from_inference(result)
 
521
  players_detections = detections[detections.class_id == PLAYER_ID]
522
 
523
  if len(players_detections.xyxy) > 0:
@@ -531,19 +551,29 @@ def analyze_football_video(video_path: str, progress=gr.Progress()) -> Tuple:
531
 
532
  print(f"โœ… Collected {len(player_crops)} player samples")
533
 
534
- # Train classifier
535
- progress(0.15, desc="๐ŸŽฏ Training team classifier...")
 
 
536
  team_classifier = TeamClassifier(device=DEVICE)
537
  team_classifier.fit(player_crops)
538
  print("โœ… Team classifier trained")
539
 
540
- # Process video
 
 
541
  cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
542
  frame_count = 0
543
- M = deque(maxlen=MAXLEN)
544
  ball_path_raw = []
 
 
 
 
 
545
 
546
- progress(0.2, desc="๐ŸŽฌ Processing video frames...")
 
547
  while True:
548
  ret, frame = cap.read()
549
  if not ret:
@@ -553,29 +583,39 @@ def analyze_football_video(video_path: str, progress=gr.Progress()) -> Tuple:
553
  tracking_manager.reset_frame()
554
 
555
  if frame_count % 30 == 0:
556
- progress(0.2 + 0.5 * (frame_count / total_frames),
557
- desc=f"๐ŸŽฌ Frame {frame_count}/{total_frames}")
558
 
559
- result = CLIENT.infer(frame, model_id=PLAYER_DETECTION_MODEL_ID)
 
560
  detections = sv.Detections.from_inference(result)
561
 
562
  if len(detections.xyxy) == 0:
563
  out.write(frame)
 
564
  continue
565
 
 
566
  ball_detections = detections[detections.class_id == BALL_ID]
 
 
567
  all_detections = detections[detections.class_id != BALL_ID]
568
  all_detections = all_detections.with_nms(threshold=0.5, class_agnostic=True)
 
 
569
  all_detections = tracker.update_with_detections(detections=all_detections)
570
 
 
571
  goalkeepers_detections = all_detections[all_detections.class_id == GOALKEEPER_ID]
572
  players_detections = all_detections[all_detections.class_id == PLAYER_ID]
573
  referees_detections = all_detections[all_detections.class_id == REFEREE_ID]
574
 
 
575
  if len(players_detections.xyxy) > 0:
576
  crops = [sv.crop_image(frame, xyxy) for xyxy in players_detections.xyxy]
577
  predicted_teams = team_classifier.predict(crops)
578
 
 
579
  for idx, tracker_id in enumerate(players_detections.tracker_id):
580
  tracking_manager.update_team_assignment(tracker_id, predicted_teams[idx])
581
  predicted_teams[idx] = tracking_manager.get_stable_team_id(
@@ -584,63 +624,117 @@ def analyze_football_video(video_path: str, progress=gr.Progress()) -> Tuple:
584
 
585
  players_detections.class_id = predicted_teams
586
 
 
587
  goalkeepers_detections.class_id = resolve_goalkeepers_team_id(
588
  players_detections, goalkeepers_detections
589
  )
 
 
590
  referees_detections.class_id -= 1
591
 
 
592
  all_detections = sv.Detections.merge([
593
  players_detections, goalkeepers_detections, referees_detections
594
  ])
 
 
595
 
 
596
  labels = [f"#{tid}" for tid in all_detections.tracker_id]
597
 
 
598
  annotated_frame = frame.copy()
599
  annotated_frame = ellipse_annotator.annotate(annotated_frame, all_detections)
600
  annotated_frame = label_annotator.annotate(annotated_frame, all_detections, labels=labels)
601
  annotated_frame = triangle_annotator.annotate(annotated_frame, ball_detections)
602
  out.write(annotated_frame)
603
 
604
- # Performance tracking with field transformation
 
 
605
  try:
606
- result_field = CLIENT.infer(frame, model_id=FIELD_DETECTION_MODEL_ID)
607
  key_points = sv.KeyPoints.from_inference(result_field)
 
 
608
  filter_mask = key_points.confidence[0] > 0.5
609
  frame_ref_pts = key_points.xy[0][filter_mask]
610
  pitch_ref_pts = np.array(CONFIG.vertices)[filter_mask]
611
- transformer = ViewTransformer(source=frame_ref_pts, target=pitch_ref_pts)
612
- M.append(transformer.m)
613
- transformer.m = np.mean(np.array(M), axis=0)
614
-
615
- # Track ball
616
- frame_ball_xy = ball_detections.get_anchors_coordinates(sv.Position.BOTTOM_CENTER)
617
- pitch_ball_xy = transformer.transform_points(frame_ball_xy)
618
- ball_path_raw.append(pitch_ball_xy)
619
-
620
- # Track all players (including goalkeepers)
621
- all_players = sv.Detections.merge([players_detections, goalkeepers_detections])
622
- players_xy = all_players.get_anchors_coordinates(sv.Position.BOTTOM_CENTER)
623
- pitch_players_xy = transformer.transform_points(players_xy)
624
 
625
- for idx, tracker_id in enumerate(all_players.tracker_id):
626
- if idx < len(pitch_players_xy):
627
- performance_tracker.update(
628
- tracker_id,
629
- pitch_players_xy[idx],
630
- all_players.class_id[idx],
631
- frame_count
632
- )
633
- except:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
634
  ball_path_raw.append(np.empty((0, 2)))
635
 
636
  cap.release()
637
  out.release()
638
  print(f"โœ… Processed {frame_count} frames")
639
 
640
- # Generate visualizations
641
- progress(0.75, desc="๐Ÿ“Š Generating performance analytics...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
642
 
643
- # Team comparison
644
  comparison_fig = create_team_comparison_plot(performance_tracker)
645
 
646
  # Combined team heatmaps
@@ -648,10 +742,11 @@ def analyze_football_video(video_path: str, progress=gr.Progress()) -> Tuple:
648
  team_heatmaps = create_combined_heatmaps(performance_tracker)
649
  cv2.imwrite(team_heatmaps_path, team_heatmaps)
650
 
651
- # Individual player heatmaps (top 6 players by distance)
652
  progress(0.85, desc="๐Ÿ—บ๏ธ Creating individual heatmaps...")
653
  teams = performance_tracker.get_all_players_by_team()
654
  top_players = []
 
655
  for team_id in [0, 1]:
656
  if team_id in teams:
657
  team_players = teams[team_id]
@@ -665,7 +760,7 @@ def analyze_football_video(video_path: str, progress=gr.Progress()) -> Tuple:
665
  heatmap = create_player_heatmap_visualization(performance_tracker, pid)
666
  individual_heatmaps.append(heatmap)
667
 
668
- # Arrange individual heatmaps in grid
669
  if len(individual_heatmaps) > 0:
670
  rows = []
671
  for i in range(0, len(individual_heatmaps), 3):
@@ -683,29 +778,39 @@ def analyze_football_video(video_path: str, progress=gr.Progress()) -> Tuple:
683
  else:
684
  individual_heatmaps_path = None
685
 
686
- # Radar view
687
- progress(0.9, desc="๐Ÿ—บ๏ธ Creating game-style radar view...")
 
 
688
  radar_path = "/tmp/radar_view_enhanced.png"
689
  try:
690
- radar_frame = create_game_style_radar(
691
- pitch_ball_xy=ball_path_raw[-1] if ball_path_raw else np.empty((0, 2)),
692
- pitch_players_xy=pitch_players_xy if 'pitch_players_xy' in locals() else np.empty((0, 2)),
693
- players_class_id=all_players.class_id if 'all_players' in locals() else np.array([]),
694
- pitch_referees_xy=np.empty((0, 2)),
695
- ball_path=ball_path_raw
696
- )
697
- cv2.imwrite(radar_path, radar_frame)
 
 
 
698
  except Exception as e:
699
  print(f"โš ๏ธ Radar view creation failed: {e}")
700
  radar_path = None
701
 
702
- # Generate summary stats
 
 
703
  progress(0.95, desc="๐Ÿ“ Generating summary report...")
704
  teams = performance_tracker.get_all_players_by_team()
705
 
706
  summary_lines = ["โœ… **Analysis Complete!**\n"]
707
- summary_lines.append(f"- Total Frames: {frame_count}")
708
- summary_lines.append(f"- Ball Path Points: {len([p for p in ball_path_raw if len(p) > 0])}\n")
 
 
 
709
 
710
  for team_id in [0, 1]:
711
  if team_id not in teams:
@@ -726,17 +831,26 @@ def analyze_football_video(video_path: str, progress=gr.Progress()) -> Tuple:
726
  for pid in teams[team_id]]
727
  player_distances.sort(key=lambda x: x[1], reverse=True)
728
 
729
- summary_lines.append(f"\n **Top Performers:**")
730
  for i, (pid, dist) in enumerate(player_distances[:3], 1):
731
  stats = performance_tracker.get_player_stats(pid)
732
  summary_lines.append(
733
  f" {i}. Player #{pid}: {dist:.1f}m, "
734
- f"Avg Speed: {stats['avg_velocity']/100:.2f}m/s"
 
735
  )
736
 
 
 
 
 
 
 
 
 
737
  summary_msg = "\n".join(summary_lines)
738
 
739
- progress(1.0, desc="โœ… Complete!")
740
 
741
  return (output_path, comparison_fig, team_heatmaps_path,
742
  individual_heatmaps_path, radar_path, summary_msg)
@@ -752,39 +866,50 @@ def analyze_football_video(video_path: str, progress=gr.Progress()) -> Tuple:
752
  # ==============================================
753
  # GRADIO INTERFACE
754
  # ==============================================
755
- with gr.Blocks(title="โšฝ Football Performance Analyzer") as iface:
756
  gr.Markdown("""
757
  # โšฝ Advanced Football Video Analyzer
758
- Upload a football match video to get comprehensive performance analytics including:
759
- - Player tracking with persistent IDs
760
- - Individual and team heatmaps
761
- - Distance covered and speed metrics
762
- - Game-style radar view with ball tracking
 
 
 
 
 
 
763
  """)
764
 
765
  with gr.Row():
766
- video_input = gr.Video(label="Upload Football Video")
767
 
768
- analyze_btn = gr.Button("๐Ÿš€ Analyze Video", variant="primary", size="lg")
769
 
770
  with gr.Row():
771
- status_output = gr.Textbox(label="Analysis Status & Summary", lines=20)
772
 
773
  with gr.Tabs():
774
  with gr.Tab("๐Ÿ“น Annotated Video"):
775
- video_output = gr.Video(label="Annotated Video with Player Tracking")
 
776
 
777
  with gr.Tab("๐Ÿ“Š Performance Comparison"):
 
778
  comparison_output = gr.Plot(label="Team Performance Metrics")
779
 
780
  with gr.Tab("๐Ÿ—บ๏ธ Team Heatmaps"):
781
- team_heatmaps_output = gr.Image(label="Combined Team Activity Heatmaps")
 
782
 
783
  with gr.Tab("๐Ÿ‘ค Individual Heatmaps"):
784
- individual_heatmaps_output = gr.Image(label="Top Players Individual Heatmaps")
 
785
 
786
  with gr.Tab("๐ŸŽฎ Game Radar View"):
787
- radar_output = gr.Image(label="Game-Style Radar with Ball Trail")
 
788
 
789
  analyze_btn.click(
790
  fn=analyze_football_video,
@@ -801,19 +926,34 @@ with gr.Blocks(title="โšฝ Football Performance Analyzer") as iface:
801
 
802
  gr.Markdown("""
803
  ---
804
- ### ๐Ÿ“‹ Features:
805
- - **Persistent Player Tracking**: IDs remain consistent throughout the video
806
- - **Performance Metrics**: Distance covered, average/max speed, zone activity
807
- - **Team Heatmaps**: Visualize team positioning and movement patterns
808
- - **Individual Analysis**: Top 6 players by distance with detailed heatmaps
809
- - **Professional Visualization**: Game-style radar view with ball trail effects
810
-
811
- ### ๐ŸŽฏ Perfect for:
812
- - Coaching staff analysis
813
- - Player performance reports
814
- - Tactical review sessions
815
- - Scouting and recruitment
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
816
  """)
817
 
818
  if __name__ == "__main__":
819
- iface.launch()
 
1
  import os
2
  from collections import deque, defaultdict
3
+ from typing import List, Tuple, Dict, Optional, Union
4
  from io import BytesIO
5
  import base64
6
 
 
62
  # ==============================================
63
  CONFIG = SoccerPitchConfiguration()
64
 
65
+ # ==============================================
66
+ # BALL PATH OUTLIER REMOVAL
67
+ # ==============================================
68
+ def replace_outliers_based_on_distance(
69
+ positions: List[np.ndarray],
70
+ distance_threshold: float
71
+ ) -> List[np.ndarray]:
72
+ """Remove outlier positions based on distance threshold"""
73
+ last_valid_position: Union[np.ndarray, None] = None
74
+ cleaned_positions: List[np.ndarray] = []
75
+
76
+ for position in positions:
77
+ if len(position) == 0:
78
+ cleaned_positions.append(position)
79
+ else:
80
+ if last_valid_position is None:
81
+ cleaned_positions.append(position)
82
+ last_valid_position = position
83
+ else:
84
+ distance = np.linalg.norm(position - last_valid_position)
85
+ if distance > distance_threshold:
86
+ cleaned_positions.append(np.array([], dtype=np.float64))
87
+ else:
88
+ cleaned_positions.append(position)
89
+ last_valid_position = position
90
+
91
+ return cleaned_positions
92
+
93
+
94
  # ==============================================
95
  # PLAYER PERFORMANCE TRACKING
96
  # ==============================================
 
99
 
100
  def __init__(self, pitch_config):
101
  self.config = pitch_config
102
+ self.player_positions = defaultdict(list)
103
+ self.player_velocities = defaultdict(list)
104
+ self.player_distances = defaultdict(float)
105
+ self.player_team = {}
106
  self.player_stats = defaultdict(lambda: {
107
  'frames_visible': 0,
108
  'avg_velocity': 0,
 
121
  self.player_positions[tracker_id].append((position[0], position[1], frame))
122
  self.player_stats[tracker_id]['frames_visible'] += 1
123
 
 
124
  if len(self.player_positions[tracker_id]) > 1:
125
  prev_pos = np.array(self.player_positions[tracker_id][-2][:2])
126
  curr_pos = np.array(position)
127
  distance = np.linalg.norm(curr_pos - prev_pos)
128
  self.player_distances[tracker_id] += distance
129
 
130
+ velocity = distance * 30 # cm/s assuming 30fps
 
131
  self.player_velocities[tracker_id].append(velocity)
132
 
 
133
  if velocity > self.player_stats[tracker_id]['max_velocity']:
134
  self.player_stats[tracker_id]['max_velocity'] = velocity
135
 
 
136
  pitch_length = self.config.length
137
  if position[0] < pitch_length / 3:
138
  self.player_stats[tracker_id]['time_in_defensive_third'] += 1
 
149
  stats['avg_velocity'] = np.mean(self.player_velocities[tracker_id])
150
 
151
  stats['total_distance'] = self.player_distances[tracker_id]
152
+ stats['total_distance_meters'] = self.player_distances[tracker_id] / 100
153
  stats['team_id'] = self.player_team.get(tracker_id, -1)
154
 
155
  return stats
 
161
 
162
  positions = np.array([(x, y) for x, y, _ in self.player_positions[tracker_id]])
163
 
 
164
  pitch_length = self.config.length
165
  pitch_width = self.config.width
166
 
 
170
  range=[[0, pitch_length], [0, pitch_width]]
171
  )
172
 
 
173
  heatmap = gaussian_filter(heatmap, sigma=3)
174
 
175
+ return heatmap.T
176
 
177
  def get_all_players_by_team(self) -> Dict[int, List[int]]:
178
  """Get all player IDs grouped by team"""
 
233
  pitch = draw_pitch(CONFIG)
234
  heatmap = performance_tracker.generate_heatmap(tracker_id, resolution=150)
235
 
 
236
  if heatmap.max() > 0:
237
  heatmap = heatmap / heatmap.max()
238
 
239
+ scale = 0.1
 
240
  padding = 50
241
 
242
  pitch_height, pitch_width = pitch.shape[:2]
243
  heatmap_resized = cv2.resize(heatmap, (pitch_width - 2*padding, pitch_height - 2*padding))
244
 
 
245
  heatmap_colored = cv2.applyColorMap((heatmap_resized * 255).astype(np.uint8), cv2.COLORMAP_JET)
246
 
 
247
  overlay = pitch.copy()
248
  overlay[padding:pitch_height-padding, padding:pitch_width-padding] = heatmap_colored
249
 
 
250
  result = cv2.addWeighted(pitch, 0.6, overlay, 0.4, 0)
251
 
 
252
  stats = performance_tracker.get_player_stats(tracker_id)
253
  team_color = "Blue" if stats['team_id'] == 0 else "Pink"
254
 
 
301
 
302
  player_labels = [f"#{pid}" for pid in player_ids]
303
 
 
304
  fig.add_trace(
305
  go.Bar(x=player_labels, y=distances, name=team_names[team_id],
306
  marker_color=colors[team_id], showlegend=True),
307
  row=1, col=1
308
  )
309
 
 
310
  fig.add_trace(
311
  go.Bar(x=player_labels, y=avg_speeds, name=team_names[team_id],
312
  marker_color=colors[team_id], showlegend=False),
313
  row=1, col=2
314
  )
315
 
 
316
  fig.add_trace(
317
  go.Bar(x=player_labels, y=max_speeds, name=team_names[team_id],
318
  marker_color=colors[team_id], showlegend=False),
319
  row=2, col=1
320
  )
321
 
 
322
  fig.add_trace(
323
  go.Bar(x=player_labels, y=attacking_time, name=team_names[team_id],
324
  marker_color=colors[team_id], showlegend=False),
 
349
  if team_id not in teams:
350
  continue
351
 
 
352
  combined_heatmap = np.zeros((150, 150))
353
  for pid in teams[team_id]:
354
  player_heatmap = performance_tracker.generate_heatmap(pid, resolution=150)
 
357
  if combined_heatmap.max() > 0:
358
  combined_heatmap = combined_heatmap / combined_heatmap.max()
359
 
 
360
  pitch = draw_pitch(CONFIG)
361
  padding = 50
362
  pitch_height, pitch_width = pitch.shape[:2]
 
388
  # HELPER FUNCTIONS
389
  # ==============================================
390
  def resolve_goalkeepers_team_id(players: sv.Detections, goalkeepers: sv.Detections) -> np.ndarray:
391
+ """Assign goalkeepers to the nearest team centroid"""
392
  if len(goalkeepers) == 0 or len(players) == 0:
393
  return np.array([])
394
  goalkeepers_xy = goalkeepers.get_anchors_coordinates(sv.Position.BOTTOM_CENTER)
 
401
  ])
402
 
403
 
 
 
 
 
 
 
 
404
  def create_game_style_radar(pitch_ball_xy, pitch_players_xy, players_class_id,
405
  pitch_referees_xy, ball_path=None):
406
  """Create game-style radar view with ball trail effect"""
407
  annotated_frame = draw_pitch(CONFIG)
408
 
409
+ # Draw ball trail with fading effect
410
  if ball_path is not None and len(ball_path) > 0:
411
  valid_path = [coords for coords in ball_path if len(coords) > 0]
412
  if len(valid_path) > 1:
 
423
  pitch=annotated_frame
424
  )
425
 
426
+ # Draw current ball position
427
  if len(pitch_ball_xy) > 0:
428
  annotated_frame = draw_points_on_pitch(
429
  CONFIG, pitch_ball_xy,
 
433
  pitch=annotated_frame
434
  )
435
 
436
+ # Draw players
437
  for team_id, color_hex in zip([0, 1], ["00BFFF", "FF1493"]):
438
  mask = players_class_id == team_id
439
  if np.any(mask):
 
445
  pitch=annotated_frame
446
  )
447
 
448
+ # Draw referees
449
  if len(pitch_referees_xy) > 0:
450
  annotated_frame = draw_points_on_pitch(
451
  CONFIG, pitch_referees_xy,
 
463
  # ==============================================
464
  def analyze_football_video(video_path: str, progress=gr.Progress()) -> Tuple:
465
  """
466
+ Complete football analysis pipeline following Colab notebook steps
467
  """
468
  if not video_path:
469
  return None, None, None, None, None, "โŒ Please upload a video file."
 
471
  try:
472
  progress(0, desc="๐Ÿ”ง Initializing...")
473
 
474
+ # IDs from Roboflow model
475
  BALL_ID, GOALKEEPER_ID, PLAYER_ID, REFEREE_ID = 0, 1, 2, 3
476
+ STRIDE = 30 # Frame sampling for training
477
+ MAXLEN = 5 # Transformation matrix smoothing
478
+ MAX_DISTANCE_THRESHOLD = 500 # Ball path outlier threshold
479
 
480
+ # Initialize managers
481
  tracking_manager = PlayerTrackingManager(max_history=10)
482
  performance_tracker = PlayerPerformanceTracker(CONFIG)
483
 
484
+ # Annotators with exact colors from notebook
485
  ellipse_annotator = sv.EllipseAnnotator(
486
  color=sv.ColorPalette.from_hex(['#00BFFF', '#FF1493', '#FFD700']),
487
  thickness=2
 
489
  label_annotator = sv.LabelAnnotator(
490
  color=sv.ColorPalette.from_hex(['#00BFFF', '#FF1493', '#FFD700']),
491
  text_color=sv.Color.from_hex('#FFFFFF'),
492
+ text_thickness=2,
493
+ text_position=sv.Position.BOTTOM_CENTER
494
  )
495
  triangle_annotator = sv.TriangleAnnotator(
496
  color=sv.Color.from_hex('#FFD700'),
 
498
  height=17
499
  )
500
 
501
+ # ByteTrack tracker with optimized settings
502
  tracker = sv.ByteTrack(
503
  track_activation_threshold=0.4,
504
  lost_track_buffer=60,
 
507
  )
508
  tracker.reset()
509
 
510
+ # Video setup
511
  cap = cv2.VideoCapture(video_path)
512
  if not cap.isOpened():
513
  return None, None, None, None, None, f"โŒ Failed to open video: {video_path}"
 
522
  output_path = "/tmp/annotated_football.mp4"
523
  out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
524
 
525
+ # ========================================
526
+ # STEP 1: Collect Player Crops for Team Classifier
527
+ # ========================================
528
+ progress(0.05, desc="๐Ÿƒ Collecting player samples (Step 1/6)...")
529
  player_crops = []
530
  frame_count = 0
531
+
532
  while frame_count < min(total_frames, 300):
533
  ret, frame = cap.read()
534
  if not ret:
 
537
  if frame_count % STRIDE == 0:
538
  result = CLIENT.infer(frame, model_id=PLAYER_DETECTION_MODEL_ID)
539
  detections = sv.Detections.from_inference(result)
540
+ detections = detections.with_nms(threshold=0.5, class_agnostic=True)
541
  players_detections = detections[detections.class_id == PLAYER_ID]
542
 
543
  if len(players_detections.xyxy) > 0:
 
551
 
552
  print(f"โœ… Collected {len(player_crops)} player samples")
553
 
554
+ # ========================================
555
+ # STEP 2: Train Team Classifier
556
+ # ========================================
557
+ progress(0.15, desc="๐ŸŽฏ Training team classifier (Step 2/6)...")
558
  team_classifier = TeamClassifier(device=DEVICE)
559
  team_classifier.fit(player_crops)
560
  print("โœ… Team classifier trained")
561
 
562
+ # ========================================
563
+ # STEP 3: Process Full Video with Tracking
564
+ # ========================================
565
  cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
566
  frame_count = 0
567
+ M = deque(maxlen=MAXLEN) # Transformation matrix smoothing
568
  ball_path_raw = []
569
+
570
+ # Store last frame data for radar
571
+ last_pitch_players_xy = None
572
+ last_players_class_id = None
573
+ last_pitch_referees_xy = None
574
 
575
+ progress(0.2, desc="๐ŸŽฌ Processing video frames (Step 3/6)...")
576
+
577
  while True:
578
  ret, frame = cap.read()
579
  if not ret:
 
583
  tracking_manager.reset_frame()
584
 
585
  if frame_count % 30 == 0:
586
+ progress(0.2 + 0.4 * (frame_count / total_frames),
587
+ desc=f"๐ŸŽฌ Processing frame {frame_count}/{total_frames}")
588
 
589
+ # Player and ball detection
590
+ result = CLIENT.infer(frame, model_id=PLAYER_DETECTION_MODEL_ID, confidence=0.3)
591
  detections = sv.Detections.from_inference(result)
592
 
593
  if len(detections.xyxy) == 0:
594
  out.write(frame)
595
+ ball_path_raw.append(np.empty((0, 2)))
596
  continue
597
 
598
+ # Separate ball from other detections
599
  ball_detections = detections[detections.class_id == BALL_ID]
600
+ ball_detections.xyxy = sv.pad_boxes(xyxy=ball_detections.xyxy, px=10)
601
+
602
  all_detections = detections[detections.class_id != BALL_ID]
603
  all_detections = all_detections.with_nms(threshold=0.5, class_agnostic=True)
604
+
605
+ # Track detections
606
  all_detections = tracker.update_with_detections(detections=all_detections)
607
 
608
+ # Separate by type
609
  goalkeepers_detections = all_detections[all_detections.class_id == GOALKEEPER_ID]
610
  players_detections = all_detections[all_detections.class_id == PLAYER_ID]
611
  referees_detections = all_detections[all_detections.class_id == REFEREE_ID]
612
 
613
+ # Team prediction with stability
614
  if len(players_detections.xyxy) > 0:
615
  crops = [sv.crop_image(frame, xyxy) for xyxy in players_detections.xyxy]
616
  predicted_teams = team_classifier.predict(crops)
617
 
618
+ # Apply stable team assignment
619
  for idx, tracker_id in enumerate(players_detections.tracker_id):
620
  tracking_manager.update_team_assignment(tracker_id, predicted_teams[idx])
621
  predicted_teams[idx] = tracking_manager.get_stable_team_id(
 
624
 
625
  players_detections.class_id = predicted_teams
626
 
627
+ # Assign goalkeeper teams
628
  goalkeepers_detections.class_id = resolve_goalkeepers_team_id(
629
  players_detections, goalkeepers_detections
630
  )
631
+
632
+ # Adjust referee class_id
633
  referees_detections.class_id -= 1
634
 
635
+ # Merge all detections
636
  all_detections = sv.Detections.merge([
637
  players_detections, goalkeepers_detections, referees_detections
638
  ])
639
+
640
+ all_detections.class_id = all_detections.class_id.astype(int)
641
 
642
+ # Create labels
643
  labels = [f"#{tid}" for tid in all_detections.tracker_id]
644
 
645
+ # Annotate frame
646
  annotated_frame = frame.copy()
647
  annotated_frame = ellipse_annotator.annotate(annotated_frame, all_detections)
648
  annotated_frame = label_annotator.annotate(annotated_frame, all_detections, labels=labels)
649
  annotated_frame = triangle_annotator.annotate(annotated_frame, ball_detections)
650
  out.write(annotated_frame)
651
 
652
+ # ========================================
653
+ # STEP 4: Field Detection & Transformation
654
+ # ========================================
655
  try:
656
+ result_field = CLIENT.infer(frame, model_id=FIELD_DETECTION_MODEL_ID, confidence=0.3)
657
  key_points = sv.KeyPoints.from_inference(result_field)
658
+
659
+ # Filter confident keypoints
660
  filter_mask = key_points.confidence[0] > 0.5
661
  frame_ref_pts = key_points.xy[0][filter_mask]
662
  pitch_ref_pts = np.array(CONFIG.vertices)[filter_mask]
 
 
 
 
 
 
 
 
 
 
 
 
 
663
 
664
+ if len(frame_ref_pts) >= 4: # Need at least 4 points for homography
665
+ transformer = ViewTransformer(source=frame_ref_pts, target=pitch_ref_pts)
666
+ M.append(transformer.m)
667
+ transformer.m = np.mean(np.array(M), axis=0)
668
+
669
+ # Transform ball position
670
+ frame_ball_xy = ball_detections.get_anchors_coordinates(sv.Position.BOTTOM_CENTER)
671
+ pitch_ball_xy = transformer.transform_points(frame_ball_xy)
672
+ ball_path_raw.append(pitch_ball_xy)
673
+
674
+ # Transform all players (including goalkeepers)
675
+ all_players = sv.Detections.merge([players_detections, goalkeepers_detections])
676
+ players_xy = all_players.get_anchors_coordinates(sv.Position.BOTTOM_CENTER)
677
+ pitch_players_xy = transformer.transform_points(players_xy)
678
+
679
+ # Transform referees
680
+ referees_xy = referees_detections.get_anchors_coordinates(sv.Position.BOTTOM_CENTER)
681
+ pitch_referees_xy = transformer.transform_points(referees_xy)
682
+
683
+ # Store for radar view
684
+ last_pitch_players_xy = pitch_players_xy
685
+ last_players_class_id = all_players.class_id
686
+ last_pitch_referees_xy = pitch_referees_xy
687
+
688
+ # Update performance tracker
689
+ for idx, tracker_id in enumerate(all_players.tracker_id):
690
+ if idx < len(pitch_players_xy):
691
+ performance_tracker.update(
692
+ tracker_id,
693
+ pitch_players_xy[idx],
694
+ all_players.class_id[idx],
695
+ frame_count
696
+ )
697
+ else:
698
+ ball_path_raw.append(np.empty((0, 2)))
699
+ except Exception as e:
700
  ball_path_raw.append(np.empty((0, 2)))
701
 
702
  cap.release()
703
  out.release()
704
  print(f"โœ… Processed {frame_count} frames")
705
 
706
+ # ========================================
707
+ # STEP 5: Clean Ball Path (Remove Outliers)
708
+ # ========================================
709
+ progress(0.65, desc="๐Ÿงน Cleaning ball trajectory (Step 4/6)...")
710
+
711
+ # Convert to proper format for cleaning
712
+ path_for_cleaning = []
713
+ for coords in ball_path_raw:
714
+ if len(coords) == 0:
715
+ path_for_cleaning.append(np.empty((0, 2), dtype=np.float32))
716
+ elif coords.shape[0] >= 2:
717
+ path_for_cleaning.append(np.empty((0, 2), dtype=np.float32))
718
+ else:
719
+ path_for_cleaning.append(coords)
720
+
721
+ # Flatten coordinates
722
+ path_flattened = [coords.flatten() for coords in path_for_cleaning]
723
+
724
+ # Remove outliers
725
+ cleaned_path = replace_outliers_based_on_distance(
726
+ [np.array(p).reshape(-1, 2) if len(p) > 0 else np.empty((0, 2)) for p in path_for_cleaning],
727
+ MAX_DISTANCE_THRESHOLD
728
+ )
729
+
730
+ print(f"โœ… Ball path cleaned: {len([p for p in cleaned_path if len(p) > 0])} valid points")
731
+
732
+ # ========================================
733
+ # STEP 6: Generate Performance Analytics
734
+ # ========================================
735
+ progress(0.75, desc="๐Ÿ“Š Generating performance analytics (Step 5/6)...")
736
 
737
+ # Team comparison charts
738
  comparison_fig = create_team_comparison_plot(performance_tracker)
739
 
740
  # Combined team heatmaps
 
742
  team_heatmaps = create_combined_heatmaps(performance_tracker)
743
  cv2.imwrite(team_heatmaps_path, team_heatmaps)
744
 
745
+ # Individual player heatmaps (top 6 by distance)
746
  progress(0.85, desc="๐Ÿ—บ๏ธ Creating individual heatmaps...")
747
  teams = performance_tracker.get_all_players_by_team()
748
  top_players = []
749
+
750
  for team_id in [0, 1]:
751
  if team_id in teams:
752
  team_players = teams[team_id]
 
760
  heatmap = create_player_heatmap_visualization(performance_tracker, pid)
761
  individual_heatmaps.append(heatmap)
762
 
763
+ # Arrange individual heatmaps in grid (3 columns)
764
  if len(individual_heatmaps) > 0:
765
  rows = []
766
  for i in range(0, len(individual_heatmaps), 3):
 
778
  else:
779
  individual_heatmaps_path = None
780
 
781
+ # ========================================
782
+ # STEP 7: Create Game-Style Radar View
783
+ # ========================================
784
+ progress(0.9, desc="๐Ÿ—บ๏ธ Creating game-style radar view (Step 6/6)...")
785
  radar_path = "/tmp/radar_view_enhanced.png"
786
  try:
787
+ if last_pitch_players_xy is not None:
788
+ radar_frame = create_game_style_radar(
789
+ pitch_ball_xy=cleaned_path[-1] if cleaned_path else np.empty((0, 2)),
790
+ pitch_players_xy=last_pitch_players_xy,
791
+ players_class_id=last_players_class_id,
792
+ pitch_referees_xy=last_pitch_referees_xy,
793
+ ball_path=cleaned_path
794
+ )
795
+ cv2.imwrite(radar_path, radar_frame)
796
+ else:
797
+ radar_path = None
798
  except Exception as e:
799
  print(f"โš ๏ธ Radar view creation failed: {e}")
800
  radar_path = None
801
 
802
+ # ========================================
803
+ # Generate Summary Report
804
+ # ========================================
805
  progress(0.95, desc="๐Ÿ“ Generating summary report...")
806
  teams = performance_tracker.get_all_players_by_team()
807
 
808
  summary_lines = ["โœ… **Analysis Complete!**\n"]
809
+ summary_lines.append(f"**Video Statistics:**")
810
+ summary_lines.append(f"- Total Frames Processed: {frame_count}")
811
+ summary_lines.append(f"- Video Resolution: {width}x{height}")
812
+ summary_lines.append(f"- Frame Rate: {fps:.2f} fps")
813
+ summary_lines.append(f"- Ball Trajectory Points: {len([p for p in cleaned_path if len(p) > 0])}\n")
814
 
815
  for team_id in [0, 1]:
816
  if team_id not in teams:
 
831
  for pid in teams[team_id]]
832
  player_distances.sort(key=lambda x: x[1], reverse=True)
833
 
834
+ summary_lines.append(f"\n **Top 3 Performers:**")
835
  for i, (pid, dist) in enumerate(player_distances[:3], 1):
836
  stats = performance_tracker.get_player_stats(pid)
837
  summary_lines.append(
838
  f" {i}. Player #{pid}: {dist:.1f}m, "
839
+ f"Avg: {stats['avg_velocity']/100:.2f}m/s, "
840
+ f"Max: {stats['max_velocity']/100:.2f}m/s"
841
  )
842
 
843
+ summary_lines.append("\n**Pipeline Steps Completed:**")
844
+ summary_lines.append("โœ… 1. Player crop collection")
845
+ summary_lines.append("โœ… 2. Team classifier training")
846
+ summary_lines.append("โœ… 3. Video processing with tracking")
847
+ summary_lines.append("โœ… 4. Ball trajectory cleaning")
848
+ summary_lines.append("โœ… 5. Performance analytics generation")
849
+ summary_lines.append("โœ… 6. Visualization creation")
850
+
851
  summary_msg = "\n".join(summary_lines)
852
 
853
+ progress(1.0, desc="โœ… Analysis Complete!")
854
 
855
  return (output_path, comparison_fig, team_heatmaps_path,
856
  individual_heatmaps_path, radar_path, summary_msg)
 
866
  # ==============================================
867
  # GRADIO INTERFACE
868
  # ==============================================
869
+ with gr.Blocks(title="โšฝ Football Performance Analyzer", theme=gr.themes.Soft()) as iface:
870
  gr.Markdown("""
871
  # โšฝ Advanced Football Video Analyzer
872
+ ### Complete Pipeline Implementation
873
+
874
+ This application follows the exact pipeline from the Colab notebook:
875
+ 1. **Player Detection** - Collect player crops using Roboflow
876
+ 2. **Team Classification** - Train SigLIP-based team classifier
877
+ 3. **Persistent Tracking** - ByteTrack with stable ID assignment
878
+ 4. **Field Transformation** - Project players onto pitch coordinates
879
+ 5. **Ball Trajectory** - Track and clean ball path with outlier removal
880
+ 6. **Performance Analytics** - Generate heatmaps and statistics
881
+
882
+ Upload a football match video to get comprehensive performance analytics!
883
  """)
884
 
885
  with gr.Row():
886
+ video_input = gr.Video(label="๐Ÿ“ค Upload Football Video")
887
 
888
+ analyze_btn = gr.Button("๐Ÿš€ Start Analysis Pipeline", variant="primary", size="lg")
889
 
890
  with gr.Row():
891
+ status_output = gr.Textbox(label="๐Ÿ“Š Analysis Summary & Statistics", lines=25)
892
 
893
  with gr.Tabs():
894
  with gr.Tab("๐Ÿ“น Annotated Video"):
895
+ gr.Markdown("### Full video with player tracking, team colors, and ball detection")
896
+ video_output = gr.Video(label="Processed Video")
897
 
898
  with gr.Tab("๐Ÿ“Š Performance Comparison"):
899
+ gr.Markdown("### Interactive charts comparing player performance metrics")
900
  comparison_output = gr.Plot(label="Team Performance Metrics")
901
 
902
  with gr.Tab("๐Ÿ—บ๏ธ Team Heatmaps"):
903
+ gr.Markdown("### Combined activity heatmaps showing team positioning")
904
+ team_heatmaps_output = gr.Image(label="Team Activity Heatmaps")
905
 
906
  with gr.Tab("๐Ÿ‘ค Individual Heatmaps"):
907
+ gr.Markdown("### Top 6 players with detailed activity analysis")
908
+ individual_heatmaps_output = gr.Image(label="Top Players Heatmaps")
909
 
910
  with gr.Tab("๐ŸŽฎ Game Radar View"):
911
+ gr.Markdown("### Game-style tactical view with ball trail")
912
+ radar_output = gr.Image(label="Tactical Radar View")
913
 
914
  analyze_btn.click(
915
  fn=analyze_football_video,
 
926
 
927
  gr.Markdown("""
928
  ---
929
+ ### ๐Ÿ”ง Technical Details:
930
+
931
+ **Detection Models:**
932
+ - Player/Ball/Referee Detection: `football-players-detection-3zvbc/11`
933
+ - Field Keypoint Detection: `football-field-detection-f07vi/14`
934
+
935
+ **Tracking & Classification:**
936
+ - ByteTrack for persistent player IDs (60-frame buffer)
937
+ - SigLIP embeddings for team classification
938
+ - Majority voting for stable team assignments
939
+
940
+ **Performance Metrics:**
941
+ - Distance covered (meters)
942
+ - Average & maximum speed (m/s)
943
+ - Zone activity (defensive/middle/attacking thirds)
944
+ - Position heatmaps with Gaussian smoothing
945
+
946
+ **Ball Tracking:**
947
+ - Field homography transformation
948
+ - Outlier removal (500cm threshold)
949
+ - Transformation matrix smoothing (5-frame window)
950
+
951
+ ### ๐Ÿ“ˆ Output Files:
952
+ - Annotated video: `/tmp/annotated_football.mp4`
953
+ - Team heatmaps: `/tmp/team_heatmaps.png`
954
+ - Individual heatmaps: `/tmp/individual_heatmaps.png`
955
+ - Radar view: `/tmp/radar_view_enhanced.png`
956
  """)
957
 
958
  if __name__ == "__main__":
959
+ iface.launch(share=True)