| |
| """Build per-frame CoT+Belief JSONL for DoTA clips. |
| |
| DoTA structure: |
| DoTA/frames/{clip}/images/000000.jpg, 000001.jpg, ... |
| DoTA/annotations/{clip}.json — has anomaly_start/end (frame idx), ego_involve, |
| accident_name, night, per-frame object list |
| DoTA/train_split.txt, val_split.txt |
| |
| We emit one JSONL record per clip with: |
| { |
| "id": str (clip name), |
| "label": int (1 = ego-involved anomaly, 0 = normal segment BEFORE anomaly), |
| "video_path": str (absolute path to frames dir), |
| "cot": {"scene": ..., "critical_objects": [...], "threat_analysis": ...}, |
| "belief": {"action": str, |
| "actions_per_frame": [T str], |
| "frame_times_sec": [T float], |
| "time_of_event_sec": float, |
| "alert_s": float, |
| "observe_s": float} |
| } |
| |
| CoT content is a rule-based placeholder (no teacher model). Replace in a later |
| pass if the budget allows running GPT-4o over DoTA val. |
| |
| Usage: |
| python -m training.VLA.build_dota_cot \ |
| --dota_root DoTA \ |
| --split val \ |
| --n_frames 8 \ |
| --fps 10 \ |
| --alert_s 0.5 --observe_s 2.5 \ |
| --output data/vla_cot/dota_val_perframe.jsonl |
| """ |
| from __future__ import annotations |
|
|
| import argparse |
| import json |
| from pathlib import Path |
| from typing import Any, Dict, List, Optional |
|
|
| import numpy as np |
|
|
| from training.VLA.augment_cot_with_belief import ( |
| derive_clip_action, per_frame_actions, |
| ) |
|
|
|
|
| |
| NIGHT_STR = {True: "night", False: "daytime"} |
|
|
|
|
| def _threat_from_accident(accident_name: str, ego_involve: bool) -> str: |
| nm = (accident_name or "").replace("_", " ").strip() |
| if not nm: |
| return "Possible traffic anomaly ahead; trajectory appears unstable." |
| if ego_involve: |
| return (f"Ego vehicle is at elevated collision risk: the scene shows " |
| f"a '{nm}' pattern developing in the ego lane.") |
| return (f"Nearby agents exhibit a '{nm}' pattern; ego not directly involved " |
| "but secondary risk cannot be ruled out.") |
|
|
|
|
| def _scene_str(accident_name: str, night: bool) -> str: |
| tag = "night" if night else "daytime" |
| nm = (accident_name or "unknown").replace("_", " ") |
| return f"Dashcam view, {tag}; anomaly pattern: {nm}." |
|
|
|
|
| def _critical_from_labels(labels: List[Dict[str, Any]], anomaly_start: int, |
| n_keep: int = 4) -> List[str]: |
| """Pick up to n_keep distinct object classes that appear near anomaly_start.""" |
| names: Dict[str, int] = {} |
| if not labels: |
| return [] |
| lo = max(0, anomaly_start - 5) |
| hi = min(len(labels), anomaly_start + 3) |
| for lab in labels[lo:hi]: |
| for obj in lab.get("objects", []) or []: |
| cat = obj.get("category") or obj.get("label") or obj.get("accident_name") |
| if cat and cat != "normal": |
| names[cat] = names.get(cat, 0) + 1 |
| if not names: |
| return [] |
| ranked = sorted(names.items(), key=lambda kv: -kv[1]) |
| return [k for k, _ in ranked[:n_keep]] |
|
|
|
|
| def _uniform_indices(total: int, n: int) -> List[int]: |
| return np.linspace(0, total - 1, n).round().astype(int).tolist() |
|
|
|
|
| def build_record( |
| clip_name: str, |
| ann: Dict[str, Any], |
| frames_root: Path, |
| n_frames: int, |
| fps: float, |
| alert_s: float, |
| observe_s: float, |
| event_anchored: bool = True, |
| lookback_s: float = 3.0, |
| post_margin_s: float = 0.0, |
| teacher_cot: Optional[Dict[str, Any]] = None, |
| ) -> Optional[Dict[str, Any]]: |
| total = int(ann.get("num_frames", 0)) |
| if total <= 0: |
| return None |
| ego = bool(ann.get("ego_involve", False)) |
| night = bool(ann.get("night", False)) |
| acc_name = str(ann.get("accident_name") or ann.get("anomaly_class") or "") |
| anomaly_start = int(ann.get("anomaly_start", -1)) |
|
|
| |
| |
| if not ego: |
| return None |
| label = 1 |
|
|
| |
| toe = float(anomaly_start) / float(fps) if anomaly_start >= 0 else None |
|
|
| if event_anchored and toe is not None: |
| end_s = min(float(total - 1) / fps, toe + post_margin_s) |
| start_s = max(0.0, end_s - (lookback_s + post_margin_s)) |
| times = np.linspace(start_s, end_s, n_frames) |
| idx = [int(round(t * fps)) for t in times] |
| sampling = "event_anchored" |
| else: |
| idx = _uniform_indices(total, n_frames) |
| sampling = "uniform" |
| idx = [max(0, min(total - 1, int(i))) for i in idx] |
| frame_times = [float(i) / float(fps) for i in idx] |
| actions_pf = per_frame_actions(label=label, |
| time_of_event=toe, |
| frame_times=frame_times, |
| alert_s=alert_s, |
| observe_s=observe_s) |
|
|
| |
| if "ALERT" in actions_pf: |
| clip_action = "ALERT" |
| elif "OBSERVE" in actions_pf: |
| clip_action = "OBSERVE" |
| else: |
| clip_action = "SILENT" |
|
|
| if teacher_cot is not None and isinstance(teacher_cot, dict) \ |
| and teacher_cot.get("scene") and teacher_cot.get("threat_analysis"): |
| cot = { |
| "scene": str(teacher_cot.get("scene", "")).strip(), |
| "critical_objects": list(teacher_cot.get("critical_objects", []) or []), |
| "threat_analysis": str(teacher_cot.get("threat_analysis", "")).strip(), |
| "source": "gpt_teacher", |
| } |
| else: |
| cot = { |
| "scene": _scene_str(acc_name, night), |
| "critical_objects": _critical_from_labels(ann.get("labels", []) or [], |
| anomaly_start), |
| "threat_analysis": _threat_from_accident(acc_name, ego), |
| "source": "rule_template", |
| } |
| belief = { |
| "action": clip_action, |
| "tta_sec": round(toe, 3) if toe is not None else -1.0, |
| "actions_per_frame": actions_pf, |
| "frame_indices": idx, |
| "frame_times_sec": [round(t, 3) for t in frame_times], |
| "time_of_event_sec": round(toe, 3) if toe is not None else -1.0, |
| "alert_s": alert_s, |
| "observe_s": observe_s, |
| "sampling": sampling, |
| "total_frames": total, |
| "fps": round(float(fps), 3), |
| "lookback_s": lookback_s, |
| "post_margin_s": post_margin_s, |
| } |
|
|
| return { |
| "id": f"dota_{clip_name}", |
| "label": label, |
| "video_path": str(frames_root / clip_name / "images"), |
| "cot": cot, |
| "belief": belief, |
| "source": "dota", |
| "accident_name": acc_name, |
| } |
|
|
|
|
| def main(): |
| ap = argparse.ArgumentParser() |
| ap.add_argument("--dota_root", default="DoTA") |
| ap.add_argument("--split", choices=["train", "val"], default="val") |
| ap.add_argument("--n_frames", type=int, default=8) |
| ap.add_argument("--fps", type=float, default=10.0) |
| ap.add_argument("--alert_s", type=float, default=0.5) |
| ap.add_argument("--observe_s", type=float, default=2.5) |
| ap.add_argument("--output", required=True) |
| ap.add_argument("--event_anchored", action="store_true", default=True, |
| help="Sample T frames in [event - lookback, event + post_margin]") |
| ap.add_argument("--no_event_anchored", dest="event_anchored", |
| action="store_false") |
| ap.add_argument("--lookback_s", type=float, default=3.0) |
| ap.add_argument("--post_margin_s", type=float, default=0.0) |
| ap.add_argument("--teacher_json", type=str, default="", |
| help="Optional JSON mapping clip_name -> {cot, usage, ...} " |
| "from GPT distillation; overrides rule template when present") |
| ap.add_argument("--limit", type=int, default=0, |
| help="If >0, only emit first N records (smoke test)") |
| args = ap.parse_args() |
|
|
| root = Path(args.dota_root).resolve() |
| split_file = root / f"{args.split}_split.txt" |
| ann_dir = root / "annotations" |
| frames_root = root / "frames" |
|
|
| with open(split_file) as f: |
| clips = [ln.strip() for ln in f if ln.strip()] |
| print(f"[dota] {args.split}: {len(clips)} clip ids") |
|
|
| teacher_map: Dict[str, Dict[str, Any]] = {} |
| if args.teacher_json: |
| tp = Path(args.teacher_json) |
| if tp.exists(): |
| raw = json.loads(tp.read_text()) |
| for k, v in raw.items(): |
| if isinstance(v, dict) and v.get("cot"): |
| teacher_map[k] = v["cot"] |
| print(f"[dota] teacher CoT loaded: {len(teacher_map)} clips from {tp}") |
| else: |
| print(f"[dota] WARN: teacher_json not found: {tp}") |
|
|
| out_path = Path(args.output) |
| out_path.parent.mkdir(parents=True, exist_ok=True) |
|
|
| n_emitted = 0 |
| n_missing = 0 |
| n_no_ego = 0 |
| with open(out_path, "w") as f_out: |
| for clip_name in clips: |
| ann_path = ann_dir / f"{clip_name}.json" |
| if not ann_path.exists(): |
| n_missing += 1 |
| continue |
| with open(ann_path) as f: |
| ann = json.load(f) |
| frames_dir = frames_root / clip_name / "images" |
| if not frames_dir.exists(): |
| n_missing += 1 |
| continue |
| rec = build_record( |
| clip_name=clip_name, |
| ann=ann, |
| frames_root=frames_root, |
| n_frames=args.n_frames, |
| fps=args.fps, |
| alert_s=args.alert_s, |
| observe_s=args.observe_s, |
| event_anchored=args.event_anchored, |
| lookback_s=args.lookback_s, |
| post_margin_s=args.post_margin_s, |
| teacher_cot=teacher_map.get(clip_name), |
| ) |
| if rec is None: |
| n_no_ego += 1 |
| continue |
| f_out.write(json.dumps(rec) + "\n") |
| n_emitted += 1 |
| if args.limit and n_emitted >= args.limit: |
| break |
|
|
| print(f"[dota] emitted {n_emitted} ego-involved clips; " |
| f"skipped non-ego={n_no_ego} missing={n_missing}") |
| print(f"[dota] saved -> {out_path}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|