feat(perception): canonicalize metric geometry
This commit is contained in:
@@ -0,0 +1,877 @@
|
||||
"""Digest-bound RAVNOVES00 geometry provider for the M4 product graph."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import time
|
||||
from collections.abc import Callable, Mapping
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from threading import Lock
|
||||
from typing import Final
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from .contracts import (
|
||||
EvidenceBasis,
|
||||
EvidenceCurrentness,
|
||||
MetricGeometry,
|
||||
ModalityOutcome,
|
||||
ObjectProposal2D,
|
||||
ObstacleObservation,
|
||||
validate_exclusive_point_ownership,
|
||||
)
|
||||
from .geometry_math import (
|
||||
GeometryAssociationProfile,
|
||||
Kb4ProjectionProfile,
|
||||
ProjectedPointCloud,
|
||||
SemanticGeometrySupport,
|
||||
geometry_only_clusters,
|
||||
project_map_points_kb4,
|
||||
semantic_geometry_support,
|
||||
)
|
||||
from .providers import SourcePacket
|
||||
from .recorded_source import RECORDED_SOURCE_PACK_ID, RecordedFrameReference
|
||||
|
||||
GEOMETRY_PROFILE_SCHEMA: Final = "missioncore.geometry-association-profile/v1"
|
||||
GEOMETRY_PROVIDER_ID: Final = "ravnoves00-geometry-association/v1"
|
||||
DEFAULT_GEOMETRY_PROFILE_PATH: Final = Path(
|
||||
"config/perception/m4-geometry-association-v1.json"
|
||||
)
|
||||
|
||||
FloatArray = npt.NDArray[np.float64]
|
||||
UInt8Array = npt.NDArray[np.uint8]
|
||||
|
||||
|
||||
class GeometryProviderError(RuntimeError):
|
||||
"""The geometry profile, evidence source or association is incompatible."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class GeometryProfile:
|
||||
profile_id: str
|
||||
provider_id: str
|
||||
source_id: str
|
||||
session_id: str
|
||||
source_pack_id: str
|
||||
source_pack_sha256: str
|
||||
frame_count: int
|
||||
point_count: int
|
||||
local_surface_model_id: str
|
||||
local_surface_sha256: str
|
||||
valid_frame_count: int
|
||||
width: int
|
||||
height: int
|
||||
coordinate_frame: str
|
||||
association: GeometryAssociationProfile
|
||||
profile_sha256: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class GeometryFrame:
|
||||
frame_index: int
|
||||
points_map: FloatArray
|
||||
point_class: UInt8Array
|
||||
sensor_position_map: FloatArray
|
||||
sensor_orientation_xyzw: FloatArray
|
||||
projection: Kb4ProjectionProfile
|
||||
surface_valid: bool
|
||||
|
||||
@property
|
||||
def source_point_count(self) -> int:
|
||||
return int(self.points_map.shape[0])
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class GeometryProviderSnapshot:
|
||||
input_frames: int
|
||||
completed_frames: int
|
||||
failed_frames: int
|
||||
proposal_count: int
|
||||
eligible_proposal_count: int
|
||||
ranged_proposal_count: int
|
||||
camera_only_proposal_count: int
|
||||
conflict_proposal_count: int
|
||||
unavailable_proposal_count: int
|
||||
outside_overlap_proposal_count: int
|
||||
sparse_proposal_count: int
|
||||
ownership_collision_proposal_count: int
|
||||
geometry_only_observation_count: int
|
||||
published_source_point_count: int
|
||||
overlapping_claims_removed: int
|
||||
core_duration_ns: int
|
||||
|
||||
@property
|
||||
def total_range_coverage(self) -> float:
|
||||
return self.ranged_proposal_count / self.proposal_count if self.proposal_count else 0.0
|
||||
|
||||
@property
|
||||
def eligible_range_coverage(self) -> float:
|
||||
if not self.eligible_proposal_count:
|
||||
return 0.0
|
||||
return self.ranged_proposal_count / self.eligible_proposal_count
|
||||
|
||||
|
||||
class RecordedGeometryStore:
|
||||
"""Verified source-pack and local-surface arrays used by one provider instance."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
source_pack_path: Path,
|
||||
local_surface_path: Path,
|
||||
profile: GeometryProfile,
|
||||
) -> None:
|
||||
self.source_pack_path = source_pack_path.resolve(strict=True)
|
||||
self.local_surface_path = local_surface_path.resolve(strict=True)
|
||||
self.profile = profile
|
||||
_verify_regular_file(
|
||||
self.source_pack_path,
|
||||
expected_sha256=profile.source_pack_sha256,
|
||||
label="source pack",
|
||||
)
|
||||
_verify_regular_file(
|
||||
self.local_surface_path,
|
||||
expected_sha256=profile.local_surface_sha256,
|
||||
label="local surface",
|
||||
)
|
||||
self._source = _load_npz(self.source_pack_path, "source pack")
|
||||
self._surface = _load_npz(self.local_surface_path, "local surface")
|
||||
self._validate()
|
||||
intrinsic = self._source["intrinsic_fx_fy_cx_cy"]
|
||||
distortion = self._source["distortion_kb4"]
|
||||
self._projection = Kb4ProjectionProfile(
|
||||
width=profile.width,
|
||||
height=profile.height,
|
||||
intrinsic_fx_fy_cx_cy=(
|
||||
float(intrinsic[0]),
|
||||
float(intrinsic[1]),
|
||||
float(intrinsic[2]),
|
||||
float(intrinsic[3]),
|
||||
),
|
||||
distortion_kb4=(
|
||||
float(distortion[0]),
|
||||
float(distortion[1]),
|
||||
float(distortion[2]),
|
||||
float(distortion[3]),
|
||||
),
|
||||
t_camera_from_lidar=np.asarray(
|
||||
self._source["t_camera_from_lidar"],
|
||||
dtype=np.float64,
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_repository(
|
||||
cls,
|
||||
repository_root: Path,
|
||||
*,
|
||||
profile: GeometryProfile | None = None,
|
||||
) -> RecordedGeometryStore:
|
||||
root = repository_root.resolve()
|
||||
selected = profile or load_geometry_profile(root / DEFAULT_GEOMETRY_PROFILE_PATH)
|
||||
source_pack = (
|
||||
root
|
||||
/ ".runtime/compute-experiments/e10/lidar-packs"
|
||||
/ selected.source_pack_id
|
||||
/ "lidar-pack.npz"
|
||||
)
|
||||
local_surface = (
|
||||
root
|
||||
/ ".runtime/compute-experiments/k1-local-surface-v1/models"
|
||||
/ selected.local_surface_model_id
|
||||
/ "local-surface.npz"
|
||||
)
|
||||
return cls(
|
||||
source_pack_path=source_pack,
|
||||
local_surface_path=local_surface,
|
||||
profile=selected,
|
||||
)
|
||||
|
||||
def frame(self, packet: SourcePacket) -> GeometryFrame | None:
|
||||
envelope = packet.envelope
|
||||
if envelope.source_id != self.profile.source_id:
|
||||
raise GeometryProviderError("packet source escaped the geometry profile")
|
||||
if envelope.session_id != self.profile.session_id:
|
||||
raise GeometryProviderError("packet session escaped the geometry profile")
|
||||
if not envelope.registered_point_increment.available:
|
||||
return None
|
||||
point_reference = packet.registered_point_increment_payload
|
||||
pose_reference = packet.pose_payload
|
||||
if (
|
||||
not isinstance(point_reference, RecordedFrameReference)
|
||||
or not isinstance(pose_reference, RecordedFrameReference)
|
||||
or point_reference.artifact_id != self.profile.source_pack_id
|
||||
or pose_reference.artifact_id != self.profile.source_pack_id
|
||||
or point_reference.frame_index != envelope.sequence
|
||||
or pose_reference.frame_index != envelope.sequence
|
||||
):
|
||||
raise GeometryProviderError("packet geometry references are not source-bound")
|
||||
frame_index = envelope.sequence
|
||||
if not 0 <= frame_index < self.profile.frame_count:
|
||||
raise GeometryProviderError("packet geometry frame index is outside the profile")
|
||||
if int(self._source["frame_indices"][frame_index]) != frame_index:
|
||||
raise GeometryProviderError("source pack frame sequence changed")
|
||||
if not bool(self._source["sample_available"][frame_index]):
|
||||
raise GeometryProviderError("packet claims unavailable source geometry as current")
|
||||
offsets = self._source["cloud_offsets"]
|
||||
start, end = int(offsets[frame_index]), int(offsets[frame_index + 1])
|
||||
return GeometryFrame(
|
||||
frame_index=frame_index,
|
||||
points_map=np.asarray(self._source["cloud_points_map"][start:end], dtype=np.float64),
|
||||
point_class=np.asarray(self._surface["point_class"][start:end], dtype=np.uint8),
|
||||
sensor_position_map=np.asarray(
|
||||
self._source["pose_positions_map"][frame_index],
|
||||
dtype=np.float64,
|
||||
),
|
||||
sensor_orientation_xyzw=np.asarray(
|
||||
self._source["pose_quaternions_map_from_lidar"][frame_index],
|
||||
dtype=np.float64,
|
||||
),
|
||||
projection=self._projection,
|
||||
surface_valid=bool(self._surface["frame_valid"][frame_index]),
|
||||
)
|
||||
|
||||
def _validate(self) -> None:
|
||||
source_required = {
|
||||
"frame_indices",
|
||||
"sample_available",
|
||||
"cloud_offsets",
|
||||
"cloud_points_map",
|
||||
"pose_positions_map",
|
||||
"pose_quaternions_map_from_lidar",
|
||||
"intrinsic_fx_fy_cx_cy",
|
||||
"distortion_kb4",
|
||||
"t_camera_from_lidar",
|
||||
}
|
||||
surface_required = {"frame_valid", "point_class"}
|
||||
if not source_required.issubset(self._source):
|
||||
raise GeometryProviderError("source pack arrays are incomplete")
|
||||
if not surface_required.issubset(self._surface):
|
||||
raise GeometryProviderError("local surface arrays are incomplete")
|
||||
frames = self.profile.frame_count
|
||||
points = self.profile.point_count
|
||||
shapes = {
|
||||
"frame_indices": (frames,),
|
||||
"sample_available": (frames,),
|
||||
"cloud_offsets": (frames + 1,),
|
||||
"cloud_points_map": (points, 3),
|
||||
"pose_positions_map": (frames, 3),
|
||||
"pose_quaternions_map_from_lidar": (frames, 4),
|
||||
"intrinsic_fx_fy_cx_cy": (4,),
|
||||
"distortion_kb4": (4,),
|
||||
"t_camera_from_lidar": (4, 4),
|
||||
}
|
||||
if any(self._source[name].shape != shape for name, shape in shapes.items()):
|
||||
raise GeometryProviderError("source pack array shapes changed")
|
||||
if self._surface["frame_valid"].shape != (frames,):
|
||||
raise GeometryProviderError("local surface frame shape changed")
|
||||
if self._surface["point_class"].shape != (points,):
|
||||
raise GeometryProviderError("local surface point shape changed")
|
||||
if int(self._source["cloud_offsets"][-1]) != points:
|
||||
raise GeometryProviderError("source point offsets do not close")
|
||||
if (
|
||||
int(np.count_nonzero(self._source["sample_available"]))
|
||||
!= self.profile.valid_frame_count
|
||||
):
|
||||
raise GeometryProviderError("source availability accounting changed")
|
||||
if int(np.count_nonzero(self._surface["frame_valid"])) != self.profile.valid_frame_count:
|
||||
raise GeometryProviderError("local surface validity accounting changed")
|
||||
if not np.array_equal(
|
||||
self._surface["frame_valid"],
|
||||
self._source["sample_available"],
|
||||
):
|
||||
raise GeometryProviderError("source and local surface availability disagree")
|
||||
point_class = np.asarray(self._surface["point_class"], dtype=np.uint8)
|
||||
if np.any(point_class > 3):
|
||||
raise GeometryProviderError("local surface point classification changed")
|
||||
|
||||
|
||||
class Ravnoves00GeometryAssociationProvider:
|
||||
"""Associate proposals with exact current points and retain unknown occupancy."""
|
||||
|
||||
provider_id: str = GEOMETRY_PROVIDER_ID
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
store: RecordedGeometryStore,
|
||||
clock_ns: Callable[[], int] = time.perf_counter_ns,
|
||||
) -> None:
|
||||
if store.profile.provider_id != self.provider_id:
|
||||
raise GeometryProviderError("geometry profile provider identity changed")
|
||||
self.store = store
|
||||
self.profile = store.profile
|
||||
self._clock_ns = clock_ns
|
||||
self._lock = Lock()
|
||||
self._input_frames = 0
|
||||
self._completed_frames = 0
|
||||
self._failed_frames = 0
|
||||
self._proposal_count = 0
|
||||
self._eligible = 0
|
||||
self._ranged = 0
|
||||
self._camera_only = 0
|
||||
self._conflict = 0
|
||||
self._unavailable = 0
|
||||
self._outside = 0
|
||||
self._sparse = 0
|
||||
self._ownership_collision = 0
|
||||
self._geometry_only = 0
|
||||
self._published_points = 0
|
||||
self._overlap_removed = 0
|
||||
self._core_duration_ns = 0
|
||||
|
||||
def associate(
|
||||
self,
|
||||
packet: SourcePacket,
|
||||
proposals: tuple[ObjectProposal2D, ...],
|
||||
) -> tuple[ObstacleObservation, ...]:
|
||||
with self._lock:
|
||||
self._input_frames += 1
|
||||
self._proposal_count += len(proposals)
|
||||
started = int(self._clock_ns())
|
||||
try:
|
||||
self._validate_proposals(packet, proposals)
|
||||
frame = self.store.frame(packet)
|
||||
if frame is None or not frame.surface_valid:
|
||||
result = tuple(
|
||||
_camera_unavailable_observation(packet, proposal, frame is None)
|
||||
for proposal in proposals
|
||||
)
|
||||
self._record_unavailable(len(proposals))
|
||||
else:
|
||||
result, metrics = self._associate_current(packet, proposals, frame)
|
||||
self._record_current(metrics)
|
||||
validate_exclusive_point_ownership(result)
|
||||
except Exception:
|
||||
with self._lock:
|
||||
self._failed_frames += 1
|
||||
self._core_duration_ns += max(0, int(self._clock_ns()) - started)
|
||||
raise
|
||||
with self._lock:
|
||||
self._completed_frames += 1
|
||||
self._core_duration_ns += max(0, int(self._clock_ns()) - started)
|
||||
return result
|
||||
|
||||
def _associate_current(
|
||||
self,
|
||||
packet: SourcePacket,
|
||||
proposals: tuple[ObjectProposal2D, ...],
|
||||
frame: GeometryFrame,
|
||||
) -> tuple[tuple[ObstacleObservation, ...], dict[str, int]]:
|
||||
projected = project_map_points_kb4(
|
||||
frame.points_map,
|
||||
position_map_xyz=frame.sensor_position_map,
|
||||
orientation_map_from_lidar_xyzw=frame.sensor_orientation_xyzw,
|
||||
profile=frame.projection,
|
||||
)
|
||||
supports = tuple(
|
||||
semantic_geometry_support(
|
||||
proposal.region.as_tuple(),
|
||||
projected=projected,
|
||||
frame_points_map=frame.points_map,
|
||||
point_class=frame.point_class,
|
||||
profile=self.profile.association,
|
||||
)
|
||||
for proposal in proposals
|
||||
)
|
||||
allocations, overlap_removed = _allocate_point_ownership(proposals, supports)
|
||||
observations: list[ObstacleObservation] = []
|
||||
metrics = {
|
||||
"eligible": 0,
|
||||
"ranged": 0,
|
||||
"camera_only": 0,
|
||||
"conflict": 0,
|
||||
"outside": 0,
|
||||
"sparse": 0,
|
||||
"ownership_collision": 0,
|
||||
"geometry_only": 0,
|
||||
"published_points": 0,
|
||||
"overlap_removed": overlap_removed,
|
||||
}
|
||||
claimed: set[int] = set()
|
||||
for index, (proposal, support) in enumerate(zip(proposals, supports, strict=True)):
|
||||
owned = allocations.get(index, np.empty(0, dtype=np.int64))
|
||||
observation = _proposal_observation(
|
||||
packet,
|
||||
proposal,
|
||||
support=support,
|
||||
owned_source_indices=owned,
|
||||
frame=frame,
|
||||
projected=projected,
|
||||
coordinate_frame=self.profile.coordinate_frame,
|
||||
)
|
||||
observations.append(observation)
|
||||
metrics["eligible"] += support.overlaps_projected_extent
|
||||
if observation.metric_geometry is not None:
|
||||
metrics["ranged"] += 1
|
||||
metrics["published_points"] += len(observation.source_point_ids)
|
||||
claimed.update(observation.source_point_ids)
|
||||
elif observation.basis is EvidenceBasis.CONFLICT:
|
||||
metrics["conflict"] += 1
|
||||
else:
|
||||
metrics["camera_only"] += 1
|
||||
if not support.overlaps_projected_extent:
|
||||
metrics["outside"] += 1
|
||||
elif "point-ownership-collision-range-withheld" in observation.reason_codes:
|
||||
metrics["ownership_collision"] += 1
|
||||
else:
|
||||
metrics["sparse"] += 1
|
||||
clusters = geometry_only_clusters(
|
||||
points_map=frame.points_map,
|
||||
point_class=frame.point_class,
|
||||
sensor_position_map=frame.sensor_position_map,
|
||||
claimed_source_indices=frozenset(claimed),
|
||||
profile=self.profile.association,
|
||||
)
|
||||
for cluster_index, cluster in enumerate(clusters):
|
||||
point_ids = tuple(sorted(int(value) for value in cluster.source_indices))
|
||||
observations.append(
|
||||
ObstacleObservation(
|
||||
observation_id=f"{packet.envelope.frame_id}:geometry:{cluster_index}",
|
||||
occupancy_key=f"{packet.envelope.frame_id}:geometry:{cluster_index}",
|
||||
source_id=packet.envelope.source_id,
|
||||
frame_id=packet.envelope.frame_id,
|
||||
evidence_time_ns=packet.envelope.timestamps.source_ns,
|
||||
basis=EvidenceBasis.LIDAR,
|
||||
currentness=EvidenceCurrentness.CURRENT,
|
||||
occupied_support=True,
|
||||
source_point_ids=point_ids,
|
||||
metric_geometry=MetricGeometry(
|
||||
coordinate_frame=self.profile.coordinate_frame,
|
||||
centroid_xyz_m=cluster.centroid_map_xyz_m,
|
||||
range_m=cluster.nearest_range_m,
|
||||
covariance_diagonal_m2=cluster.covariance_diagonal_m2,
|
||||
),
|
||||
proposal_ids=(),
|
||||
semantic_hint=None,
|
||||
reason_codes=("unassociated-current-occupied-component",),
|
||||
)
|
||||
)
|
||||
metrics["geometry_only"] += 1
|
||||
metrics["published_points"] += len(point_ids)
|
||||
return tuple(observations), metrics
|
||||
|
||||
def _validate_proposals(
|
||||
self,
|
||||
packet: SourcePacket,
|
||||
proposals: tuple[ObjectProposal2D, ...],
|
||||
) -> None:
|
||||
if any(
|
||||
proposal.source_id != packet.envelope.source_id
|
||||
or proposal.frame_id != packet.envelope.frame_id
|
||||
for proposal in proposals
|
||||
):
|
||||
raise GeometryProviderError("proposal escaped its source packet")
|
||||
if len({proposal.proposal_id for proposal in proposals}) != len(proposals):
|
||||
raise GeometryProviderError("proposal identities are duplicated")
|
||||
|
||||
def _record_unavailable(self, count: int) -> None:
|
||||
with self._lock:
|
||||
self._camera_only += count
|
||||
self._unavailable += count
|
||||
|
||||
def _record_current(self, metrics: Mapping[str, int]) -> None:
|
||||
with self._lock:
|
||||
self._eligible += metrics["eligible"]
|
||||
self._ranged += metrics["ranged"]
|
||||
self._camera_only += metrics["camera_only"]
|
||||
self._conflict += metrics["conflict"]
|
||||
self._outside += metrics["outside"]
|
||||
self._sparse += metrics["sparse"]
|
||||
self._ownership_collision += metrics["ownership_collision"]
|
||||
self._geometry_only += metrics["geometry_only"]
|
||||
self._published_points += metrics["published_points"]
|
||||
self._overlap_removed += metrics["overlap_removed"]
|
||||
|
||||
def snapshot(self) -> GeometryProviderSnapshot:
|
||||
with self._lock:
|
||||
return GeometryProviderSnapshot(
|
||||
input_frames=self._input_frames,
|
||||
completed_frames=self._completed_frames,
|
||||
failed_frames=self._failed_frames,
|
||||
proposal_count=self._proposal_count,
|
||||
eligible_proposal_count=self._eligible,
|
||||
ranged_proposal_count=self._ranged,
|
||||
camera_only_proposal_count=self._camera_only,
|
||||
conflict_proposal_count=self._conflict,
|
||||
unavailable_proposal_count=self._unavailable,
|
||||
outside_overlap_proposal_count=self._outside,
|
||||
sparse_proposal_count=self._sparse,
|
||||
ownership_collision_proposal_count=self._ownership_collision,
|
||||
geometry_only_observation_count=self._geometry_only,
|
||||
published_source_point_count=self._published_points,
|
||||
overlapping_claims_removed=self._overlap_removed,
|
||||
core_duration_ns=self._core_duration_ns,
|
||||
)
|
||||
|
||||
|
||||
def load_geometry_profile(path: Path) -> GeometryProfile:
|
||||
resolved = path.resolve(strict=True)
|
||||
_verify_regular_file(resolved, expected_sha256=None, label="geometry profile")
|
||||
raw = resolved.read_bytes()
|
||||
try:
|
||||
value = json.loads(raw)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise GeometryProviderError("geometry profile JSON is invalid") from exc
|
||||
document = _object(value, "geometry profile")
|
||||
_exact_keys(
|
||||
document,
|
||||
{
|
||||
"schema_version",
|
||||
"profile_id",
|
||||
"provider_id",
|
||||
"source",
|
||||
"local_surface",
|
||||
"projection",
|
||||
"association",
|
||||
"policy",
|
||||
"authority",
|
||||
},
|
||||
"geometry profile",
|
||||
)
|
||||
if document["schema_version"] != GEOMETRY_PROFILE_SCHEMA:
|
||||
raise GeometryProviderError("geometry profile schema is incompatible")
|
||||
if document["provider_id"] != GEOMETRY_PROVIDER_ID:
|
||||
raise GeometryProviderError("geometry provider identity is incompatible")
|
||||
source = _object(document["source"], "geometry source")
|
||||
surface = _object(document["local_surface"], "local surface")
|
||||
projection = _object(document["projection"], "geometry projection")
|
||||
association = _object(document["association"], "geometry association")
|
||||
policy = _object(document["policy"], "geometry policy")
|
||||
authority = _object(document["authority"], "geometry authority")
|
||||
_exact_keys(
|
||||
source,
|
||||
{
|
||||
"source_id",
|
||||
"session_id",
|
||||
"source_pack_id",
|
||||
"source_pack_sha256",
|
||||
"frame_count",
|
||||
"point_count",
|
||||
},
|
||||
"geometry source",
|
||||
)
|
||||
_exact_keys(
|
||||
surface,
|
||||
{"model_id", "artifact_sha256", "valid_frame_count"},
|
||||
"local surface",
|
||||
)
|
||||
_exact_keys(projection, {"model", "width", "height", "coordinate_frame"}, "projection")
|
||||
association_keys = set(GeometryAssociationProfile.__dataclass_fields__)
|
||||
_exact_keys(association, association_keys, "geometry association")
|
||||
expected_policy = {
|
||||
"camera_owns_semantic_hint": True,
|
||||
"geometry_can_invent_semantic_class": False,
|
||||
"geometry_only_range_estimator": "nearest-euclidean-sensor-distance/v1",
|
||||
"one_owner_per_source_point": True,
|
||||
"absence_of_points_means_free": False,
|
||||
"overlap_eligibility": "bbox-intersects-current-projected-point-extent/v1",
|
||||
"point_ownership_priority": "smallest-bbox-then-score-then-proposal-id/v1",
|
||||
"proposal_range_estimator": "median-camera-z-of-owned-current-points/v1",
|
||||
"unknown_is_occupied": True,
|
||||
"threshold_tuning_allowed": False,
|
||||
}
|
||||
if policy != expected_policy:
|
||||
raise GeometryProviderError("geometry policy is incompatible")
|
||||
if authority != {
|
||||
"ground_truth": False,
|
||||
"physical_live": False,
|
||||
"commands_enabled": False,
|
||||
"actuation_allowed": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}:
|
||||
raise GeometryProviderError("geometry authority is incompatible")
|
||||
if projection["model"] != "KB4":
|
||||
raise GeometryProviderError("geometry projection model is incompatible")
|
||||
profile = GeometryProfile(
|
||||
profile_id=_string(document, "profile_id"),
|
||||
provider_id=_string(document, "provider_id"),
|
||||
source_id=_string(source, "source_id"),
|
||||
session_id=_string(source, "session_id"),
|
||||
source_pack_id=_string(source, "source_pack_id"),
|
||||
source_pack_sha256=_digest(source, "source_pack_sha256"),
|
||||
frame_count=_positive_integer(source, "frame_count"),
|
||||
point_count=_positive_integer(source, "point_count"),
|
||||
local_surface_model_id=_string(surface, "model_id"),
|
||||
local_surface_sha256=_digest(surface, "artifact_sha256"),
|
||||
valid_frame_count=_positive_integer(surface, "valid_frame_count"),
|
||||
width=_positive_integer(projection, "width"),
|
||||
height=_positive_integer(projection, "height"),
|
||||
coordinate_frame=_string(projection, "coordinate_frame"),
|
||||
association=GeometryAssociationProfile(
|
||||
bbox_inset_fraction=_number(association, "bbox_inset_fraction"),
|
||||
depth_cluster_minimum_gap_m=_number(
|
||||
association,
|
||||
"depth_cluster_minimum_gap_m",
|
||||
),
|
||||
depth_cluster_gap_fraction=_number(
|
||||
association,
|
||||
"depth_cluster_gap_fraction",
|
||||
),
|
||||
spatial_cluster_radius_m=_number(association, "spatial_cluster_radius_m"),
|
||||
semantic_minimum_occupied_points=_positive_integer(
|
||||
association,
|
||||
"semantic_minimum_occupied_points",
|
||||
),
|
||||
semantic_minimum_occupied_voxels=_positive_integer(
|
||||
association,
|
||||
"semantic_minimum_occupied_voxels",
|
||||
),
|
||||
semantic_voxel_size_m=_number(association, "semantic_voxel_size_m"),
|
||||
conflict_minimum_classified_points=_positive_integer(
|
||||
association,
|
||||
"conflict_minimum_classified_points",
|
||||
),
|
||||
conflict_surface_fraction=_number(association, "conflict_surface_fraction"),
|
||||
geometry_local_radius_m=_number(association, "geometry_local_radius_m"),
|
||||
geometry_voxel_size_m=_number(association, "geometry_voxel_size_m"),
|
||||
geometry_minimum_cluster_points=_positive_integer(
|
||||
association,
|
||||
"geometry_minimum_cluster_points",
|
||||
),
|
||||
geometry_minimum_cluster_voxels=_positive_integer(
|
||||
association,
|
||||
"geometry_minimum_cluster_voxels",
|
||||
),
|
||||
maximum_geometry_clusters_per_frame=_positive_integer(
|
||||
association,
|
||||
"maximum_geometry_clusters_per_frame",
|
||||
),
|
||||
),
|
||||
profile_sha256=hashlib.sha256(raw).hexdigest(),
|
||||
)
|
||||
if profile.source_pack_id != RECORDED_SOURCE_PACK_ID:
|
||||
raise GeometryProviderError("geometry profile does not bind the admitted source pack")
|
||||
if profile.valid_frame_count > profile.frame_count:
|
||||
raise GeometryProviderError("geometry valid frame count exceeds the source")
|
||||
return profile
|
||||
|
||||
|
||||
def _allocate_point_ownership(
|
||||
proposals: tuple[ObjectProposal2D, ...],
|
||||
supports: tuple[SemanticGeometrySupport, ...],
|
||||
) -> tuple[dict[int, npt.NDArray[np.int64]], int]:
|
||||
eligible = {index: support for index, support in enumerate(supports) if support.qualified}
|
||||
claims: dict[int, list[int]] = {}
|
||||
for index, support in eligible.items():
|
||||
for source_index in support.occupied_source_indices:
|
||||
claims.setdefault(int(source_index), []).append(index)
|
||||
winners = {
|
||||
source_index: min(candidates, key=lambda index: _proposal_priority(proposals[index]))
|
||||
for source_index, candidates in claims.items()
|
||||
}
|
||||
allocations = {
|
||||
index: np.asarray(
|
||||
[
|
||||
int(source_index)
|
||||
for source_index in support.occupied_source_indices
|
||||
if winners[int(source_index)] == index
|
||||
],
|
||||
dtype=np.int64,
|
||||
)
|
||||
for index, support in eligible.items()
|
||||
}
|
||||
removed = sum(
|
||||
int(eligible[index].occupied_source_indices.size - allocation.size)
|
||||
for index, allocation in allocations.items()
|
||||
)
|
||||
return allocations, removed
|
||||
|
||||
|
||||
def _proposal_priority(proposal: ObjectProposal2D) -> tuple[float, float, str]:
|
||||
left, top, right, bottom = proposal.region.as_tuple()
|
||||
return ((right - left) * (bottom - top), -proposal.objectness, proposal.proposal_id)
|
||||
|
||||
|
||||
def _proposal_observation(
|
||||
packet: SourcePacket,
|
||||
proposal: ObjectProposal2D,
|
||||
*,
|
||||
support: SemanticGeometrySupport,
|
||||
owned_source_indices: npt.NDArray[np.int64],
|
||||
frame: GeometryFrame,
|
||||
projected: ProjectedPointCloud,
|
||||
coordinate_frame: str,
|
||||
) -> ObstacleObservation:
|
||||
point_ids: tuple[int, ...] = ()
|
||||
metric: MetricGeometry | None = None
|
||||
reason_codes: tuple[str, ...]
|
||||
basis: EvidenceBasis
|
||||
if support.qualified and owned_source_indices.size:
|
||||
point_ids = tuple(sorted(int(value) for value in owned_source_indices))
|
||||
points = frame.points_map[owned_source_indices]
|
||||
centroid = np.median(points, axis=0)
|
||||
covariance = points.var(axis=0)
|
||||
depth_by_source = {
|
||||
int(source_index): float(depth)
|
||||
for source_index, depth in zip(
|
||||
support.occupied_source_indices,
|
||||
support.occupied_depths_m,
|
||||
strict=True,
|
||||
)
|
||||
}
|
||||
range_m = float(np.median([depth_by_source[index] for index in point_ids]))
|
||||
metric = MetricGeometry(
|
||||
coordinate_frame=coordinate_frame,
|
||||
centroid_xyz_m=(
|
||||
float(centroid[0]),
|
||||
float(centroid[1]),
|
||||
float(centroid[2]),
|
||||
),
|
||||
range_m=range_m,
|
||||
covariance_diagonal_m2=(
|
||||
float(covariance[0]),
|
||||
float(covariance[1]),
|
||||
float(covariance[2]),
|
||||
),
|
||||
)
|
||||
basis = EvidenceBasis.FUSED
|
||||
reason_codes = ("current-connected-occupied-lidar-support",)
|
||||
if owned_source_indices.size != support.occupied_source_indices.size:
|
||||
reason_codes += ("exclusive-point-ownership-arbitration",)
|
||||
elif support.qualified:
|
||||
basis = EvidenceBasis.CAMERA
|
||||
reason_codes = (
|
||||
"current-connected-occupied-lidar-support",
|
||||
"point-ownership-collision-range-withheld",
|
||||
)
|
||||
elif support.conflict:
|
||||
basis = EvidenceBasis.CONFLICT
|
||||
reason_codes = ("camera-region-observed-as-local-surface",)
|
||||
elif not support.overlaps_projected_extent:
|
||||
basis = EvidenceBasis.CAMERA
|
||||
reason_codes = ("outside-projected-lidar-overlap",)
|
||||
else:
|
||||
basis = EvidenceBasis.CAMERA
|
||||
reason_codes = ("sparse-or-unqualified-occupied-support",)
|
||||
return ObstacleObservation(
|
||||
observation_id=f"{packet.envelope.frame_id}:{proposal.proposal_id}",
|
||||
occupancy_key=f"{packet.envelope.frame_id}:{proposal.proposal_id}",
|
||||
source_id=packet.envelope.source_id,
|
||||
frame_id=packet.envelope.frame_id,
|
||||
evidence_time_ns=packet.envelope.timestamps.source_ns,
|
||||
basis=basis,
|
||||
currentness=EvidenceCurrentness.CURRENT,
|
||||
occupied_support=metric is not None,
|
||||
source_point_ids=point_ids,
|
||||
metric_geometry=metric,
|
||||
proposal_ids=(proposal.proposal_id,),
|
||||
semantic_hint=proposal.semantic_hint,
|
||||
reason_codes=reason_codes,
|
||||
)
|
||||
|
||||
|
||||
def _camera_unavailable_observation(
|
||||
packet: SourcePacket,
|
||||
proposal: ObjectProposal2D,
|
||||
source_unavailable: bool,
|
||||
) -> ObstacleObservation:
|
||||
status = packet.envelope.registered_point_increment
|
||||
if source_unavailable and status.outcome is ModalityOutcome.STALE:
|
||||
currentness = EvidenceCurrentness.STALE
|
||||
reason = "registered-point-increment-stale"
|
||||
else:
|
||||
currentness = EvidenceCurrentness.UNAVAILABLE
|
||||
reason = (
|
||||
"registered-point-increment-unavailable"
|
||||
if source_unavailable
|
||||
else "local-surface-unavailable"
|
||||
)
|
||||
return ObstacleObservation(
|
||||
observation_id=f"{packet.envelope.frame_id}:{proposal.proposal_id}",
|
||||
occupancy_key=f"{packet.envelope.frame_id}:{proposal.proposal_id}",
|
||||
source_id=packet.envelope.source_id,
|
||||
frame_id=packet.envelope.frame_id,
|
||||
evidence_time_ns=packet.envelope.timestamps.source_ns,
|
||||
basis=EvidenceBasis.CAMERA,
|
||||
currentness=currentness,
|
||||
occupied_support=False,
|
||||
source_point_ids=(),
|
||||
metric_geometry=None,
|
||||
proposal_ids=(proposal.proposal_id,),
|
||||
semantic_hint=proposal.semantic_hint,
|
||||
reason_codes=(reason,),
|
||||
)
|
||||
|
||||
|
||||
def _load_npz(path: Path, label: str) -> dict[str, npt.NDArray[np.generic]]:
|
||||
try:
|
||||
with np.load(path, allow_pickle=False) as archive:
|
||||
return {name: np.asarray(archive[name]) for name in archive.files}
|
||||
except (OSError, ValueError) as exc:
|
||||
raise GeometryProviderError(f"{label} cannot be opened") from exc
|
||||
|
||||
|
||||
def _verify_regular_file(path: Path, *, expected_sha256: str | None, label: str) -> None:
|
||||
if not path.is_file() or path.is_symlink():
|
||||
raise GeometryProviderError(f"{label} must be a regular file")
|
||||
if expected_sha256 is not None and _file_sha256(path) != expected_sha256:
|
||||
raise GeometryProviderError(f"{label} digest changed")
|
||||
|
||||
|
||||
def _file_sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _object(value: object, label: str) -> dict[str, object]:
|
||||
if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
|
||||
raise GeometryProviderError(f"{label} must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def _exact_keys(document: Mapping[str, object], expected: set[str], label: str) -> None:
|
||||
if set(document) != expected:
|
||||
raise GeometryProviderError(f"{label} fields are incompatible")
|
||||
|
||||
|
||||
def _string(document: Mapping[str, object], key: str) -> str:
|
||||
value = document.get(key)
|
||||
if not isinstance(value, str) or not value:
|
||||
raise GeometryProviderError(f"{key} must be a nonempty string")
|
||||
return value
|
||||
|
||||
|
||||
def _positive_integer(document: Mapping[str, object], key: str) -> int:
|
||||
value = document.get(key)
|
||||
if not isinstance(value, int) or isinstance(value, bool) or value < 1:
|
||||
raise GeometryProviderError(f"{key} must be a positive integer")
|
||||
return value
|
||||
|
||||
|
||||
def _number(document: Mapping[str, object], key: str) -> float:
|
||||
value = document.get(key)
|
||||
if not isinstance(value, (int, float)) or isinstance(value, bool):
|
||||
raise GeometryProviderError(f"{key} must be numeric")
|
||||
result = float(value)
|
||||
if not math.isfinite(result):
|
||||
raise GeometryProviderError(f"{key} must be finite")
|
||||
return result
|
||||
|
||||
|
||||
def _digest(document: Mapping[str, object], key: str) -> str:
|
||||
value = _string(document, key)
|
||||
if len(value) != 64 or any(character not in "0123456789abcdef" for character in value):
|
||||
raise GeometryProviderError(f"{key} must be a SHA-256 digest")
|
||||
return value
|
||||
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_GEOMETRY_PROFILE_PATH",
|
||||
"GEOMETRY_PROFILE_SCHEMA",
|
||||
"GEOMETRY_PROVIDER_ID",
|
||||
"GeometryFrame",
|
||||
"GeometryProfile",
|
||||
"GeometryProviderError",
|
||||
"GeometryProviderSnapshot",
|
||||
"Ravnoves00GeometryAssociationProvider",
|
||||
"RecordedGeometryStore",
|
||||
"load_geometry_profile",
|
||||
]
|
||||
Reference in New Issue
Block a user