Cursor Agent commited on
Commit
b4ffed7
·
1 Parent(s): 8aaaae2

Enhance ball rings with neon glow effect (additive blending)

Browse files
Files changed (1) hide show
  1. app.py +64 -25
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 ring_presence is None:
1279
- ring_presence = np.zeros_like(mask_np, dtype=np.float32)
1280
- if ring_presence.shape != mask_np.shape:
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
- if ring_color_map is None:
1308
- h, w = mask_np.shape
1309
- ring_color_map = np.zeros((h, w, 3), dtype=np.float32)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- ring_color_map = np.clip(ring_color_map, 0.0, 1.0)
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):