Simon9 commited on
Commit
086e40b
·
verified ·
1 Parent(s): 633d62b

Update pipeline_full.py

Browse files
Files changed (1) hide show
  1. pipeline_full.py +148 -44
pipeline_full.py CHANGED
@@ -390,6 +390,8 @@ def train_team_classifier_on_video(video_path: str, stride: int = 30) -> None:
390
 
391
 
392
  def resolve_goalkeepers_team_id(players: sv.Detections, goalkeepers: sv.Detections) -> np.ndarray:
 
 
393
  goalkeepers_xy = goalkeepers.get_anchors_coordinates(sv.Position.BOTTOM_CENTER)
394
  players_xy = players.get_anchors_coordinates(sv.Position.BOTTOM_CENTER)
395
  team_0_centroid = players_xy[players.class_id == 0].mean(axis=0)
@@ -739,7 +741,7 @@ def step_ball_path(video_path: str, out_dir: str) -> Dict[str, Any]:
739
  }
740
 
741
 
742
- # -------------------- 8. NEW: full-match analysis + event-annotated video --------------------
743
 
744
 
745
  def step_analyze_and_annotate_video(video_path: str, out_dir: str) -> Dict[str, Any]:
@@ -748,15 +750,23 @@ def step_analyze_and_annotate_video(video_path: str, out_dir: str) -> Dict[str,
748
  * tracks players & ball
749
  * computes distance & speed per player (pitch coordinates)
750
  * estimates ball possession per team & per player
751
- * detects simple events (pass, tackle/interception, clearance, shot)
752
- * renders an annotated MP4 with overlays
 
 
 
 
 
 
 
 
753
  """
754
  ensure_models_loaded()
755
  os.makedirs(out_dir, exist_ok=True)
756
 
757
  video_info = sv.VideoInfo.from_video_path(video_path)
758
  fps = video_info.fps
759
- dt = 1.0 / fps
760
 
761
  tracker = sv.ByteTrack()
762
  tracker.reset()
@@ -768,11 +778,30 @@ def step_analyze_and_annotate_video(video_path: str, out_dir: str) -> Dict[str,
768
  distance_covered_m = defaultdict(float) # tid -> meters
769
  possession_time_player = defaultdict(float) # tid -> seconds
770
  possession_time_team = defaultdict(float) # team_id -> seconds
771
- team_of_player = {} # tid -> team_id
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
772
  events: List[Dict[str, Any]] = []
773
 
774
- # last positions for speed / distance
775
- last_pitch_pos: Dict[int, np.ndarray] = {}
776
  prev_owner_tid: Optional[int] = None
777
  prev_ball_pos_pitch: Optional[np.ndarray] = None
778
 
@@ -808,8 +837,9 @@ def step_analyze_and_annotate_video(video_path: str, out_dir: str) -> Dict[str,
808
  frame_generator = sv.get_video_frames_generator(video_path)
809
 
810
  with sink:
811
- for frame_idx, frame in enumerate(tqdm(frame_generator, total=video_info.total_frames,
812
- desc="analyze + annotate")):
 
813
  t = frame_idx * dt
814
 
815
  # --- detections + tracking ---
@@ -834,6 +864,14 @@ def step_analyze_and_annotate_video(video_path: str, out_dir: str) -> Dict[str,
834
  frame_ref = key_points.xy[0][filt]
835
  pitch_ref = np.array(PITCH_CONFIG.vertices)[filt]
836
 
 
 
 
 
 
 
 
 
837
  transformer = ViewTransformer(source=frame_ref, target=pitch_ref)
838
  Ms.append(transformer.m)
839
  transformer.m = np.mean(np.array(Ms), axis=0)
@@ -841,31 +879,55 @@ def step_analyze_and_annotate_video(video_path: str, out_dir: str) -> Dict[str,
841
  # --- team classification & pitch positions ---
842
  frame_players_xy_pitch = None
843
  frame_ball_pos_pitch = None
 
 
844
 
845
  if len(players_dets) > 0:
846
  crops = [sv.crop_image(frame, xyxy) for xyxy in players_dets.xyxy]
847
  team_preds = TEAM_CLASSIFIER.predict(crops)
848
  players_dets.class_id = team_preds # now class_id = team_id (0/1)
849
 
850
- frame_players_xy_img = players_dets.get_anchors_coordinates(sv.Position.BOTTOM_CENTER)
851
- frame_players_xy_pitch = transformer.transform_points(points=frame_players_xy_img)
 
 
 
 
 
 
852
 
853
  for tid, team_id, pos_pitch in zip(
854
  players_dets.tracker_id, players_dets.class_id, frame_players_xy_pitch
855
  ):
856
  tid_int = int(tid)
857
  team_of_player[tid_int] = int(team_id)
 
858
 
859
- prev_pos = last_pitch_pos.get(tid_int)
860
  speed_kmh = 0.0
861
  if prev_pos is not None:
862
  dist_m = float(np.linalg.norm(pos_pitch - prev_pos))
863
  distance_covered_m[tid_int] += dist_m
864
- speed_kmh = (dist_m / dt) * 3.6 # m/s -> km/h
865
- last_pitch_pos[tid_int] = pos_pitch
 
 
 
 
 
 
 
 
 
 
 
 
 
866
 
867
  if len(ball_dets) > 0:
868
- frame_ball_xy_img = ball_dets.get_anchors_coordinates(sv.Position.BOTTOM_CENTER)
 
 
869
  frame_ball_xy_pitch = transformer.transform_points(points=frame_ball_xy_img)
870
  frame_ball_pos_pitch = frame_ball_xy_pitch[0]
871
 
@@ -886,22 +948,28 @@ def step_analyze_and_annotate_video(video_path: str, out_dir: str) -> Dict[str,
886
  if owner_team is not None:
887
  possession_time_team[owner_team] += dt
888
 
889
- # --- event detection (simple heuristics) ---
890
  def register_event(ev: Dict[str, Any], text: str):
891
  nonlocal current_event_text, event_text_frames_left
892
  events.append(ev)
893
- current_event_text = text
894
- event_text_frames_left = EVENT_TEXT_DURATION_FRAMES
 
895
 
896
- # possession change events, passes, tackles, interceptions
897
  if owner_tid != prev_owner_tid:
 
 
 
898
  if owner_tid is not None and prev_owner_tid is not None:
899
  prev_team = team_of_player.get(prev_owner_tid)
900
  cur_team = team_of_player.get(owner_tid)
901
 
902
  travel_m = 0.0
903
  if prev_ball_pos_pitch is not None and frame_ball_pos_pitch is not None:
904
- travel_m = float(np.linalg.norm(frame_ball_pos_pitch - prev_ball_pos_pitch))
 
 
905
 
906
  MIN_PASS_TRAVEL_M = 3.0
907
 
@@ -919,15 +987,15 @@ def step_analyze_and_annotate_video(video_path: str, out_dir: str) -> Dict[str,
919
  },
920
  f"Pass: #{prev_owner_tid} → #{owner_tid} (Team {cur_team})",
921
  )
 
 
922
  elif prev_team != cur_team:
923
  # tackle vs interception
924
  d_pp = 999.0
925
- if frame_players_xy_pitch is not None:
926
- # get current positions
927
- pos_prev = last_pitch_pos.get(int(prev_owner_tid))
928
- pos_cur = last_pitch_pos.get(int(owner_tid))
929
- if pos_prev is not None and pos_cur is not None:
930
- d_pp = float(np.linalg.norm(pos_prev - pos_cur))
931
  ev_type = "tackle" if d_pp < 3.0 else "interception"
932
  label = "Tackle" if ev_type == "tackle" else "Interception"
933
  register_event(
@@ -937,10 +1005,17 @@ def step_analyze_and_annotate_video(video_path: str, out_dir: str) -> Dict[str,
937
  "from_tid": int(prev_owner_tid),
938
  "to_tid": int(owner_tid),
939
  "team_id": int(cur_team),
940
- "extra": {"player_distance_m": d_pp, "ball_travel_m": travel_m},
 
 
 
941
  },
942
  f"{label}: #{owner_tid} wins ball from #{prev_owner_tid}",
943
  )
 
 
 
 
944
 
945
  # generic possession-change event
946
  register_event(
@@ -949,13 +1024,15 @@ def step_analyze_and_annotate_video(video_path: str, out_dir: str) -> Dict[str,
949
  "t": float(t),
950
  "from_tid": int(prev_owner_tid) if prev_owner_tid is not None else None,
951
  "to_tid": int(owner_tid) if owner_tid is not None else None,
952
- "team_id": int(team_of_player.get(owner_tid)) if owner_tid is not None else None,
 
 
953
  "extra": {},
954
  },
955
  "" if owner_tid is None else f"Team {team_of_player.get(owner_tid)} in possession",
956
  )
957
 
958
- # shot / clearance based on ball speed & direction
959
  if (
960
  prev_ball_pos_pitch is not None
961
  and frame_ball_pos_pitch is not None
@@ -964,7 +1041,7 @@ def step_analyze_and_annotate_video(video_path: str, out_dir: str) -> Dict[str,
964
  v = (frame_ball_pos_pitch - prev_ball_pos_pitch) / dt # m/s
965
  speed_mps = float(np.linalg.norm(v))
966
  speed_kmh = speed_mps * 3.6
967
- HIGH_SPEED_KMH = 18.0
968
 
969
  if speed_kmh > HIGH_SPEED_KMH:
970
  shooter_team = team_of_player.get(owner_tid)
@@ -988,6 +1065,7 @@ def step_analyze_and_annotate_video(video_path: str, out_dir: str) -> Dict[str,
988
  },
989
  f"Shot by #{owner_tid} (Team {shooter_team}) – {speed_kmh:.1f} km/h",
990
  )
 
991
  else:
992
  register_event(
993
  {
@@ -1000,25 +1078,23 @@ def step_analyze_and_annotate_video(video_path: str, out_dir: str) -> Dict[str,
1000
  },
1001
  f"Clearance by #{owner_tid} (Team {shooter_team})",
1002
  )
 
1003
 
1004
  prev_owner_tid = owner_tid
1005
  prev_ball_pos_pitch = frame_ball_pos_pitch
 
1006
 
1007
  # --- frame drawing ---
1008
  annotated = frame.copy()
1009
 
1010
- # build labels for players: id + speed + distance
1011
  player_labels: List[str] = []
1012
  if frame_players_xy_pitch is not None and len(players_dets) > 0:
1013
  for tid, pos_pitch in zip(players_dets.tracker_id, frame_players_xy_pitch):
1014
  tid_int = int(tid)
1015
- prev_pos = last_pitch_pos.get(tid_int)
1016
- speed_kmh = 0.0
1017
- if prev_pos is not None:
1018
- dist_m = float(np.linalg.norm(pos_pitch - prev_pos))
1019
- speed_kmh = (dist_m / dt) * 3.6
1020
- d_total = distance_covered_m[tid_int]
1021
  team_id = team_of_player.get(tid_int, -1)
 
 
1022
  player_labels.append(
1023
  f"#{tid_int} T{team_id} {speed_kmh:4.1f} km/h {d_total:.1f} m"
1024
  )
@@ -1035,10 +1111,21 @@ def step_analyze_and_annotate_video(video_path: str, out_dir: str) -> Dict[str,
1035
 
1036
  # --- HUD: possession percentages ---
1037
  total_poss_time = sum(possession_time_team.values()) + 1e-6
1038
- team0_pct = 100.0 * possession_time_team.get(0, 0.0) / total_poss_time
1039
- team1_pct = 100.0 * possession_time_team.get(1, 0.0) / total_poss_time
 
 
 
 
 
 
 
 
1040
 
1041
- hud_text = f"Team 0 Ball Control: {team0_pct:5.2f}% Team 1 Ball Control: {team1_pct:5.2f}%"
 
 
 
1042
  cv2.rectangle(
1043
  annotated,
1044
  (20, annotated.shape[0] - 60),
@@ -1059,7 +1146,9 @@ def step_analyze_and_annotate_video(video_path: str, out_dir: str) -> Dict[str,
1059
 
1060
  # --- event banner ---
1061
  if event_text_frames_left > 0 and current_event_text:
1062
- cv2.rectangle(annotated, (20, 20), (annotated.shape[1] - 20, 90), (255, 255, 255), -1)
 
 
1063
  cv2.putText(
1064
  annotated,
1065
  current_event_text,
@@ -1082,10 +1171,21 @@ def step_analyze_and_annotate_video(video_path: str, out_dir: str) -> Dict[str,
1082
 
1083
  stats = {
1084
  "distance_covered_m": {str(tid): float(d) for tid, d in distance_covered_m.items()},
1085
- "possession_time_player_s": {str(tid): float(t_sec) for tid, t_sec in possession_time_player.items()},
1086
- "possession_time_team_s": {int(team): float(t_sec) for team, t_sec in possession_time_team.items()},
 
 
 
 
1087
  "possession_percent_team": possession_percent_team,
1088
  "team_of_player": {str(tid): int(team) for tid, team in team_of_player.items()},
 
 
 
 
 
 
 
1089
  }
1090
 
1091
  return {
@@ -1124,7 +1224,11 @@ def run_full_pipeline(video_path: str, job_dir: str) -> Dict[str, Any]:
1124
  update_progress("ball_path", 0.60, "Computing ball path and heatmap...")
1125
  ball_paths = step_ball_path(video_path, os.path.join(job_dir, "ball_path"))
1126
 
1127
- update_progress("events_video", 0.80, "Analyzing match and rendering event-annotated video...")
 
 
 
 
1128
  analysis_out = step_analyze_and_annotate_video(
1129
  video_path, os.path.join(job_dir, "analysis")
1130
  )
 
390
 
391
 
392
  def resolve_goalkeepers_team_id(players: sv.Detections, goalkeepers: sv.Detections) -> np.ndarray:
393
+ if len(goalkeepers) == 0 or len(players) == 0:
394
+ return np.array([])
395
  goalkeepers_xy = goalkeepers.get_anchors_coordinates(sv.Position.BOTTOM_CENTER)
396
  players_xy = players.get_anchors_coordinates(sv.Position.BOTTOM_CENTER)
397
  team_0_centroid = players_xy[players.class_id == 0].mean(axis=0)
 
741
  }
742
 
743
 
744
+ # -------------------- 8. full-match analysis + event-annotated video --------------------
745
 
746
 
747
  def step_analyze_and_annotate_video(video_path: str, out_dir: str) -> Dict[str, Any]:
 
750
  * tracks players & ball
751
  * computes distance & speed per player (pitch coordinates)
752
  * estimates ball possession per team & per player
753
+ * estimates time spent in defensive/middle/attacking thirds
754
+ * detects simple events:
755
+ - passes (successful between teammates)
756
+ - tackles / interceptions (winning ball from opponent)
757
+ - clearances
758
+ - shots (high-speed ball towards goal)
759
+ * renders an annotated MP4 with overlays:
760
+ - per-player labels: id, team, speed, distance
761
+ - possession HUD per team
762
+ - event banners
763
  """
764
  ensure_models_loaded()
765
  os.makedirs(out_dir, exist_ok=True)
766
 
767
  video_info = sv.VideoInfo.from_video_path(video_path)
768
  fps = video_info.fps
769
+ dt = 1.0 / max(fps, 1.0)
770
 
771
  tracker = sv.ByteTrack()
772
  tracker.reset()
 
778
  distance_covered_m = defaultdict(float) # tid -> meters
779
  possession_time_player = defaultdict(float) # tid -> seconds
780
  possession_time_team = defaultdict(float) # team_id -> seconds
781
+ team_of_player: Dict[int, int] = {} # tid -> team_id
782
+
783
+ # per-player richer stats for coaches
784
+ player_stats: Dict[int, Dict[str, Any]] = defaultdict(
785
+ lambda: {
786
+ "distance_m": 0.0,
787
+ "max_speed_kmh": 0.0,
788
+ "time_def_third_s": 0.0,
789
+ "time_mid_third_s": 0.0,
790
+ "time_att_third_s": 0.0,
791
+ "touches": 0,
792
+ "successful_passes": 0,
793
+ "received_passes": 0,
794
+ "shots": 0,
795
+ "tackles": 0,
796
+ "interceptions": 0,
797
+ "clearances": 0,
798
+ }
799
+ )
800
+
801
  events: List[Dict[str, Any]] = []
802
 
803
+ # last positions for speed / distance (per frame)
804
+ prev_positions: Dict[int, np.ndarray] = {}
805
  prev_owner_tid: Optional[int] = None
806
  prev_ball_pos_pitch: Optional[np.ndarray] = None
807
 
 
837
  frame_generator = sv.get_video_frames_generator(video_path)
838
 
839
  with sink:
840
+ for frame_idx, frame in enumerate(
841
+ tqdm(frame_generator, total=video_info.total_frames, desc="analyze + annotate")
842
+ ):
843
  t = frame_idx * dt
844
 
845
  # --- detections + tracking ---
 
864
  frame_ref = key_points.xy[0][filt]
865
  pitch_ref = np.array(PITCH_CONFIG.vertices)[filt]
866
 
867
+ if len(frame_ref) < 4:
868
+ # Not enough field points: just draw detections and skip advanced stats
869
+ annotated = frame.copy()
870
+ annotated = ellipse_annotator.annotate(scene=annotated, detections=players_dets)
871
+ annotated = triangle_annotator.annotate(scene=annotated, detections=ball_dets)
872
+ sink.write_frame(annotated)
873
+ continue
874
+
875
  transformer = ViewTransformer(source=frame_ref, target=pitch_ref)
876
  Ms.append(transformer.m)
877
  transformer.m = np.mean(np.array(Ms), axis=0)
 
879
  # --- team classification & pitch positions ---
880
  frame_players_xy_pitch = None
881
  frame_ball_pos_pitch = None
882
+ current_positions: Dict[int, np.ndarray] = {}
883
+ current_speed_kmh: Dict[int, float] = {}
884
 
885
  if len(players_dets) > 0:
886
  crops = [sv.crop_image(frame, xyxy) for xyxy in players_dets.xyxy]
887
  team_preds = TEAM_CLASSIFIER.predict(crops)
888
  players_dets.class_id = team_preds # now class_id = team_id (0/1)
889
 
890
+ frame_players_xy_img = players_dets.get_anchors_coordinates(
891
+ sv.Position.BOTTOM_CENTER
892
+ )
893
+ frame_players_xy_pitch = transformer.transform_points(
894
+ points=frame_players_xy_img
895
+ )
896
+
897
+ pitch_length = PITCH_CONFIG.length
898
 
899
  for tid, team_id, pos_pitch in zip(
900
  players_dets.tracker_id, players_dets.class_id, frame_players_xy_pitch
901
  ):
902
  tid_int = int(tid)
903
  team_of_player[tid_int] = int(team_id)
904
+ current_positions[tid_int] = pos_pitch
905
 
906
+ prev_pos = prev_positions.get(tid_int)
907
  speed_kmh = 0.0
908
  if prev_pos is not None:
909
  dist_m = float(np.linalg.norm(pos_pitch - prev_pos))
910
  distance_covered_m[tid_int] += dist_m
911
+ player_stats[tid_int]["distance_m"] += dist_m
912
+ speed_kmh = (dist_m / dt) * 3.6
913
+ player_stats[tid_int]["max_speed_kmh"] = max(
914
+ player_stats[tid_int]["max_speed_kmh"], speed_kmh
915
+ )
916
+ current_speed_kmh[tid_int] = speed_kmh
917
+
918
+ # zone times: defensive / middle / attacking thirds
919
+ x_pos = pos_pitch[0]
920
+ if x_pos < pitch_length / 3.0:
921
+ player_stats[tid_int]["time_def_third_s"] += dt
922
+ elif x_pos < 2.0 * pitch_length / 3.0:
923
+ player_stats[tid_int]["time_mid_third_s"] += dt
924
+ else:
925
+ player_stats[tid_int]["time_att_third_s"] += dt
926
 
927
  if len(ball_dets) > 0:
928
+ frame_ball_xy_img = ball_dets.get_anchors_coordinates(
929
+ sv.Position.BOTTOM_CENTER
930
+ )
931
  frame_ball_xy_pitch = transformer.transform_points(points=frame_ball_xy_img)
932
  frame_ball_pos_pitch = frame_ball_xy_pitch[0]
933
 
 
948
  if owner_team is not None:
949
  possession_time_team[owner_team] += dt
950
 
951
+ # --- helper to register events & banner text ---
952
  def register_event(ev: Dict[str, Any], text: str):
953
  nonlocal current_event_text, event_text_frames_left
954
  events.append(ev)
955
+ if text:
956
+ current_event_text = text
957
+ event_text_frames_left = EVENT_TEXT_DURATION_FRAMES
958
 
959
+ # --- possession change events, passes, tackles, interceptions ---
960
  if owner_tid != prev_owner_tid:
961
+ if owner_tid is not None:
962
+ player_stats[owner_tid]["touches"] += 1
963
+
964
  if owner_tid is not None and prev_owner_tid is not None:
965
  prev_team = team_of_player.get(prev_owner_tid)
966
  cur_team = team_of_player.get(owner_tid)
967
 
968
  travel_m = 0.0
969
  if prev_ball_pos_pitch is not None and frame_ball_pos_pitch is not None:
970
+ travel_m = float(
971
+ np.linalg.norm(frame_ball_pos_pitch - prev_ball_pos_pitch)
972
+ )
973
 
974
  MIN_PASS_TRAVEL_M = 3.0
975
 
 
987
  },
988
  f"Pass: #{prev_owner_tid} → #{owner_tid} (Team {cur_team})",
989
  )
990
+ player_stats[prev_owner_tid]["successful_passes"] += 1
991
+ player_stats[owner_tid]["received_passes"] += 1
992
  elif prev_team != cur_team:
993
  # tackle vs interception
994
  d_pp = 999.0
995
+ pos_prev = prev_positions.get(int(prev_owner_tid))
996
+ pos_cur = current_positions.get(int(owner_tid))
997
+ if pos_prev is not None and pos_cur is not None:
998
+ d_pp = float(np.linalg.norm(pos_prev - pos_cur))
 
 
999
  ev_type = "tackle" if d_pp < 3.0 else "interception"
1000
  label = "Tackle" if ev_type == "tackle" else "Interception"
1001
  register_event(
 
1005
  "from_tid": int(prev_owner_tid),
1006
  "to_tid": int(owner_tid),
1007
  "team_id": int(cur_team),
1008
+ "extra": {
1009
+ "player_distance_m": d_pp,
1010
+ "ball_travel_m": travel_m,
1011
+ },
1012
  },
1013
  f"{label}: #{owner_tid} wins ball from #{prev_owner_tid}",
1014
  )
1015
+ if ev_type == "tackle":
1016
+ player_stats[owner_tid]["tackles"] += 1
1017
+ else:
1018
+ player_stats[owner_tid]["interceptions"] += 1
1019
 
1020
  # generic possession-change event
1021
  register_event(
 
1024
  "t": float(t),
1025
  "from_tid": int(prev_owner_tid) if prev_owner_tid is not None else None,
1026
  "to_tid": int(owner_tid) if owner_tid is not None else None,
1027
+ "team_id": int(team_of_player.get(owner_tid))
1028
+ if owner_tid is not None
1029
+ else None,
1030
  "extra": {},
1031
  },
1032
  "" if owner_tid is None else f"Team {team_of_player.get(owner_tid)} in possession",
1033
  )
1034
 
1035
+ # --- shot / clearance based on ball speed & direction ---
1036
  if (
1037
  prev_ball_pos_pitch is not None
1038
  and frame_ball_pos_pitch is not None
 
1041
  v = (frame_ball_pos_pitch - prev_ball_pos_pitch) / dt # m/s
1042
  speed_mps = float(np.linalg.norm(v))
1043
  speed_kmh = speed_mps * 3.6
1044
+ HIGH_SPEED_KMH = 18.0 # threshold for "hard" actions
1045
 
1046
  if speed_kmh > HIGH_SPEED_KMH:
1047
  shooter_team = team_of_player.get(owner_tid)
 
1065
  },
1066
  f"Shot by #{owner_tid} (Team {shooter_team}) – {speed_kmh:.1f} km/h",
1067
  )
1068
+ player_stats[owner_tid]["shots"] += 1
1069
  else:
1070
  register_event(
1071
  {
 
1078
  },
1079
  f"Clearance by #{owner_tid} (Team {shooter_team})",
1080
  )
1081
+ player_stats[owner_tid]["clearances"] += 1
1082
 
1083
  prev_owner_tid = owner_tid
1084
  prev_ball_pos_pitch = frame_ball_pos_pitch
1085
+ prev_positions = current_positions
1086
 
1087
  # --- frame drawing ---
1088
  annotated = frame.copy()
1089
 
1090
+ # build labels for players: id + team + current speed + total distance
1091
  player_labels: List[str] = []
1092
  if frame_players_xy_pitch is not None and len(players_dets) > 0:
1093
  for tid, pos_pitch in zip(players_dets.tracker_id, frame_players_xy_pitch):
1094
  tid_int = int(tid)
 
 
 
 
 
 
1095
  team_id = team_of_player.get(tid_int, -1)
1096
+ speed_kmh = current_speed_kmh.get(tid_int, 0.0)
1097
+ d_total = distance_covered_m[tid_int]
1098
  player_labels.append(
1099
  f"#{tid_int} T{team_id} {speed_kmh:4.1f} km/h {d_total:.1f} m"
1100
  )
 
1111
 
1112
  # --- HUD: possession percentages ---
1113
  total_poss_time = sum(possession_time_team.values()) + 1e-6
1114
+ team0_pct = (
1115
+ 100.0 * possession_time_team.get(0, 0.0) / total_poss_time
1116
+ if total_poss_time > 0
1117
+ else 0.0
1118
+ )
1119
+ team1_pct = (
1120
+ 100.0 * possession_time_team.get(1, 0.0) / total_poss_time
1121
+ if total_poss_time > 0
1122
+ else 0.0
1123
+ )
1124
 
1125
+ hud_text = (
1126
+ f"Team 0 Ball Control: {team0_pct:5.2f}% "
1127
+ f"Team 1 Ball Control: {team1_pct:5.2f}%"
1128
+ )
1129
  cv2.rectangle(
1130
  annotated,
1131
  (20, annotated.shape[0] - 60),
 
1146
 
1147
  # --- event banner ---
1148
  if event_text_frames_left > 0 and current_event_text:
1149
+ cv2.rectangle(
1150
+ annotated, (20, 20), (annotated.shape[1] - 20, 90), (255, 255, 255), -1
1151
+ )
1152
  cv2.putText(
1153
  annotated,
1154
  current_event_text,
 
1171
 
1172
  stats = {
1173
  "distance_covered_m": {str(tid): float(d) for tid, d in distance_covered_m.items()},
1174
+ "possession_time_player_s": {
1175
+ str(tid): float(t_sec) for tid, t_sec in possession_time_player.items()
1176
+ },
1177
+ "possession_time_team_s": {
1178
+ int(team): float(t_sec) for team, t_sec in possession_time_team.items()
1179
+ },
1180
  "possession_percent_team": possession_percent_team,
1181
  "team_of_player": {str(tid): int(team) for tid, team in team_of_player.items()},
1182
+ "player_stats": {
1183
+ str(tid): {
1184
+ k: float(v) if isinstance(v, (int, float)) else v
1185
+ for k, v in stats_dict.items()
1186
+ }
1187
+ for tid, stats_dict in player_stats.items()
1188
+ },
1189
  }
1190
 
1191
  return {
 
1224
  update_progress("ball_path", 0.60, "Computing ball path and heatmap...")
1225
  ball_paths = step_ball_path(video_path, os.path.join(job_dir, "ball_path"))
1226
 
1227
+ update_progress(
1228
+ "events_video",
1229
+ 0.80,
1230
+ "Analyzing match, computing speed/distance, and rendering event-annotated video...",
1231
+ )
1232
  analysis_out = step_analyze_and_annotate_video(
1233
  video_path, os.path.join(job_dir, "analysis")
1234
  )