feat(soccer): player motion analytics — speed, distance, direction, and run-all - #58
feat(soccer): player motion analytics — speed, distance, direction, and run-all#58SkalskiP wants to merge 50 commits into
Conversation
Add analytics/ package with CLI entry point, shared support utilities (tracker factory, Kalman velocity helpers, detector factories, annotation helpers), and DIRECTION mode: BoTSORT tracking + team-colored ground ellipses + image-space Kalman joystick direction dots, no homography. requirements.txt already contains trackers and inference>=0.45.0. Co-authored-by: Cursor <cursoragent@cursor.com>
Add analytics/homography.py (RansacViewTransformer, PitchHomographyTracker, replay_tracker_transforms, MetricContext, ensure_pitch_homography_maps, homography_from_keypoints_radar, keypoints_from_inference_field — in-memory only, no disk cache) and analytics/speed.py: SPEED mode that builds a MetricContext, computes per-frame Kalman ground speed via the gap-filled H map, and renders m/s + km/h badges + radar minimap per player. Co-authored-by: Cursor <cursoragent@cursor.com>
Extend support.py with PlayerTrack, collect_tracks, compute_kinematics (homography mode — smoothed pitch trajectory integration, gated H only), and cumulative_distance_at_frame. Add analytics/distance.py: DISTANCE mode runs a two-pass pipeline (collect tracks → compute_kinematics → render per-player cumulative-distance labels) and appends a 3-second distance-ranking leaderboard end-card. Co-authored-by: Cursor <cursoragent@cursor.com>
…races Add analytics/player_focus.py: PLAYER_FOCUS mode. Default (no --track-id): annotate all players, per-tracker-id hashed-color radar traces on minimap, full-frame ellipses + joystick dots. With --track-id N: spotlight that player (Gaussian dim), HUD showing live Kalman ground speed (gap-filled H) + cumulative kinematics distance, radar minimap with that player's trace highlighted. Co-authored-by: Cursor <cursoragent@cursor.com>
analytics/main.py only puts examples/soccer on sys.path, but the sports library is not pip-installed and lives at the repo root. Add the repo root to sys.path in analytics/__init__.py so the analytics features can import sports regardless of the current working directory. Co-authored-by: Cursor <cursoragent@cursor.com>
Add an additive, optional goalkeeper team-assignment path ported and adapted
from world_cup_projects (common/teams.py + common/pitch.py) into a new
analytics/goalkeepers.py:
- infer_goal_defenders: defensive-block (3 most defensive players per team)
goal-side handshake instead of whole-team centroids
- resolve_goalkeepers_team_by_goal: GK -> team defending the nearer goal mouth
- apply_goalkeeper_teams_by_goal: per-frame GK team from pitch distance to goals
- warmup_goal_defenders_radar + stabilize_goalkeeper_teams: clip-level lock of
each GK tracklet to one defending team (sports-radar pitch space)
- compute_goalkeeper_lock / apply_goalkeeper_frame: wiring helpers
New CLI flag in analytics/main.py: --gk-assignment {goal_distance,centroid},
default goal_distance. SPEED / DISTANCE / PLAYER_FOCUS use the goal-distance
path (per-frame assignment + clip lock) when selected, and fall back to the
existing centroid resolve_goalkeepers_team_id otherwise. DIRECTION has no
homography by design, so it always uses the centroid rule. Existing repo
behavior (centroid) is unchanged and remains available as the fallback.
Co-authored-by: Cursor <cursoragent@cursor.com>
…pass Add lock_teams_by_tracklet_majority (ported from world_cup_projects/common/ teams.py) and a single detect->track->classify pass (compute_clip_locks) that returns the per-tracklet team lock, the goalkeeper lock, and the locked goal defenders, replacing the GK-only compute_goalkeeper_lock. Co-authored-by: Cursor <cursoragent@cursor.com>
…e, team-colored direction dot Port from world_cup_projects: 5-frame moving-average on image feet before warping to pitch (HOMOGRAPHY_XY_SMOOTH/_smooth_xy); defended goal-mouth shading on the radar (draw_goals_on_pitch); radial m/s speed badge chip + legend (draw_speed_badge/draw_speed_legend) replacing the raw putText label; and team-colored joystick dots with an optional show_ids switch for ellipses. Co-authored-by: Cursor <cursoragent@cursor.com>
…on modes Wire compute_clip_locks into direction/speed/distance/player_focus: apply the clip-level team-id lock before GK assignment, draw radial speed badges + legend (speed, player_focus follow-all), shade defended goals on the radar/trace minimaps, and render team-colored direction dots with no track-id numbers in DIRECTION. Co-authored-by: Cursor <cursoragent@cursor.com>
…entry Add DIRECTION/SPEED/DISTANCE/PLAYER_FOCUS to main.py's Mode enum and dispatch them to the analytics/ helper modules, move the analytics CLI flags into main.py's argparse (existing six modes and their defaults unchanged), add a repo-root bootstrap so 'import sports' resolves, and delete the separate analytics/main.py entry point. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…ed pitch The gated pitch homography picked plain-vs-mirror by keypoint reprojection error alone, which on a near-symmetric pitch could lock onto the mirrored fit and corrupt the radar, goalkeeper goal-distance assignment, and distance. Add a layout score (valid_pitch_cm + _players_on_pitch_score + _score_homography_ candidate) that prefers the orientation placing players in-bounds with the teams separated, thread optional per-frame player detections through _fit_frame, PitchHomographyTracker.update, ensure_pitch_homography_maps, and replay_tracker_transforms, and detect players inline in the homography pass. Co-authored-by: Cursor <cursoragent@cursor.com>
The minimap mixed homography sources (gated radar H for live dots, ungated keypoint H for traces) and could draw a mirrored gated H. Make the keypoint-radar H the single source of truth for both traces and live dots in speed and player_focus (gated radar H only as a fallback), drop the dead 'or True' guard, filter obviously off-pitch warps, and feed per-frame player detections into the homography pass so its gated orientation is layout-aware too. Co-authored-by: Cursor <cursoragent@cursor.com>
…asses Adds analytics/cache.py (FrameCache + build_or_load_detections/keypoints) and a build_metric_from_maps helper so the per-frame player detections and pitch keypoints are computed once and reused across the homography, clip-lock, first, and render passes (and across reruns), instead of re-detecting every pass. Co-authored-by: Cursor <cursoragent@cursor.com>
…potlight Tracker ids are minted from a class-level counter shared across tracker instances, so the goalkeeper-lock, first, and render passes produced disjoint id spaces; the PLAYER_FOCUS --track-id spotlight could never match and per-tracklet locks were effectively no-ops. Reset the counter in create_player_tracker so every pass starts ids from 0, and in PLAYER_FOCUS read Kalman velocity from the already-advanced render tracker instead of running a second tracker.update that re-fragmented ids. Co-authored-by: Cursor <cursoragent@cursor.com>
Pass a zero-arg detector factory into build_or_load_detections/keypoints so the detector model is only loaded on a cache miss. On a cache hit the run serves detections and pitch keypoints straight from disk and never initializes the player/pitch models. Co-authored-by: Cursor <cursoragent@cursor.com>
The cache holds regenerable per-frame detections and pitch keypoints and must never be committed. Co-authored-by: Cursor <cursoragent@cursor.com>
The ground ellipse used half the bounding-box width and a full 360-degree ellipse, so players' markers rendered too small. Match sv.EllipseAnnotator geometry instead: semi-axes of the full box width and 0.35x that width with the open -45..235 degree arc, shared by every analytics mode. Co-authored-by: Cursor <cursoragent@cursor.com>
… ids, drop id labels In single-track mode only the focused player is now annotated (ellipse + speed badge); the rest stay dimmed by the spotlight with no markers of their own. Raw tracker-id numbers are no longer drawn in either focus mode, and the HUD names the subject as FOCUS PLAYER rather than exposing the id. Track ids are kept consistent across the goalkeeper-lock, first, and render passes (each pass uses a fresh tracker whose shared id counter is reset), so --track-id refers to one stable player for the whole clip. Co-authored-by: Cursor <cursoragent@cursor.com>
…goalkeepers Key the clip-level team lock on tracker_id rather than detected class so a track whose class flips between player and goalkeeper gets one combined team for the whole clip. Goalkeeper-detected frames now have their team resolved (goal-distance or centroid) before the majority vote, so they contribute votes too, and the unified lock is applied as the final team step in every analytics render (direction/speed/distance/player_focus) regardless of per-frame class. Co-authored-by: Cursor <cursoragent@cursor.com>
…onal track-id chips Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…raphy Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Alpha-blend the minimap panel over the game footage (cv2.addWeighted on the panel ROI) instead of pasting it opaque, so both the radar and the underlying play stay visible. Adds a shared RADAR_MINIMAP_ALPHA (~0.6) and an overlay_minimap helper used by the radar drawing for every demo that draws a minimap. Co-authored-by: Cursor <cursoragent@cursor.com>
…us-all look PLAYER_FOCUS now shows instant Kalman ground speed (the speed badge) and accumulated distance (cumulative_distance_at_frame) as on-player chips in both modes: follow-all annotates every tracked player, and the spotlight shows the chips for the focused player, replacing the old lateral HUD text panel (spotlight dim + radar minimap kept). Distance kinematics are computed for all tracked players so every player can show its accumulated distance. DISTANCE is restyled to the same follow-all look (all players + trace radar + speed/distance chips) while keeping its distance leaderboard end-card so it stays distinct from PLAYER_FOCUS follow-all. Factor the shared annotate-all-players plus trace-radar rendering into support.helpers (annotate_motion_overlay, build_trace_minimap, render_follow_all_frame) used by both demos. Co-authored-by: Cursor <cursoragent@cursor.com>
Route every analytics feature through a shared trackable builder that drops referee rows and outfield players that flicker onto a referee, flags whole tracklets that ever coincide with a referee box (collected once in the clip lock pass) and excludes them from tracking, kinematics and annotation. Also enforce at most one goalkeeper per team per frame before tracking and align the CMC frame-pass flag (botsort only) and the Kalman velocity state-index guard with the rest of the motion stack. Co-authored-by: Cursor <cursoragent@cursor.com>
Lower the cumulative-distance kinematics floor from 10 frames to 2 (default plus the DISTANCE and PLAYER_FOCUS call sites) so short-lived tracks are no longer zeroed out. The smoothing and per-frame physical-step cap already guard against spurious distance from brief tracks. Co-authored-by: Cursor <cursoragent@cursor.com>
A candidate homography can clear the reprojection gate yet teleport the whole image->pitch mapping for a single frame, spiking the radar trace past the median smoothing and off-pitch outlier filter. Add a frame-to-frame jump gate: after the reprojection gate, measure the largest pitch-space displacement (cm) of the frame's keypoint correspondences between the previous accepted H and the new candidate, and reject the candidate (holding the previous locked H like a gap) when it exceeds SPEED_GATE_MAX_JUMP_CM (600 cm). The gate fires only between consecutive accepts so the first frame and post-gap recovery are exempt, protecting both the gated speed/distance transforms and the radar/trace H. Co-authored-by: Cursor <cursoragent@cursor.com>
…oint H + jump gate) The visible radar (minimap, per-track traces, live dots) read the gated, mirror- capable radar_transforms, whose mirror branch let the minimap occasionally flip. Switch the visible radar to a single shared homography fitted to the plain pitch vertices only (no plain-vs-mirror candidates), so it physically cannot flip, and recover stability without the mirror branch by holding the previous accepted matrix on large frame-to-frame jumps (same jump rejection as the gated H, applied only between consecutive accepts) plus a small EMA blend of the matrix. Trace median smoothing and the off-pitch outlier filter are unchanged. The gated radar_transforms (mirror + layout scoring + jump gate) still back the metrics (speed / distance / goalkeeper goal-distance); only the visible radar uses the no-mirror map. Co-authored-by: Cursor <cursoragent@cursor.com>
…H, no extra smoothing) Drop the jump gate, EMA blend and held-matrix logic from the visible minimap homography so the radar follows the raw per-frame no-mirror keypoint fit exactly like the reference. Also remove the radar-trace median smoothing and off-pitch outlier filter so the trace is drawn one-to-one from the per-frame positions. The mirror-capable, jump-gated tracker H stays reserved for the metrics. Co-authored-by: Cursor <cursoragent@cursor.com>
…ce (track/homography/kinematics 1x) Introduce ClipAnalysis to factor the shared expensive pipeline (cached detections/keypoints, single BoTSORT tracking pass with stable tracker_ids, referee filtering, team + goalkeeper resolution, MetricContext homography maps, and kinematics) into one reusable computation per clip. Refactor distance.py and player_focus.py to always replay from a precomputed ClipAnalysis, and give direction.py / speed.py a dual path: the standalone single-mode CLI keeps its own tracker (byte-for-byte unchanged), while run-all replays the shared analysis. Add the run_all orchestrator and Mode.ALL wiring so all five renders share one BoTSORT pass; a tracker-build counter verifies tracking runs exactly once. Co-authored-by: Cursor <cursoragent@cursor.com>
…oothing Port world_cup draw_kalman_joystick_dots sizing into draw_joystick_dots: dot radius scales with bbox width (round(width*0.13) clamped 3..8) and a sqrt speed stick reaches the ground-ellipse edge at full deflection, replacing the fixed dot_radius=5 / fixed arm model. Drop the dark outline ring for parity. Restore radar trace polyline smoothing in draw_trace_on_minimap: median-smooth the visible points (HOMOGRAPHY_PITCH_SMOOTH window) and drop off-pitch warps via valid_pitch_cm (margin_cm=80) so spikes stop rendering. Cosmetic only - the no-mirror keypoint minimap homography is untouched. Applies to DISTANCE and PLAYER_FOCUS (shared build_trace_minimap). Co-authored-by: Cursor <cursoragent@cursor.com>
…facts, README + lint for PR Co-authored-by: Cursor <cursoragent@cursor.com>
… purpose Rename support.py → player_motion.py (shared tracking, kinematics, team helpers, and render utilities) and clip_analysis.py → clip_pipeline.py (orchestrated per-clip computation producing ClipAnalysis). Update imports and module docstrings; no behavior changes. Co-authored-by: Cursor <cursoragent@cursor.com>
…ied trace smoothing window Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…zed homography Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…k-assignment flag Co-authored-by: Cursor <cursoragent@cursor.com>
…IRECTION Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Extract _speed_by_tid and _draw_speed_overlay in speed.py so distance and speed_and_distance reuse the same Kalman speed loop and badge rendering. Co-authored-by: Cursor <cursoragent@cursor.com>
Remove PR-scoped and legacy naming notes from shared module headers. Co-authored-by: Cursor <cursoragent@cursor.com>
Show the team-colored joystick dot on the ellipse center for slow or undefined velocity so standing players stay visible in DIRECTION and SPEED modes. Co-authored-by: Cursor <cursoragent@cursor.com>
These functions had no callers on the speed/distance branch; modes use _draw_speed_overlay instead. Co-authored-by: Cursor <cursoragent@cursor.com>
soccer-analytics-speed+distance
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
This PR contains more than 5,000 lines. I am not able to review every line with enough attention, especially since GitHub does not load the full diff in the browser. To make the review manageable, we need to split this PR into smaller, focused PRs and merge each one separately. So far, the project structure has followed this pattern:
The tools should work beyond this single use case. They should be composable and reusable in other projects. Please also group them by responsibility. For example:
Please also address the following points:
Comments used only as visual separators are unnecessary. For example: # -- paths ----------------------------------------------------------------# ---------------------------------------------------------------------------
# Build-or-load helpers (the public entry points used by the analytics features)
# ---------------------------------------------------------------------------# ── end-card (3 seconds) — keeps DISTANCE distinct from follow-all ──Please remove comments like these. Comments should explain non-obvious decisions or logic, rather than divide the file into visual sections. I suggest starting by splitting the current PR into smaller parts based on clear responsibilities. Once this is done, I will be able to review each part properly and provide more useful feedback. |
Summary
Adds player-motion analytics to the soccer demo (
examples/soccer/), building on the existing detection, tracking, and team-classification pipeline.