Update pipeline_full.py
Browse files- 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.
|
| 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 |
-
*
|
| 752 |
-
*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 772 |
events: List[Dict[str, Any]] = []
|
| 773 |
|
| 774 |
-
# last positions for speed / distance
|
| 775 |
-
|
| 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(
|
| 812 |
-
|
|
|
|
| 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(
|
| 851 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 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 |
-
|
| 865 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 866 |
|
| 867 |
if len(ball_dets) > 0:
|
| 868 |
-
frame_ball_xy_img = ball_dets.get_anchors_coordinates(
|
|
|
|
|
|
|
| 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 |
-
# ---
|
| 890 |
def register_event(ev: Dict[str, Any], text: str):
|
| 891 |
nonlocal current_event_text, event_text_frames_left
|
| 892 |
events.append(ev)
|
| 893 |
-
|
| 894 |
-
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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 |
-
|
| 926 |
-
|
| 927 |
-
|
| 928 |
-
|
| 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": {
|
|
|
|
|
|
|
|
|
|
| 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))
|
|
|
|
|
|
|
| 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 =
|
| 1039 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1040 |
|
| 1041 |
-
hud_text =
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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": {
|
| 1086 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
)
|