Files
calibration/imu_lidar/contracts.py
T

118 lines
3.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Shared contracts for the LiDARIMU calibration pipeline."""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Any
import numpy as np
class TransformConvention(str, Enum):
"""The only transform convention used by this project."""
T_A_B = "T_A_B maps points from frame B into frame A"
class CalibrationMode(str, Enum):
ROTATION_ONLY = "rotation_only"
FULL_SE3 = "full_se3"
class CalibrationStatus(str, Enum):
NOT_RUN = "not_run"
BLOCKED = "blocked"
ROTATION_ONLY_ACCEPTED = "rotation_only_accepted"
FULL_SE3_ACCEPTED = "full_se3_accepted"
FULL_SE3_REJECTED = "full_se3_rejected_due_to_observability"
@dataclass(frozen=True)
class SessionInput:
"""Input paths for one independently recorded session."""
session_id: str
imu_source: Path
lidar_source: Path
board_configuration_id: str | None = None
@dataclass(frozen=True)
class CalibrationRequest:
"""Top-level calibration request."""
vehicle_config: Path | None
sessions: tuple[SessionInput, ...] = ()
requested_mode: CalibrationMode = CalibrationMode.ROTATION_ONLY
output_directory: Path | None = None
max_iterations: int = 2
min_pair_rotation_deg: float = 3.0
min_pair_translation_m: float = 0.3
time_offset_search_s: float = 1.0
@dataclass
class CalibrationResult:
"""Result envelope written by finalize after pipeline gates."""
status: CalibrationStatus = CalibrationStatus.NOT_RUN
message: str = "Calibration has not been executed."
details: dict[str, Any] = field(default_factory=dict)
T_IMU_lidar: np.ndarray | None = None
time_offset_s: float | None = None
@dataclass(frozen=True)
class ImuSeries:
"""Normalized IMU samples.
``t_s`` is the native IMU clock in seconds (need not match LiDAR epoch).
Gyro must be rad/s; accelerometer must be m/s^2.
"""
t_s: np.ndarray
gyro_rad_s: np.ndarray
acc_m_s2: np.ndarray
def __post_init__(self) -> None:
object.__setattr__(self, "t_s", np.asarray(self.t_s, dtype=float).reshape(-1))
object.__setattr__(self, "gyro_rad_s", np.asarray(self.gyro_rad_s, dtype=float).reshape(-1, 3))
object.__setattr__(self, "acc_m_s2", np.asarray(self.acc_m_s2, dtype=float).reshape(-1, 3))
n = self.t_s.size
if self.gyro_rad_s.shape != (n, 3) or self.acc_m_s2.shape != (n, 3):
raise ValueError("IMU arrays must share the same length and have shape (N, 3)")
@dataclass(frozen=True)
class LidarFrame:
"""One LiDAR sweep in Cartesian sensor coordinates."""
frame_id: str
t_start_s: float
t_end_s: float
points_xyz: np.ndarray
path: Path | None = None
@property
def t_mid_s(self) -> float:
return 0.5 * (self.t_start_s + self.t_end_s)
@dataclass(frozen=True)
class MotionPair:
"""One relative-motion observation between keyframes i and j."""
session_id: str
i: int
j: int
t_i_s: float
t_j_s: float
R_A: np.ndarray
R_B: np.ndarray
t_A_m: np.ndarray | None = None
t_B_m: np.ndarray | None = None
fitness: float = 0.0
metadata: dict[str, Any] = field(default_factory=dict)