feat(perception): add DDRNet full-video vegetation replay
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user