marquee / events.py
mamuflih13's picture
chores
4778b0b
Raw
History Blame Contribute Delete
2.19 kB
import numpy as np
def _smooth(values: np.ndarray, k: int = 3) -> np.ndarray:
"""Moving average over the sampled motion. Kills 1-frame spikes."""
if k < 2 or len(values) < k:
return values
return np.convolve(values, np.ones(k) / k, mode="same")
def pick_key_events(motion: dict, fps: float, max_events: int = 10,
min_gap_sec: float = 1.5, pct_floor: float = 60.0) -> list[float]:
"""motion: {frame_idx: score}. Returns sorted list of timestamps (sec).
pct_floor: only frames above this percentile of (smoothed) motion can be
events — tune up for "only the biggest moments", down for "more chatter".
"""
if not motion:
return [0.5]
frames = sorted(motion)
scores = _smooth(np.array([motion[f] for f in frames], dtype=float))
total_sec = frames[-1] / fps if fps else 0.0
# how many events does a clip this long deserve?
cap = max(1, min(max_events, int(total_sec / min_gap_sec) or 1))
floor = np.percentile(scores, pct_floor)
# local maxima above the floor = genuine "moments"
peaks = []
n = len(frames)
for i in range(n):
s = scores[i]
if s < floor:
continue
left = scores[i - 1] if i > 0 else -np.inf
right = scores[i + 1] if i < n - 1 else -np.inf
if s >= left and s >= right:
peaks.append((frames[i], s))
if not peaks: # flat clip -> evenly spaced
if total_sec <= 0:
return [0.5]
return [round((i + 0.5) * total_sec / cap, 2) for i in range(cap)]
# strongest first, then suppress neighbours within min_gap (spread them out)
peaks.sort(key=lambda kv: kv[1], reverse=True)
min_gap_frames = int(min_gap_sec * fps)
chosen: list[int] = []
for frame_idx, _ in peaks:
if all(abs(frame_idx - c) >= min_gap_frames for c in chosen):
chosen.append(frame_idx)
if len(chosen) >= cap:
break
chosen.sort()
return [round(c / fps, 2) for c in chosen]
def events_to_frames(timestamps: list[float], fps: float) -> list[int]:
return [int(round(ts * fps)) for ts in timestamps]