#!/usr/bin/env python3 """Split tracking JSON into one JSON file per frame while preserving track IDs. Input format: [ { "image_name": "1741824631_0", "detections": [ { "class_id": 0, "confidence": 0.91, "bbox": [x1, y1, x2, y2], "track_id": 7, ... } ] }, ... ] Output format (one file per frame, evaluator-friendly): { "0": { "type": "0", "type_name": "vehicle", "score": "0.91", "box2d": [x1, y1, x2, y2], "xyzlhwyaw": [...], "face_cls": "front", "cut_cls": "0", "cut_cls_name": "nocut", "track_id": 7 }, "1": { ... } } Usage: python tools/temporal_analysis/split_tracking_json.py \ --input /path/to/roi0.json \ --output-dir /path/to/json_results """ import argparse import json import os from pathlib import Path from concurrent.futures import ThreadPoolExecutor from merge_tracking_results import normalize_image_name CLASS_ID_TO_NAME = { 0: "vehicle", 1: "pedestrian", 2: "bicycle", 3: "rider", 4: "roadblock", 5: "head", 6: "tsr", 7: "guideboard", 8: "plate", 9: "wheel", 10: "tl_border", 11: "tl_wick", 12: "tl_num", 13: "tricycle", } def format_scalar(value): """Keep ints as ints, convert everything else to plain JSON numbers/strings.""" if value is None: return value if isinstance(value, bool): return value if isinstance(value, int): return value if isinstance(value, float): return float(value) return value def normalize_detection(det, keep_all_fields=False): """Convert one tracking detection to evaluator-friendly single-frame JSON schema.""" class_id = int(det.get("class_id", -1)) bbox = det.get("bbox", []) confidence = det.get("confidence", 0.0) item = { "type": str(class_id), "type_name": det.get("type_name") or CLASS_ID_TO_NAME.get(class_id, ""), "score": confidence, "box2d": [format_scalar(v) for v in bbox[:4]], "face_cls": det.get("face_cls", "none"), "cut_cls": str(det.get("cut_cls", -1)), "cut_cls_name": det.get("cut_cls_name", "none"), } if "track_id" in det and det.get("track_id") is not None: item["track_id"] = int(det["track_id"]) if "lane_assignment" in det and det.get("lane_assignment") is not None: item["lane_assignment"] = int(det["lane_assignment"]) if "frameId" in det and det.get("frameId") is not None: item["frameId"] = det["frameId"] if "timestamp" in det and det.get("timestamp") is not None: item["timestamp"] = format_scalar(det["timestamp"]) if "version" in det and det.get("version") is not None: item["version"] = det["version"] if "anchor" in det and det.get("anchor") is not None: item["anchor"] = det["anchor"] if "roi_id" in det and det.get("roi_id") is not None: item["roi_id"] = int(det["roi_id"]) object_3d = det.get("object_3d") if object_3d is not None: item["xyzlhwyaw"] = [format_scalar(v) for v in object_3d[:7]] object_3d_ego = det.get("object_3d_ego") if object_3d_ego is not None: item["xyzlhwyaw_ego"] = [format_scalar(v) for v in object_3d_ego[:7]] if keep_all_fields: for key, value in det.items(): if key in item or key in {"class_id", "confidence", "bbox", "object_3d", "object_3d_ego"}: continue item[key] = value return item def frame_output_name(frame): """Choose output filename stem for one frame.""" image_name = frame.get("image_name") or "" stem = Path(image_name).stem if image_name else "" return normalize_image_name(stem) if stem else "unknown" def build_output_data(frame, keep_all_fields=False): """Convert one tracking frame to per-frame output payload.""" output_data = {} for idx, det in enumerate(frame.get("detections", [])): output_data[str(idx)] = normalize_detection(det, keep_all_fields=keep_all_fields) return output_data def write_one_frame(frame, output_dir, keep_all_fields=False, pretty=False): """Write one output JSON file and return its path.""" frame_name = frame_output_name(frame) output_data = build_output_data(frame, keep_all_fields=keep_all_fields) output_path = output_dir / f"{frame_name}.json" dump_kwargs = { "ensure_ascii": False, } if pretty: dump_kwargs["indent"] = 2 else: dump_kwargs["separators"] = (",", ":") with open(output_path, "w", encoding="utf-8") as f: json.dump(output_data, f, **dump_kwargs) return output_path def split_tracking_json(input_path, output_dir, keep_all_fields=False, num_workers=1, pretty=False): """Split a tracking JSON list into one frame file per item.""" input_path = Path(input_path) output_dir = Path(output_dir) output_dir.mkdir(parents=True, exist_ok=True) with open(input_path, "r", encoding="utf-8") as f: frames = json.load(f) if not isinstance(frames, list): raise ValueError(f"Expected list of frames in {input_path}, got {type(frames).__name__}") max_workers = max(1, int(num_workers)) if max_workers == 1 or len(frames) <= 1: for frame in frames: write_one_frame(frame, output_dir, keep_all_fields=keep_all_fields, pretty=pretty) return len(frames) with ThreadPoolExecutor(max_workers=max_workers) as executor: futures = [ executor.submit( write_one_frame, frame, output_dir, keep_all_fields, pretty, ) for frame in frames ] for future in futures: future.result() return len(frames) def default_output_dir(input_path): """Return a default output directory next to the input JSON.""" input_path = Path(input_path) return input_path.parent / f"{input_path.stem}_split" def main(): parser = argparse.ArgumentParser( description="Split tracking JSON into evaluator-friendly per-frame JSON files." ) parser.add_argument("--input", required=True, help="Path to roi0.json / tracking.json / merge.json") parser.add_argument("--output-dir", default=None, help="Directory to write per-frame JSON files (default: _split)") parser.add_argument("--keep-all-fields", action="store_true", help="Keep extra tracking fields beyond the evaluator-friendly schema") parser.add_argument("--num-workers", type=int, default=max(1, min(16, os.cpu_count() or 1)), help="Number of parallel workers for per-frame JSON writing (default: %(default)s)") parser.add_argument("--pretty", action="store_true", help="Write indented JSON for readability; default uses compact JSON for speed") args = parser.parse_args() output_dir = Path(args.output_dir) if args.output_dir else default_output_dir(args.input) written = split_tracking_json( input_path=args.input, output_dir=output_dir, keep_all_fields=args.keep_all_fields, num_workers=args.num_workers, pretty=args.pretty, ) print(f"Input tracking JSON : {args.input}") print(f"Output directory : {output_dir}") print(f"Frames written : {written}") print(f"Worker count : {args.num_workers}") print(f"Pretty JSON : {args.pretty}") if __name__ == "__main__": main()