添加 LiDAR-IMU 外参标定流水线与说明文档
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
"""Shared contracts for the LiDAR–IMU 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)
|
||||
Reference in New Issue
Block a user