Mirko Trasciatti commited on
Commit
6ae1c1a
·
1 Parent(s): bc8994a

Feather ball mask for live and ghost overlays

Browse files
Files changed (1) hide show
  1. app.py +45 -0
app.py CHANGED
@@ -924,6 +924,14 @@ def compose_frame(state: AppState, frame_idx: int, remove_bg: bool = False) -> I
924
  combined_mask = current_union_mask
925
  if combined_mask is None:
926
  combined_mask = np.zeros((frame_np.shape[0], frame_np.shape[1]), dtype=np.float32)
 
 
 
 
 
 
 
 
927
  result_np = _apply_cutout_fx(state, frame_np, combined_mask)
928
  out_img = Image.fromarray(result_np)
929
  else:
@@ -932,6 +940,17 @@ def compose_frame(state: AppState, frame_idx: int, remove_bg: bool = False) -> I
932
  overlay_masks = {oid: mask for oid, mask in masks.items() if oid != BALL_OBJECT_ID}
933
  if overlay_masks:
934
  out_img = overlay_masks_on_frame(out_img, overlay_masks, state.color_by_obj, alpha=0.65)
 
 
 
 
 
 
 
 
 
 
 
935
 
936
  if ghost_mask is not None:
937
  ghost_np = np.clip(ghost_mask.astype(np.float32), 0.0, 1.0)
@@ -1118,6 +1137,7 @@ def _build_ball_trail_mask(state: AppState, frame_idx: int) -> np.ndarray | None
1118
  if mask_np.ndim == 3:
1119
  mask_np = mask_np.squeeze()
1120
  mask_np = np.clip(mask_np, 0.0, 1.0)
 
1121
  if trail_mask is None:
1122
  trail_mask = np.zeros_like(mask_np, dtype=np.float32)
1123
  if trail_mask.shape != mask_np.shape:
@@ -1149,6 +1169,31 @@ def _compute_mask_centroid(mask: np.ndarray) -> tuple[int, int] | None:
1149
  return cx, cy
1150
 
1151
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1152
  def _update_centroids_for_frame(state: AppState, frame_idx: int):
1153
  if state is None:
1154
  return
 
924
  combined_mask = current_union_mask
925
  if combined_mask is None:
926
  combined_mask = np.zeros((frame_np.shape[0], frame_np.shape[1]), dtype=np.float32)
927
+ # Apply falloff to ball component when rendering foreground
928
+ if BALL_OBJECT_ID in masks:
929
+ ball_mask = masks[BALL_OBJECT_ID]
930
+ if ball_mask is not None:
931
+ combined_mask = np.maximum(
932
+ combined_mask,
933
+ _apply_radial_falloff(np.clip(ball_mask.astype(np.float32), 0.0, 1.0)),
934
+ )
935
  result_np = _apply_cutout_fx(state, frame_np, combined_mask)
936
  out_img = Image.fromarray(result_np)
937
  else:
 
940
  overlay_masks = {oid: mask for oid, mask in masks.items() if oid != BALL_OBJECT_ID}
941
  if overlay_masks:
942
  out_img = overlay_masks_on_frame(out_img, overlay_masks, state.color_by_obj, alpha=0.65)
943
+ # Overlay feathered ball on top
944
+ if BALL_OBJECT_ID in masks:
945
+ ball_mask = masks[BALL_OBJECT_ID]
946
+ if ball_mask is not None:
947
+ ball_alpha = _apply_radial_falloff(ball_mask)
948
+ if ball_alpha is not None and ball_alpha.max() > FX_EPS:
949
+ base_np = np.array(out_img).astype(np.float32) / 255.0
950
+ color = np.array(state.color_by_obj.get(BALL_OBJECT_ID, (255, 255, 0)), dtype=np.float32) / 255.0
951
+ alpha = np.clip(ball_alpha[..., None], 0.0, 1.0)
952
+ base_np = (1.0 - alpha) * base_np + alpha * color
953
+ out_img = Image.fromarray(np.clip(base_np * 255.0, 0, 255).astype(np.uint8))
954
 
955
  if ghost_mask is not None:
956
  ghost_np = np.clip(ghost_mask.astype(np.float32), 0.0, 1.0)
 
1137
  if mask_np.ndim == 3:
1138
  mask_np = mask_np.squeeze()
1139
  mask_np = np.clip(mask_np, 0.0, 1.0)
1140
+ mask_np = _apply_radial_falloff(mask_np)
1141
  if trail_mask is None:
1142
  trail_mask = np.zeros_like(mask_np, dtype=np.float32)
1143
  if trail_mask.shape != mask_np.shape:
 
1169
  return cx, cy
1170
 
1171
 
1172
+ def _apply_radial_falloff(mask: np.ndarray, strength: float = 1.5) -> np.ndarray:
1173
+ if mask is None:
1174
+ return None
1175
+ mask_np = np.clip(mask.astype(np.float32), 0.0, 1.0)
1176
+ if mask_np.ndim == 3:
1177
+ mask_np = mask_np.squeeze()
1178
+ if mask_np.max() <= FX_EPS:
1179
+ return mask_np
1180
+
1181
+ centroid = _compute_mask_centroid(mask_np)
1182
+ if centroid is None:
1183
+ return mask_np
1184
+ cx, cy = centroid
1185
+
1186
+ h, w = mask_np.shape
1187
+ yy, xx = np.ogrid[:h, :w]
1188
+ dist = np.sqrt((xx - cx) ** 2 + (yy - cy) ** 2)
1189
+ max_dist = dist[mask_np > FX_EPS].max() if np.any(mask_np > FX_EPS) else 0.0
1190
+ if max_dist <= FX_EPS:
1191
+ return mask_np
1192
+ falloff = 1.0 - np.clip(dist / max_dist, 0.0, 1.0)
1193
+ falloff = np.power(falloff, strength)
1194
+ return np.clip(mask_np * falloff, 0.0, 1.0)
1195
+
1196
+
1197
  def _update_centroids_for_frame(state: AppState, frame_idx: int):
1198
  if state is None:
1199
  return