feat: initial commit - massive-cloud point cloud processing system
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
"""Result dataclasses for coupler_panel.
|
||||
|
||||
These types form the **stable public schema** of the package; their
|
||||
field names and semantics are guarded by ``SCHEMA_VERSION`` (see
|
||||
``_version.py``).
|
||||
|
||||
All numeric units are **mm** for distances and **degree** for angles
|
||||
unless otherwise noted.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Building blocks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Hole:
|
||||
"""A single detected (or GT) circular hole on the panel."""
|
||||
|
||||
center: tuple[float, float, float] # (x, y, z), mm
|
||||
radius_mm: float
|
||||
fit_residual_rms_mm: float # 0 for GT
|
||||
role: str # "locating" | "central" | "unknown"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class HoleSet:
|
||||
"""The four locating holes plus an optional central hole.
|
||||
|
||||
``locating`` is ordered consistently between GT and detection so that
|
||||
Kabsch pairing is index-aligned.
|
||||
"""
|
||||
|
||||
locating: tuple[Hole, Hole, Hole, Hole]
|
||||
central: Hole | None = None
|
||||
|
||||
def locating_centers(self) -> np.ndarray:
|
||||
"""Return a (4, 3) ndarray of locating-hole centres."""
|
||||
return np.asarray([h.center for h in self.locating], dtype=np.float64)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PanelPlane:
|
||||
"""The fitted (or GT) panel plane."""
|
||||
|
||||
normal: tuple[float, float, float] # unit vector, n_x > 0 enforced
|
||||
point_on_plane: tuple[float, float, float]
|
||||
d_offset: float # n . x + d = 0
|
||||
inlier_ratio: float # 1.0 for GT
|
||||
inlier_residual_rms_mm: float # 0 for GT
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ReferenceFrame:
|
||||
"""Reference geometry derived from the four-hole panel centre."""
|
||||
|
||||
center_C: tuple[float, float, float]
|
||||
# Reference line L = C + lam * e_x (always parallel to the +x axis).
|
||||
line_direction: tuple[float, float, float] = (1.0, 0.0, 0.0)
|
||||
# Reference plane: z = C_z. Stored as plane normal + offset.
|
||||
reference_plane_z: float = 0.0
|
||||
# Longitudinal mid-plane: y = C_y.
|
||||
longitudinal_mid_y: float = 0.0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StageTiming:
|
||||
"""Per-stage wall-clock and peak-memory cost."""
|
||||
|
||||
stage: str
|
||||
seconds: float
|
||||
peak_kib: float
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Top-level result
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class CouplerMeasurement:
|
||||
"""Top-level result of the coupler-panel measurement pipeline.
|
||||
|
||||
See ``docs/USAGE.md`` for the stable-API contract.
|
||||
"""
|
||||
|
||||
schema_version: str
|
||||
algorithm_version: str
|
||||
|
||||
# --- Primary measurands ---
|
||||
theta_right_deg: float # atan2(n_y, n_x), with right_axis_sign applied
|
||||
theta_ref_deg: float # atan2(n_z, n_x)
|
||||
D_y_max_mm: float # max |y_i - C_y| over panel ROI
|
||||
D_z_max_mm: float # max |z_i - C_z| over panel ROI
|
||||
dx_y_max_mm: float # |A| * D_y_max, A = -n_y / n_x
|
||||
dx_z_max_mm: float # |B| * D_z_max, B = -n_z / n_x
|
||||
|
||||
# --- Geometry ---
|
||||
center_C: tuple[float, float, float]
|
||||
normal: tuple[float, float, float] # n_x > 0
|
||||
holes: HoleSet
|
||||
plane: PanelPlane
|
||||
frame: ReferenceFrame
|
||||
|
||||
# --- Diagnostics ---
|
||||
quality_flags: list[str] = field(default_factory=list)
|
||||
timings: list[StageTiming] = field(default_factory=list)
|
||||
report: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""Return a fully JSON-serialisable dict."""
|
||||
return _to_jsonable(asdict(self))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _to_jsonable(obj: Any) -> Any:
|
||||
"""Recursively convert numpy / tuple structures into plain Python types."""
|
||||
if isinstance(obj, dict):
|
||||
return {k: _to_jsonable(v) for k, v in obj.items()}
|
||||
if isinstance(obj, (list, tuple)):
|
||||
return [_to_jsonable(v) for v in obj]
|
||||
if isinstance(obj, np.ndarray):
|
||||
return obj.tolist()
|
||||
if isinstance(obj, (np.floating, np.integer)):
|
||||
return obj.item()
|
||||
return obj
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Hole",
|
||||
"HoleSet",
|
||||
"PanelPlane",
|
||||
"ReferenceFrame",
|
||||
"StageTiming",
|
||||
"CouplerMeasurement",
|
||||
]
|
||||
Reference in New Issue
Block a user