176 lines
5.5 KiB
Python
176 lines
5.5 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
|
|
from k1link.perception.contracts import (
|
|
EvidenceBasis,
|
|
EvidenceCurrentness,
|
|
MetricGeometry,
|
|
ObstacleObservation,
|
|
)
|
|
from k1link.perception.geometry_math import ProjectedPointCloud
|
|
from k1link.perception.mask_grounding_semantics import (
|
|
MASK_GROUNDING_EVIDENCE_SCHEMA,
|
|
MaskBindingResolution,
|
|
MaskGroundingDetection,
|
|
MaskLabelResolution,
|
|
bind_mask_instances_to_geometry,
|
|
cluster_mask_instances,
|
|
load_mask_grounding_evidence,
|
|
resolve_mask_instance_label,
|
|
)
|
|
|
|
|
|
def _observation(ordinal: int, source_ids: tuple[int, ...]) -> ObstacleObservation:
|
|
return ObstacleObservation(
|
|
observation_id=f"frame-000253:geometry:{ordinal}",
|
|
occupancy_key=f"frame-000253:occupancy:{ordinal}",
|
|
source_id="RAVNOVES00",
|
|
frame_id="frame-000253",
|
|
evidence_time_ns=253,
|
|
basis=EvidenceBasis.LIDAR,
|
|
currentness=EvidenceCurrentness.CURRENT,
|
|
occupied_support=True,
|
|
source_point_ids=source_ids,
|
|
metric_geometry=MetricGeometry(
|
|
coordinate_frame="map",
|
|
centroid_xyz_m=(2.0 + ordinal, 0.0, 0.5),
|
|
range_m=2.0 + ordinal,
|
|
covariance_diagonal_m2=(0.1, 0.1, 0.1),
|
|
),
|
|
proposal_ids=(),
|
|
semantic_hint=None,
|
|
reason_codes=("test-geometry-only",),
|
|
)
|
|
|
|
|
|
def _projected() -> ProjectedPointCloud:
|
|
return ProjectedPointCloud(
|
|
pixels_xy=np.asarray(
|
|
(
|
|
(10.0, 10.0),
|
|
(11.0, 10.0),
|
|
(10.0, 11.0),
|
|
(11.0, 11.0),
|
|
(30.0, 30.0),
|
|
(31.0, 30.0),
|
|
(30.0, 31.0),
|
|
(31.0, 31.0),
|
|
),
|
|
dtype=np.float64,
|
|
),
|
|
depths_m=np.ones(8, dtype=np.float64),
|
|
source_indices=np.arange(8, dtype=np.int64),
|
|
source_point_count=8,
|
|
camera_front_point_count=8,
|
|
)
|
|
|
|
|
|
def _detection(
|
|
detection_id: str,
|
|
class_name: str,
|
|
confidence: float,
|
|
mask: np.ndarray,
|
|
) -> MaskGroundingDetection:
|
|
packed = mask.astype(np.uint8)
|
|
return MaskGroundingDetection(
|
|
detection_id=detection_id,
|
|
prompt_set_id="urban-static/v0",
|
|
class_id=0,
|
|
class_name=class_name,
|
|
confidence=confidence,
|
|
box_xyxy=(1.0, 1.0, 40.0, 40.0),
|
|
mask_sha256=hashlib.sha256(packed.tobytes()).hexdigest(),
|
|
mask=mask,
|
|
)
|
|
|
|
|
|
def test_empty_early_v0_ledger_is_read_without_inventing_boxes(tmp_path: Path) -> None:
|
|
path = tmp_path / "frame-000253.npz"
|
|
np.savez_compressed(
|
|
path,
|
|
schema_version=np.asarray(MASK_GROUNDING_EVIDENCE_SCHEMA),
|
|
source_file_sha256=np.asarray("a" * 64),
|
|
source_pixel_sha256=np.asarray("b" * 64),
|
|
class_ids=np.empty(0, dtype=np.int16),
|
|
class_names=np.empty(0, dtype="U128"),
|
|
scores=np.empty(0, dtype=np.float32),
|
|
boxes_xyxy=np.empty(0, dtype=np.float32),
|
|
masks=np.empty((0, 600, 800), dtype=np.uint8),
|
|
)
|
|
|
|
evidence = load_mask_grounding_evidence(path, prompt_set_id="urban-agents/v0")
|
|
|
|
assert evidence.detections == ()
|
|
assert evidence.source_file_sha256 == "a" * 64
|
|
|
|
|
|
def test_same_mask_becomes_one_instance_and_conflicting_name_stays_ambiguous() -> None:
|
|
mask = np.zeros((600, 800), dtype=np.bool_)
|
|
mask[5:20, 5:20] = True
|
|
instances = cluster_mask_instances(
|
|
(
|
|
_detection("trash", "trash bin", 0.43, mask),
|
|
_detection("cart", "shopping cart", 0.41, mask),
|
|
)
|
|
)
|
|
|
|
bindings = bind_mask_instances_to_geometry(
|
|
instances,
|
|
observations=(_observation(0, (0, 1, 2, 3)), _observation(1, (4, 5, 6, 7))),
|
|
projected=_projected(),
|
|
)
|
|
label = resolve_mask_instance_label(instances[0])
|
|
|
|
assert len(instances) == 1
|
|
assert bindings[0].resolution is MaskBindingResolution.SELECTED
|
|
assert bindings[0].selected_observation_id == "frame-000253:geometry:0"
|
|
assert label.resolution is MaskLabelResolution.AMBIGUOUS
|
|
assert label.selected_label is None
|
|
|
|
|
|
def test_mask_covering_two_obstacles_does_not_claim_either() -> None:
|
|
mask = np.zeros((600, 800), dtype=np.bool_)
|
|
mask[5:40, 5:40] = True
|
|
instance = cluster_mask_instances((_detection("wide", "trash bin", 0.7, mask),))
|
|
|
|
binding = bind_mask_instances_to_geometry(
|
|
instance,
|
|
observations=(_observation(0, (0, 1, 2, 3)), _observation(1, (4, 5, 6, 7))),
|
|
projected=_projected(),
|
|
)[0]
|
|
|
|
assert binding.resolution is MaskBindingResolution.AMBIGUOUS
|
|
assert binding.selected_observation_id is None
|
|
assert binding.reason_code == "mask-covers-multiple-geometry-observations"
|
|
|
|
|
|
def test_two_distinct_masks_cannot_claim_one_geometry_observation() -> None:
|
|
narrow = np.zeros((600, 800), dtype=np.bool_)
|
|
narrow[9:13, 9:13] = True
|
|
wide = np.zeros((600, 800), dtype=np.bool_)
|
|
wide[1:25, 1:25] = True
|
|
instances = cluster_mask_instances(
|
|
(
|
|
_detection("narrow", "bollard", 0.8, narrow),
|
|
_detection("wide", "post", 0.8, wide),
|
|
)
|
|
)
|
|
|
|
bindings = bind_mask_instances_to_geometry(
|
|
instances,
|
|
observations=(_observation(0, (0, 1, 2, 3)),),
|
|
projected=_projected(),
|
|
)
|
|
|
|
assert len(instances) == 2
|
|
assert all(item.resolution is MaskBindingResolution.AMBIGUOUS for item in bindings)
|
|
assert all(item.selected_observation_id is None for item in bindings)
|
|
assert all(
|
|
item.reason_code == "observation-claimed-by-multiple-mask-instances"
|
|
for item in bindings
|
|
)
|