#!/usr/bin/env python3 """Build motion_pairs.json next to an existing summary without re-solving extrinsic. Use this once for older calibration outputs that predate automatic pair caching. """ from __future__ import annotations import argparse import json import sys from pathlib import Path import numpy as np ROOT = Path(__file__).resolve().parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from imu_lidar.imu_audit import audit_imu from imu_lidar.imu_io import load_imu_samples from imu_lidar.keyframes import build_keyframes from imu_lidar.lidar_io import load_lidar_frames from imu_lidar.motion_pairs import build_motion_pairs from imu_lidar.motion_pairs_io import build_motion_pairs_payload, save_motion_pairs def _load_summary_meta(summary_path: Path) -> tuple[float, np.ndarray, str]: summary = json.loads(summary_path.read_text(encoding="utf-8")) delta_t = float(summary.get("time_offset_s") or 0.0) session = (summary.get("details") or {}).get("sessions", [{}])[0] session_id = str(session.get("session_id") or summary_path.parent.name) bias = np.asarray( (session.get("imu_audit") or {}).get("gyro_bias_rad_s") or (session.get("joint") or {}).get("gyro_bias_rad_s") or [0.0, 0.0, 0.0], dtype=float, ).reshape(3) return delta_t, bias, session_id def export_one( *, lidar: Path, imu: Path, summary: Path, output: Path | None, min_rotation_deg: float, min_translation_m: float, ) -> Path: delta_t, bias_from_summary, session_id = _load_summary_meta(summary) imu_series = load_imu_samples(imu) # Prefer freshly audited bias if summary bias is missing/zeros. if float(np.linalg.norm(bias_from_summary)) < 1e-12: bias = audit_imu(imu_series).gyro_bias_rad_s else: bias = bias_from_summary frames = load_lidar_frames(lidar) keyframes = build_keyframes( frames, min_translation_m=min_translation_m, min_rotation_deg=min_rotation_deg, ) pair_set = build_motion_pairs( session_id=session_id, keyframes=list(keyframes.frames), keyframe_indices=keyframes.indices, imu=imu_series, delta_t_s=delta_t, gyro_bias_rad_s=bias, min_rotation_deg=min_rotation_deg, min_translation_m=min_translation_m, ) prepared = [ { "session_id": session_id, "time_offset_s": delta_t, "gyro_bias_rad_s": np.asarray(bias, dtype=float).reshape(3), "pairs": pair_set.pairs, } ] payload = build_motion_pairs_payload(prepared_sessions=prepared) out = output or (summary.parent / "motion_pairs.json") save_motion_pairs(out, payload) print(f"wrote {out} ({len(pair_set.pairs)} pairs, session={session_id}, dt={delta_t:.6f})") return out def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--lidar", type=Path, required=True) parser.add_argument("--imu", type=Path, required=True) parser.add_argument("--summary", type=Path, required=True) parser.add_argument("--output", type=Path, default=None, help="Default: /motion_pairs.json") parser.add_argument("--min-pair-rotation-deg", type=float, default=2.0) parser.add_argument("--min-pair-translation-m", type=float, default=0.3) args = parser.parse_args() export_one( lidar=args.lidar, imu=args.imu, summary=args.summary, output=args.output, min_rotation_deg=args.min_pair_rotation_deg, min_translation_m=args.min_pair_translation_m, ) return 0 if __name__ == "__main__": raise SystemExit(main())