Spaces:
Sleeping
Sleeping
Reorder flow: draw lines on scrubbed frame, split Detect, click-to-select players
Browse files- app.py +101 -74
- pipeline/overlay.py +19 -3
app.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
| 1 |
"""
|
| 2 |
VAR-style offside visualizer — Gradio app.
|
| 3 |
|
| 4 |
-
|
| 5 |
-
upload
|
| 6 |
-
->
|
| 7 |
-
->
|
| 8 |
|
| 9 |
The GPU is touched ONLY inside pipeline.gpu.reconstruct_frame(); every other
|
| 10 |
-
callback
|
| 11 |
"""
|
| 12 |
|
| 13 |
import os
|
|
@@ -20,7 +20,7 @@ import numpy as np
|
|
| 20 |
import gradio as gr
|
| 21 |
|
| 22 |
from pipeline.video import probe_video, grab_frame
|
| 23 |
-
from pipeline.overlay import annotate_detections, draw_lines
|
| 24 |
from pipeline import geometry as G
|
| 25 |
|
| 26 |
|
|
@@ -28,65 +28,87 @@ from pipeline import geometry as G
|
|
| 28 |
# Stage (a): upload + frame scrubbing (pure CPU)
|
| 29 |
# ============================================================================
|
| 30 |
def on_upload(video_path):
|
| 31 |
-
"""New clip: size the slider, show frame 0,
|
| 32 |
if not video_path:
|
| 33 |
-
return gr.update(maximum=1, value=0), None, "Upload a clip to begin.",
|
|
|
|
| 34 |
n, fps = probe_video(video_path)
|
| 35 |
n_max = max(n - 1, 0)
|
|
|
|
| 36 |
return (
|
| 37 |
gr.update(maximum=max(n_max, 1), value=0),
|
| 38 |
-
|
| 39 |
-
f"{n} frames @ {fps:.1f} fps — scrub to the moment the ball is played
|
|
|
|
| 40 |
n_max,
|
|
|
|
|
|
|
|
|
|
| 41 |
)
|
| 42 |
|
| 43 |
|
| 44 |
def on_scrub(video_path, idx):
|
|
|
|
| 45 |
if not video_path:
|
| 46 |
-
return None
|
| 47 |
-
|
|
|
|
| 48 |
|
| 49 |
|
| 50 |
def step_frame(idx, delta, n_max):
|
| 51 |
-
"""Clamp prev/next within [0, slider max]."""
|
| 52 |
return max(0, min(int(n_max), int(idx) + delta))
|
| 53 |
|
| 54 |
|
| 55 |
# ============================================================================
|
| 56 |
-
# Stage (b)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
# ============================================================================
|
| 58 |
def on_detect(video_path, idx, bbox_thr):
|
| 59 |
-
"""The one GPU step.
|
| 60 |
from pipeline.gpu import reconstruct_frame
|
| 61 |
if not video_path:
|
| 62 |
-
return None,
|
| 63 |
people = reconstruct_frame(video_path, idx, bbox_thr)
|
| 64 |
if not people:
|
| 65 |
-
return (None,
|
| 66 |
-
[],
|
| 67 |
-
annotated = annotate_detections(grab_frame(video_path, idx), people)
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
return (annotated,
|
| 72 |
-
gr.update(choices=choices, value=[]),
|
| 73 |
-
gr.update(choices=choices, value=[]),
|
| 74 |
-
people, msg)
|
| 75 |
|
| 76 |
|
| 77 |
# ============================================================================
|
| 78 |
-
# Stage (d): click
|
| 79 |
# ============================================================================
|
| 80 |
-
def
|
| 81 |
-
"""
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
|
| 92 |
# ============================================================================
|
|
@@ -96,9 +118,10 @@ def on_build(video_path, idx, bbox_thr, selected_ids, line_pts, flip_up,
|
|
| 96 |
attack_dir, defender_ids):
|
| 97 |
from pipeline.gpu import reconstruct_frame, get_faces
|
| 98 |
if not selected_ids:
|
| 99 |
-
return None, "
|
| 100 |
if not line_pts or len(line_pts) < 4:
|
| 101 |
-
return None, "
|
|
|
|
| 102 |
|
| 103 |
people = reconstruct_frame(video_path, idx, bbox_thr)
|
| 104 |
faces = get_faces()
|
|
@@ -112,7 +135,6 @@ def on_build(video_path, idx, bbox_thr, selected_ids, line_pts, flip_up,
|
|
| 112 |
attack_sign = +1 if attack_dir == "toward +X" else -1
|
| 113 |
dset = [int(d) for d in (defender_ids or [])]
|
| 114 |
plane_x = G.offside_plane_x(placed, attack_sign, dset)
|
| 115 |
-
|
| 116 |
fig = G.build_scene(placed, faces, plane_x, attack_sign, dset)
|
| 117 |
|
| 118 |
allX = np.vstack(list(placed.values()))[:, 0]
|
|
@@ -121,8 +143,8 @@ def on_build(video_path, idx, bbox_thr, selected_ids, line_pts, flip_up,
|
|
| 121 |
visible=True, label="Drag the offside plane (X, m)")
|
| 122 |
|
| 123 |
warn = " ⚠ heights look wrong — toggle 'flip up'." if med < 1.0 else ""
|
| 124 |
-
|
| 125 |
-
|
| 126 |
|
| 127 |
|
| 128 |
def on_plane(placed, plane_x, attack_sign, defender_ids):
|
|
@@ -130,9 +152,8 @@ def on_plane(placed, plane_x, attack_sign, defender_ids):
|
|
| 130 |
from pipeline.gpu import get_faces
|
| 131 |
if not placed:
|
| 132 |
return gr.update()
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
return fig
|
| 136 |
|
| 137 |
|
| 138 |
# ============================================================================
|
|
@@ -141,49 +162,52 @@ def on_plane(placed, plane_x, attack_sign, defender_ids):
|
|
| 141 |
with gr.Blocks(title="VAR Offside Visualizer") as demo:
|
| 142 |
gr.Markdown(
|
| 143 |
"## VAR-style Offside Visualizer\n"
|
| 144 |
-
"Upload
|
| 145 |
-
"
|
| 146 |
"_Scale comes from reconstructed body height, so positions are approximate "
|
| 147 |
"metres — good for relative offside ordering, not sub-10 cm calls._"
|
| 148 |
)
|
| 149 |
|
| 150 |
# session state
|
| 151 |
st_nmax = gr.State(0) # last valid frame index
|
| 152 |
-
|
|
|
|
| 153 |
st_lines = gr.State([]) # clicked line points
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
| 157 |
|
| 158 |
-
# --- stage (a) ---
|
| 159 |
video = gr.Video(label="1. Upload match clip")
|
| 160 |
status = gr.Markdown()
|
|
|
|
| 161 |
with gr.Row():
|
| 162 |
frame_slider = gr.Slider(0, 1, value=0, step=1,
|
| 163 |
label="2. Scrub to the offside frame")
|
| 164 |
with gr.Row():
|
| 165 |
prev_btn = gr.Button("◀ prev frame")
|
| 166 |
next_btn = gr.Button("next frame ▶")
|
| 167 |
-
frame_view = gr.Image(label="Current frame", interactive=False)
|
| 168 |
|
| 169 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
with gr.Row():
|
| 171 |
thr = gr.Slider(0.0, 0.95, value=0.85, step=0.05, label="Detection confidence")
|
| 172 |
-
detect_btn = gr.Button("
|
| 173 |
-
detect_view = gr.Image(
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
line_status = gr.Markdown()
|
| 177 |
-
sel = gr.CheckboxGroup(choices=[], label="4. Select players to analyze (indices)")
|
| 178 |
|
| 179 |
-
#
|
|
|
|
| 180 |
with gr.Row():
|
| 181 |
flip = gr.Checkbox(False, label="flip up (if players are upside-down)")
|
| 182 |
attack = gr.Radio(["toward +X", "toward -X"], value="toward +X",
|
| 183 |
label="Attacking direction")
|
| 184 |
-
|
| 185 |
-
choices=[], label="Defenders (incl. GK) — sets the offside line")
|
| 186 |
-
build_btn = gr.Button("5. Build 3D scene + offside line", variant="primary")
|
| 187 |
|
| 188 |
scene = gr.Plot(label="3D scene")
|
| 189 |
plane_slider = gr.Slider(-10, 10, value=0, step=0.05, visible=False,
|
|
@@ -191,21 +215,24 @@ with gr.Blocks(title="VAR Offside Visualizer") as demo:
|
|
| 191 |
build_status = gr.Markdown()
|
| 192 |
|
| 193 |
# --- wiring ---
|
| 194 |
-
video.change(on_upload, [video],
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
|
|
|
|
|
|
|
|
|
| 200 |
|
| 201 |
detect_btn.click(on_detect, [video, frame_slider, thr],
|
| 202 |
-
[detect_view,
|
| 203 |
-
detect_view.select(
|
| 204 |
-
[
|
| 205 |
|
| 206 |
build_btn.click(
|
| 207 |
on_build,
|
| 208 |
-
[video, frame_slider, thr,
|
| 209 |
[scene, build_status, plane_slider, st_placed, st_attack, st_defenders])
|
| 210 |
plane_slider.change(on_plane, [st_placed, plane_slider, st_attack, st_defenders],
|
| 211 |
[scene])
|
|
|
|
| 1 |
"""
|
| 2 |
VAR-style offside visualizer — Gradio app.
|
| 3 |
|
| 4 |
+
Flow:
|
| 5 |
+
upload -> scrub to the frame -> draw 2 goal-parallel lines on that frame
|
| 6 |
+
-> Detect players (GPU, once/frame) -> click players to select
|
| 7 |
+
-> mark defenders -> Build the 3D scene with a draggable offside plane.
|
| 8 |
|
| 9 |
The GPU is touched ONLY inside pipeline.gpu.reconstruct_frame(); every other
|
| 10 |
+
callback here runs on cached numpy and stays on the CPU.
|
| 11 |
"""
|
| 12 |
|
| 13 |
import os
|
|
|
|
| 20 |
import gradio as gr
|
| 21 |
|
| 22 |
from pipeline.video import probe_video, grab_frame
|
| 23 |
+
from pipeline.overlay import annotate_detections, draw_lines, pick_box
|
| 24 |
from pipeline import geometry as G
|
| 25 |
|
| 26 |
|
|
|
|
| 28 |
# Stage (a): upload + frame scrubbing (pure CPU)
|
| 29 |
# ============================================================================
|
| 30 |
def on_upload(video_path):
|
| 31 |
+
"""New clip: size the slider, show frame 0, reset lines/frame state."""
|
| 32 |
if not video_path:
|
| 33 |
+
return (gr.update(maximum=1, value=0), None, "Upload a clip to begin.",
|
| 34 |
+
0, None, [], "")
|
| 35 |
n, fps = probe_video(video_path)
|
| 36 |
n_max = max(n - 1, 0)
|
| 37 |
+
frame = grab_frame(video_path, 0)
|
| 38 |
return (
|
| 39 |
gr.update(maximum=max(n_max, 1), value=0),
|
| 40 |
+
frame,
|
| 41 |
+
f"{n} frames @ {fps:.1f} fps — scrub to the moment the ball is played, "
|
| 42 |
+
"then click 2 goal-parallel lines on the frame.",
|
| 43 |
n_max,
|
| 44 |
+
frame, # st_frame: clean copy for line redraws
|
| 45 |
+
[], # st_lines reset
|
| 46 |
+
"", # line_status reset
|
| 47 |
)
|
| 48 |
|
| 49 |
|
| 50 |
def on_scrub(video_path, idx):
|
| 51 |
+
"""Show the new frame and reset any half-drawn lines on it."""
|
| 52 |
if not video_path:
|
| 53 |
+
return None, None, [], ""
|
| 54 |
+
frame = grab_frame(video_path, int(idx))
|
| 55 |
+
return frame, frame, [], ""
|
| 56 |
|
| 57 |
|
| 58 |
def step_frame(idx, delta, n_max):
|
|
|
|
| 59 |
return max(0, min(int(n_max), int(idx) + delta))
|
| 60 |
|
| 61 |
|
| 62 |
# ============================================================================
|
| 63 |
+
# Stage (b): draw 2 goal-parallel lines on the scrubbed frame (CPU)
|
| 64 |
+
# ============================================================================
|
| 65 |
+
def on_line_click(line_pts, clean_frame, evt: gr.SelectData):
|
| 66 |
+
"""Collect 4 clicks = 2 lines; always redraw from the clean frame (no drift)."""
|
| 67 |
+
pts = list(line_pts) if line_pts else []
|
| 68 |
+
if len(pts) >= 4:
|
| 69 |
+
pts = []
|
| 70 |
+
pts.append([float(evt.index[0]), float(evt.index[1])])
|
| 71 |
+
img = draw_lines(clean_frame, pts)
|
| 72 |
+
status = {1: "line 1: 1/2", 2: "line 1 set", 3: "line 2: 1/2",
|
| 73 |
+
4: "both lines set ✓ — now Detect players"}.get(len(pts), "")
|
| 74 |
+
return pts, img, status
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
# ============================================================================
|
| 78 |
+
# Stage (c): detect (GPU, cached) + show boxes (CPU after the call)
|
| 79 |
# ============================================================================
|
| 80 |
def on_detect(video_path, idx, bbox_thr):
|
| 81 |
+
"""The one GPU step. Lazy-imported so the app boots without the model."""
|
| 82 |
from pipeline.gpu import reconstruct_frame
|
| 83 |
if not video_path:
|
| 84 |
+
return None, [], [], "Upload a clip first.", gr.update(choices=[], value=[])
|
| 85 |
people = reconstruct_frame(video_path, idx, bbox_thr)
|
| 86 |
if not people:
|
| 87 |
+
return (None, [], [], "No players detected — lower the confidence slider.",
|
| 88 |
+
gr.update(choices=[], value=[]))
|
| 89 |
+
annotated = annotate_detections(grab_frame(video_path, idx), people, [])
|
| 90 |
+
msg = (f"Detected {len(people)} players. Click a box to select a player "
|
| 91 |
+
"(click again to deselect).")
|
| 92 |
+
return annotated, people, [], msg, gr.update(choices=[], value=[])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
|
| 95 |
# ============================================================================
|
| 96 |
+
# Stage (d): click players to select (CPU)
|
| 97 |
# ============================================================================
|
| 98 |
+
def on_select_player(people, selected, clean_frame, cur_def, evt: gr.SelectData):
|
| 99 |
+
"""Toggle the player whose box was clicked; refresh highlight + defender choices."""
|
| 100 |
+
if not people:
|
| 101 |
+
return None, selected or [], "Detect players first.", gr.update()
|
| 102 |
+
hit = pick_box(people, evt.index[0], evt.index[1])
|
| 103 |
+
sel = list(selected or [])
|
| 104 |
+
if hit is not None:
|
| 105 |
+
sel.remove(hit) if hit in sel else sel.append(hit)
|
| 106 |
+
sel = sorted(sel)
|
| 107 |
+
img = annotate_detections(clean_frame, people, sel)
|
| 108 |
+
msg = (f"Selected players: {sel}. Mark defenders below, then Build."
|
| 109 |
+
if sel else "Click a box to select a player.")
|
| 110 |
+
keep_def = [d for d in (cur_def or []) if d in sel]
|
| 111 |
+
return img, sel, msg, gr.update(choices=sel, value=keep_def)
|
| 112 |
|
| 113 |
|
| 114 |
# ============================================================================
|
|
|
|
| 118 |
attack_dir, defender_ids):
|
| 119 |
from pipeline.gpu import reconstruct_frame, get_faces
|
| 120 |
if not selected_ids:
|
| 121 |
+
return None, "Click at least one player to select.", gr.update(), None, +1, []
|
| 122 |
if not line_pts or len(line_pts) < 4:
|
| 123 |
+
return None, "Draw 2 goal-parallel lines (4 points) on the frame first.", \
|
| 124 |
+
gr.update(), None, +1, []
|
| 125 |
|
| 126 |
people = reconstruct_frame(video_path, idx, bbox_thr)
|
| 127 |
faces = get_faces()
|
|
|
|
| 135 |
attack_sign = +1 if attack_dir == "toward +X" else -1
|
| 136 |
dset = [int(d) for d in (defender_ids or [])]
|
| 137 |
plane_x = G.offside_plane_x(placed, attack_sign, dset)
|
|
|
|
| 138 |
fig = G.build_scene(placed, faces, plane_x, attack_sign, dset)
|
| 139 |
|
| 140 |
allX = np.vstack(list(placed.values()))[:, 0]
|
|
|
|
| 143 |
visible=True, label="Drag the offside plane (X, m)")
|
| 144 |
|
| 145 |
warn = " ⚠ heights look wrong — toggle 'flip up'." if med < 1.0 else ""
|
| 146 |
+
return (fig, f"Median player height {med:.2f} m (expect ~1.7–1.9).{warn}",
|
| 147 |
+
plane_update, placed, attack_sign, dset)
|
| 148 |
|
| 149 |
|
| 150 |
def on_plane(placed, plane_x, attack_sign, defender_ids):
|
|
|
|
| 152 |
from pipeline.gpu import get_faces
|
| 153 |
if not placed:
|
| 154 |
return gr.update()
|
| 155 |
+
return G.build_scene(placed, get_faces(), float(plane_x),
|
| 156 |
+
int(attack_sign), defender_ids or [])
|
|
|
|
| 157 |
|
| 158 |
|
| 159 |
# ============================================================================
|
|
|
|
| 162 |
with gr.Blocks(title="VAR Offside Visualizer") as demo:
|
| 163 |
gr.Markdown(
|
| 164 |
"## VAR-style Offside Visualizer\n"
|
| 165 |
+
"Upload → scrub → **click 2 goal-parallel lines** → Detect → "
|
| 166 |
+
"**click players to select** → mark defenders → Build.\n\n"
|
| 167 |
"_Scale comes from reconstructed body height, so positions are approximate "
|
| 168 |
"metres — good for relative offside ordering, not sub-10 cm calls._"
|
| 169 |
)
|
| 170 |
|
| 171 |
# session state
|
| 172 |
st_nmax = gr.State(0) # last valid frame index
|
| 173 |
+
st_frame = gr.State(None) # clean RGB of the current frame
|
| 174 |
+
st_people = gr.State([]) # slim detections
|
| 175 |
st_lines = gr.State([]) # clicked line points
|
| 176 |
+
st_selected = gr.State([]) # player ids selected by clicking
|
| 177 |
+
st_placed = gr.State(None) # placed meshes after build
|
| 178 |
+
st_attack = gr.State(+1)
|
| 179 |
+
st_defenders = gr.State([])
|
| 180 |
|
|
|
|
| 181 |
video = gr.Video(label="1. Upload match clip")
|
| 182 |
status = gr.Markdown()
|
| 183 |
+
|
| 184 |
with gr.Row():
|
| 185 |
frame_slider = gr.Slider(0, 1, value=0, step=1,
|
| 186 |
label="2. Scrub to the offside frame")
|
| 187 |
with gr.Row():
|
| 188 |
prev_btn = gr.Button("◀ prev frame")
|
| 189 |
next_btn = gr.Button("next frame ▶")
|
|
|
|
| 190 |
|
| 191 |
+
# Stage (b): lines are drawn directly on the scrubbed frame
|
| 192 |
+
frame_view = gr.Image(label="3. Click 2 goal-parallel lines here (4 points)",
|
| 193 |
+
interactive=True)
|
| 194 |
+
line_status = gr.Markdown()
|
| 195 |
+
|
| 196 |
+
# Stage (c)/(d): detect, then click boxes to select
|
| 197 |
with gr.Row():
|
| 198 |
thr = gr.Slider(0.0, 0.95, value=0.85, step=0.05, label="Detection confidence")
|
| 199 |
+
detect_btn = gr.Button("4. Detect players (GPU)", variant="primary")
|
| 200 |
+
detect_view = gr.Image(label="5. Click a player's box to select (click again to deselect)",
|
| 201 |
+
interactive=True)
|
| 202 |
+
select_status = gr.Markdown()
|
|
|
|
|
|
|
| 203 |
|
| 204 |
+
# Stage (e)/(f)
|
| 205 |
+
defenders = gr.CheckboxGroup(choices=[], label="6. Defenders (incl. GK) — sets the offside line")
|
| 206 |
with gr.Row():
|
| 207 |
flip = gr.Checkbox(False, label="flip up (if players are upside-down)")
|
| 208 |
attack = gr.Radio(["toward +X", "toward -X"], value="toward +X",
|
| 209 |
label="Attacking direction")
|
| 210 |
+
build_btn = gr.Button("7. Build 3D scene + offside line", variant="primary")
|
|
|
|
|
|
|
| 211 |
|
| 212 |
scene = gr.Plot(label="3D scene")
|
| 213 |
plane_slider = gr.Slider(-10, 10, value=0, step=0.05, visible=False,
|
|
|
|
| 215 |
build_status = gr.Markdown()
|
| 216 |
|
| 217 |
# --- wiring ---
|
| 218 |
+
video.change(on_upload, [video],
|
| 219 |
+
[frame_slider, frame_view, status, st_nmax, st_frame, st_lines, line_status])
|
| 220 |
+
frame_slider.change(on_scrub, [video, frame_slider],
|
| 221 |
+
[frame_view, st_frame, st_lines, line_status])
|
| 222 |
+
prev_btn.click(lambda i, m: step_frame(i, -1, m), [frame_slider, st_nmax], [frame_slider])
|
| 223 |
+
next_btn.click(lambda i, m: step_frame(i, +1, m), [frame_slider, st_nmax], [frame_slider])
|
| 224 |
+
|
| 225 |
+
frame_view.select(on_line_click, [st_lines, st_frame],
|
| 226 |
+
[st_lines, frame_view, line_status])
|
| 227 |
|
| 228 |
detect_btn.click(on_detect, [video, frame_slider, thr],
|
| 229 |
+
[detect_view, st_people, st_selected, select_status, defenders])
|
| 230 |
+
detect_view.select(on_select_player, [st_people, st_selected, st_frame, defenders],
|
| 231 |
+
[detect_view, st_selected, select_status, defenders])
|
| 232 |
|
| 233 |
build_btn.click(
|
| 234 |
on_build,
|
| 235 |
+
[video, frame_slider, thr, st_selected, st_lines, flip, attack, defenders],
|
| 236 |
[scene, build_status, plane_slider, st_placed, st_attack, st_defenders])
|
| 237 |
plane_slider.change(on_plane, [st_placed, plane_slider, st_attack, st_defenders],
|
| 238 |
[scene])
|
pipeline/overlay.py
CHANGED
|
@@ -7,17 +7,33 @@ import cv2
|
|
| 7 |
import numpy as np
|
| 8 |
|
| 9 |
|
| 10 |
-
def annotate_detections(frame_rgb, people):
|
| 11 |
-
"""Draw numbered
|
|
|
|
| 12 |
img = cv2.cvtColor(frame_rgb, cv2.COLOR_RGB2BGR)
|
| 13 |
for i, p in enumerate(people):
|
| 14 |
x1, y1, x2, y2 = [int(v) for v in p["bbox"]]
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
| 16 |
cv2.putText(img, str(i), (x1, max(y1 - 6, 14)),
|
| 17 |
cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 3)
|
| 18 |
return cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
| 19 |
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
def draw_lines(frame_rgb, pts):
|
| 22 |
"""Draw the clicked points and the (up to two) line segments.
|
| 23 |
|
|
|
|
| 7 |
import numpy as np
|
| 8 |
|
| 9 |
|
| 10 |
+
def annotate_detections(frame_rgb, people, selected_ids=()):
|
| 11 |
+
"""Draw numbered boxes; selected players get a thick yellow box, others thin green."""
|
| 12 |
+
sel = set(int(i) for i in selected_ids)
|
| 13 |
img = cv2.cvtColor(frame_rgb, cv2.COLOR_RGB2BGR)
|
| 14 |
for i, p in enumerate(people):
|
| 15 |
x1, y1, x2, y2 = [int(v) for v in p["bbox"]]
|
| 16 |
+
if i in sel:
|
| 17 |
+
cv2.rectangle(img, (x1, y1), (x2, y2), (0, 230, 255), 3) # yellow, thick
|
| 18 |
+
else:
|
| 19 |
+
cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) # green, thin
|
| 20 |
cv2.putText(img, str(i), (x1, max(y1 - 6, 14)),
|
| 21 |
cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 3)
|
| 22 |
return cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
| 23 |
|
| 24 |
|
| 25 |
+
def pick_box(people, x, y):
|
| 26 |
+
"""Return the index of the smallest box containing (x, y), or None."""
|
| 27 |
+
hit, best_area = None, None
|
| 28 |
+
for i, p in enumerate(people):
|
| 29 |
+
x1, y1, x2, y2 = p["bbox"]
|
| 30 |
+
if x1 <= x <= x2 and y1 <= y <= y2:
|
| 31 |
+
area = (x2 - x1) * (y2 - y1)
|
| 32 |
+
if best_area is None or area < best_area:
|
| 33 |
+
best_area, hit = area, i
|
| 34 |
+
return hit
|
| 35 |
+
|
| 36 |
+
|
| 37 |
def draw_lines(frame_rgb, pts):
|
| 38 |
"""Draw the clicked points and the (up to two) line segments.
|
| 39 |
|