feat: add local surface review triage

This commit is contained in:
DCCONSTRUCTIONS
2026-07-26 00:11:42 +03:00
parent 3449b2bc3e
commit c6c48fbc38
12 changed files with 1273 additions and 33 deletions
+2
View File
@@ -110,6 +110,7 @@ from .lidar_local_surface import (
DEFAULT_K1_LOCAL_SURFACE_PROFILE,
K1_LOCAL_SURFACE_FRAME_SCHEMA,
K1_LOCAL_SURFACE_REPORT_SCHEMA,
K1_LOCAL_SURFACE_REVIEW_SCHEMA,
K1_LOCAL_SURFACE_SCHEMA,
K1_LOCAL_SURFACE_TIMELINE_SCHEMA,
K1LocalSurfaceProfile,
@@ -209,6 +210,7 @@ __all__ = [
"LIDAR_GROUND_FRAME_SCHEMA",
"K1_LOCAL_SURFACE_FRAME_SCHEMA",
"K1_LOCAL_SURFACE_REPORT_SCHEMA",
"K1_LOCAL_SURFACE_REVIEW_SCHEMA",
"K1_LOCAL_SURFACE_SCHEMA",
"K1_LOCAL_SURFACE_TIMELINE_SCHEMA",
"LIDAR_FIELD_REVIEW_REPORT_SCHEMA",
+206 -1
View File
@@ -11,7 +11,7 @@ from collections.abc import Mapping
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, Final
from typing import Any, Final, cast
import numpy as np
import numpy.typing as npt
@@ -29,9 +29,14 @@ K1_LOCAL_SURFACE_SCHEMA: Final = "missioncore.k1-local-surface/v1"
K1_LOCAL_SURFACE_REPORT_SCHEMA: Final = "missioncore.k1-local-surface-report/v1"
K1_LOCAL_SURFACE_FRAME_SCHEMA: Final = "missioncore.k1-local-surface-frame/v1"
K1_LOCAL_SURFACE_TIMELINE_SCHEMA: Final = "missioncore.k1-local-surface-timeline/v1"
K1_LOCAL_SURFACE_REVIEW_SCHEMA: Final = "missioncore.k1-local-surface-review/v1"
K1_LOCAL_SURFACE_ARRAYS_NAME: Final = "local-surface.npz"
K1_LOCAL_SURFACE_REPORT_NAME: Final = "local-surface.json"
K1_LOCAL_SURFACE_MANIFEST_NAME: Final = "manifest.json"
K1_LOCAL_SURFACE_REVIEW_PROFILE_ID: Final = "missioncore-local-surface-attention/v1"
K1_LOCAL_SURFACE_REVIEW_TAIL_M: Final = 0.45
K1_LOCAL_SURFACE_REVIEW_INLIER_FLOOR: Final = 0.85
K1_LOCAL_SURFACE_REVIEW_HIGH_SCORE: Final = 2.0
POINT_UNCLASSIFIED: Final = 0
POINT_SURFACE: Final = 1
@@ -631,6 +636,195 @@ class K1LocalSurfaceV1:
"authority": self.report["authority"],
}
def review_detail(self, source: E10LidarFieldSource) -> dict[str, object]:
_validate_source_binding(self, source)
criteria = self._review_criteria()
reason_counts = {
"prediction-tail": 0,
"prediction-inlier-drop": 0,
"surface-height-jump": 0,
"surface-slope-jump": 0,
"surface-roughness-jump": 0,
}
if not self.has_temporal_qualification:
return {
"schema_version": K1_LOCAL_SURFACE_REVIEW_SCHEMA,
"review_profile_id": K1_LOCAL_SURFACE_REVIEW_PROFILE_ID,
"model_id": self.model_id,
"source_pack_id": source.pack_id,
"session_id": source.identity["session_id"],
"available": False,
"criteria": criteria,
"summary": {
"item_count": 0,
"episode_count": 0,
"high_attention_count": 0,
"review_attention_count": 0,
"reason_counts": reason_counts,
},
"items": [],
"ground_truth": False,
"access": "read-only",
"authority": self.report["authority"],
}
tail_threshold = float(criteria["prediction_tail_residual_p95_m"])
inlier_floor = float(criteria["prediction_inlier_fraction_floor"])
height_threshold = float(criteria["surface_height_jump_m"])
slope_threshold = float(criteria["surface_slope_jump_deg"])
roughness_threshold = float(criteria["surface_roughness_jump_m"])
chronological: list[dict[str, object]] = []
last_review_frame: int | None = None
episode_index = 0
for frame_index in range(source.frame_count):
reasons: list[str] = []
ratios: list[float] = []
prediction_available = bool(
self.arrays["prediction_available"][frame_index]
)
prediction_p95 = float(
self.arrays["prediction_residual_p95_m"][frame_index]
)
prediction_inlier = float(
self.arrays["prediction_inlier_fraction"][frame_index]
)
if prediction_available and prediction_p95 >= tail_threshold:
reasons.append("prediction-tail")
ratios.append(prediction_p95 / tail_threshold)
if prediction_available and prediction_inlier < inlier_floor:
reasons.append("prediction-inlier-drop")
ratios.append((1.0 - prediction_inlier) / (1.0 - inlier_floor))
temporal_compared = bool(self.arrays["temporal_compared"][frame_index])
height_delta = float(self.arrays["height_delta_m"][frame_index])
slope_delta = float(self.arrays["slope_delta_deg"][frame_index])
roughness_delta = float(self.arrays["roughness_delta_m"][frame_index])
if temporal_compared and height_delta >= height_threshold:
reasons.append("surface-height-jump")
ratios.append(height_delta / height_threshold)
if temporal_compared and slope_delta >= slope_threshold:
reasons.append("surface-slope-jump")
ratios.append(slope_delta / slope_threshold)
if temporal_compared and roughness_delta >= roughness_threshold:
reasons.append("surface-roughness-jump")
ratios.append(roughness_delta / roughness_threshold)
if not reasons:
continue
if last_review_frame is None or frame_index - last_review_frame > 2:
episode_index += 1
last_review_frame = frame_index
for reason in reasons:
reason_counts[reason] += 1
score = max(ratios)
chronological.append(
{
"rank": 0,
"frame_index": frame_index,
"source_frame_index": int(
source.arrays["source_frame_indices"][frame_index]
),
"session_seconds": float(
source.arrays["session_seconds"][frame_index]
),
"episode_id": f"episode-{episode_index:02d}",
"attention": (
"high"
if score >= K1_LOCAL_SURFACE_REVIEW_HIGH_SCORE
else "review"
),
"attention_score": score,
"reasons": reasons,
"prediction": {
"available": prediction_available,
"residual_p50_m": float(
self.arrays["prediction_residual_p50_m"][frame_index]
),
"residual_p95_m": prediction_p95,
"inlier_fraction": prediction_inlier,
},
"temporal": {
"compared": temporal_compared,
"height_delta_m": height_delta,
"slope_delta_deg": slope_delta,
"roughness_delta_m": roughness_delta,
},
"surface": {
"sensor_height_m": float(
self.arrays["sensor_height_m"][frame_index]
),
"slope_deg": float(self.arrays["slope_deg"][frame_index]),
"roughness_m": float(
self.arrays["roughness_m"][frame_index]
),
"confidence": float(
self.arrays["confidence"][frame_index]
),
},
"step_candidate_point_count": int(
self.arrays["step_candidate_point_count"][frame_index]
),
}
)
items = sorted(
chronological,
key=lambda item: (
-cast(float, item["attention_score"]),
cast(int, item["frame_index"]),
),
)
for rank, item in enumerate(items, start=1):
item["rank"] = rank
high_attention_count = sum(
item["attention"] == "high" for item in items
)
return {
"schema_version": K1_LOCAL_SURFACE_REVIEW_SCHEMA,
"review_profile_id": K1_LOCAL_SURFACE_REVIEW_PROFILE_ID,
"model_id": self.model_id,
"source_pack_id": source.pack_id,
"session_id": source.identity["session_id"],
"available": True,
"criteria": criteria,
"summary": {
"item_count": len(items),
"episode_count": episode_index,
"high_attention_count": high_attention_count,
"review_attention_count": len(items) - high_attention_count,
"reason_counts": reason_counts,
},
"items": items,
"ground_truth": False,
"access": "read-only",
"authority": self.report["authority"],
}
def _review_criteria(self) -> dict[str, float | int]:
profile = _object(self.identity.get("profile"), "K1 local-surface profile")
temporal = _object(
profile.get("temporal_qualification"),
"K1 local-surface temporal profile",
)
return {
"prediction_tail_residual_p95_m": K1_LOCAL_SURFACE_REVIEW_TAIL_M,
"prediction_inlier_fraction_floor": (
K1_LOCAL_SURFACE_REVIEW_INLIER_FLOOR
),
"surface_height_jump_m": _positive_number(
temporal.get("height_jump_m"),
"K1 local-surface height jump threshold",
),
"surface_slope_jump_deg": _positive_number(
temporal.get("slope_jump_deg"),
"K1 local-surface slope jump threshold",
),
"surface_roughness_jump_m": _positive_number(
temporal.get("roughness_jump_m"),
"K1 local-surface roughness jump threshold",
),
"high_attention_score": K1_LOCAL_SURFACE_REVIEW_HIGH_SCORE,
"episode_max_frame_gap": 2,
}
def build_k1_local_surface(
source: E10LidarFieldSource,
@@ -1479,6 +1673,17 @@ def _nonnegative_int(value: object, label: str) -> int:
return value
def _positive_number(value: object, label: str) -> float:
if (
not isinstance(value, (int, float))
or isinstance(value, bool)
or not math.isfinite(value)
or value <= 0.0
):
raise LidarGroundError(f"{label} is invalid")
return float(value)
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream: