Spaces:
Running
Running
| """Event-level 評估:預測事件與 GT 事件的配對與指標計算。 | |
| 協定(README 同步聲明): | |
| - 預測事件與「GT 事件 ± tol」有任何時間交集即為候選配對; | |
| - 每個 GT 以最大交集貪婪配一個預測(一對一); | |
| - TP = 配對成功的 GT 數;FN = 未配對 GT; | |
| FP = fall 影片中未配對的預測 + ADL 影片中的所有預測 | |
| (同一 GT 的重複預測算 FP:懲罰事件碎裂); | |
| - 另報 video-level specificity(無任何預測事件的 ADL 影片比例),對齊文獻報法。 | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass, field | |
| Interval = tuple[float, float] | |
| def interval_overlap(a: Interval, b: Interval) -> float: | |
| """兩區間的交集長度(秒);不相交為 0。""" | |
| lo = max(a[0], b[0]) | |
| hi = min(a[1], b[1]) | |
| return max(0.0, hi - lo) | |
| class MatchResult: | |
| tp: int | |
| fp: int | |
| fn: int | |
| matches: list[tuple[Interval, Interval]] = field(default_factory=list) # (pred, gt) | |
| def match_events( | |
| preds: list[Interval], gts: list[Interval], tol_s: float = 0.5 | |
| ) -> MatchResult: | |
| """單支影片內的貪婪一對一配對。""" | |
| used = [False] * len(preds) | |
| matches: list[tuple[Interval, Interval]] = [] | |
| for gt in sorted(gts): | |
| expanded = (gt[0] - tol_s, gt[1] + tol_s) | |
| best_i, best_ov = None, 0.0 | |
| for i, p in enumerate(preds): | |
| if used[i]: | |
| continue | |
| ov = interval_overlap(p, expanded) | |
| if ov > best_ov: | |
| best_i, best_ov = i, ov | |
| if best_i is not None and best_ov > 0.0: | |
| used[best_i] = True | |
| matches.append((preds[best_i], gt)) | |
| tp = len(matches) | |
| fn = len(gts) - tp | |
| fp = sum(1 for u in used if not u) | |
| return MatchResult(tp=tp, fp=fp, fn=fn, matches=matches) | |
| def evaluate_videos(videos: list[dict], tol_s: float = 0.5) -> dict: | |
| """彙整多支影片的 event-level 指標。 | |
| Args: | |
| videos: 每支影片一個 dict: | |
| ``{"name": str, "is_adl": bool, "preds": [(s,e)...], "gts": [(s,e)...]}`` | |
| tol_s: GT 區間兩端的容忍(秒)。 | |
| Returns: | |
| 指標 dict(precision/recall/f1 在分母為 0 時為 None,不假造數字), | |
| 含 per-video 明細供失敗分析挑案例。 | |
| """ | |
| tp = fp = fn = 0 | |
| adl_total = adl_clean = 0 | |
| per_video = [] | |
| for v in videos: | |
| res = match_events(list(v.get("preds", [])), list(v.get("gts", [])), tol_s) | |
| tp += res.tp | |
| fp += res.fp | |
| fn += res.fn | |
| if v.get("is_adl", False): | |
| adl_total += 1 | |
| if not v.get("preds"): | |
| adl_clean += 1 | |
| per_video.append( | |
| { | |
| "name": v["name"], | |
| "is_adl": bool(v.get("is_adl", False)), | |
| "tp": res.tp, | |
| "fp": res.fp, | |
| "fn": res.fn, | |
| "preds": [list(p) for p in v.get("preds", [])], | |
| "gts": [list(g) for g in v.get("gts", [])], | |
| } | |
| ) | |
| precision = tp / (tp + fp) if (tp + fp) > 0 else None | |
| recall = tp / (tp + fn) if (tp + fn) > 0 else None | |
| f1 = ( | |
| 2 * precision * recall / (precision + recall) | |
| if precision is not None and recall is not None and (precision + recall) > 0 | |
| else None | |
| ) | |
| specificity = adl_clean / adl_total if adl_total > 0 else None | |
| return { | |
| "tol_s": tol_s, | |
| "tp": tp, | |
| "fp": fp, | |
| "fn": fn, | |
| "precision": precision, | |
| "recall": recall, | |
| "f1": f1, | |
| "video_level_specificity": specificity, | |
| "n_videos": len(videos), | |
| "n_adl_videos": adl_total, | |
| "per_video": per_video, | |
| } | |