feat(perception): add mixed-route vegetation review

This commit is contained in:
DCCONSTRUCTIONS
2026-08-28 23:13:48 +03:00
parent e10b96b546
commit af1e530162
12 changed files with 2332 additions and 17 deletions
@@ -0,0 +1,417 @@
"""Seal RAVNOVES004TREE mixed-route review into the existing vegetation LAB."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import shutil
import tempfile
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
import numpy as np
from PIL import Image, ImageDraw
from k1link.laboratory.vegetation_shadow_lab import (
LAB_SCHEMA,
RESULT_PREFIX,
VegetationShadowLabError,
canonical_json,
sha256_path,
)
REVIEW_SCHEMA = "missioncore.mixed-route-review-pack/v1"
DDRNET_SCHEMA = "missioncore.mixed-route-ddrnet-islands/v1"
TGS_SCHEMA = "missioncore.mixed-route-tgs-result/v1"
FRAME_COUNT = 10
PHASES = (
"rural",
"rural",
"rural",
"rural",
"rural",
"transition",
"urban",
"urban",
"urban",
"urban",
)
TGS_COLORS = {
0: (5, 7, 9),
1: (132, 188, 86),
2: (235, 112, 122),
3: (150, 154, 163),
}
def _read_json(path: Path, label: str) -> dict[str, Any]:
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise VegetationShadowLabError(f"{label} is invalid") from exc
if not isinstance(value, dict):
raise VegetationShadowLabError(f"{label} must be an object")
return value
def _artifact(
source: Path,
staging: Path,
relative: str,
artifacts: list[dict[str, object]],
*,
role: str,
media_type: str,
) -> dict[str, object]:
if source.is_symlink() or not source.is_file():
raise VegetationShadowLabError(f"mixed-route artifact is unavailable: {relative}")
target = staging / relative
target.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
shutil.copyfile(source, target)
descriptor = {
"role": role,
"path": relative,
"byte_length": target.stat().st_size,
"sha256": sha256_path(target),
"media_type": media_type,
}
artifacts.append(descriptor)
return descriptor
def _image_proof(descriptor: dict[str, object]) -> dict[str, object]:
return {"path": descriptor["path"], "sha256": descriptor["sha256"]}
def _render_tgs_costmaps(tgs_root: Path, destination: Path) -> list[Path]:
result = _read_json(tgs_root / "result.json", "mixed-route TGS result")
evidence = result.get("evidence")
costmap = result.get("costmap")
if (
result.get("schema_version") != TGS_SCHEMA
or result.get("status") != "passed-review-only"
or not isinstance(evidence, dict)
or not isinstance(costmap, dict)
or result.get("summary", {}).get("frame_count") != FRAME_COUNT
or result.get("authority", {}).get("actuation_allowed") is not False
):
raise VegetationShadowLabError("mixed-route TGS contract changed")
evidence_path = tgs_root / str(evidence.get("path"))
if (
not evidence_path.is_file()
or evidence.get("bytes") != evidence_path.stat().st_size
or evidence.get("sha256") != sha256_path(evidence_path)
):
raise VegetationShadowLabError("mixed-route TGS evidence changed")
with np.load(evidence_path, allow_pickle=False) as archive:
centers = archive["costmap_cell_centers_xy_m"]
states = archive["causal_rolling_1s_costmap_states"]
if centers.shape != (2244, 2) or states.shape != (FRAME_COUNT, 2244):
raise VegetationShadowLabError("mixed-route TGS costmap shape changed")
radius = float(costmap["radius_m"])
cell_size = float(costmap["cell_size_m"])
size = 600
scale = size / (radius * 2.0)
outputs: list[Path] = []
destination.mkdir(mode=0o700, parents=True, exist_ok=True)
for slot in range(FRAME_COUNT):
image = Image.new("RGB", (size, size), TGS_COLORS[0])
draw = ImageDraw.Draw(image)
half = cell_size * scale / 2.0
for center, state in zip(centers, states[slot], strict=True):
x = (float(center[0]) + radius) * scale
y = (radius - float(center[1])) * scale
draw.rectangle((x - half, y - half, x + half, y + half), fill=TGS_COLORS[int(state)])
rover_w = 0.8 * scale
rover_l = 1.0 * scale
cx = size / 2.0
cy = size / 2.0
draw.rectangle(
(cx - rover_w / 2, cy - rover_l / 2, cx + rover_w / 2, cy + rover_l / 2),
outline=(255, 255, 255),
width=3,
)
path = destination / f"frame-{slot + 1:06d}.png"
image.save(path, format="PNG", optimize=True)
outputs.append(path)
return outputs
def seal_mixed_route_vegetation_review(
*,
base_lab_root: Path,
review_pack_root: Path,
eomt_root: Path,
ddrnet_root: Path,
tgs_root: Path,
output_root: Path,
) -> Path:
base_root = base_lab_root.resolve(strict=True)
base = _read_json(base_root / "result.json", "base vegetation LAB")
base_identity = base.get("identity")
if (
base.get("schema_version") != LAB_SCHEMA
or not isinstance(base_identity, dict)
or hashlib.sha256(canonical_json(base_identity)).hexdigest()
!= base.get("identity_sha256")
or base.get("result_id") != base_root.name
or not base_root.name.startswith(RESULT_PREFIX)
or base.get("authority", {}).get("commands_enabled") is not False
):
raise VegetationShadowLabError("base vegetation LAB proof changed")
pack_root = review_pack_root.resolve(strict=True)
pack = _read_json(pack_root / "manifest.json", "mixed-route review pack")
timeline_path = pack_root / str(pack.get("timeline", {}).get("path"))
if (
pack.get("schema_version") != REVIEW_SCHEMA
or pack.get("frame_count") != FRAME_COUNT
or pack.get("identity", {}).get("session_id") != "20260828T130511Z_viewer_live"
or pack.get("identity", {}).get("ground_truth") is not False
or not timeline_path.is_file()
or pack.get("timeline", {}).get("sha256") != sha256_path(timeline_path)
):
raise VegetationShadowLabError("mixed-route review pack changed")
timeline = [json.loads(line) for line in timeline_path.read_text(encoding="utf-8").splitlines()]
if len(timeline) != FRAME_COUNT:
raise VegetationShadowLabError("mixed-route timeline is incomplete")
eomt = _read_json(eomt_root / "run-report.partial.json", "mixed-route EoMT result")
ddrnet = _read_json(ddrnet_root / "result.json", "mixed-route DDRNet result")
tgs = _read_json(tgs_root / "result.json", "mixed-route TGS result")
if (
eomt.get("input", {}).get("frames_admitted") != FRAME_COUNT
or eomt.get("metrics", {}).get("frames_processed") != FRAME_COUNT
or eomt.get("ground_truth") is not False
or ddrnet.get("schema_version") != DDRNET_SCHEMA
or ddrnet.get("source", {}).get("pack_id") != pack["pack_id"]
or len(ddrnet.get("frames", [])) != FRAME_COUNT
or ddrnet.get("authority", {}).get("candidate_accepted") is not False
or tgs.get("schema_version") != TGS_SCHEMA
or tgs.get("source", {}).get("review_pack_id") != pack["pack_id"]
or tgs.get("summary", {}).get("frame_count") != FRAME_COUNT
):
raise VegetationShadowLabError("mixed-route model identities differ")
output_root.mkdir(mode=0o700, parents=True, exist_ok=True)
temporary = Path(tempfile.mkdtemp(prefix=".mixed-route-vegetation-", dir=output_root))
artifacts: list[dict[str, object]] = []
try:
tgs_images = _render_tgs_costmaps(tgs_root, temporary / ".tgs-render")
cases: list[dict[str, object]] = []
tgs_anchors = {
int(row["slot"]): row
for row in tgs["anchors"]
if row.get("profile_id") == "causal_rolling_1s"
}
for slot, row in enumerate(timeline):
case_id = f"route-{slot + 1:02d}"
relative_root = f"route-review/{case_id}"
source_descriptor = _artifact(
pack_root / "frames" / f"frame-{slot + 1:06d}.png",
temporary,
f"{relative_root}/source.png",
artifacts,
role="mixed-route-source-frame",
media_type="image/png",
)
city_descriptor = _artifact(
eomt_root / "overlay-frames" / f"frame-{slot + 1:06d}.png",
temporary,
f"{relative_root}/city.png",
artifacts,
role="mixed-route-eomt-overlay",
media_type="image/png",
)
vegetation_descriptor = _artifact(
ddrnet_root / "overlay-frames" / f"frame-{slot + 1:06d}.png",
temporary,
f"{relative_root}/vegetation.png",
artifacts,
role="mixed-route-ddrnet-overlay",
media_type="image/png",
)
tgs_descriptor = _artifact(
tgs_images[slot],
temporary,
f"{relative_root}/tgs.png",
artifacts,
role="mixed-route-tgs-costmap",
media_type="image/png",
)
anchor = tgs_anchors[slot]
cases.append(
{
"case_id": case_id,
"phase": PHASES[slot],
"source_sequence": int(row["source_sequence"]),
"session_seconds": float(row["session_seconds"]),
"assets": {
"source": _image_proof(source_descriptor),
"city": _image_proof(city_descriptor),
"vegetation": _image_proof(vegetation_descriptor),
"tgs": _image_proof(tgs_descriptor),
},
"tgs": {
"ground_cells": int(anchor["ground_cell_count"]),
"occupied_cells": int(anchor["nonground_cell_count"]),
"rejected_cells": int(anchor["rejected_cell_count"]),
"unobserved_cells": int(anchor["unobserved_cell_count"]),
},
}
)
shutil.rmtree(temporary / ".tgs-render")
proofs = {}
for key, path in (
("base", base_root / "result.json"),
("eomt", eomt_root / "run-report.partial.json"),
("ddrnet", ddrnet_root / "result.json"),
("tgs", tgs_root / "result.json"),
):
descriptor = _artifact(
path,
temporary,
f"proofs/{key}.json",
artifacts,
role="mixed-route-proof",
media_type="application/json",
)
proofs[key] = _image_proof(descriptor)
_artifact(
tgs_root / str(tgs["evidence"]["path"]),
temporary,
"proofs/tgs-evidence.npz",
artifacts,
role="mixed-route-tgs-evidence",
media_type="application/x-npz",
)
route_review = {
"source_id": "RAVNOVES004TREE",
"session_id": "20260828T130511Z_viewer_live",
"pack_id": pack["pack_id"],
"frame_count": FRAME_COUNT,
"ground_truth": False,
"selection_policy": "same-scene-camera-lidar-aligned-review-islands/v1",
"models": {
"city": {
"name": "EoMT Cityscapes",
"frames": FRAME_COUNT,
"inference_fps": eomt["metrics"]["inference_frames_per_second"],
"end_to_end_p95_ms": eomt["metrics"]["latency_ms"]["end_to_end_ms"]["p95"],
},
"vegetation": {
"name": ddrnet["candidate"]["loaded_model_name"],
"result_id": ddrnet["result_id"],
"frames": FRAME_COUNT,
"latency_p95_ms": ddrnet["timing"]["latency_ms_p95"],
},
"tgs": {
"name": "TRAVEL/TGS causal rolling 1 s",
"frames": FRAME_COUNT,
"latency_p95_ms": tgs["timing"]["wall_seconds_p95"] * 1000.0,
"cell_size_m": tgs["costmap"]["cell_size_m"],
"radius_m": tgs["costmap"]["radius_m"],
},
},
"cases": cases,
"proofs": proofs,
"limitations": [
"Ten aligned review islands are not a complete route timeline.",
"RAVNOVES004TREE has no manual truth.",
"DDRNet vegetation subtypes remain visually noisy and are not planner authority.",
"TGS does not prove ditch or negative-obstacle detection.",
"People and vehicles require an independent fail-safe detector and STOP path.",
],
}
authority = {
"commands_enabled": False,
"navigation_or_safety_accepted": False,
"actuation_accepted": False,
"camera_semantics_can_clear_rigid_geometry": False,
}
identity = {
"lab_id": "lab-v1-vegetation-mission-policy",
"base_result_id": base["result_id"],
"selected_candidate": base_identity["selected_candidate"],
"candidate_metrics": base_identity["candidate_metrics"],
"source": {
"shadow_session": "RAVNOVES004TREE",
"shadow_camera": "sensor.camera.right",
"shadow_frame_count": FRAME_COUNT,
"video_shadow_frame_count": 0,
},
"route_review": route_review,
"authority": authority,
}
identity_sha256 = hashlib.sha256(canonical_json(identity)).hexdigest()
result_id = f"{RESULT_PREFIX}{identity_sha256}"
manifest = {
"schema_version": LAB_SCHEMA,
"result_id": result_id,
"identity_sha256": identity_sha256,
"created_at_utc": datetime.now(UTC).isoformat(),
"ground_truth": False,
"status": "visual-shadow-ready-policy-not-authorized",
"identity": identity,
"source": identity["source"],
"route_video": None,
"route_review": route_review,
"method": {
"completeness": "bounded-review-islands",
"execution_class": "ai-inference",
"pipeline_id": "ravnoves004tree-eomt-ddrnet-causal-tgs-review/v1",
},
"metrics": {"candidates": base["metrics"]["candidates"]},
"decision": {
"selected_candidate": base_identity["selected_candidate"],
"visual_shadow_ready": True,
"full_video_shadow_ready": False,
"mission_policy_ready_for_configuration": True,
"multilayer_policy_review_ready": True,
"navigation_accepted": False,
"production_accepted": False,
},
"limitations": route_review["limitations"],
"authority": authority,
"catalogs": {"goose": [], "ravnoves": []},
"artifacts": artifacts,
}
(temporary / "result.json").write_bytes(canonical_json(manifest) + b"\n")
destination = output_root / result_id
if destination.exists():
raise VegetationShadowLabError("immutable mixed-route LAB result already exists")
os.replace(temporary, destination)
return destination
except Exception:
shutil.rmtree(temporary, ignore_errors=True)
raise
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--base-lab-root", type=Path, required=True)
parser.add_argument("--review-pack-root", type=Path, required=True)
parser.add_argument("--eomt-root", type=Path, required=True)
parser.add_argument("--ddrnet-root", type=Path, required=True)
parser.add_argument("--tgs-root", type=Path, required=True)
parser.add_argument("--output-root", type=Path, required=True)
args = parser.parse_args()
print(
seal_mixed_route_vegetation_review(
base_lab_root=args.base_lab_root,
review_pack_root=args.review_pack_root,
eomt_root=args.eomt_root,
ddrnet_root=args.ddrnet_root,
tgs_root=args.tgs_root,
output_root=args.output_root,
)
)
if __name__ == "__main__":
main()