| |
| """ |
| Generate deterministic train/val split manifests for SFT. |
| |
| Rules |
| ----- |
| - NEXAR train positive + negative β 85/15 hash split by video_id |
| - DADA positive β 85/15 hash split; exclude acc_frame >= num_frames |
| - DADA non-ego β 85/15 hash split; accident_time kept for sampling density only |
| - DADA negative (3 videos) β all train |
| - FOLDER ASSIGNMENT is the source of truth (ignore stale `accident` boolean) |
| - All timestamps are 20 Hz frame indices |
| |
| Outputs (in --out_dir) |
| ---------------------- |
| nexar_train.json, nexar_val.json |
| dada_pos_train.json, dada_pos_val.json |
| dada_noneego_train.json, dada_noneego_val.json |
| dada_neg_train.json |
| nexar_test_public.json (diagnostic only β NOT used for checkpoint selection) |
| """ |
|
|
| import argparse |
| import hashlib |
| import json |
| import logging |
| from datetime import date |
| from pathlib import Path |
| from typing import Any, Dict, List, Optional |
|
|
| logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") |
| logger = logging.getLogger(__name__) |
|
|
| FRAME_RATE_HZ = 20 |
|
|
|
|
| |
|
|
| def _hash_split(video_id: str, val_pct: int = 15) -> str: |
| """Deterministic split: val if MD5(video_id) % 100 < val_pct.""" |
| h = int(hashlib.md5(video_id.encode()).hexdigest(), 16) |
| return "val" if (h % 100) < val_pct else "train" |
|
|
|
|
| def _load_ann(path: Path) -> Optional[Dict[str, Any]]: |
| try: |
| return json.loads(path.read_text(encoding="utf-8", errors="ignore").lstrip("\ufeff")) |
| except Exception as e: |
| logger.debug(f"Failed to load {path}: {e}") |
| return None |
|
|
|
|
| def _safe_int(x: Any) -> Optional[int]: |
| if x is None: |
| return None |
| try: |
| return int(float(str(x).strip())) |
| except Exception: |
| return None |
|
|
|
|
| def _count_frames(vd: Path) -> int: |
| return sum(1 for f in vd.iterdir() if f.suffix.lower() in {".jpg", ".jpeg", ".png"}) |
|
|
|
|
| def _entry( |
| video_id: str, |
| source: str, |
| category: str, |
| source_dir: Path, |
| num_frames: int, |
| accident_frame: Optional[int], |
| risky_frame: Optional[int], |
| metadata: Dict[str, Any], |
| ) -> Dict[str, Any]: |
| return { |
| "video_id": video_id, |
| "source": source, |
| "category": category, |
| "source_dir": str(source_dir.resolve()), |
| "num_frames": num_frames, |
| "accident_frame": accident_frame, |
| "risky_frame": risky_frame, |
| "metadata": metadata, |
| } |
|
|
|
|
| def _meta(ann: Dict[str, Any]) -> Dict[str, Any]: |
| return { |
| "accident_type": ann.get("accident_type", ""), |
| "weather": ann.get("weather", ""), |
| "road_type": ann.get("road_type", ""), |
| "car_speed": ann.get("car_speed", ""), |
| "time_of_day": ann.get("time_of_day", ""), |
| } |
|
|
|
|
| |
|
|
| def process_nexar_train(nexar_root: Path, val_pct: int) -> Dict[str, List]: |
| splits: Dict[str, List] = {"train": [], "val": []} |
| train_dir = nexar_root / "train" |
| if not train_dir.exists(): |
| logger.warning(f"NEXAR train not found: {train_dir}") |
| return splits |
|
|
| for cat_folder, cat_label in [("positive", "ego_positive"), ("negative", "safe_neg")]: |
| cat_dir = train_dir / cat_folder |
| if not cat_dir.exists(): |
| continue |
| ok = skip = 0 |
| for vd in sorted(cat_dir.iterdir()): |
| if not vd.is_dir(): |
| continue |
| ann = _load_ann(vd / "annotation.json") |
| if ann is None: |
| continue |
| nf = _count_frames(vd) |
| if nf == 0: |
| continue |
|
|
| video_id = f"nexar_{vd.name}" |
|
|
| |
| if cat_label == "ego_positive": |
| acc = _safe_int(ann.get("accident_time_local") or ann.get("accident_time")) |
| rsk = _safe_int(ann.get("risky_time_local") or ann.get("risky_time")) |
| if acc is None or acc >= nf: |
| skip += 1 |
| continue |
| else: |
| acc = rsk = None |
|
|
| e = _entry(video_id, "nexar", cat_label, vd, nf, acc, rsk, _meta(ann)) |
| splits[_hash_split(video_id, val_pct)].append(e) |
| ok += 1 |
| logger.info(f" NEXAR train/{cat_folder}: {ok} ok, {skip} skipped") |
|
|
| return splits |
|
|
|
|
| def process_nexar_test_public(nexar_root: Path) -> List: |
| """Diagnostic only β NOT used for checkpoint selection.""" |
| entries = [] |
| test_dir = nexar_root / "test-public" |
| if not test_dir.exists(): |
| return entries |
|
|
| for cat_folder, cat_label in [("positive", "ego_positive"), ("negative", "safe_neg")]: |
| cat_dir = test_dir / cat_folder |
| if not cat_dir.exists(): |
| continue |
| for vd in sorted(cat_dir.iterdir()): |
| if not vd.is_dir(): |
| continue |
| ann = _load_ann(vd / "annotation.json") |
| if ann is None: |
| continue |
| nf = _count_frames(vd) |
| if nf == 0: |
| continue |
|
|
| video_id = f"nexar_{vd.name}" |
| if cat_label == "ego_positive": |
| acc = _safe_int(ann.get("accident_time_local") or ann.get("accident_time")) |
| rsk = _safe_int(ann.get("risky_time_local") or ann.get("risky_time")) |
| if acc is None or acc >= nf: |
| continue |
| else: |
| acc = rsk = None |
|
|
| entries.append(_entry(video_id, "nexar", cat_label, vd, nf, acc, rsk, _meta(ann))) |
|
|
| return entries |
|
|
|
|
| |
|
|
| def process_dada_positive(dada_root: Path, val_pct: int) -> Dict[str, List]: |
| splits: Dict[str, List] = {"train": [], "val": []} |
| pos_dir = dada_root / "positive" |
| if not pos_dir.exists(): |
| logger.warning(f"DADA positive not found: {pos_dir}") |
| return splits |
|
|
| ok = skip_nf = skip_acc = 0 |
| for vd in sorted(pos_dir.iterdir()): |
| if not vd.is_dir(): |
| continue |
| ann = _load_ann(vd / "annotation.json") |
| if ann is None: |
| continue |
| nf = _count_frames(vd) |
| if nf == 0: |
| skip_nf += 1 |
| continue |
|
|
| |
| acc = _safe_int(ann.get("accident_time")) |
| rsk = _safe_int(ann.get("risky_time")) |
|
|
| if acc is None or acc >= nf: |
| skip_acc += 1 |
| logger.debug(f"DADA pos skip {vd.name}: acc={acc}, nf={nf}") |
| continue |
|
|
| |
| if rsk is not None: |
| rsk = max(0, rsk) |
|
|
| video_id = f"dada_{vd.name}" |
| e = _entry(video_id, "dada", "ego_positive", vd, nf, acc, rsk, _meta(ann)) |
| splits[_hash_split(video_id, val_pct)].append(e) |
| ok += 1 |
|
|
| logger.info(f" DADA positive: {ok} ok, {skip_acc} invalid acc_frame, {skip_nf} no frames") |
| return splits |
|
|
|
|
| def process_dada_noneego(dada_root: Path, val_pct: int) -> Dict[str, List]: |
| splits: Dict[str, List] = {"train": [], "val": []} |
| ne_dir = dada_root / "non-ego" |
| if not ne_dir.exists(): |
| logger.warning(f"DADA non-ego not found: {ne_dir}") |
| return splits |
|
|
| ok = 0 |
| for vd in sorted(ne_dir.iterdir()): |
| if not vd.is_dir(): |
| continue |
| ann = _load_ann(vd / "annotation.json") |
| if ann is None: |
| continue |
| nf = _count_frames(vd) |
| if nf == 0: |
| continue |
|
|
| |
| |
| acc = _safe_int(ann.get("accident_time")) |
| rsk = _safe_int(ann.get("risky_time")) |
|
|
| |
| if acc is not None: |
| acc = min(max(0, acc), nf - 1) |
| if rsk is not None: |
| rsk = min(max(0, rsk), nf - 1) |
|
|
| video_id = f"dada_{vd.name}" |
| e = _entry(video_id, "dada", "non_ego", vd, nf, acc, rsk, _meta(ann)) |
| splits[_hash_split(video_id, val_pct)].append(e) |
| ok += 1 |
|
|
| logger.info(f" DADA non-ego: {ok} total") |
| return splits |
|
|
|
|
| def process_dada_negative(dada_root: Path) -> List: |
| """All DADA negatives go to train (only 3 videos).""" |
| entries = [] |
| neg_dir = dada_root / "negative" |
| if not neg_dir.exists(): |
| return entries |
|
|
| for vd in sorted(neg_dir.iterdir()): |
| if not vd.is_dir(): |
| continue |
| ann = _load_ann(vd / "annotation.json") |
| if ann is None: |
| continue |
| nf = _count_frames(vd) |
| if nf == 0: |
| continue |
|
|
| video_id = f"dada_{vd.name}" |
| entries.append(_entry(video_id, "dada", "safe_neg", vd, nf, None, None, _meta(ann))) |
|
|
| logger.info(f" DADA negative (all train): {len(entries)}") |
| return entries |
|
|
|
|
| |
|
|
| def write_manifest(out_dir: Path, name: str, split: str, videos: List) -> Path: |
| out_dir.mkdir(parents=True, exist_ok=True) |
| manifest = { |
| "name": name, |
| "split": split, |
| "generated_at": str(date.today()), |
| "frame_rate_hz": FRAME_RATE_HZ, |
| "num_videos": len(videos), |
| "category_counts": { |
| cat: sum(1 for v in videos if v["category"] == cat) |
| for cat in ("ego_positive", "non_ego", "safe_neg") |
| }, |
| "videos": videos, |
| } |
| path = out_dir / f"{name}.json" |
| path.write_text(json.dumps(manifest, indent=2)) |
| logger.info(f" β {path} ({len(videos)} videos)") |
| return path |
|
|
|
|
| |
|
|
| def main(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument("--nexar_root", default="PROJECT_ROOT/NEXAR_COLLISION/dataset") |
| parser.add_argument("--dada_root", default="PROJECT_ROOT/DADA-2000") |
| parser.add_argument("--out_dir", default="PROJECT_ROOT/data/sft_manifests") |
| parser.add_argument("--val_pct", type=int, default=15, help="Percent of videos in val (default 15)") |
| args = parser.parse_args() |
|
|
| nexar_root = Path(args.nexar_root) |
| dada_root = Path(args.dada_root) |
| out_dir = Path(args.out_dir) |
| val_pct = args.val_pct |
|
|
| logger.info("=" * 60) |
| logger.info("Generating SFT split manifests") |
| logger.info(f" NEXAR: {nexar_root}") |
| logger.info(f" DADA: {dada_root}") |
| logger.info(f" Out: {out_dir}") |
| logger.info(f" Val %: {val_pct}%") |
| logger.info("=" * 60) |
|
|
| |
| logger.info("Processing NEXAR train...") |
| nexar = process_nexar_train(nexar_root, val_pct) |
| write_manifest(out_dir, "nexar_train", "train", nexar["train"]) |
| write_manifest(out_dir, "nexar_val", "val", nexar["val"]) |
|
|
| logger.info("Processing NEXAR test-public (diagnostic)...") |
| nexar_test = process_nexar_test_public(nexar_root) |
| write_manifest(out_dir, "nexar_test_public", "test_public", nexar_test) |
|
|
| |
| logger.info("Processing DADA positive...") |
| dada_pos = process_dada_positive(dada_root, val_pct) |
| write_manifest(out_dir, "dada_pos_train", "train", dada_pos["train"]) |
| write_manifest(out_dir, "dada_pos_val", "val", dada_pos["val"]) |
|
|
| logger.info("Processing DADA non-ego...") |
| dada_ne = process_dada_noneego(dada_root, val_pct) |
| write_manifest(out_dir, "dada_noneego_train", "train", dada_ne["train"]) |
| write_manifest(out_dir, "dada_noneego_val", "val", dada_ne["val"]) |
|
|
| logger.info("Processing DADA negative...") |
| dada_neg = process_dada_negative(dada_root) |
| write_manifest(out_dir, "dada_neg_train", "train", dada_neg) |
|
|
| |
| n_pos_tr = (sum(1 for e in nexar["train"] if e["category"]=="ego_positive") |
| + len(dada_pos["train"])) |
| n_pos_val = (sum(1 for e in nexar["val"] if e["category"]=="ego_positive") |
| + len(dada_pos["val"])) |
| n_ne_tr = len(dada_ne["train"]) |
| n_ne_val = len(dada_ne["val"]) |
| n_neg_tr = (sum(1 for e in nexar["train"] if e["category"]=="safe_neg") |
| + len(dada_neg)) |
| n_neg_val = sum(1 for e in nexar["val"] if e["category"]=="safe_neg") |
|
|
| logger.info("") |
| logger.info("=" * 60) |
| logger.info("SUMMARY") |
| logger.info(f" TRAIN ego_positive : {n_pos_tr}") |
| logger.info(f" TRAIN non_ego : {n_ne_tr}") |
| logger.info(f" TRAIN safe_neg : {n_neg_tr}") |
| logger.info(f" ---") |
| logger.info(f" VAL ego_positive : {n_pos_val} β checkpoint selection") |
| logger.info(f" VAL non_ego : {n_ne_val} β false-alert monitoring") |
| logger.info(f" VAL safe_neg : {n_neg_val}") |
| logger.info(f" TEST (nexar only) : {len(nexar_test)} (diagnostic, NOT for ckpt sel.)") |
| logger.info("=" * 60) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|