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
@@ -0,0 +1,454 @@
"""Separate semantic-class evaluation over geometry-bound object projections.
This companion contour does not mutate the class-free M4.8 truth or score
object presence. It evaluates canonical semantic resolution only after a
separate two-reviewer, adjudicated semantic label set exists.
"""
from __future__ import annotations
import json
import math
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Final
from k1link.perception.object_understanding import (
ObjectSemanticVocabulary,
ObjectUnderstanding,
SemanticResolution,
validate_object_understanding,
)
SEMANTIC_TRUTH_LABEL_SCHEMA: Final = "missioncore.semantic-object-truth-label/v0"
SEMANTIC_QUALITY_PROFILE_SCHEMA: Final = "missioncore.semantic-object-quality-profile/v0"
SEMANTIC_QUALITY_REPORT_SCHEMA: Final = "missioncore.semantic-object-quality-report/v0"
_IDENTIFIER = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$")
_FALSE_AUTHORITY: Final = {
"physical_live": False,
"commands_enabled": False,
"actuation_allowed": False,
"navigation_or_safety_accepted": False,
}
class SemanticObjectQualityError(ValueError):
"""A semantic truth, profile or evaluation input is incompatible."""
@dataclass(frozen=True, slots=True)
class SemanticTruthLabel:
"""One independently reviewed canonical class bound to existing geometry."""
label_id: str
observation_id: str
source_id: str
frame_id: str
class_id: str
reviewer_count: int
adjudicated: bool
def __post_init__(self) -> None:
for value, label in (
(self.label_id, "semantic truth label id"),
(self.observation_id, "semantic truth observation id"),
(self.source_id, "semantic truth source id"),
(self.frame_id, "semantic truth frame id"),
(self.class_id, "semantic truth class id"),
):
_identifier(value, label)
if (
not isinstance(self.reviewer_count, int)
or isinstance(self.reviewer_count, bool)
or self.reviewer_count < 2
):
raise SemanticObjectQualityError("semantic truth requires two independent reviewers")
if self.adjudicated is not True:
raise SemanticObjectQualityError("semantic truth must be adjudicated")
def to_dict(self) -> dict[str, object]:
return {
"schema_version": SEMANTIC_TRUTH_LABEL_SCHEMA,
"label_id": self.label_id,
"observation_id": self.observation_id,
"source_id": self.source_id,
"frame_id": self.frame_id,
"class_id": self.class_id,
"reviewer_count": self.reviewer_count,
"adjudicated": self.adjudicated,
}
@classmethod
def from_dict(cls, value: object) -> SemanticTruthLabel:
document = _object(value, "semantic truth label")
_exact_keys(
document,
{
"schema_version",
"label_id",
"observation_id",
"source_id",
"frame_id",
"class_id",
"reviewer_count",
"adjudicated",
},
"semantic truth label",
)
if document.get("schema_version") != SEMANTIC_TRUTH_LABEL_SCHEMA:
raise SemanticObjectQualityError("semantic truth label schema is incompatible")
return cls(
label_id=_string(document, "label_id"),
observation_id=_string(document, "observation_id"),
source_id=_string(document, "source_id"),
frame_id=_string(document, "frame_id"),
class_id=_string(document, "class_id"),
reviewer_count=_integer(document, "reviewer_count"),
adjudicated=_boolean(document, "adjudicated"),
)
@dataclass(frozen=True, slots=True)
class SemanticObjectQualityProfile:
"""Bounded evaluation policy for a frozen semantic provider candidate."""
profile_id: str
vocabulary_id: str
top_k: int
minimum_prediction_coverage: float
minimum_exact_top1_accuracy: float
minimum_coarse_group_accuracy: float
minimum_exact_top_k_recall: float
maximum_unresolved_fraction: float
maximum_conflict_fraction: float
def __post_init__(self) -> None:
_identifier(self.profile_id, "semantic quality profile id")
_identifier(self.vocabulary_id, "semantic quality vocabulary id")
if (
not isinstance(self.top_k, int)
or isinstance(self.top_k, bool)
or not 1 <= self.top_k <= 5
):
raise SemanticObjectQualityError("semantic quality top-k is invalid")
for value, label in (
(self.minimum_prediction_coverage, "minimum prediction coverage"),
(self.minimum_exact_top1_accuracy, "minimum exact top-1 accuracy"),
(self.minimum_coarse_group_accuracy, "minimum coarse group accuracy"),
(self.minimum_exact_top_k_recall, "minimum exact top-k recall"),
(self.maximum_unresolved_fraction, "maximum unresolved fraction"),
(self.maximum_conflict_fraction, "maximum conflict fraction"),
):
_fraction(value, label)
@dataclass(frozen=True, slots=True)
class SemanticQualityCase:
"""One deterministic truth/prediction comparison row."""
label_id: str
observation_id: str
truth_class_id: str
resolution: str
selected_class_id: str | None
exact_top1_correct: bool
coarse_group_correct: bool
exact_top_k_hit: bool
def to_dict(self) -> dict[str, object]:
return {
"label_id": self.label_id,
"observation_id": self.observation_id,
"truth_class_id": self.truth_class_id,
"resolution": self.resolution,
"selected_class_id": self.selected_class_id,
"exact_top1_correct": self.exact_top1_correct,
"coarse_group_correct": self.coarse_group_correct,
"exact_top_k_hit": self.exact_top_k_hit,
}
@dataclass(frozen=True, slots=True)
class SemanticObjectQualityResult:
"""In-memory deterministic semantic report and comparison ledger."""
report: dict[str, object]
cases: tuple[SemanticQualityCase, ...]
def load_semantic_object_quality_profile(path: Path) -> SemanticObjectQualityProfile:
"""Read a strict semantic evaluation profile."""
try:
document = json.loads(path.expanduser().resolve(strict=True).read_text("utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise SemanticObjectQualityError("semantic quality profile cannot be read") from exc
root = _object(document, "semantic quality profile")
_exact_keys(
root,
{
"schema_version",
"profile_id",
"vocabulary_id",
"top_k",
"thresholds",
"scope",
"authority",
},
"semantic quality profile",
)
if root.get("schema_version") != SEMANTIC_QUALITY_PROFILE_SCHEMA:
raise SemanticObjectQualityError("semantic quality profile schema is incompatible")
scope = _object(root.get("scope"), "semantic quality scope")
_exact_keys(
scope,
{
"object_presence_scored",
"semantic_class_scored",
"risk_policy_scored",
"requires_separate_adjudicated_semantic_truth",
},
"semantic quality scope",
)
if scope != {
"object_presence_scored": False,
"semantic_class_scored": True,
"risk_policy_scored": False,
"requires_separate_adjudicated_semantic_truth": True,
}:
raise SemanticObjectQualityError("semantic quality scope changed")
if _object(root.get("authority"), "semantic quality authority") != _FALSE_AUTHORITY:
raise SemanticObjectQualityError("semantic quality authority changed")
thresholds = _object(root.get("thresholds"), "semantic quality thresholds")
_exact_keys(
thresholds,
{
"minimum_prediction_coverage",
"minimum_exact_top1_accuracy",
"minimum_coarse_group_accuracy",
"minimum_exact_top_k_recall",
"maximum_unresolved_fraction",
"maximum_conflict_fraction",
},
"semantic quality thresholds",
)
return SemanticObjectQualityProfile(
profile_id=_string(root, "profile_id"),
vocabulary_id=_string(root, "vocabulary_id"),
top_k=_integer(root, "top_k"),
minimum_prediction_coverage=_number(thresholds, "minimum_prediction_coverage"),
minimum_exact_top1_accuracy=_number(thresholds, "minimum_exact_top1_accuracy"),
minimum_coarse_group_accuracy=_number(thresholds, "minimum_coarse_group_accuracy"),
minimum_exact_top_k_recall=_number(thresholds, "minimum_exact_top_k_recall"),
maximum_unresolved_fraction=_number(thresholds, "maximum_unresolved_fraction"),
maximum_conflict_fraction=_number(thresholds, "maximum_conflict_fraction"),
)
def score_semantic_object_quality(
*,
predictions: tuple[ObjectUnderstanding, ...],
truth: tuple[SemanticTruthLabel, ...],
vocabulary: ObjectSemanticVocabulary,
profile: SemanticObjectQualityProfile,
) -> SemanticObjectQualityResult:
"""Score semantics on geometry-bound truth without scoring object presence."""
if not truth or any(not isinstance(item, SemanticTruthLabel) for item in truth):
raise SemanticObjectQualityError("semantic truth set is invalid")
if any(not isinstance(item, ObjectUnderstanding) for item in predictions):
raise SemanticObjectQualityError("semantic prediction set is invalid")
if profile.vocabulary_id != vocabulary.vocabulary_id:
raise SemanticObjectQualityError("semantic quality vocabulary changed")
truth_ids = tuple(item.observation_id for item in truth)
if len(set(truth_ids)) != len(truth_ids):
raise SemanticObjectQualityError("semantic truth observations must be unique")
prediction_ids = tuple(item.observation.observation_id for item in predictions)
if len(set(prediction_ids)) != len(prediction_ids):
raise SemanticObjectQualityError("semantic prediction observations must be unique")
declared_classes = {item.class_id for item in vocabulary.classes}
if any(item.class_id not in declared_classes for item in truth):
raise SemanticObjectQualityError("semantic truth uses an undeclared class")
for candidate in predictions:
validate_object_understanding(candidate, vocabulary)
by_observation = {item.observation.observation_id: item for item in predictions}
cases: list[SemanticQualityCase] = []
predicted_count = 0
selected_count = 0
conflict_count = 0
for label in truth:
prediction = by_observation.get(label.observation_id)
if prediction is None:
cases.append(
SemanticQualityCase(
label_id=label.label_id,
observation_id=label.observation_id,
truth_class_id=label.class_id,
resolution="unavailable",
selected_class_id=None,
exact_top1_correct=False,
coarse_group_correct=False,
exact_top_k_hit=False,
)
)
continue
predicted_count += 1
observation = prediction.observation
if observation.source_id != label.source_id or observation.frame_id != label.frame_id:
raise SemanticObjectQualityError("semantic prediction escaped its truth source frame")
selected = prediction.semantic.selected_class_id
if prediction.semantic.resolution is SemanticResolution.SELECTED:
selected_count += 1
if prediction.semantic.resolution is SemanticResolution.CONFLICT:
conflict_count += 1
exact = selected == label.class_id
coarse = selected is not None and _coarse_group(selected, vocabulary) == _coarse_group(
label.class_id, vocabulary
)
top_k_ids = {item.class_id for item in prediction.hypotheses[: profile.top_k]}
cases.append(
SemanticQualityCase(
label_id=label.label_id,
observation_id=label.observation_id,
truth_class_id=label.class_id,
resolution=prediction.semantic.resolution.value,
selected_class_id=selected,
exact_top1_correct=exact,
coarse_group_correct=coarse,
exact_top_k_hit=label.class_id in top_k_ids,
)
)
truth_count = len(truth)
exact_accuracy = sum(item.exact_top1_correct for item in cases) / truth_count
coarse_accuracy = sum(item.coarse_group_correct for item in cases) / truth_count
top_k_recall = sum(item.exact_top_k_hit for item in cases) / truth_count
prediction_coverage = predicted_count / truth_count
unresolved_fraction = (truth_count - selected_count) / truth_count
conflict_fraction = conflict_count / truth_count
gates = {
"prediction_coverage": (prediction_coverage >= profile.minimum_prediction_coverage),
"exact_top1_accuracy": (exact_accuracy >= profile.minimum_exact_top1_accuracy),
"coarse_group_accuracy": (coarse_accuracy >= profile.minimum_coarse_group_accuracy),
"exact_top_k_recall": (top_k_recall >= profile.minimum_exact_top_k_recall),
"unresolved_fraction": (unresolved_fraction <= profile.maximum_unresolved_fraction),
"conflict_fraction": (conflict_fraction <= profile.maximum_conflict_fraction),
}
report: dict[str, object] = {
"schema_version": SEMANTIC_QUALITY_REPORT_SCHEMA,
"profile_id": profile.profile_id,
"vocabulary_id": vocabulary.vocabulary_id,
"scope": {
"object_presence_scored": False,
"semantic_class_scored": True,
"risk_policy_scored": False,
},
"metrics": {
"truth_count": truth_count,
"prediction_count": predicted_count,
"selected_count": selected_count,
"conflict_count": conflict_count,
"prediction_coverage": prediction_coverage,
"exact_top1_accuracy": exact_accuracy,
"coarse_group_accuracy": coarse_accuracy,
"exact_top_k_recall": top_k_recall,
"unresolved_fraction": unresolved_fraction,
"conflict_fraction": conflict_fraction,
},
"gates": gates,
"candidate_semantic_gate_passed": all(gates.values()),
"limitations": [
"Object presence and geometry quality remain owned by the class-free contour.",
"Semantic qualification does not qualify risk policy or physical motion.",
"No navigation, safety, command or actuation authority is granted.",
],
"authority": dict(_FALSE_AUTHORITY),
}
return SemanticObjectQualityResult(report=report, cases=tuple(cases))
def _coarse_group(
class_id: str,
vocabulary: ObjectSemanticVocabulary,
) -> str:
ancestors = vocabulary.ancestors(class_id)
if not ancestors:
return class_id
non_root = tuple(item for item in ancestors if item != "object.unknown")
return non_root[-1] if non_root else class_id
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 SemanticObjectQualityError(f"{label} must be an object")
return value
def _exact_keys(document: dict[str, object], expected: set[str], label: str) -> None:
if set(document) != expected:
raise SemanticObjectQualityError(f"{label} fields are incompatible")
def _string(document: dict[str, object], key: str) -> str:
value = document.get(key)
if not isinstance(value, str) or not value:
raise SemanticObjectQualityError(f"{key} must be a nonempty string")
return value
def _integer(document: dict[str, object], key: str) -> int:
value = document.get(key)
if not isinstance(value, int) or isinstance(value, bool):
raise SemanticObjectQualityError(f"{key} must be an integer")
return value
def _boolean(document: dict[str, object], key: str) -> bool:
value = document.get(key)
if not isinstance(value, bool):
raise SemanticObjectQualityError(f"{key} must be boolean")
return value
def _number(document: dict[str, object], key: str) -> float:
value = document.get(key)
if (
not isinstance(value, (int, float))
or isinstance(value, bool)
or not math.isfinite(float(value))
):
raise SemanticObjectQualityError(f"{key} must be finite")
return float(value)
def _fraction(value: object, label: str) -> float:
if (
not isinstance(value, (int, float))
or isinstance(value, bool)
or not math.isfinite(float(value))
or not 0.0 <= float(value) <= 1.0
):
raise SemanticObjectQualityError(f"{label} must be within [0, 1]")
return float(value)
def _identifier(value: str, label: str) -> None:
if not isinstance(value, str) or _IDENTIFIER.fullmatch(value) is None:
raise SemanticObjectQualityError(f"{label} is not a safe identifier")
__all__ = [
"SEMANTIC_QUALITY_PROFILE_SCHEMA",
"SEMANTIC_QUALITY_REPORT_SCHEMA",
"SEMANTIC_TRUTH_LABEL_SCHEMA",
"SemanticObjectQualityError",
"SemanticObjectQualityProfile",
"SemanticObjectQualityResult",
"SemanticQualityCase",
"SemanticTruthLabel",
"load_semantic_object_quality_profile",
"score_semantic_object_quality",
]