Mirko Trasciatti
commited on
Commit
·
6ae1c1a
1
Parent(s):
bc8994a
Feather ball mask for live and ghost overlays
Browse files
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
|