feat(perception): define semantic object understanding

This commit is contained in:
DCCONSTRUCTIONS
2026-08-25 16:43:47 +03:00
parent 1084984da2
commit 6276bbf324
28 changed files with 7028 additions and 0 deletions
+158
View File
@@ -0,0 +1,158 @@
from __future__ import annotations
from pathlib import Path
import numpy as np
from PIL import Image
from k1link.perception.contracts import (
EvidenceBasis,
EvidenceCurrentness,
MetricGeometry,
ObstacleObservation,
)
from k1link.perception.geometry import GeometryFrame
from k1link.perception.geometry_math import Kb4ProjectionProfile
from k1link.perception.geometry_semantic_roi import (
build_geometry_semantic_rois,
materialize_geometry_semantic_crop,
select_geometry_roi_detections,
)
from k1link.perception.object_understanding import load_object_semantic_vocabulary
from k1link.perception.open_vocabulary_semantics import (
OpenVocabularyDetection,
fuse_open_vocabulary_detections,
load_open_vocabulary_semantic_profile,
)
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
PROFILE_PATH = REPOSITORY_ROOT / "config/perception/open-vocabulary-semantic-shadow-v0.json"
VOCABULARY_PATH = REPOSITORY_ROOT / "config/perception/object-semantic-vocabulary-v0.json"
def _point_for_pixel(u: float, v: float, *, z: float = 5.0) -> tuple[float, float, float]:
theta_x = (u - 400.0) / 100.0
theta_y = (v - 300.0) / 100.0
ray = np.asarray((np.tan(theta_x), np.tan(theta_y), 1.0), dtype=np.float64)
return tuple(float(item) for item in ray * z) # type: ignore[return-value]
def _frame() -> GeometryFrame:
points = np.asarray(
(
_point_for_pixel(350.0, 250.0),
_point_for_pixel(450.0, 250.0),
_point_for_pixel(350.0, 350.0),
_point_for_pixel(450.0, 350.0),
_point_for_pixel(600.0, 300.0),
),
dtype=np.float64,
)
return GeometryFrame(
frame_index=121,
points_map=points,
point_class=np.full(points.shape[0], 2, dtype=np.uint8),
sensor_position_map=np.zeros(3, dtype=np.float64),
sensor_orientation_xyzw=np.asarray((0.0, 0.0, 0.0, 1.0), dtype=np.float64),
projection=Kb4ProjectionProfile(
width=800,
height=600,
intrinsic_fx_fy_cx_cy=(100.0, 100.0, 400.0, 300.0),
distortion_kb4=(0.0, 0.0, 0.0, 0.0),
t_camera_from_lidar=np.eye(4, dtype=np.float64),
),
surface_valid=True,
)
def _observation(ordinal: int, source_point_ids: tuple[int, ...]) -> ObstacleObservation:
return ObstacleObservation(
observation_id=f"frame-000121:observation-{ordinal}",
occupancy_key=f"frame-000121:occupancy-{ordinal}",
source_id="RAVNOVES00",
frame_id="frame-000121",
evidence_time_ns=121,
basis=EvidenceBasis.LIDAR,
currentness=EvidenceCurrentness.CURRENT,
occupied_support=True,
source_point_ids=source_point_ids,
metric_geometry=MetricGeometry(
coordinate_frame="map",
centroid_xyz_m=(1.0, 2.0, 0.5),
range_m=2.2,
covariance_diagonal_m2=(0.1, 0.1, 0.1),
),
proposal_ids=(),
semantic_hint=None,
reason_codes=("test-geometry-only",),
)
def test_geometry_points_own_crop_and_sparse_observation_stays_unprojected(
tmp_path: Path,
) -> None:
result = build_geometry_semantic_rois(
frame=_frame(),
observations=(_observation(0, (0, 1, 2, 3)), _observation(1, (4,))),
)
assert len(result.rois) == 1
assert result.not_projected_observations[0].observation_id.endswith("observation-1")
roi = result.rois[0]
assert roi.observation.source_point_ids == (0, 1, 2, 3)
assert roi.core_region.x_min < 400.0 < roi.core_region.x_max
assert roi.core_region.y_min < 300.0 < roi.core_region.y_max
assert roi.crop_region.x_min <= roi.core_region.x_min
assert roi.crop_region.y_max >= roi.core_region.y_max
source = tmp_path / "frame-000121.png"
Image.new("RGB", (800, 600), (114, 114, 114)).save(source)
destination = tmp_path / roi.crop_name
materialize_geometry_semantic_crop(
image_path=source,
roi=roi,
destination=destination,
)
with Image.open(destination) as crop:
assert crop.size == (
int(roi.crop_region.x_max - roi.crop_region.x_min),
int(roi.crop_region.y_max - roi.crop_region.y_min),
)
def test_roi_selection_returns_only_cluster_covering_geometry_core() -> None:
profile = load_open_vocabulary_semantic_profile(PROFILE_PATH)
vocabulary = load_object_semantic_vocabulary(VOCABULARY_PATH)
roi = build_geometry_semantic_rois(
frame=_frame(),
observations=(_observation(0, (0, 1, 2, 3)),),
).rois[0]
fusion = fuse_open_vocabulary_detections(
(
OpenVocabularyDetection(
detection_id="inside",
source_id="RAVNOVES00",
frame_id="frame-000121",
prompt_set_id="urban-static/v0",
raw_label="trash bin",
confidence=0.8,
region=roi.core_region,
),
OpenVocabularyDetection(
detection_id="outside",
source_id="RAVNOVES00",
frame_id="frame-000121",
prompt_set_id="urban-static/v0",
raw_label="traffic cone",
confidence=0.9,
region=type(roi.core_region)(10.0, 10.0, 50.0, 50.0),
),
),
profile=profile,
vocabulary=vocabulary,
valid_fov_mask=np.ones((600, 800), dtype=np.bool_),
)
selected = select_geometry_roi_detections(roi, fusion)
assert tuple(item.detection_id for item in selected) == ("inside",)
@@ -0,0 +1,53 @@
from __future__ import annotations
from pathlib import Path
from k1link.perception.geometry_semantic_shadow_replay import (
read_geometry_semantic_shadow_replay,
)
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
RESULT_ROOT = (
REPOSITORY_ROOT
/ ".runtime/compute-experiments/m48s-semantic-shadow/geometry-first-results"
/ (
"m48s-geometry-semantic-shadow-"
"1a6c5d04fb3f7aef4385ee28fc67d3e69f4528a77e8e1194b1283530ef73c7c2"
)
)
def test_geometry_first_semantic_shadow_is_complete_but_not_accepted() -> None:
result = read_geometry_semantic_shadow_replay(RESULT_ROOT)
assert result.completed is True
assert result.accepted is False
assert result.metrics["geometry"] == {
"not_projected_observation_count": 15,
"observation_count": 73,
"roi_count": 58,
}
semantics = result.metrics["semantics"]
assert isinstance(semantics, dict)
assert semantics["resolution_counts"] == {
"ambiguous": 6,
"selected": 11,
"unresolved": 56,
}
assert semantics["selected_class_counts"] == {
"human.adult": 1,
"static.bollard": 1,
"static.concrete-hemisphere": 1,
"vehicle.car": 8,
}
assert result.report["decision"] == {
"geometry_first_binding_completed": True,
"navigation_or_safety_accepted": False,
"reason_codes": [
"two-reviewer-independent-truth-unavailable",
"open-vocabulary-geometry-roi-remains-experimental-shadow",
"unknown-and-unresolved-objects-remain-route-around-obstacles",
],
"semantic_quality_accepted": False,
}
assert result.report["authority"]["commands_enabled"] is False # type: ignore[index]
@@ -0,0 +1,46 @@
from __future__ import annotations
import json
from pathlib import Path
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
RESULT_ID = (
"m48s-mask-grounding-dino-shadow-"
"b0a37f265223b4138754f76d5d7b8d17e7c4f5395481990f169fccc1645baba8"
)
RESULT_ROOT = (
REPOSITORY_ROOT
/ ".runtime/compute-experiments/m48s-semantic-shadow/mask-grounding-dino-results"
/ RESULT_ID
)
def test_mask_grounding_dino_shadow_is_complete_but_semantically_rejected() -> None:
manifest = json.loads((RESULT_ROOT / "manifest.json").read_text("utf-8"))
report = json.loads((RESULT_ROOT / "report.json").read_text("utf-8"))
assert manifest["result_id"] == RESULT_ID
assert manifest["completed"] is True
assert manifest["accepted"] is False
assert manifest["metrics"]["frames"] == {"completed": 11, "requested": 11}
assert manifest["metrics"]["geometry_observation_count"] == 73
assert manifest["metrics"]["raw_detection_count"] == 25
assert manifest["metrics"]["mask_instance_count"] == 7
assert manifest["metrics"]["geometry_binding_resolution_counts"] == {
"ambiguous": 2,
"selected": 4,
"unresolved": 1,
}
assert manifest["metrics"]["combined_resolution_counts"] == {
"ambiguous": 6,
"unresolved": 1,
}
assert manifest["metrics"]["single_class_agent_probe"] == {
"detection_counts": {"adult": 0, "child": 0, "dog": 0},
"frame_indices": [253, 1228],
"passed": False,
}
assert report["decision"]["semantic_quality_accepted"] is False
assert report["decision"]["agent_semantics_accepted"] is False
assert report["authority"]["commands_enabled"] is False
assert report["authority"]["navigation_or_safety_accepted"] is False
+175
View File
@@ -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
)
+317
View File
@@ -0,0 +1,317 @@
from __future__ import annotations
import copy
import json
from dataclasses import replace
from pathlib import Path
import pytest
from k1link.perception.contracts import (
EvidenceBasis,
EvidenceCurrentness,
MetricGeometry,
MotionState,
ObstacleObservation,
)
from k1link.perception.object_understanding import (
AdvisoryResponse,
AdvisoryRiskAssessment,
AgencyState,
EvidenceKind,
EvidenceProvenance,
ObjectStateEstimate,
ObjectUnderstanding,
ObjectUnderstandingError,
RiskBasis,
RiskLevel,
SemanticDecision,
SemanticHypothesis,
SemanticResolution,
StateBasis,
load_object_semantic_vocabulary,
validate_object_understanding,
)
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
VOCABULARY_PATH = REPOSITORY_ROOT / "config/perception/object-semantic-vocabulary-v0.json"
def _observation(*, semantic_hint: str | None = None) -> ObstacleObservation:
return ObstacleObservation(
observation_id="observation-vehicle-1",
occupancy_key="occupied-component-17",
source_id="RAVNOVES00",
frame_id="frame-000253",
evidence_time_ns=35_421_857_292,
basis=EvidenceBasis.FUSED,
currentness=EvidenceCurrentness.CURRENT,
occupied_support=True,
source_point_ids=(4, 7, 9),
metric_geometry=MetricGeometry(
coordinate_frame="map",
centroid_xyz_m=(4.0, 1.0, 0.5),
range_m=4.15,
covariance_diagonal_m2=(0.04, 0.04, 0.09),
),
proposal_ids=("proposal-vehicle-1",),
semantic_hint=semantic_hint,
reason_codes=("current-qualified-points",),
)
def _detector_evidence(*, frame_id: str = "frame-000253") -> EvidenceProvenance:
return EvidenceProvenance(
evidence_id="evidence-grounding-dino-1",
kind=EvidenceKind.DETECTOR,
source_id="RAVNOVES00",
frame_id=frame_id,
provider_id="grounding-dino-open-vocabulary/v1",
model_id="grounding-dino-tensorrt",
model_revision="worker-006-probe-20260825",
preprocess_id="kb4-rectified-rgb/v1",
prompt_set_id="urban-risk-groups/v0",
)
def _temporal_evidence() -> EvidenceProvenance:
return EvidenceProvenance(
evidence_id="evidence-temporal-1",
kind=EvidenceKind.TEMPORAL,
source_id="RAVNOVES00",
frame_id="frame-000253",
provider_id="temporal-occupied/v1",
model_id=None,
model_revision=None,
preprocess_id=None,
)
def _car_understanding() -> ObjectUnderstanding:
detector = _detector_evidence()
temporal = _temporal_evidence()
return ObjectUnderstanding(
understanding_id="understanding-vehicle-1",
vocabulary_id="missioncore.urban-object-semantics/v0",
generated_monotonic_ns=1_020_000,
observation=_observation(semantic_hint="car"),
hypotheses=(
SemanticHypothesis(
rank=1,
class_id="vehicle.car",
raw_label="car",
confidence=0.79,
evidence_ids=(detector.evidence_id,),
),
SemanticHypothesis(
rank=2,
class_id="vehicle.heavy",
raw_label="heavy vehicle",
confidence=0.12,
evidence_ids=(detector.evidence_id,),
),
),
semantic=SemanticDecision(
resolution=SemanticResolution.SELECTED,
selected_class_id="vehicle.car",
selected_confidence=0.79,
reason_codes=("top-hypothesis-qualified",),
),
state=ObjectStateEstimate(
motion=MotionState.STATIONARY,
motion_confidence=0.85,
agency=AgencyState.SELF_PROPELLED,
agency_basis=StateBasis.CLASS_PRIOR,
evidence_ids=(detector.evidence_id, temporal.evidence_id),
reason_codes=("stationary-observed-vehicle-prior-retained",),
),
risk=AdvisoryRiskAssessment(
policy_id="urban-object-risk/v0",
level=RiskLevel.ELEVATED,
confidence=0.71,
basis=RiskBasis.FUSED,
responses=(
AdvisoryResponse.MONITOR,
AdvisoryResponse.REDUCE_SPEED,
AdvisoryResponse.ROUTE_AROUND,
),
evidence_ids=(detector.evidence_id, temporal.evidence_id),
reason_codes=("stationary-vehicle-may-start-moving",),
),
provenance=(detector, temporal),
)
def _unknown_understanding() -> ObjectUnderstanding:
return ObjectUnderstanding(
understanding_id="understanding-unknown-1",
vocabulary_id="missioncore.urban-object-semantics/v0",
generated_monotonic_ns=1_020_000,
observation=_observation(),
hypotheses=(),
semantic=SemanticDecision(
resolution=SemanticResolution.UNRESOLVED,
selected_class_id=None,
selected_confidence=None,
reason_codes=("semantic-evidence-unavailable",),
),
state=ObjectStateEstimate(
motion=MotionState.UNKNOWN,
motion_confidence=0.0,
agency=AgencyState.UNKNOWN,
agency_basis=StateBasis.UNKNOWN,
evidence_ids=(),
reason_codes=("state-evidence-unavailable",),
),
risk=AdvisoryRiskAssessment(
policy_id="urban-object-risk/v0",
level=RiskLevel.UNKNOWN,
confidence=0.0,
basis=RiskBasis.UNKNOWN,
responses=(AdvisoryResponse.ROUTE_AROUND,),
evidence_ids=(),
reason_codes=("unknown-object-remains-occupied",),
),
provenance=(),
)
def test_object_understanding_round_trip_is_strict_and_keeps_v1_geometry() -> None:
value = _car_understanding()
observation_before = value.observation.to_dict()
document = json.loads(json.dumps(value.to_dict()))
restored = ObjectUnderstanding.from_dict(document)
assert restored == value
assert restored.observation.to_dict() == observation_before
assert restored.occupancy_identity == "occupied-component-17"
assert restored.observation.metric_geometry is not None
assert restored.observation.metric_geometry.range_m == pytest.approx(4.15)
document["unexpected"] = True
with pytest.raises(ObjectUnderstandingError, match="fields are incompatible"):
ObjectUnderstanding.from_dict(document)
def test_stationary_vehicle_keeps_separate_self_propelled_prior_and_advisory_risk() -> None:
value = _car_understanding()
assert value.state.motion is MotionState.STATIONARY
assert value.state.agency is AgencyState.SELF_PROPELLED
assert value.state.agency_basis is StateBasis.CLASS_PRIOR
assert value.risk.level is RiskLevel.ELEVATED
assert AdvisoryResponse.REDUCE_SPEED in value.risk.responses
assert value.authority.commands_enabled is False
assert value.authority.actuation_allowed is False
assert value.authority.navigation_or_safety_accepted is False
def test_unknown_semantics_never_erase_metric_occupancy() -> None:
value = _unknown_understanding()
assert value.semantic.resolution is SemanticResolution.UNRESOLVED
assert value.hypotheses == ()
assert value.observation.occupied_support is True
assert value.observation.source_point_ids == (4, 7, 9)
assert value.occupancy_identity == value.observation.occupancy_identity
assert value.risk.responses == (AdvisoryResponse.ROUTE_AROUND,)
def test_ranked_hypotheses_and_selected_class_must_agree() -> None:
value = _car_understanding()
with pytest.raises(ObjectUnderstandingError, match="ordered by confidence"):
replace(
value,
hypotheses=(
replace(value.hypotheses[0], confidence=0.10),
replace(value.hypotheses[1], confidence=0.90),
),
)
with pytest.raises(ObjectUnderstandingError, match="match one ranked hypothesis"):
replace(
value,
semantic=replace(
value.semantic,
selected_class_id="animal.dog",
selected_confidence=0.79,
),
)
with pytest.raises(ObjectUnderstandingError, match="two hypotheses"):
replace(
value,
hypotheses=value.hypotheses[:1],
semantic=SemanticDecision(
resolution=SemanticResolution.CONFLICT,
selected_class_id=None,
selected_confidence=None,
reason_codes=("provider-disagreement",),
),
)
def test_evidence_cannot_escape_geometry_frame_or_be_fabricated() -> None:
value = _car_understanding()
with pytest.raises(ObjectUnderstandingError, match="escaped"):
replace(value, provenance=(_detector_evidence(frame_id="frame-000254"),))
with pytest.raises(ObjectUnderstandingError, match="unknown evidence"):
replace(
value,
hypotheses=(
replace(value.hypotheses[0], evidence_ids=("missing-evidence",)),
value.hypotheses[1],
),
)
def test_executable_vocabulary_resolves_current_urban_labels_and_hierarchy() -> None:
vocabulary = load_object_semantic_vocabulary(VOCABULARY_PATH)
assert vocabulary.vocabulary_id == "missioncore.urban-object-semantics/v0"
assert vocabulary.resolve_label("trash bin") == "static.trash-bin"
assert vocabulary.resolve_label("Dog") == "animal.dog"
assert vocabulary.resolve_label("sidewalk-curb") == "terrain.curb"
assert {
label: vocabulary.resolve_label(label)
for label in ("car", "person", "bicycle", "road sign")
} == {
"car": "vehicle.car",
"person": "human.unknown",
"bicycle": "vehicle.bicycle",
"road sign": "static.road-sign",
}
assert vocabulary.resolve_label("unseen alien object") is None
assert vocabulary.ancestors("human.child") == (
"human.unknown",
"object.unknown",
)
validate_object_understanding(_car_understanding(), vocabulary)
def test_vocabulary_validation_rejects_undeclared_class_and_authority_change(
tmp_path: Path,
) -> None:
vocabulary = load_object_semantic_vocabulary(VOCABULARY_PATH)
value = _car_understanding()
with pytest.raises(ObjectUnderstandingError, match="undeclared classes"):
validate_object_understanding(
replace(
value,
hypotheses=(
replace(value.hypotheses[0], class_id="vehicle.hovercraft"),
value.hypotheses[1],
),
semantic=replace(
value.semantic,
selected_class_id="vehicle.hovercraft",
),
),
vocabulary,
)
document = json.loads(VOCABULARY_PATH.read_text("utf-8"))
incompatible = copy.deepcopy(document)
incompatible["policies"]["planner_command_authority"] = True
path = tmp_path / "vocabulary.json"
path.write_text(json.dumps(incompatible), "utf-8")
with pytest.raises(ObjectUnderstandingError, match="authority policy"):
load_object_semantic_vocabulary(path)
+281
View File
@@ -0,0 +1,281 @@
from __future__ import annotations
from pathlib import Path
import numpy as np
import pytest
from k1link.perception.contracts import (
BoundingRegion2D,
EvidenceBasis,
EvidenceCurrentness,
MetricGeometry,
ObstacleObservation,
)
from k1link.perception.object_understanding import (
AdvisoryResponse,
AgencyState,
SemanticResolution,
StateBasis,
load_object_semantic_vocabulary,
)
from k1link.perception.open_vocabulary_semantics import (
OpenVocabularyDetection,
OpenVocabularySemanticError,
bind_object_understandings,
fuse_open_vocabulary_detections,
load_open_vocabulary_semantic_profile,
parse_tao_grounding_dino_labels,
)
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
PROFILE_PATH = REPOSITORY_ROOT / "config/perception/open-vocabulary-semantic-shadow-v0.json"
VOCABULARY_PATH = REPOSITORY_ROOT / "config/perception/object-semantic-vocabulary-v0.json"
VALID_FOV_MASK = np.ones((600, 800), dtype=np.bool_)
def _detection(
detection_id: str,
raw_label: str,
confidence: float,
region: tuple[float, float, float, float],
*,
prompt_set_id: str = "urban-static/v0",
) -> OpenVocabularyDetection:
return OpenVocabularyDetection(
detection_id=detection_id,
source_id="RAVNOVES00",
frame_id="frame-000121",
prompt_set_id=prompt_set_id,
raw_label=raw_label,
confidence=confidence,
region=BoundingRegion2D(*region),
)
def _observation(
proposal_id: str | None,
*,
ordinal: int,
) -> ObstacleObservation:
return ObstacleObservation(
observation_id=f"frame-000121:observation-{ordinal}",
occupancy_key=f"frame-000121:occupancy-{ordinal}",
source_id="RAVNOVES00",
frame_id="frame-000121",
evidence_time_ns=121,
basis=EvidenceBasis.FUSED if proposal_id else EvidenceBasis.LIDAR,
currentness=EvidenceCurrentness.CURRENT,
occupied_support=True,
source_point_ids=(ordinal,),
metric_geometry=MetricGeometry(
coordinate_frame="map",
centroid_xyz_m=(1.0, 2.0, 0.5),
range_m=2.2,
covariance_diagonal_m2=(0.1, 0.1, 0.1),
),
proposal_ids=(proposal_id,) if proposal_id else (),
semantic_hint="static.trash-bin" if proposal_id else None,
reason_codes=("test-current-occupied-support",),
)
def test_profile_is_raw_kb4_and_preserves_false_authority() -> None:
profile = load_open_vocabulary_semantic_profile(PROFILE_PATH)
assert profile.coordinate_space == "raw-kb4"
assert (profile.width, profile.height) == (800, 600)
assert profile.provider_id == "nvidia-tao-grounding-dino-trt/v1"
assert profile.model_sha256 == (
"6895acdc6b588e923f753e37b3bd18869e064256e5ecc1b2b9853e8c51125f94"
)
assert tuple(item.prompt_set_id for item in profile.prompt_groups) == (
"urban-static/v0",
"urban-agents/v0",
"urban-vehicles/v0",
)
def test_tao_parser_keeps_source_coordinates_and_rejects_rectified_boxes(
tmp_path: Path,
) -> None:
profile = load_open_vocabulary_semantic_profile(PROFILE_PATH)
labels = tmp_path / "frame-000121.txt"
labels.write_text(
"trash bin 0.00 0 0.00 336.0 123.0 359.0 160.0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.826\n",
"utf-8",
)
parsed = parse_tao_grounding_dino_labels(
labels,
source_id="RAVNOVES00",
frame_id="frame-000121",
prompt_set_id="urban-static/v0",
profile=profile,
)
assert len(parsed) == 1
assert parsed[0].raw_label == "trash bin"
assert parsed[0].region.as_tuple() == (336.0, 123.0, 359.0, 160.0)
labels.write_text(
"trash bin 0.00 0 0.00 336.0 123.0 900.0 160.0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.826\n",
"utf-8",
)
with pytest.raises(OpenVocabularySemanticError, match="raw image coordinate space"):
parse_tao_grounding_dino_labels(
labels,
source_id="RAVNOVES00",
frame_id="frame-000121",
prompt_set_id="urban-static/v0",
profile=profile,
)
def test_tao_parser_translates_geometry_crop_back_to_raw_coordinates(tmp_path: Path) -> None:
profile = load_open_vocabulary_semantic_profile(PROFILE_PATH)
labels = tmp_path / "frame-000121-geometry-roi-000.txt"
labels.write_text(
"dog 0.00 0 0.00 10.0 20.0 50.0 70.0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.714\n",
"utf-8",
)
parsed = parse_tao_grounding_dino_labels(
labels,
source_id="RAVNOVES00",
frame_id="frame-000121",
prompt_set_id="urban-agents/v0",
profile=profile,
image_width=96,
image_height=96,
offset_x=200.0,
offset_y=150.0,
detection_scope_id="frame-000121:geometry-roi-000",
)
assert parsed[0].detection_id.startswith("frame-000121:geometry-roi-000")
assert parsed[0].region.as_tuple() == (210.0, 170.0, 250.0, 220.0)
def test_prompt_collisions_fuse_before_geometry_and_background_box_is_removed() -> None:
profile = load_open_vocabulary_semantic_profile(PROFILE_PATH)
vocabulary = load_object_semantic_vocabulary(VOCABULARY_PATH)
detections = (
_detection("trash", "trash bin", 0.82, (300.0, 100.0, 360.0, 200.0)),
_detection("cone", "traffic cone", 0.35, (301.0, 101.0, 361.0, 201.0)),
_detection(
"dog",
"dog",
0.71,
(100.0, 250.0, 180.0, 340.0),
prompt_set_id="urban-agents/v0",
),
_detection(
"background",
"concrete hemisphere",
0.66,
(0.0, 100.0, 800.0, 600.0),
),
)
result = fuse_open_vocabulary_detections(
detections,
profile=profile,
vocabulary=vocabulary,
valid_fov_mask=VALID_FOV_MASK,
)
assert result.input_detection_count == 4
assert result.invalid_area_count == 1
assert len(result.bindings) == 2
assert sum(len(item.detections) for item in result.bindings) == 3
trash = next(
item for item in result.bindings if item.proposal.semantic_hint == "static.trash-bin"
)
assert len(trash.detections) == 2
assert trash.proposal.objectness == pytest.approx(0.82)
def test_binding_keeps_ranked_semantics_separate_from_state_risk_and_occupancy() -> None:
profile = load_open_vocabulary_semantic_profile(PROFILE_PATH)
vocabulary = load_object_semantic_vocabulary(VOCABULARY_PATH)
fusion = fuse_open_vocabulary_detections(
(
_detection("trash", "trash bin", 0.82, (300.0, 100.0, 360.0, 200.0)),
_detection("cone", "traffic cone", 0.35, (301.0, 101.0, 361.0, 201.0)),
),
profile=profile,
vocabulary=vocabulary,
valid_fov_mask=VALID_FOV_MASK,
)
proposal = fusion.proposals[0]
observations = (
_observation(proposal.proposal_id, ordinal=1),
_observation(None, ordinal=2),
)
understandings = bind_object_understandings(
observations,
bindings=fusion.bindings,
profile=profile,
vocabulary=vocabulary,
generated_monotonic_ns=121,
)
semantic = understandings[0]
assert tuple(item.class_id for item in semantic.hypotheses) == (
"static.trash-bin",
"static.traffic-cone",
)
assert semantic.semantic.resolution is SemanticResolution.SELECTED
assert semantic.semantic.selected_class_id == "static.trash-bin"
assert semantic.state.agency is AgencyState.INERT
assert semantic.state.agency_basis is StateBasis.CLASS_PRIOR
assert semantic.state.motion.value == "unknown"
assert semantic.risk.level.value == "unknown"
assert semantic.risk.responses == (AdvisoryResponse.ROUTE_AROUND,)
assert semantic.authority.navigation_or_safety_accepted is False
assert semantic.observation.source_point_ids == (1,)
geometry_only = understandings[1]
assert geometry_only.semantic.resolution is SemanticResolution.UNRESOLVED
assert geometry_only.hypotheses == ()
assert geometry_only.observation.source_point_ids == (2,)
def test_close_semantic_scores_remain_ambiguous() -> None:
profile = load_open_vocabulary_semantic_profile(PROFILE_PATH)
vocabulary = load_object_semantic_vocabulary(VOCABULARY_PATH)
fusion = fuse_open_vocabulary_detections(
(
_detection(
"adult",
"adult person",
0.64,
(100.0, 50.0, 200.0, 350.0),
prompt_set_id="urban-agents/v0",
),
_detection(
"child",
"child",
0.59,
(101.0, 51.0, 201.0, 351.0),
prompt_set_id="urban-agents/v0",
),
),
profile=profile,
vocabulary=vocabulary,
valid_fov_mask=VALID_FOV_MASK,
)
proposal = fusion.proposals[0]
result = bind_object_understandings(
(_observation(proposal.proposal_id, ordinal=1),),
bindings=fusion.bindings,
profile=profile,
vocabulary=vocabulary,
generated_monotonic_ns=121,
)[0]
assert result.semantic.resolution is SemanticResolution.AMBIGUOUS
assert result.semantic.selected_class_id is None
assert result.state.agency is AgencyState.UNKNOWN
assert result.state.agency_basis is StateBasis.UNKNOWN
+263
View File
@@ -0,0 +1,263 @@
from __future__ import annotations
import json
from pathlib import Path
import pytest
from k1link.laboratory.semantic_object_quality import (
SemanticObjectQualityError,
SemanticTruthLabel,
load_semantic_object_quality_profile,
score_semantic_object_quality,
)
from k1link.perception.contracts import (
EvidenceBasis,
EvidenceCurrentness,
MetricGeometry,
MotionState,
ObstacleObservation,
)
from k1link.perception.object_understanding import (
AdvisoryResponse,
AdvisoryRiskAssessment,
AgencyState,
EvidenceKind,
EvidenceProvenance,
ObjectStateEstimate,
ObjectUnderstanding,
RiskBasis,
RiskLevel,
SemanticDecision,
SemanticHypothesis,
SemanticResolution,
StateBasis,
load_object_semantic_vocabulary,
)
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
VOCABULARY_PATH = REPOSITORY_ROOT / "config/perception/object-semantic-vocabulary-v0.json"
PROFILE_PATH = REPOSITORY_ROOT / "config/perception/m48s-semantic-object-quality-v0.json"
def _observation(index: int) -> ObstacleObservation:
return ObstacleObservation(
observation_id=f"observation-{index}",
occupancy_key=f"occupied-{index}",
source_id="RAVNOVES00",
frame_id=f"frame-{index:06d}",
evidence_time_ns=index * 1_000_000,
basis=EvidenceBasis.FUSED,
currentness=EvidenceCurrentness.CURRENT,
occupied_support=True,
source_point_ids=(index,),
metric_geometry=MetricGeometry(
coordinate_frame="map",
centroid_xyz_m=(float(index), 0.0, 0.5),
range_m=float(index + 1),
covariance_diagonal_m2=(0.04, 0.04, 0.09),
),
proposal_ids=(f"proposal-{index}",),
semantic_hint=None,
reason_codes=("current-qualified-points",),
)
def _prediction(
index: int,
*,
hypotheses: tuple[tuple[str, str, float], ...],
selected_class_id: str | None,
) -> ObjectUnderstanding:
observation = _observation(index)
evidence = EvidenceProvenance(
evidence_id=f"semantic-evidence-{index}",
kind=EvidenceKind.DETECTOR,
source_id=observation.source_id,
frame_id=observation.frame_id,
provider_id="semantic-candidate/v0",
model_id="semantic-model",
model_revision="candidate-1",
preprocess_id="rgb/v1",
prompt_set_id="urban-risk-groups/v0",
)
ranked = tuple(
SemanticHypothesis(
rank=rank,
class_id=class_id,
raw_label=raw_label,
confidence=confidence,
evidence_ids=(evidence.evidence_id,),
)
for rank, (class_id, raw_label, confidence) in enumerate(hypotheses, start=1)
)
selected = next(
(item for item in ranked if item.class_id == selected_class_id),
None,
)
return ObjectUnderstanding(
understanding_id=f"understanding-{index}",
vocabulary_id="missioncore.urban-object-semantics/v0",
generated_monotonic_ns=index * 1_000_000 + 1,
observation=observation,
hypotheses=ranked,
semantic=SemanticDecision(
resolution=(
SemanticResolution.SELECTED
if selected is not None
else SemanticResolution.UNRESOLVED
),
selected_class_id=None if selected is None else selected.class_id,
selected_confidence=None if selected is None else selected.confidence,
reason_codes=(
"top-hypothesis-qualified"
if selected is not None
else "semantic-evidence-insufficient",
),
),
state=ObjectStateEstimate(
motion=MotionState.UNKNOWN,
motion_confidence=0.0,
agency=AgencyState.UNKNOWN,
agency_basis=StateBasis.UNKNOWN,
evidence_ids=(),
reason_codes=("motion-evidence-unavailable",),
),
risk=AdvisoryRiskAssessment(
policy_id="urban-object-risk/v0",
level=RiskLevel.UNKNOWN,
confidence=0.0,
basis=RiskBasis.UNKNOWN,
responses=(AdvisoryResponse.ROUTE_AROUND,),
evidence_ids=(),
reason_codes=("semantic-quality-does-not-score-risk",),
),
provenance=(evidence,),
)
def _truth(index: int, class_id: str) -> SemanticTruthLabel:
return SemanticTruthLabel(
label_id=f"semantic-label-{index}",
observation_id=f"observation-{index}",
source_id="RAVNOVES00",
frame_id=f"frame-{index:06d}",
class_id=class_id,
reviewer_count=2,
adjudicated=True,
)
def test_semantic_quality_scores_exact_group_topk_and_unresolved_separately() -> None:
vocabulary = load_object_semantic_vocabulary(VOCABULARY_PATH)
profile = load_semantic_object_quality_profile(PROFILE_PATH)
predictions = (
_prediction(
1,
hypotheses=(
("human.unknown", "person", 0.70),
("human.child", "child", 0.65),
),
selected_class_id="human.unknown",
),
_prediction(
2,
hypotheses=(("animal.dog", "dog", 0.80),),
selected_class_id="animal.dog",
),
_prediction(3, hypotheses=(), selected_class_id=None),
)
truth = (
_truth(1, "human.child"),
_truth(2, "animal.dog"),
_truth(3, "vehicle.car"),
)
result = score_semantic_object_quality(
predictions=predictions,
truth=truth,
vocabulary=vocabulary,
profile=profile,
)
metrics = result.report["metrics"]
assert isinstance(metrics, dict)
assert metrics["prediction_coverage"] == pytest.approx(1.0)
assert metrics["exact_top1_accuracy"] == pytest.approx(1 / 3)
assert metrics["coarse_group_accuracy"] == pytest.approx(2 / 3)
assert metrics["exact_top_k_recall"] == pytest.approx(2 / 3)
assert metrics["unresolved_fraction"] == pytest.approx(1 / 3)
assert result.report["candidate_semantic_gate_passed"] is False
assert result.cases[0].exact_top1_correct is False
assert result.cases[0].coarse_group_correct is True
assert result.cases[0].exact_top_k_hit is True
def test_semantic_quality_reports_missing_projection_without_calling_it_detection() -> None:
vocabulary = load_object_semantic_vocabulary(VOCABULARY_PATH)
profile = load_semantic_object_quality_profile(PROFILE_PATH)
result = score_semantic_object_quality(
predictions=(
_prediction(
1,
hypotheses=(("human.child", "child", 0.90),),
selected_class_id="human.child",
),
),
truth=(_truth(1, "human.child"), _truth(2, "animal.dog")),
vocabulary=vocabulary,
profile=profile,
)
metrics = result.report["metrics"]
assert isinstance(metrics, dict)
assert metrics["prediction_coverage"] == pytest.approx(0.5)
assert result.cases[1].resolution == "unavailable"
scope = result.report["scope"]
assert isinstance(scope, dict)
assert scope["object_presence_scored"] is False
def test_semantic_truth_is_strict_and_requires_independent_adjudication() -> None:
label = _truth(1, "human.child")
document = json.loads(json.dumps(label.to_dict()))
assert SemanticTruthLabel.from_dict(document) == label
document["category"] = "child"
with pytest.raises(SemanticObjectQualityError, match="fields are incompatible"):
SemanticTruthLabel.from_dict(document)
with pytest.raises(SemanticObjectQualityError, match="two independent reviewers"):
SemanticTruthLabel(
label_id="semantic-label-1",
observation_id="observation-1",
source_id="RAVNOVES00",
frame_id="frame-000001",
class_id="human.child",
reviewer_count=1,
adjudicated=True,
)
with pytest.raises(SemanticObjectQualityError, match="must be adjudicated"):
SemanticTruthLabel(
label_id="semantic-label-1",
observation_id="observation-1",
source_id="RAVNOVES00",
frame_id="frame-000001",
class_id="human.child",
reviewer_count=2,
adjudicated=False,
)
def test_semantic_quality_profile_stays_separate_from_presence_and_risk(
tmp_path: Path,
) -> None:
profile = load_semantic_object_quality_profile(PROFILE_PATH)
assert profile.profile_id == "m48s-urban-semantic-object-quality/v0"
assert profile.vocabulary_id == "missioncore.urban-object-semantics/v0"
assert profile.top_k == 5
document = json.loads(PROFILE_PATH.read_text("utf-8"))
document["scope"]["object_presence_scored"] = True
changed = tmp_path / "m48s-profile-changed.json"
changed.write_text(json.dumps(document), "utf-8")
with pytest.raises(SemanticObjectQualityError, match="scope changed"):
load_semantic_object_quality_profile(changed)
@@ -0,0 +1,47 @@
from __future__ import annotations
from pathlib import Path
from k1link.perception.semantic_shadow_replay import read_semantic_shadow_replay
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
RESULT_ROOT = (
REPOSITORY_ROOT
/ ".runtime/compute-experiments/m48s-semantic-shadow/results"
/ "m48s-semantic-shadow-a237a8860bd4655b4fae981ba23a5e4d86a560ed243ee7efdf867d87f5071739"
)
def test_bounded_raw_kb4_semantic_shadow_is_complete_but_not_accepted() -> None:
result = read_semantic_shadow_replay(RESULT_ROOT)
assert result.completed is True
assert result.accepted is False
assert result.metrics["frames"] == {
"completed": 11,
"requested": 11,
"source_available": 11,
}
assert result.metrics["detections"] == {
"below_confidence": 0,
"fused_proposals": 164,
"invalid_area": 16,
"outside_valid_fov": 0,
"raw": 310,
"retained": 294,
}
semantics = result.metrics["semantics"]
assert isinstance(semantics, dict)
assert semantics["resolution_counts"] == {
"ambiguous": 4,
"selected": 40,
"unresolved": 165,
}
assert result.report["decision"] == {
"navigation_or_safety_accepted": False,
"next_gate": "M48S human-reviewed semantic object quality slice",
"raw_kb4_coordinate_binding_completed": True,
"risk_policy_accepted": False,
"semantic_quality_accepted": False,
}
assert result.report["authority"]["commands_enabled"] is False