| 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 |
|
|
| |
| cap = max(1, min(max_events, int(total_sec / min_gap_sec) or 1)) |
|
|
| floor = np.percentile(scores, pct_floor) |
|
|
| |
| 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: |
| if total_sec <= 0: |
| return [0.5] |
| return [round((i + 0.5) * total_sec / cap, 2) for i in range(cap)] |
|
|
| |
| 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] |