feat(perception): define semantic object understanding
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
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
|
||||
)
|
||||
Reference in New Issue
Block a user