refactor(lab): перевести RAV004 на канонический Rerun pipeline
This commit is contained in:
@@ -1047,6 +1047,11 @@ app.include_router(
|
||||
if session_recorded_camera_frame_service is not None
|
||||
else None
|
||||
),
|
||||
jobs_root=REPOSITORY_ROOT / ".runtime" / "compute-jobs",
|
||||
rerun_overlay_cache_root=(
|
||||
session_store.data_dir / "laboratory-rerun-overlays"
|
||||
),
|
||||
ffmpeg_path=_ffmpeg,
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
|
||||
@@ -124,6 +124,8 @@ class RecordedBlueprintRequest(StrictApiModel):
|
||||
active_view: Literal["spatial", "perception", "perception3d", "metrics"] = "spatial"
|
||||
view_reset_generation: Literal[0, 1] = 0
|
||||
unified_perception: StrictBool = False
|
||||
semantic_layer: Literal["city", "vegetation"] | None = None
|
||||
plan_view: StrictBool = False
|
||||
show_detections_2d: StrictBool = False
|
||||
show_segmentation: StrictBool = False
|
||||
show_cuboids_3d: StrictBool = False
|
||||
@@ -935,6 +937,8 @@ def build_session_router(
|
||||
active_view=request.active_view,
|
||||
view_reset_generation=request.view_reset_generation,
|
||||
unified_perception=request.unified_perception,
|
||||
semantic_layer=request.semantic_layer,
|
||||
plan_view=request.plan_view,
|
||||
show_detections_2d=request.show_detections_2d,
|
||||
show_segmentation=request.show_segmentation,
|
||||
show_cuboids_3d=request.show_cuboids_3d,
|
||||
|
||||
@@ -12,13 +12,21 @@ import zipfile
|
||||
from collections.abc import Callable
|
||||
from functools import lru_cache
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import Any, Final
|
||||
from typing import Any, Final, Literal
|
||||
|
||||
import numpy as np
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
from fastapi.responses import FileResponse, JSONResponse, Response
|
||||
from PIL import Image
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from k1link.laboratory.canonical_rerun_overlay import (
|
||||
CanonicalLabOverlayError,
|
||||
_mask_component_boxes,
|
||||
canonical_lab_overlay,
|
||||
canonical_recording_id,
|
||||
)
|
||||
from k1link.laboratory.evidence_registry import LaboratoryEvidenceDefinition
|
||||
from k1link.laboratory.evidence_report import (
|
||||
LaboratoryEvidenceReportError,
|
||||
@@ -34,6 +42,17 @@ from k1link.sessions.canonical_lab_spatial import (
|
||||
RootProvider = Callable[[], Path | None]
|
||||
CanonicalRecordingProvider = Callable[[str], tuple[Path, str] | None]
|
||||
CameraFrameProvider = Callable[[str, int], RecordedCameraFrame]
|
||||
|
||||
|
||||
class CanonicalLabRerunRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
application_id: Literal["nodedc_mission_core_recorded"]
|
||||
recording_id: str = Field(
|
||||
min_length=1,
|
||||
max_length=128,
|
||||
pattern=r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$",
|
||||
)
|
||||
_MAX_DOCUMENT_BYTES: Final = 1024 * 1024
|
||||
_CANONICAL_ROUTE_CHUNK_FRAMES: Final = 24
|
||||
_DEFINITION: Final = LaboratoryEvidenceDefinition(
|
||||
@@ -57,6 +76,9 @@ def build_vegetation_shadow_lab_router(
|
||||
root_provider: RootProvider = lambda: None,
|
||||
canonical_recording_provider: CanonicalRecordingProvider | None = None,
|
||||
camera_frame_provider: CameraFrameProvider | None = None,
|
||||
jobs_root: Path | None = None,
|
||||
rerun_overlay_cache_root: Path | None = None,
|
||||
ffmpeg_path: Path | None = None,
|
||||
) -> APIRouter:
|
||||
return _build_vegetation_lab_router(
|
||||
prefix="/api/v1/laboratory/vegetation-shadow",
|
||||
@@ -64,6 +86,9 @@ def build_vegetation_shadow_lab_router(
|
||||
root_provider=root_provider,
|
||||
canonical_recording_provider=canonical_recording_provider,
|
||||
camera_frame_provider=camera_frame_provider,
|
||||
jobs_root=jobs_root,
|
||||
rerun_overlay_cache_root=rerun_overlay_cache_root,
|
||||
ffmpeg_path=ffmpeg_path,
|
||||
)
|
||||
|
||||
|
||||
@@ -84,6 +109,9 @@ def _build_vegetation_lab_router(
|
||||
root_provider: RootProvider,
|
||||
canonical_recording_provider: CanonicalRecordingProvider | None = None,
|
||||
camera_frame_provider: CameraFrameProvider | None = None,
|
||||
jobs_root: Path | None = None,
|
||||
rerun_overlay_cache_root: Path | None = None,
|
||||
ffmpeg_path: Path | None = None,
|
||||
) -> APIRouter:
|
||||
router = APIRouter(
|
||||
prefix=prefix,
|
||||
@@ -270,6 +298,61 @@ def _build_vegetation_lab_router(
|
||||
},
|
||||
)
|
||||
|
||||
@router.post("/{result_id}/canonical-overlay.rrd")
|
||||
async def get_canonical_rerun_overlay(
|
||||
result_id: str,
|
||||
request: CanonicalLabRerunRequest,
|
||||
) -> FileResponse:
|
||||
"""Project LAB-only evidence into the base recording's native clock."""
|
||||
|
||||
if (
|
||||
canonical_recording_provider is None
|
||||
or jobs_root is None
|
||||
or rerun_overlay_cache_root is None
|
||||
or ffmpeg_path is None
|
||||
):
|
||||
raise HTTPException(status_code=503, detail="Canonical LAB Rerun overlay unavailable")
|
||||
candidate = _resolve_candidate(root_provider, definition, result_id)
|
||||
manifest = _read_verified(candidate, definition)
|
||||
route, _ = _full_route_context(candidate, manifest)
|
||||
recording = canonical_recording_provider(str(route["session_id"]))
|
||||
if recording is None:
|
||||
raise HTTPException(status_code=409, detail="Canonical spatial recording is not ready")
|
||||
recording_path, generation_sha256 = recording
|
||||
try:
|
||||
expected_recording_id = await run_in_threadpool(
|
||||
canonical_recording_id,
|
||||
recording_path,
|
||||
)
|
||||
if request.recording_id != expected_recording_id:
|
||||
raise HTTPException(status_code=412, detail="Canonical recording identity changed")
|
||||
artifact = await run_in_threadpool(
|
||||
canonical_lab_overlay,
|
||||
candidate,
|
||||
manifest,
|
||||
recording_id=request.recording_id,
|
||||
base_generation_sha256=generation_sha256,
|
||||
jobs_root=jobs_root,
|
||||
cache_root=rerun_overlay_cache_root,
|
||||
ffmpeg_path=ffmpeg_path,
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except CanonicalLabOverlayError as exc:
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="Canonical LAB Rerun overlay failed verification",
|
||||
) from exc
|
||||
return FileResponse(
|
||||
artifact.path,
|
||||
media_type="application/vnd.rerun.rrd",
|
||||
headers={
|
||||
"Cache-Control": "private, max-age=31536000, immutable",
|
||||
"ETag": f'"{artifact.sha256}"',
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
},
|
||||
)
|
||||
|
||||
@router.get("/{result_id}/timeline")
|
||||
def get_canonical_route_timeline(result_id: str) -> dict[str, object]:
|
||||
candidate = _resolve_candidate(root_provider, definition, result_id)
|
||||
@@ -811,80 +894,6 @@ def _semantic_component_proposals_cached(
|
||||
return tuple(proposals[:32])
|
||||
|
||||
|
||||
def _mask_component_boxes(
|
||||
mask: np.ndarray,
|
||||
class_id: int,
|
||||
*,
|
||||
minimum_pixels: int,
|
||||
) -> list[tuple[int, int, int, int, int]]:
|
||||
"""Return 8-connected run-length components without an OpenCV dependency."""
|
||||
|
||||
if mask.ndim != 2 or minimum_pixels < 1:
|
||||
return []
|
||||
parents: list[int] = []
|
||||
runs: list[tuple[int, int, int, int]] = []
|
||||
|
||||
def root(index: int) -> int:
|
||||
while parents[index] != index:
|
||||
parents[index] = parents[parents[index]]
|
||||
index = parents[index]
|
||||
return index
|
||||
|
||||
def union(left: int, right: int) -> None:
|
||||
left_root = root(left)
|
||||
right_root = root(right)
|
||||
if left_root != right_root:
|
||||
parents[right_root] = left_root
|
||||
|
||||
previous: list[int] = []
|
||||
for row_index, row in enumerate(mask):
|
||||
matches = np.flatnonzero(row == class_id)
|
||||
if matches.size == 0:
|
||||
previous = []
|
||||
continue
|
||||
split_at = np.flatnonzero(np.diff(matches) > 1) + 1
|
||||
groups = np.split(matches, split_at)
|
||||
current: list[int] = []
|
||||
previous_cursor = 0
|
||||
for group in groups:
|
||||
start = int(group[0])
|
||||
stop = int(group[-1]) + 1
|
||||
run_index = len(runs)
|
||||
runs.append((row_index, start, stop, stop - start))
|
||||
parents.append(run_index)
|
||||
current.append(run_index)
|
||||
while (
|
||||
previous_cursor < len(previous)
|
||||
and runs[previous[previous_cursor]][2] < start
|
||||
):
|
||||
previous_cursor += 1
|
||||
candidate_cursor = previous_cursor
|
||||
while candidate_cursor < len(previous):
|
||||
previous_index = previous[candidate_cursor]
|
||||
_, previous_start, previous_stop, _ = runs[previous_index]
|
||||
if previous_start > stop:
|
||||
break
|
||||
union(run_index, previous_index)
|
||||
candidate_cursor += 1
|
||||
previous = current
|
||||
|
||||
components: dict[int, list[int]] = {}
|
||||
for run_index, (row, start, stop, count) in enumerate(runs):
|
||||
component = components.setdefault(root(run_index), [start, row, stop, row + 1, 0])
|
||||
component[0] = min(component[0], start)
|
||||
component[1] = min(component[1], row)
|
||||
component[2] = max(component[2], stop)
|
||||
component[3] = max(component[3], row + 1)
|
||||
component[4] += count
|
||||
result = [
|
||||
(left, top, right, bottom, count)
|
||||
for left, top, right, bottom, count in components.values()
|
||||
if count >= minimum_pixels and right - left >= 2 and bottom - top >= 3
|
||||
]
|
||||
result.sort(key=lambda box: (-box[4], box[1], box[0]))
|
||||
return result
|
||||
|
||||
|
||||
def _route_tgs_anchor_payload(path: Path, source_sequence: int) -> dict[str, object]:
|
||||
before = path.stat()
|
||||
with np.load(path, allow_pickle=False) as archive:
|
||||
|
||||
Reference in New Issue
Block a user