feat(icp): extrinsic dataclass + JSON persistence

This commit is contained in:
L'électron rare
2026-05-14 11:59:24 +02:00
parent 9e71e7d757
commit ecc499296e
2 changed files with 99 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
"""iPhone LiDAR (ARKit world) <-> webcam (Multi-HMR camera) extrinsic.
Persisted as a small JSON document so calibration survives across launches.
The default location is ``~/.config/av-live/lidar_extrinsic.json``; override
with the ``ICP_LIDAR_EXTRINSIC`` env var.
"""
from __future__ import annotations
import json
import os
from dataclasses import dataclass, field
from pathlib import Path
import numpy as np
DEFAULT_EXTRINSIC_PATH = Path.home() / ".config" / "av-live" / "lidar_extrinsic.json"
@dataclass
class Extrinsic:
"""4x4 rigid transform from ARKit world frame to Multi-HMR camera frame."""
T_arkit_to_cam: np.ndarray = field(default_factory=lambda: np.eye(4))
confidence: float = 0.0
captured_at_iso: str = ""
@staticmethod
def identity() -> "Extrinsic":
return Extrinsic(T_arkit_to_cam=np.eye(4), confidence=0.0, captured_at_iso="")
def save_extrinsic(e: Extrinsic, path: Path | None = None) -> Path:
path = Path(path) if path is not None else _path_from_env()
path.parent.mkdir(parents=True, exist_ok=True)
payload = {
"T_arkit_to_cam": e.T_arkit_to_cam.astype(float).tolist(),
"confidence": float(e.confidence),
"captured_at_iso": e.captured_at_iso,
}
path.write_text(json.dumps(payload, indent=2))
return path
def load_extrinsic(path: Path | None = None) -> Extrinsic:
path = Path(path) if path is not None else _path_from_env()
if not path.exists():
return Extrinsic.identity()
payload = json.loads(path.read_text())
return Extrinsic(
T_arkit_to_cam=np.array(payload["T_arkit_to_cam"], dtype=np.float64),
confidence=float(payload.get("confidence", 0.0)),
captured_at_iso=str(payload.get("captured_at_iso", "")),
)
def _path_from_env() -> Path:
p = os.environ.get("ICP_LIDAR_EXTRINSIC")
return Path(p) if p else DEFAULT_EXTRINSIC_PATH
+41
View File
@@ -0,0 +1,41 @@
"""Tests for LiDAR <-> webcam extrinsic calibration persistence."""
from __future__ import annotations
import json
from pathlib import Path
import numpy as np
import pytest
def test_extrinsic_default_is_identity() -> None:
from data_only_viz.lidar_calib import Extrinsic
e = Extrinsic.identity()
np.testing.assert_allclose(e.T_arkit_to_cam, np.eye(4))
assert e.confidence == 0.0
assert e.captured_at_iso == ""
def test_extrinsic_roundtrip_json(tmp_path: Path) -> None:
from data_only_viz.lidar_calib import Extrinsic, load_extrinsic, save_extrinsic
T = np.eye(4)
T[:3, 3] = [0.1, -0.05, 0.30]
e = Extrinsic(T_arkit_to_cam=T, confidence=0.95, captured_at_iso="2026-05-14T12:00:00Z")
path = tmp_path / "extrinsic.json"
save_extrinsic(e, path)
loaded = load_extrinsic(path)
np.testing.assert_allclose(loaded.T_arkit_to_cam, T, atol=1e-10)
assert loaded.confidence == pytest.approx(0.95)
assert loaded.captured_at_iso == "2026-05-14T12:00:00Z"
def test_load_extrinsic_missing_path_returns_identity(tmp_path: Path) -> None:
from data_only_viz.lidar_calib import load_extrinsic
e = load_extrinsic(tmp_path / "does-not-exist.json")
np.testing.assert_allclose(e.T_arkit_to_cam, np.eye(4))
assert e.confidence == 0.0