| import json
|
| import os
|
| from typing import Dict, List, Tuple, Optional, Iterable
|
| from tqdm import tqdm
|
|
|
|
|
| PRED_PATH = "eval/proactive/streaming_po/new_results/spo_resut.jsonl"
|
| GT_PATH = "spo_gt.jsonl"
|
|
|
| THRESHOLDS = [0.5, 0.6, 0.7, 0.8]
|
| WINDOW_SIZE = 5
|
|
|
|
|
| def load_jsonl(path: str) -> List[dict]:
|
| data = []
|
| with open(path, "r", encoding="utf-8") as f:
|
| for line in f:
|
| line = line.strip()
|
| if not line:
|
| continue
|
| try:
|
| data.append(json.loads(line))
|
| except json.JSONDecodeError:
|
| pass
|
| return data
|
|
|
| def load_json_or_jsonl(path: str) -> List[dict]:
|
| """Support both JSON array and JSONL formats"""
|
| with open(path, "r", encoding="utf-8") as f:
|
| head = f.read(1)
|
| f.seek(0)
|
| if head == '[':
|
| return json.load(f)
|
| else:
|
| return load_jsonl(path)
|
|
|
| def build_gt_index(gt_items: Iterable[dict]) -> Dict[str, Tuple[int, int]]:
|
| """Build GT index {id: (start, end)}"""
|
| gt = {}
|
| for it in gt_items:
|
| vid = str(it.get("id"))
|
| seg = None
|
| ans = it.get("answer", [])
|
|
|
|
|
| if isinstance(ans, dict):
|
| ans_list = [ans]
|
| elif isinstance(ans, list):
|
| ans_list = ans
|
| else:
|
| continue
|
|
|
| if ans_list:
|
|
|
|
|
| first_ans = ans_list[0]
|
| seg_field = first_ans.get("segment")
|
|
|
| if isinstance(seg_field, list) and len(seg_field) > 0:
|
| if isinstance(seg_field[0], list):
|
| s, e = seg_field[0]
|
| elif len(seg_field) == 2 and isinstance(seg_field[0], (int, float)):
|
| s, e = seg_field
|
| else:
|
| continue
|
| seg = (int(s), int(e))
|
|
|
| if seg is not None:
|
| gt[vid] = seg
|
| return gt
|
|
|
| def get_sliding_triggers(raw: List[float], thr: float, w: int) -> List[int]:
|
| """
|
| Return all trigger points filtered by sliding window (using the window end index).
|
| Logic: if w consecutive points >= threshold, record the index of the w-th point.
|
| """
|
| if w <= 0 or w > len(raw):
|
| return []
|
|
|
| triggers = []
|
| count = 0
|
|
|
|
|
| for i in range(w):
|
| if raw[i] >= thr:
|
| count += 1
|
| if count == w:
|
| triggers.append(w - 1)
|
|
|
|
|
| for end in range(w, len(raw)):
|
|
|
| if raw[end - w] >= thr:
|
| count -= 1
|
|
|
| if raw[end] >= thr:
|
| count += 1
|
|
|
| if count == w:
|
| triggers.append(end)
|
|
|
| return triggers
|
|
|
| def count_distinct_groups(indices: List[int]) -> int:
|
| """
|
| Count the number of independent trigger groups.
|
| [10, 11, 12] -> 1 group
|
| [10, 11, 50, 51] -> 2 groups
|
| """
|
| if not indices:
|
| return 0
|
| groups = 1
|
| for i in range(1, len(indices)):
|
| if indices[i] - indices[i - 1] > 1:
|
| groups += 1
|
| return groups
|
|
|
| def main():
|
| print(f"Loading predictions from: {PRED_PATH}")
|
| preds = load_jsonl(PRED_PATH)
|
|
|
| print(f"Loading ground-truth from: {GT_PATH}")
|
| gt_items = load_json_or_jsonl(GT_PATH)
|
| gt = build_gt_index(gt_items)
|
|
|
|
|
|
|
| stats = {thr: {
|
| 'total': 0,
|
| 'correct': 0,
|
| 'early': 0,
|
| 'late': 0,
|
| 'miss': 0,
|
| 'repeated': 0
|
| } for thr in THRESHOLDS}
|
|
|
| missing_gt_count = 0
|
| used_count = 0
|
|
|
| for item in tqdm(preds, desc="Evaluating"):
|
| vid = str(item.get("id"))
|
| raw = item.get("raw_probs", [])
|
| if not isinstance(raw, list) or not raw:
|
| continue
|
|
|
| seg = gt.get(vid)
|
| if seg is None:
|
| missing_gt_count += 1
|
| continue
|
|
|
| used_count += 1
|
| start, end = seg
|
|
|
| for thr in THRESHOLDS:
|
| stats[thr]['total'] += 1
|
|
|
|
|
|
|
| triggers = get_sliding_triggers(raw, thr, WINDOW_SIZE)
|
|
|
|
|
| if not triggers:
|
| stats[thr]['miss'] += 1
|
| continue
|
|
|
|
|
|
|
| first_idx = triggers[0]
|
|
|
| if first_idx < start:
|
| stats[thr]['early'] += 1
|
| elif first_idx > end:
|
| stats[thr]['late'] += 1
|
| else:
|
| stats[thr]['correct'] += 1
|
|
|
|
|
|
|
| num_groups = count_distinct_groups(triggers)
|
| if num_groups > 1:
|
| stats[thr]['repeated'] += 1
|
|
|
| print("\n" + "=" * 95)
|
| print(f"ERROR ANALYSIS REPORT (Window Size = {WINDOW_SIZE})")
|
| print(f"Total Preds: {len(preds)} | Used (Has GT): {used_count} | Missing GT: {missing_gt_count}")
|
| print("=" * 95)
|
|
|
|
|
|
|
| header = f"{'Thr':<5} | {'Acc/Correct':<12} | {'Early':<10} | {'Late':<10} | {'Missed':<10} | {'Repeated%':<10}"
|
| print(header)
|
| print("-" * len(header))
|
|
|
| for thr in THRESHOLDS:
|
| s = stats[thr]
|
| total = s['total']
|
| if total == 0:
|
| continue
|
|
|
| p_corr = s['correct'] / total * 100
|
| p_early = s['early'] / total * 100
|
| p_late = s['late'] / total * 100
|
| p_miss = s['miss'] / total * 100
|
| p_rep = s['repeated'] / total * 100
|
|
|
| print(f"{thr:<5.1f} | {p_corr:<12.1f} | {p_early:<10.1f} | {p_late:<10.1f} | {p_miss:<10.1f} | {p_rep:<10.1f}")
|
|
|
| print("=" * 95)
|
| print("Metrics Definition:")
|
| print("1. Correct: First trigger (after window) falls inside [Start, End].")
|
| print("2. Early: First trigger falls before Start.")
|
| print("3. Late: First trigger falls after End.")
|
| print("4. Missed: No trigger detected throughout the video.")
|
| print("5. Repeated%: Percentage of videos where distinct triggers occurred more than once.")
|
|
|
| if __name__ == "__main__":
|
| main() |