feat(perception): add DDRNet full-video vegetation replay

This commit is contained in:
DCCONSTRUCTIONS
2026-08-28 01:13:18 +03:00
parent 67e6ae98e2
commit 30080c51aa
10 changed files with 729 additions and 64 deletions
+173 -1
View File
@@ -5,17 +5,28 @@ from __future__ import annotations
import argparse
import hashlib
import json
import re
import shutil
import tempfile
import zipfile
from datetime import UTC, datetime
from pathlib import Path, PurePosixPath
from typing import Any, Final
from k1link.laboratory.m47_reference_graph import read_m47_reference_graph_lab
LAB_SCHEMA: Final = "missioncore.lab-v1-vegetation-shadow/v1"
WORKER_SCHEMA: Final = "missioncore.lab-v1-goose-vegetation-run/v1"
RESULT_PREFIX: Final = "lab-v1-vegetation-shadow-"
_CANDIDATES: Final = ("ddrnet", "ppliteseg")
_MODES: Final = ("goose", "ravnoves")
_VIDEO_MODE: Final = "ravnoves-video"
_VIDEO_FRAME_COUNT: Final = 4489
_M4_RESULT_ID: Final = re.compile(r"^m4-threat-replay-[a-f0-9]{64}$")
_VIDEO_WORKER_RESULT_ID: Final = re.compile(
r"^lab-v1-ravnoves-video-ddrnet-[a-f0-9]{64}$",
)
_SHA256: Final = re.compile(r"^[a-f0-9]{64}$")
_FOCUS_ORDER: Final = (
"high_grass",
"low_grass",
@@ -195,6 +206,106 @@ def _validation_metric_summary(result: dict[str, Any], candidate: str) -> dict[s
}
def _validated_video_semantics(
root: Path,
result: dict[str, Any],
) -> tuple[dict[str, object], Path]:
source = _object(result.get("source"), "DDRNet video source")
video = _object(result.get("video_semantics"), "DDRNet video semantics")
archive = _object(video.get("mask_archive"), "DDRNet video mask archive")
taxonomy = _object(video.get("taxonomy"), "DDRNet video taxonomy")
classes = taxonomy.get("classes")
base_m4_result_id = video.get("base_m4_result_id")
worker_result_id = result.get("result_id")
aggregate_prediction_pixels = video.get("aggregate_prediction_pixels")
if (
source.get("input_count") != _VIDEO_FRAME_COUNT
or source.get("ground_truth_available") is not False
or taxonomy.get("schema_version")
!= "missioncore.lab-v1-vegetation-taxonomy/v1"
or not isinstance(classes, list)
or len(classes) != 64
or not isinstance(base_m4_result_id, str)
or _M4_RESULT_ID.fullmatch(base_m4_result_id) is None
or not isinstance(worker_result_id, str)
or _VIDEO_WORKER_RESULT_ID.fullmatch(worker_result_id) is None
or not isinstance(aggregate_prediction_pixels, list)
or len(aggregate_prediction_pixels) != 64
or any(type(count) is not int or count < 0 for count in aggregate_prediction_pixels)
or sum(aggregate_prediction_pixels) != _VIDEO_FRAME_COUNT * 800 * 600
or video.get("center_crop_xyxy") != [100, 0, 700, 600]
or video.get("outside_crop_state") != "undefined"
or archive.get("path") != "semantic-masks.zip"
or archive.get("frame_count") != _VIDEO_FRAME_COUNT
or archive.get("width") != 800
or archive.get("height") != 600
or archive.get("encoding") != "uint8-class-id-png"
or archive.get("media_type") != "application/zip"
or archive.get("sequence_binding")
!= "sequence-0-to-masks/frame-000001.png"
):
raise VegetationShadowLabError("DDRNet full-video contract changed")
for expected_id, raw_class in enumerate(classes):
row = _object(raw_class, "DDRNet taxonomy class")
color = row.get("color_rgb")
if (
row.get("class_id") != expected_id
or not isinstance(row.get("label"), str)
or not row["label"]
or row.get("disposition")
not in ({"undefined"} if expected_id == 0 else {"prediction"})
or not isinstance(color, list)
or len(color) != 3
or any(not isinstance(channel, int) or not 0 <= channel <= 255 for channel in color)
):
raise VegetationShadowLabError("DDRNet video taxonomy changed")
archive_path = root / "semantic-masks.zip"
expected_sha256 = archive.get("sha256")
expected_bytes = archive.get("byte_length")
if (
archive_path.is_symlink()
or not archive_path.is_file()
or type(expected_bytes) is not int
or expected_bytes <= 0
or archive_path.stat().st_size != expected_bytes
or not isinstance(expected_sha256, str)
or _SHA256.fullmatch(expected_sha256) is None
or sha256_path(archive_path) != expected_sha256
):
raise VegetationShadowLabError("DDRNet video mask archive proof changed")
expected_members = [
f"masks/frame-{sequence + 1:06d}.png"
for sequence in range(_VIDEO_FRAME_COUNT)
]
try:
with zipfile.ZipFile(archive_path) as frozen:
members = frozen.infolist()
if (
[member.filename for member in members] != expected_members
or any(
member.is_dir()
or member.file_size < 8
or member.file_size > 1024 * 1024
for member in members
)
):
raise VegetationShadowLabError("DDRNet video mask sequence changed")
except zipfile.BadZipFile as exc:
raise VegetationShadowLabError("DDRNet video mask archive is invalid") from exc
return {
"worker_result_id": worker_result_id,
"base_m4_result_id": base_m4_result_id,
"frame_count": _VIDEO_FRAME_COUNT,
"width": 800,
"height": 600,
"center_crop_xyxy": [100, 0, 700, 600],
"outside_crop_state": "undefined",
"sequence_binding": archive["sequence_binding"],
"taxonomy": taxonomy,
"aggregate_prediction_pixels": aggregate_prediction_pixels,
}, archive_path
def seal_vegetation_shadow_lab(
*,
ddrnet_goose_root: Path,
@@ -202,6 +313,8 @@ def seal_vegetation_shadow_lab(
ddrnet_ravnoves_root: Path,
ppliteseg_ravnoves_root: Path,
output_root: Path,
ddrnet_ravnoves_video_root: Path | None = None,
m47_reference_graph_lab_root: Path | None = None,
) -> Path:
roots = {
("ddrnet", "goose"): ddrnet_goose_root.resolve(),
@@ -218,6 +331,29 @@ def seal_vegetation_shadow_lab(
if cases[("ddrnet", mode)].keys() != cases[("ppliteseg", mode)].keys():
raise VegetationShadowLabError(f"{mode} candidate case islands differ")
selected = _selected_candidate(results)
if (ddrnet_ravnoves_video_root is None) != (m47_reference_graph_lab_root is None):
raise VegetationShadowLabError("full-video Worker and M4.7 roots must be paired")
route_video: dict[str, object] | None = None
route_video_archive: Path | None = None
video_result: dict[str, Any] | None = None
if ddrnet_ravnoves_video_root is not None and m47_reference_graph_lab_root is not None:
video_root = ddrnet_ravnoves_video_root.resolve()
video_result = _read_worker_result(
video_root,
candidate="ddrnet",
mode=_VIDEO_MODE,
)
route_video, route_video_archive = _validated_video_semantics(video_root, video_result)
m47 = read_m47_reference_graph_lab(m47_reference_graph_lab_root)
m47_source = _object(m47.report.get("source"), "M4.7 source")
m47_visual = _object(m47.report.get("visual_evidence"), "M4.7 visual evidence")
if (
m47_source.get("source_id") != "RAVNOVES00"
or m47_visual.get("linked_result_id") != route_video["base_m4_result_id"]
or m47_visual.get("timeline_frames") != _VIDEO_FRAME_COUNT
):
raise VegetationShadowLabError("M4.7 video binding differs from DDRNet source")
route_video["m47_reference_graph_result_id"] = m47.result_id
output_root.mkdir(mode=0o700, parents=True, exist_ok=True)
temporary = Path(tempfile.mkdtemp(prefix=".lab-v1-vegetation-", dir=output_root))
@@ -315,6 +451,34 @@ def seal_vegetation_shadow_lab(
"path": relative,
"sha256": descriptor["sha256"],
}
if video_result is not None and route_video is not None and route_video_archive is not None:
video_root = ddrnet_ravnoves_video_root.resolve() # type: ignore[union-attr]
worker_descriptor = _copy_artifact(
video_root / "result.json",
temporary,
"worker/ddrnet-ravnoves-video.json",
artifacts,
role="worker-result",
media_type="application/json",
)
worker_proofs["ddrnet_ravnoves_video"] = {
"result_id": video_result.get("result_id"),
"path": worker_descriptor["path"],
"sha256": worker_descriptor["sha256"],
}
archive_descriptor = _copy_artifact(
route_video_archive,
temporary,
"video/ddrnet-semantic-masks.zip",
artifacts,
role="route-semantic-mask-archive",
media_type="application/zip",
)
route_video["mask_archive"] = {
"path": archive_descriptor["path"],
"sha256": archive_descriptor["sha256"],
"byte_length": archive_descriptor["byte_length"],
}
candidate_metrics: dict[str, object] = {}
for candidate in _CANDIDATES:
@@ -348,11 +512,13 @@ def seal_vegetation_shadow_lab(
"shadow_session": "RAVNOVES00",
"shadow_camera": "sensor.camera.right",
"shadow_frame_count": 12,
"video_shadow_frame_count": _VIDEO_FRAME_COUNT if route_video else 0,
},
"selected_candidate": selected,
"candidate_metrics": candidate_metrics,
"worker_proofs": worker_proofs,
"visual_catalog_sha256": hashlib.sha256(canonical_json(catalogs)).hexdigest(),
"route_video": route_video,
"authority": authority,
}
identity_sha256 = hashlib.sha256(canonical_json(identity)).hexdigest()
@@ -366,6 +532,7 @@ def seal_vegetation_shadow_lab(
"status": "visual-shadow-ready-policy-not-authorized",
"identity": identity,
"source": identity["source"],
"route_video": route_video,
"method": {
"completeness": "complete",
"execution_class": "ai-inference",
@@ -375,13 +542,14 @@ def seal_vegetation_shadow_lab(
"decision": {
"selected_candidate": selected,
"visual_shadow_ready": True,
"full_video_shadow_ready": route_video is not None,
"mission_policy_ready_for_configuration": True,
"navigation_accepted": False,
"production_accepted": False,
},
"limitations": [
"GOOSE validation is external-domain qualification, not RAVNOVES ground truth.",
"The RAVNOVES shadow remains in Worker proofs and is not catalogued as vegetation evidence because it has no independent labels.",
"The full RAVNOVES DDRNet playback is prediction-only and has no independent labels.",
"Vegetation semantics never clears rigid LiDAR/TGS occupancy.",
"Undefined pixels outside the 600x600 center crop remain fail-closed.",
],
@@ -407,6 +575,8 @@ def _parse_args() -> argparse.Namespace:
parser.add_argument("--ddrnet-ravnoves-root", type=Path, required=True)
parser.add_argument("--ppliteseg-ravnoves-root", type=Path, required=True)
parser.add_argument("--output-root", type=Path, required=True)
parser.add_argument("--ddrnet-ravnoves-video-root", type=Path)
parser.add_argument("--m47-reference-graph-lab-root", type=Path)
return parser.parse_args()
@@ -418,6 +588,8 @@ def main() -> None:
ddrnet_ravnoves_root=args.ddrnet_ravnoves_root,
ppliteseg_ravnoves_root=args.ppliteseg_ravnoves_root,
output_root=args.output_root,
ddrnet_ravnoves_video_root=args.ddrnet_ravnoves_video_root,
m47_reference_graph_lab_root=args.m47_reference_graph_lab_root,
)
print(destination)
+45 -1
View File
@@ -3,15 +3,17 @@
from __future__ import annotations
import copy
import hashlib
import json
import re
import zipfile
from collections.abc import Callable
from functools import lru_cache
from pathlib import Path, PurePosixPath
from typing import Any, Final
from fastapi import APIRouter, HTTPException
from fastapi.responses import FileResponse
from fastapi.responses import FileResponse, Response
from k1link.laboratory.evidence_registry import LaboratoryEvidenceDefinition
from k1link.laboratory.evidence_report import (
@@ -86,6 +88,48 @@ def build_vegetation_shadow_lab_router(
},
)
@router.get("/{result_id}/masks/{sequence}")
def get_video_mask(result_id: str, sequence: int) -> Response:
candidate = _resolve_candidate(root_provider, result_id)
manifest = _read_verified(candidate)
route_video = manifest.get("route_video")
if not isinstance(route_video, dict) or not 0 <= sequence < 4489:
raise HTTPException(status_code=404, detail="Vegetation video mask not found")
archive = route_video.get("mask_archive")
if not isinstance(archive, dict) or archive.get("path") != "video/ddrnet-semantic-masks.zip":
raise HTTPException(status_code=404, detail="Vegetation video mask not found")
archive_path = candidate / "video" / "ddrnet-semantic-masks.zip"
member = f"masks/frame-{sequence + 1:06d}.png"
try:
before = archive_path.stat()
with zipfile.ZipFile(archive_path) as frozen:
info = frozen.getinfo(member)
if info.is_dir() or info.file_size < 8 or info.file_size > 1024 * 1024:
raise ValueError("Vegetation video mask member is invalid")
payload = frozen.read(info)
after = archive_path.stat()
if (
before.st_size != after.st_size
or before.st_mtime_ns != after.st_mtime_ns
or len(payload) != info.file_size
):
raise ValueError("Vegetation video mask archive changed during read")
except (KeyError, OSError, ValueError, zipfile.BadZipFile):
raise HTTPException(
status_code=503,
detail="Vegetation video mask failed verification",
) from None
digest = hashlib.sha256(payload).hexdigest()
return Response(
content=payload,
media_type="image/png",
headers={
"Cache-Control": "private, max-age=31536000, immutable",
"ETag": f'"{digest}"',
"X-Content-Type-Options": "nosniff",
},
)
return router