Files
lipan 29f1d45d66 feat: 算法升级、整车配准集成与性能分析文档
算法更新: delivery v1.2.1、平整度v1.5、测距v2.2、整车配准v1.3
适配器: vehicle-registration 新增、各算法 manifest 和 adapter 更新
后端: taskCadFiles/source 字段迁移、derivedPointClouds 注册、取消任务联动
前端: ElapsedTime 时长显示、两步算法流程、canRun 逻辑修复
文档: 新增5份算法性能分析报告
2026-06-25 08:22:20 +00:00

149 lines
4.4 KiB
Python

"""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",
]