"""Pretty-print calibration summary.json and optionally write a small preview figure.""" from __future__ import annotations import argparse import json from pathlib import Path def _load(path: Path) -> dict: return json.loads(path.read_text(encoding="utf-8")) def _session0(summary: dict) -> dict: sessions = summary.get("details", {}).get("sessions", []) return sessions[0] if sessions else {} def print_report(summary: dict, truth: dict | None = None) -> None: print("---------- calibration report ----------") print(f"status : {summary.get('status')}") print(f"message: {summary.get('message')}") dt = summary.get("time_offset_s") if dt is not None: print(f"δt : {dt:.6f} s (t_imu = t_lidar + δt)") if truth and "delta_t_s" in truth: print(f" truth={truth['delta_t_s']:.6f} s err={dt - truth['delta_t_s']:+.6f} s") t_block = summary.get("T_IMU_lidar") if isinstance(t_block, dict): rpy = t_block.get("rpy_deg_xyz") trans = t_block.get("translation_m") if rpy is not None: print(f"RPY xyz: [{rpy[0]:.3f}, {rpy[1]:.3f}, {rpy[2]:.3f}] deg") if truth and "yaw_extrinsic_deg" in truth: print(f" yaw truth≈{truth['yaw_extrinsic_deg']:.3f} deg") if trans is not None: print(f"t : [{trans[0]:.4f}, {trans[1]:.4f}, {trans[2]:.4f}] m") session = _session0(summary) handeye = session.get("handeye") or {} joint = session.get("joint") or {} if handeye: print( "handeye: " f"pairs={handeye.get('pair_count')} " f"rms={handeye.get('residual_rms_deg')}° " f"median={handeye.get('residual_median_deg')}° " f"ok={handeye.get('ok')}" ) if joint: obs = joint.get("observability") or {} print( "joint : " f"rot_rms={joint.get('residual_rms_rot_deg')}° " f"trans_rms={joint.get('residual_rms_trans_m')} m " f"trans_accepted={joint.get('translation_accepted')}" ) print( "observ : " f"rotation={obs.get('rotation_observable')} " f"translation={obs.get('translation_observable')} " f"cond_R={obs.get('condition_rotation')}" ) if joint.get("gravity_m_s2") is not None: print(f"gravity: {joint.get('gravity_m_s2')}") if joint.get("gyro_bias_rad_s") is not None: print(f"b_g : {joint.get('gyro_bias_rad_s')}") print("----------------------------------------") def maybe_plot(summary: dict, plot_path: Path, truth: dict | None = None) -> None: try: import matplotlib.pyplot as plt except ImportError: print("(matplotlib not installed; skip plot)") return session = _session0(summary) handeye = session.get("handeye") or {} joint = session.get("joint") or {} labels = [] values = [] if handeye.get("residual_rms_deg") is not None: labels.append("handeye\nRMS (°)") values.append(float(handeye["residual_rms_deg"])) if joint.get("residual_rms_rot_deg") is not None: labels.append("joint rot\nRMS (°)") values.append(float(joint["residual_rms_rot_deg"])) if joint.get("residual_rms_trans_m") is not None: labels.append("joint trans\nRMS (m)") values.append(float(joint["residual_rms_trans_m"])) dt = summary.get("time_offset_s") if dt is not None: labels.append("|δt| (s)") values.append(abs(float(dt))) if truth and "delta_t_s" in truth: labels.append("|δt err| (s)") values.append(abs(float(dt) - float(truth["delta_t_s"]))) if not labels: print("(no numeric fields to plot)") return fig, ax = plt.subplots(figsize=(7.5, 3.8)) ax.bar(labels, values, color="#3b6ea5") ax.set_title(f"Calibration preview — {summary.get('status')}") ax.set_ylabel("value") ax.grid(axis="y", alpha=0.3) fig.tight_layout() plot_path.parent.mkdir(parents=True, exist_ok=True) fig.savefig(plot_path, dpi=120) plt.close(fig) print(f"wrote plot: {plot_path}") def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description="Show LiDAR–IMU calibration summary") parser.add_argument("--summary", type=Path, required=True, help="Path to summary.json") parser.add_argument("--truth-meta", type=Path, default=None, help="Optional synthetic meta.json") parser.add_argument("--plot", type=Path, default=None, help="Optional PNG path for a bar chart") args = parser.parse_args(argv) summary = _load(args.summary) truth = _load(args.truth_meta) if args.truth_meta and args.truth_meta.exists() else None print_report(summary, truth) if args.plot is not None: maybe_plot(summary, args.plot, truth) return 0 if __name__ == "__main__": raise SystemExit(main())