Mirko Trasciatti commited on
Commit
3cc2963
·
1 Parent(s): 4e87ae0

Annotate rendered frames and sync kick status

Browse files
Files changed (1) hide show
  1. app.py +50 -10
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
- if state_in.kick_frame is not None:
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
- frames_np.append(np.array(img)[:, :, ::-1]) # BGR for cv2
 
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(