feat(lidar): admit and benchmark GOOSE baseline
This commit is contained in:
@@ -10,15 +10,25 @@ import re
|
||||
import shutil
|
||||
import time
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import Any, Final, Protocol
|
||||
from typing import Any, Final
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from k1link.ground_segmentation import (
|
||||
DEFAULT_GROUND_BENCHMARK_PROFILE,
|
||||
GroundBenchmarkProfile,
|
||||
GroundSegmentation,
|
||||
GroundSegmenter,
|
||||
LocalPercentileGroundSegmenter,
|
||||
)
|
||||
from k1link.ground_segmentation import (
|
||||
GroundSegmentationError as LidarGroundError,
|
||||
)
|
||||
|
||||
from .lidar_contract import LidarContractError, sensor_frame_xyzi
|
||||
from .lidar_replay import LidarReplayPackV2
|
||||
|
||||
@@ -41,189 +51,6 @@ _ANNOTATION_TEMPLATE_ID = re.compile(r"^ground-annotation-template-[a-f0-9]{64}$
|
||||
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
|
||||
_GIT_SHA1 = re.compile(r"^[a-f0-9]{40}$")
|
||||
|
||||
BoolArray = npt.NDArray[np.bool_]
|
||||
FloatArray = npt.NDArray[np.floating[Any]]
|
||||
|
||||
|
||||
class LidarGroundError(ValueError):
|
||||
"""Ground benchmark evidence violates the diagnostic-only contract."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class GroundBenchmarkProfile:
|
||||
profile_id: str = "k1-vendor-map-ground-ab/v1"
|
||||
pose_binding_threshold_ms: float = 100.0
|
||||
current_cell_size_m: float = 0.5
|
||||
current_local_radius_m: float = 2.5
|
||||
current_lower_percentile: float = 8.0
|
||||
current_maximum_below_ground_m: float = 0.25
|
||||
current_maximum_above_ground_m: float = 0.12
|
||||
current_minimum_local_points: int = 8
|
||||
patchwork_sensor_height_proxy_m: float = 0.0
|
||||
patchwork_map_vertical_origin_offset_m: float = 0.0
|
||||
patchwork_height_evidence: str = "missing"
|
||||
patchwork_minimum_range_m: float = 0.1
|
||||
patchwork_maximum_range_m: float = 20.0
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
finite_values = (
|
||||
self.pose_binding_threshold_ms,
|
||||
self.current_cell_size_m,
|
||||
self.current_local_radius_m,
|
||||
self.current_lower_percentile,
|
||||
self.current_maximum_below_ground_m,
|
||||
self.current_maximum_above_ground_m,
|
||||
self.patchwork_sensor_height_proxy_m,
|
||||
self.patchwork_map_vertical_origin_offset_m,
|
||||
self.patchwork_minimum_range_m,
|
||||
self.patchwork_maximum_range_m,
|
||||
)
|
||||
if not all(math.isfinite(value) for value in finite_values):
|
||||
raise LidarGroundError("Ground benchmark profile must be finite")
|
||||
if (
|
||||
not 0 < self.pose_binding_threshold_ms <= 10_000
|
||||
or not 0 < self.current_cell_size_m <= 100
|
||||
or not 0 < self.current_local_radius_m <= 1_000
|
||||
or not 0 <= self.current_lower_percentile <= 100
|
||||
or not 0 <= self.current_maximum_below_ground_m <= 100
|
||||
or not 0 <= self.current_maximum_above_ground_m <= 100
|
||||
or not 1 <= self.current_minimum_local_points <= 1_000_000
|
||||
or not 0 <= self.patchwork_sensor_height_proxy_m <= 10
|
||||
or not -10 <= self.patchwork_map_vertical_origin_offset_m <= 10
|
||||
or not 0 <= self.patchwork_minimum_range_m < self.patchwork_maximum_range_m <= 1_000
|
||||
or self.patchwork_height_evidence
|
||||
not in {"missing", "operator-estimated", "runtime-calibrated"}
|
||||
):
|
||||
raise LidarGroundError("Ground benchmark profile is invalid")
|
||||
if self.patchwork_height_evidence == "missing" and (
|
||||
self.patchwork_sensor_height_proxy_m != 0
|
||||
or self.patchwork_map_vertical_origin_offset_m != 0
|
||||
):
|
||||
raise LidarGroundError("Ground benchmark cannot apply height without height evidence")
|
||||
if (
|
||||
self.patchwork_height_evidence != "missing"
|
||||
and self.patchwork_sensor_height_proxy_m <= 0
|
||||
):
|
||||
raise LidarGroundError(
|
||||
"Ground benchmark height evidence requires a positive sensor height"
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": "missioncore.lidar-ground-benchmark-profile/v1",
|
||||
"profile_id": self.profile_id,
|
||||
"pose_binding": {
|
||||
"basis": "nearest-recorded-host-monotonic-arrival",
|
||||
"threshold_ms": self.pose_binding_threshold_ms,
|
||||
},
|
||||
"current_baseline": {
|
||||
"provider_id": "missioncore-local-percentile-ground/v1",
|
||||
"derived_from": "local-ground-relative-object-support-v1",
|
||||
"cell_size_m": self.current_cell_size_m,
|
||||
"local_radius_m": self.current_local_radius_m,
|
||||
"lower_percentile": self.current_lower_percentile,
|
||||
"maximum_below_ground_m": self.current_maximum_below_ground_m,
|
||||
"maximum_above_ground_m": self.current_maximum_above_ground_m,
|
||||
"minimum_local_points": self.current_minimum_local_points,
|
||||
},
|
||||
"candidate": {
|
||||
"provider_id": "patchworkpp/v1.4.1",
|
||||
"sensor_height_proxy_m": self.patchwork_sensor_height_proxy_m,
|
||||
"map_vertical_origin_offset_m": (self.patchwork_map_vertical_origin_offset_m),
|
||||
"height_evidence": self.patchwork_height_evidence,
|
||||
"minimum_range_m": self.patchwork_minimum_range_m,
|
||||
"maximum_range_m": self.patchwork_maximum_range_m,
|
||||
"enable_rnr": True,
|
||||
"enable_rvpf": True,
|
||||
"enable_tgr": True,
|
||||
},
|
||||
"input_normalization": {
|
||||
"current": "vendor-map-xyz",
|
||||
"candidate": "best-effort-map-to-lidar-pose-inversion",
|
||||
"physical_sensor_height_known": (
|
||||
self.patchwork_height_evidence == "runtime-calibrated"
|
||||
),
|
||||
"sensor_scan_geometry_known": False,
|
||||
},
|
||||
"authority": {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
DEFAULT_GROUND_BENCHMARK_PROFILE: Final = GroundBenchmarkProfile()
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class GroundSegmentation:
|
||||
ground_mask: BoolArray
|
||||
assigned_mask: BoolArray
|
||||
latency_ms: float
|
||||
|
||||
|
||||
class GroundSegmenter(Protocol):
|
||||
@property
|
||||
def identity(self) -> Mapping[str, object]: ...
|
||||
|
||||
def segment(self, xyzi: npt.NDArray[np.float32]) -> GroundSegmentation: ...
|
||||
|
||||
|
||||
class LocalPercentileGroundSegmenter:
|
||||
"""Full-frame diagnostic extension of the existing E19 local ground heuristic."""
|
||||
|
||||
def __init__(self, profile: GroundBenchmarkProfile) -> None:
|
||||
self.profile = profile
|
||||
|
||||
@property
|
||||
def identity(self) -> Mapping[str, object]:
|
||||
return {
|
||||
"provider_id": "missioncore-local-percentile-ground/v1",
|
||||
"implementation_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
"ground_truth": False,
|
||||
}
|
||||
|
||||
def segment(self, xyzi: npt.NDArray[np.float32]) -> GroundSegmentation:
|
||||
points = _xyzi(xyzi)
|
||||
started = time.perf_counter_ns()
|
||||
xyz = points[:, :3].astype(np.float64, copy=False)
|
||||
cell_size = self.profile.current_cell_size_m
|
||||
cell_keys = np.floor(xyz[:, :2] / cell_size).astype(np.int64)
|
||||
unique_cells, inverse = np.unique(cell_keys, axis=0, return_inverse=True)
|
||||
ground = np.zeros(points.shape[0], dtype=np.bool_)
|
||||
global_ground_z = float(np.percentile(xyz[:, 2], self.profile.current_lower_percentile))
|
||||
radius_squared = self.profile.current_local_radius_m**2
|
||||
for cell_index in range(unique_cells.shape[0]):
|
||||
point_indices = np.flatnonzero(inverse == cell_index)
|
||||
if point_indices.size == 0:
|
||||
continue
|
||||
center_xy = np.median(xyz[point_indices, :2], axis=0)
|
||||
delta_xy = xyz[:, :2] - center_xy
|
||||
local = xyz[
|
||||
np.einsum("ij,ij->i", delta_xy, delta_xy) <= radius_squared,
|
||||
2,
|
||||
]
|
||||
ground_z = (
|
||||
float(
|
||||
np.percentile(
|
||||
local,
|
||||
self.profile.current_lower_percentile,
|
||||
)
|
||||
)
|
||||
if local.size >= self.profile.current_minimum_local_points
|
||||
else global_ground_z
|
||||
)
|
||||
z = xyz[point_indices, 2]
|
||||
ground[point_indices] = (
|
||||
z >= ground_z - self.profile.current_maximum_below_ground_m
|
||||
) & (z <= ground_z + self.profile.current_maximum_above_ground_m)
|
||||
latency_ms = (time.perf_counter_ns() - started) / 1_000_000
|
||||
return GroundSegmentation(
|
||||
ground_mask=ground,
|
||||
assigned_mask=np.ones(points.shape[0], dtype=np.bool_),
|
||||
latency_ms=latency_ms,
|
||||
)
|
||||
|
||||
|
||||
class PatchworkPPGroundSegmenter:
|
||||
"""Runtime-only adapter for the pinned official Patchwork++ Python binding."""
|
||||
|
||||
Reference in New Issue
Block a user