Mirko Trasciatti
commited on
Commit
·
3cc2963
1
Parent(s):
4e87ae0
Annotate rendered frames and sync kick status
Browse files
app.py
CHANGED
|
@@ -450,6 +450,26 @@ def _maybe_upscale_for_display(image: Image.Image) -> Image.Image:
|
|
| 450 |
return image.resize((target_width, target_height), Image.BILINEAR)
|
| 451 |
|
| 452 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 453 |
def compose_frame(state: AppState, frame_idx: int, remove_bg: bool = False) -> Image.Image:
|
| 454 |
if state is None or state.video_frames is None or len(state.video_frames) == 0:
|
| 455 |
return None
|
|
@@ -902,6 +922,24 @@ def _format_impact_status(state: AppState) -> str:
|
|
| 902 |
return f"Impact frame: {frame}{time_part}{speed_text}"
|
| 903 |
|
| 904 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 905 |
def _recompute_motion_metrics(state: AppState, target_obj_id: int = 1):
|
| 906 |
centers = state.ball_centers.get(target_obj_id)
|
| 907 |
if not centers or len(centers) < 3:
|
|
@@ -1322,6 +1360,7 @@ def propagate_masks(GLOBAL_STATE: gr.State):
|
|
| 1322 |
gr.update(),
|
| 1323 |
_build_kick_plot(GLOBAL_STATE),
|
| 1324 |
_format_impact_status(GLOBAL_STATE),
|
|
|
|
| 1325 |
)
|
| 1326 |
|
| 1327 |
processor = deepcopy(GLOBAL_STATE.processor)
|
|
@@ -1342,6 +1381,7 @@ def propagate_masks(GLOBAL_STATE: gr.State):
|
|
| 1342 |
gr.update(),
|
| 1343 |
_build_kick_plot(GLOBAL_STATE),
|
| 1344 |
_format_impact_status(GLOBAL_STATE),
|
|
|
|
| 1345 |
)
|
| 1346 |
|
| 1347 |
last_frame_idx = 0
|
|
@@ -1375,6 +1415,7 @@ def propagate_masks(GLOBAL_STATE: gr.State):
|
|
| 1375 |
gr.update(value=frame_idx),
|
| 1376 |
_build_kick_plot(GLOBAL_STATE),
|
| 1377 |
_format_impact_status(GLOBAL_STATE),
|
|
|
|
| 1378 |
)
|
| 1379 |
|
| 1380 |
text = f"Propagated masks across {processed} frames for {len(inference_session.obj_ids)} objects."
|
|
@@ -1386,6 +1427,7 @@ def propagate_masks(GLOBAL_STATE: gr.State):
|
|
| 1386 |
gr.update(value=last_frame_idx),
|
| 1387 |
_build_kick_plot(GLOBAL_STATE),
|
| 1388 |
_format_impact_status(GLOBAL_STATE),
|
|
|
|
| 1389 |
)
|
| 1390 |
|
| 1391 |
|
|
@@ -1889,24 +1931,24 @@ with gr.Blocks(title="SAM2 Video (Transformers) - Interactive Segmentation", the
|
|
| 1889 |
if s is not None and val is not None:
|
| 1890 |
s.min_impact_speed_kmh = float(val)
|
| 1891 |
_recompute_motion_metrics(s)
|
| 1892 |
-
return _build_kick_plot(s), _format_impact_status(s)
|
| 1893 |
|
| 1894 |
def _update_goal_distance(s: AppState, val: float):
|
| 1895 |
if s is not None and val is not None:
|
| 1896 |
s.goal_distance_m = float(val)
|
| 1897 |
_recompute_motion_metrics(s)
|
| 1898 |
-
return _build_kick_plot(s), _format_impact_status(s)
|
| 1899 |
|
| 1900 |
min_impact_speed_slider.change(
|
| 1901 |
_update_min_impact_speed,
|
| 1902 |
inputs=[GLOBAL_STATE, min_impact_speed_slider],
|
| 1903 |
-
outputs=[kick_plot, impact_status],
|
| 1904 |
)
|
| 1905 |
|
| 1906 |
goal_distance_slider.change(
|
| 1907 |
_update_goal_distance,
|
| 1908 |
inputs=[GLOBAL_STATE, goal_distance_slider],
|
| 1909 |
-
outputs=[kick_plot, impact_status],
|
| 1910 |
)
|
| 1911 |
|
| 1912 |
def _auto_detect_ball(
|
|
@@ -1958,10 +2000,7 @@ with gr.Blocks(title="SAM2 Video (Transformers) - Interactive Segmentation", the
|
|
| 1958 |
)
|
| 1959 |
|
| 1960 |
status_text = f"✅ Auto-detected ball at ({x_center}, {y_center}) (conf={conf:.2f})"
|
| 1961 |
-
|
| 1962 |
-
status_text += f" | Kick frame ≈ {state_in.kick_frame}"
|
| 1963 |
-
else:
|
| 1964 |
-
status_text += " | Kick frame: not detected"
|
| 1965 |
return (
|
| 1966 |
preview_img,
|
| 1967 |
gr.update(value=status_text, visible=True),
|
|
@@ -1999,7 +2038,8 @@ with gr.Blocks(title="SAM2 Video (Transformers) - Interactive Segmentation", the
|
|
| 1999 |
img = s.composited_frames.get(idx)
|
| 2000 |
if img is None:
|
| 2001 |
img = compose_frame(s, idx, remove_bg=False)
|
| 2002 |
-
|
|
|
|
| 2003 |
# Periodically release CPU mem to reduce pressure
|
| 2004 |
if (idx + 1) % 60 == 0:
|
| 2005 |
gc.collect()
|
|
@@ -2022,7 +2062,7 @@ with gr.Blocks(title="SAM2 Video (Transformers) - Interactive Segmentation", the
|
|
| 2022 |
propagate_btn.click(
|
| 2023 |
propagate_masks,
|
| 2024 |
inputs=[GLOBAL_STATE],
|
| 2025 |
-
outputs=[GLOBAL_STATE, propagate_status, frame_slider, kick_plot, impact_status],
|
| 2026 |
)
|
| 2027 |
|
| 2028 |
reset_btn.click(
|
|
|
|
| 450 |
return image.resize((target_width, target_height), Image.BILINEAR)
|
| 451 |
|
| 452 |
|
| 453 |
+
def _annotate_frame_index(image: Image.Image, frame_idx: int) -> Image.Image:
|
| 454 |
+
if image is None:
|
| 455 |
+
return image
|
| 456 |
+
annotated = image.copy()
|
| 457 |
+
draw = ImageDraw.Draw(annotated)
|
| 458 |
+
text = f"Frame {frame_idx}"
|
| 459 |
+
padding = 6
|
| 460 |
+
try:
|
| 461 |
+
bbox = draw.textbbox((0, 0), text)
|
| 462 |
+
text_w = bbox[2] - bbox[0]
|
| 463 |
+
text_h = bbox[3] - bbox[1]
|
| 464 |
+
except AttributeError:
|
| 465 |
+
text_w, text_h = draw.textsize(text)
|
| 466 |
+
x0, y0 = padding, padding
|
| 467 |
+
x1, y1 = x0 + text_w + padding, y0 + text_h + padding
|
| 468 |
+
draw.rectangle([(x0 - padding // 2, y0 - padding // 2), (x1, y1)], fill=(0, 0, 0))
|
| 469 |
+
draw.text((x0, y0), text, fill=(255, 255, 255))
|
| 470 |
+
return annotated
|
| 471 |
+
|
| 472 |
+
|
| 473 |
def compose_frame(state: AppState, frame_idx: int, remove_bg: bool = False) -> Image.Image:
|
| 474 |
if state is None or state.video_frames is None or len(state.video_frames) == 0:
|
| 475 |
return None
|
|
|
|
| 922 |
return f"Impact frame: {frame}{time_part}{speed_text}"
|
| 923 |
|
| 924 |
|
| 925 |
+
def _format_kick_status(state: AppState) -> str:
|
| 926 |
+
if state is None or not isinstance(state, AppState):
|
| 927 |
+
return "Kick frame: not computed"
|
| 928 |
+
frame = state.kick_frame
|
| 929 |
+
if frame is None:
|
| 930 |
+
frame = getattr(state, "kick_debug_kick_frame", None)
|
| 931 |
+
if frame is None:
|
| 932 |
+
if state.kick_debug_frames:
|
| 933 |
+
return "Kick frame: not detected"
|
| 934 |
+
return "Kick frame: not computed"
|
| 935 |
+
if state.kick_frame is None and frame is not None:
|
| 936 |
+
state.kick_frame = frame
|
| 937 |
+
time_part = ""
|
| 938 |
+
if state.video_fps and state.video_fps > 1e-6:
|
| 939 |
+
time_part = f" (~{frame / state.video_fps:.2f}s)"
|
| 940 |
+
return f"Kick frame ≈ {frame}{time_part}"
|
| 941 |
+
|
| 942 |
+
|
| 943 |
def _recompute_motion_metrics(state: AppState, target_obj_id: int = 1):
|
| 944 |
centers = state.ball_centers.get(target_obj_id)
|
| 945 |
if not centers or len(centers) < 3:
|
|
|
|
| 1360 |
gr.update(),
|
| 1361 |
_build_kick_plot(GLOBAL_STATE),
|
| 1362 |
_format_impact_status(GLOBAL_STATE),
|
| 1363 |
+
gr.update(value=_format_kick_status(GLOBAL_STATE), visible=True),
|
| 1364 |
)
|
| 1365 |
|
| 1366 |
processor = deepcopy(GLOBAL_STATE.processor)
|
|
|
|
| 1381 |
gr.update(),
|
| 1382 |
_build_kick_plot(GLOBAL_STATE),
|
| 1383 |
_format_impact_status(GLOBAL_STATE),
|
| 1384 |
+
gr.update(value=_format_kick_status(GLOBAL_STATE), visible=True),
|
| 1385 |
)
|
| 1386 |
|
| 1387 |
last_frame_idx = 0
|
|
|
|
| 1415 |
gr.update(value=frame_idx),
|
| 1416 |
_build_kick_plot(GLOBAL_STATE),
|
| 1417 |
_format_impact_status(GLOBAL_STATE),
|
| 1418 |
+
gr.update(value=_format_kick_status(GLOBAL_STATE), visible=True),
|
| 1419 |
)
|
| 1420 |
|
| 1421 |
text = f"Propagated masks across {processed} frames for {len(inference_session.obj_ids)} objects."
|
|
|
|
| 1427 |
gr.update(value=last_frame_idx),
|
| 1428 |
_build_kick_plot(GLOBAL_STATE),
|
| 1429 |
_format_impact_status(GLOBAL_STATE),
|
| 1430 |
+
gr.update(value=_format_kick_status(GLOBAL_STATE), visible=True),
|
| 1431 |
)
|
| 1432 |
|
| 1433 |
|
|
|
|
| 1931 |
if s is not None and val is not None:
|
| 1932 |
s.min_impact_speed_kmh = float(val)
|
| 1933 |
_recompute_motion_metrics(s)
|
| 1934 |
+
return _build_kick_plot(s), _format_impact_status(s), gr.update(value=_format_kick_status(s), visible=True)
|
| 1935 |
|
| 1936 |
def _update_goal_distance(s: AppState, val: float):
|
| 1937 |
if s is not None and val is not None:
|
| 1938 |
s.goal_distance_m = float(val)
|
| 1939 |
_recompute_motion_metrics(s)
|
| 1940 |
+
return _build_kick_plot(s), _format_impact_status(s), gr.update(value=_format_kick_status(s), visible=True)
|
| 1941 |
|
| 1942 |
min_impact_speed_slider.change(
|
| 1943 |
_update_min_impact_speed,
|
| 1944 |
inputs=[GLOBAL_STATE, min_impact_speed_slider],
|
| 1945 |
+
outputs=[kick_plot, impact_status, ball_status],
|
| 1946 |
)
|
| 1947 |
|
| 1948 |
goal_distance_slider.change(
|
| 1949 |
_update_goal_distance,
|
| 1950 |
inputs=[GLOBAL_STATE, goal_distance_slider],
|
| 1951 |
+
outputs=[kick_plot, impact_status, ball_status],
|
| 1952 |
)
|
| 1953 |
|
| 1954 |
def _auto_detect_ball(
|
|
|
|
| 2000 |
)
|
| 2001 |
|
| 2002 |
status_text = f"✅ Auto-detected ball at ({x_center}, {y_center}) (conf={conf:.2f})"
|
| 2003 |
+
status_text += f" | {_format_kick_status(state_in)}"
|
|
|
|
|
|
|
|
|
|
| 2004 |
return (
|
| 2005 |
preview_img,
|
| 2006 |
gr.update(value=status_text, visible=True),
|
|
|
|
| 2038 |
img = s.composited_frames.get(idx)
|
| 2039 |
if img is None:
|
| 2040 |
img = compose_frame(s, idx, remove_bg=False)
|
| 2041 |
+
img_with_idx = _annotate_frame_index(img, idx)
|
| 2042 |
+
frames_np.append(np.array(img_with_idx)[:, :, ::-1]) # BGR for cv2
|
| 2043 |
# Periodically release CPU mem to reduce pressure
|
| 2044 |
if (idx + 1) % 60 == 0:
|
| 2045 |
gc.collect()
|
|
|
|
| 2062 |
propagate_btn.click(
|
| 2063 |
propagate_masks,
|
| 2064 |
inputs=[GLOBAL_STATE],
|
| 2065 |
+
outputs=[GLOBAL_STATE, propagate_status, frame_slider, kick_plot, impact_status, ball_status],
|
| 2066 |
)
|
| 2067 |
|
| 2068 |
reset_btn.click(
|