Spaces:
Runtime error
Runtime error
Cursor Agent commited on
Commit ·
b4ffed7
1
Parent(s): 8aaaae2
Enhance ball rings with neon glow effect (additive blending)
Browse files
app.py
CHANGED
|
@@ -790,6 +790,39 @@ def _speed_to_ring_color(speed_kmh: float) -> tuple[float, float, float]:
|
|
| 790 |
return SPEED_COLOR_ABOVE_MAX
|
| 791 |
|
| 792 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 793 |
def _angle_between(v1: tuple[float, float], v2: tuple[float, float]) -> float:
|
| 794 |
x1, y1 = v1
|
| 795 |
x2, y2 = v2
|
|
@@ -824,6 +857,7 @@ SPEED_COLOR_STOPS = [
|
|
| 824 |
(130.0, (255 / 255.0, 77 / 255.0, 109 / 255.0)), # Strong Pink
|
| 825 |
]
|
| 826 |
SPEED_COLOR_ABOVE_MAX = (255 / 255.0, 162 / 255.0, 0 / 255.0) # Neon Orange
|
|
|
|
| 827 |
|
| 828 |
|
| 829 |
def _maybe_upscale_for_display(image: Image.Image) -> Image.Image:
|
|
@@ -1252,6 +1286,8 @@ def _build_ball_ring_mask(
|
|
| 1252 |
if start_idx >= end_idx:
|
| 1253 |
return None
|
| 1254 |
|
|
|
|
|
|
|
| 1255 |
ring_presence: np.ndarray | None = None
|
| 1256 |
ring_color_map: np.ndarray | None = None
|
| 1257 |
fps = state.video_fps if state.video_fps and state.video_fps > 0 else 25.0
|
|
@@ -1275,9 +1311,9 @@ def _build_ball_ring_mask(
|
|
| 1275 |
mask_np = np.clip(mask_np, 0.0, 1.0)
|
| 1276 |
if mask_np.max() <= FX_EPS:
|
| 1277 |
continue
|
| 1278 |
-
if
|
| 1279 |
-
|
| 1280 |
-
if
|
| 1281 |
continue
|
| 1282 |
centroid = _compute_mask_centroid(mask_np)
|
| 1283 |
if centroid is None:
|
|
@@ -1304,50 +1340,53 @@ def _build_ball_ring_mask(
|
|
| 1304 |
speed_kmh = max(0.0, (distance_m / time_s) * 3.6)
|
| 1305 |
color_vec = np.array(_speed_to_ring_color(speed_kmh), dtype=np.float32)
|
| 1306 |
|
| 1307 |
-
|
| 1308 |
-
|
| 1309 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1310 |
|
| 1311 |
-
ring_local = np.zeros_like(mask_np, dtype=np.float32)
|
| 1312 |
-
|
| 1313 |
-
# Multi-pass drawing for neon effect: Glow -> Mid -> Core
|
| 1314 |
-
# 1. Outer Glow
|
| 1315 |
t_glow = max(1, int(round(thick_val * 4.0)))
|
| 1316 |
cv2.circle(ring_local, center, radius_int, 0.3, thickness=t_glow)
|
| 1317 |
-
|
| 1318 |
-
# 2. Inner Glow
|
| 1319 |
t_mid = max(1, int(round(thick_val * 2.0)))
|
| 1320 |
cv2.circle(ring_local, center, radius_int, 0.6, thickness=t_mid)
|
| 1321 |
|
| 1322 |
-
# 3. Core (Sharp)
|
| 1323 |
t_core = max(1, int(round(thick_val)))
|
| 1324 |
cv2.circle(ring_local, center, radius_int, 1.0, thickness=t_core)
|
| 1325 |
|
| 1326 |
-
feather_val = getattr(state, "fx_ring_feather", BALL_RING_FEATHER_SIGMA)
|
| 1327 |
ring_local = cv2.GaussianBlur(ring_local, (0, 0), sigmaX=feather_val, sigmaY=feather_val)
|
| 1328 |
if ring_local.max() <= FX_EPS:
|
| 1329 |
continue
|
| 1330 |
|
| 1331 |
-
gamma_val = getattr(state, "fx_ring_gamma", BALL_RING_INTENSITY_GAMMA)
|
| 1332 |
if abs(gamma_val - 1.0) > 1e-6:
|
| 1333 |
ring_local = np.power(np.clip(ring_local, 0.0, 1.0), gamma_val)
|
| 1334 |
|
| 1335 |
ring_local = np.clip(ring_local, 0.0, 1.0)
|
| 1336 |
-
|
| 1337 |
ring_presence = np.maximum(ring_presence, ring_local)
|
| 1338 |
ring_color_map += ring_local[..., None] * color_vec
|
| 1339 |
|
| 1340 |
-
if (
|
| 1341 |
-
ring_presence is None
|
| 1342 |
-
or ring_color_map is None
|
| 1343 |
-
or ring_presence.max() <= FX_EPS
|
| 1344 |
-
):
|
| 1345 |
return None
|
| 1346 |
|
| 1347 |
-
|
| 1348 |
-
ring_presence = np.clip(ring_presence, 0.0, 1.0)
|
| 1349 |
-
|
| 1350 |
-
return ring_presence, ring_color_map
|
| 1351 |
|
| 1352 |
|
| 1353 |
def _ensure_color_for_obj(state: AppState, obj_id: int):
|
|
|
|
| 790 |
return SPEED_COLOR_ABOVE_MAX
|
| 791 |
|
| 792 |
|
| 793 |
+
def _median_smooth_radii(radii: list[float]) -> list[float]:
|
| 794 |
+
if not radii:
|
| 795 |
+
return []
|
| 796 |
+
if len(radii) < 3:
|
| 797 |
+
return radii[:]
|
| 798 |
+
smoothed: list[float] = []
|
| 799 |
+
n = len(radii)
|
| 800 |
+
for i in range(n):
|
| 801 |
+
window = radii[max(0, i - 1):min(n, i + 2)]
|
| 802 |
+
smoothed.append(float(statistics.median(window)))
|
| 803 |
+
return smoothed
|
| 804 |
+
|
| 805 |
+
|
| 806 |
+
def _clamp_radii(radii: list[float], clamp_ratio: float = RING_RADIUS_CLAMP_RATIO) -> list[float]:
|
| 807 |
+
if not radii:
|
| 808 |
+
return []
|
| 809 |
+
clamped: list[float] = []
|
| 810 |
+
for i, value in enumerate(radii):
|
| 811 |
+
val = max(0.0, float(value))
|
| 812 |
+
if i == 0:
|
| 813 |
+
clamped.append(val)
|
| 814 |
+
continue
|
| 815 |
+
prev = clamped[-1]
|
| 816 |
+
min_allowed = prev * (1.0 - clamp_ratio)
|
| 817 |
+
max_allowed = prev * (1.0 + clamp_ratio)
|
| 818 |
+
if prev <= FX_EPS:
|
| 819 |
+
min_allowed = 0.0
|
| 820 |
+
max_allowed = val
|
| 821 |
+
val = min(max(val, min_allowed), max_allowed)
|
| 822 |
+
clamped.append(val)
|
| 823 |
+
return clamped
|
| 824 |
+
|
| 825 |
+
|
| 826 |
def _angle_between(v1: tuple[float, float], v2: tuple[float, float]) -> float:
|
| 827 |
x1, y1 = v1
|
| 828 |
x2, y2 = v2
|
|
|
|
| 857 |
(130.0, (255 / 255.0, 77 / 255.0, 109 / 255.0)), # Strong Pink
|
| 858 |
]
|
| 859 |
SPEED_COLOR_ABOVE_MAX = (255 / 255.0, 162 / 255.0, 0 / 255.0) # Neon Orange
|
| 860 |
+
RING_RADIUS_CLAMP_RATIO = 0.2 # ±20%
|
| 861 |
|
| 862 |
|
| 863 |
def _maybe_upscale_for_display(image: Image.Image) -> Image.Image:
|
|
|
|
| 1286 |
if start_idx >= end_idx:
|
| 1287 |
return None
|
| 1288 |
|
| 1289 |
+
ring_entries: list[tuple[int, tuple[int, int], float, np.ndarray, float]] = []
|
| 1290 |
+
canvas_shape: tuple[int, int] | None = None
|
| 1291 |
ring_presence: np.ndarray | None = None
|
| 1292 |
ring_color_map: np.ndarray | None = None
|
| 1293 |
fps = state.video_fps if state.video_fps and state.video_fps > 0 else 25.0
|
|
|
|
| 1311 |
mask_np = np.clip(mask_np, 0.0, 1.0)
|
| 1312 |
if mask_np.max() <= FX_EPS:
|
| 1313 |
continue
|
| 1314 |
+
if canvas_shape is None:
|
| 1315 |
+
canvas_shape = mask_np.shape
|
| 1316 |
+
if canvas_shape != mask_np.shape:
|
| 1317 |
continue
|
| 1318 |
centroid = _compute_mask_centroid(mask_np)
|
| 1319 |
if centroid is None:
|
|
|
|
| 1340 |
speed_kmh = max(0.0, (distance_m / time_s) * 3.6)
|
| 1341 |
color_vec = np.array(_speed_to_ring_color(speed_kmh), dtype=np.float32)
|
| 1342 |
|
| 1343 |
+
ring_entries.append((idx, center, radius, color_vec, thick_val))
|
| 1344 |
+
|
| 1345 |
+
if not ring_entries or canvas_shape is None:
|
| 1346 |
+
return None
|
| 1347 |
+
|
| 1348 |
+
raw_radii = [entry[2] for entry in ring_entries]
|
| 1349 |
+
smoothed = _median_smooth_radii(raw_radii)
|
| 1350 |
+
smoothed = _clamp_radii(smoothed)
|
| 1351 |
+
|
| 1352 |
+
h, w = canvas_shape
|
| 1353 |
+
ring_presence = np.zeros((h, w), dtype=np.float32)
|
| 1354 |
+
ring_color_map = np.zeros((h, w, 3), dtype=np.float32)
|
| 1355 |
+
|
| 1356 |
+
feather_val = getattr(state, "fx_ring_feather", BALL_RING_FEATHER_SIGMA)
|
| 1357 |
+
gamma_val = getattr(state, "fx_ring_gamma", BALL_RING_INTENSITY_GAMMA)
|
| 1358 |
+
|
| 1359 |
+
for (entry, smooth_radius) in zip(ring_entries, smoothed):
|
| 1360 |
+
_, center, _, color_vec, thick_val = entry
|
| 1361 |
+
radius_val = max(1.0, smooth_radius)
|
| 1362 |
+
radius_int = max(1, int(round(radius_val)))
|
| 1363 |
+
|
| 1364 |
+
ring_local = np.zeros((h, w), dtype=np.float32)
|
| 1365 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1366 |
t_glow = max(1, int(round(thick_val * 4.0)))
|
| 1367 |
cv2.circle(ring_local, center, radius_int, 0.3, thickness=t_glow)
|
| 1368 |
+
|
|
|
|
| 1369 |
t_mid = max(1, int(round(thick_val * 2.0)))
|
| 1370 |
cv2.circle(ring_local, center, radius_int, 0.6, thickness=t_mid)
|
| 1371 |
|
|
|
|
| 1372 |
t_core = max(1, int(round(thick_val)))
|
| 1373 |
cv2.circle(ring_local, center, radius_int, 1.0, thickness=t_core)
|
| 1374 |
|
|
|
|
| 1375 |
ring_local = cv2.GaussianBlur(ring_local, (0, 0), sigmaX=feather_val, sigmaY=feather_val)
|
| 1376 |
if ring_local.max() <= FX_EPS:
|
| 1377 |
continue
|
| 1378 |
|
|
|
|
| 1379 |
if abs(gamma_val - 1.0) > 1e-6:
|
| 1380 |
ring_local = np.power(np.clip(ring_local, 0.0, 1.0), gamma_val)
|
| 1381 |
|
| 1382 |
ring_local = np.clip(ring_local, 0.0, 1.0)
|
|
|
|
| 1383 |
ring_presence = np.maximum(ring_presence, ring_local)
|
| 1384 |
ring_color_map += ring_local[..., None] * color_vec
|
| 1385 |
|
| 1386 |
+
if ring_presence.max() <= FX_EPS or ring_color_map.max() <= FX_EPS:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1387 |
return None
|
| 1388 |
|
| 1389 |
+
return np.clip(ring_presence, 0.0, 1.0), np.clip(ring_color_map, 0.0, 1.0)
|
|
|
|
|
|
|
|
|
|
| 1390 |
|
| 1391 |
|
| 1392 |
def _ensure_color_for_obj(state: AppState, obj_id: int):
|