refactor(lab): restore canonical RAV004 replay

This commit is contained in:
DCCONSTRUCTIONS
2026-08-30 01:22:44 +03:00
parent 74da6437e9
commit f1cbe0061a
21 changed files with 1181 additions and 719 deletions
+207 -36
View File
@@ -5,7 +5,7 @@ transport. This adapter reads the immutable recording once, indexes the
recorded source cloud and sensor pose, estimates the session sensor height from
the initial stationary cloud, and returns both the current increment and a
bounded accumulated local-SLAM cloud in a ground-rebased body frame. Camera,
spatial layers and the common timeline can therefore be driven by one host
spatial layers and the common timeline can therefore be driven by one media
clock without a per-LAB coordinate adapter.
"""
@@ -21,7 +21,7 @@ from typing import Any, Final
import numpy as np
import rerun_bindings as rr_bindings
CANONICAL_LAB_SPATIAL_PROFILE: Final = "source-paced-ground-v2"
CANONICAL_LAB_SPATIAL_PROFILE: Final = "source-paced-ground-v3"
_POINT_ENTITY: Final = "/world/points"
_POSE_ENTITY: Final = "/world/sensor_pose"
_TRAJECTORY_ENTITY: Final = "/world/trajectory"
@@ -35,11 +35,15 @@ _HEIGHT_CALIBRATION_MAX_FRAMES: Final = 120
_HEIGHT_NEAR_MIN_RADIUS_M: Final = 1.0
_HEIGHT_NEAR_MAX_RADIUS_M: Final = 6.0
_HEIGHT_LOWER_QUANTILE: Final = 0.025
_LOCAL_HEIGHT_QUANTILE: Final = 0.10
_LOCAL_HEIGHT_HALF_WINDOW_SECONDS: Final = 1.0
_LOCAL_SLAM_HISTORY_SECONDS: Final = 5.0
_LOCAL_SLAM_RADIUS_M: Final = 30.0
_LOCAL_SLAM_VERTICAL_LIMIT_M: Final = 6.0
_LOCAL_SLAM_VOXEL_SIZE_M: Final = 0.12
_LOCAL_SLAM_POINT_LIMIT: Final = 27_000
_FORWARD_HALF_WINDOW_SECONDS: Final = 1.0
_FORWARD_MINIMUM_DISPLACEMENT_M: Final = 0.15
@dataclass(frozen=True)
@@ -233,6 +237,67 @@ def _map_points_to_body(
return body.astype(np.float32)
def _gravity_stable_basis_map_from_body(
poses: _TimedPoses,
target_time_ns: int,
) -> tuple[np.ndarray, str]:
"""Return a right-handed forward/left/up base frame in the RFU map.
Rerun declares this recording map as RFU, while the metric LAB scene
consumes points as forward/left/up. The LiDAR quaternion columns are sensor
right/forward/up and also contain rover or handheld roll/pitch, so they are
not a body basis. Route displacement owns yaw when available; the sensor's
local +Y (Rerun Forward) projected onto map gravity is the stationary
fallback. Map +Z always owns up.
"""
center = _latest_index(poses.times_ns, target_time_ns)
half_window_ns = round(_FORWARD_HALF_WINDOW_SECONDS * 1_000_000_000)
first = _latest_index(poses.times_ns, max(0, target_time_ns - half_window_ns))
last = min(
len(poses.times_ns) - 1,
max(0, bisect_right(poses.times_ns, target_time_ns + half_window_ns) - 1),
)
route = poses.translations[last] - poses.translations[first]
route_xy = np.asarray([route[0], route[1], 0.0], dtype=np.float64)
route_norm = float(np.linalg.norm(route_xy))
sensor_rotation = _rotation_map_from_body(poses.quaternions_xyzw[center])
sensor_forward = np.asarray(
[sensor_rotation[0, 1], sensor_rotation[1, 1], 0.0],
dtype=np.float64,
)
sensor_forward_norm = float(np.linalg.norm(sensor_forward))
if sensor_forward_norm <= 1e-9:
raise ValueError("Recorded LAB sensor forward axis is invalid")
sensor_forward /= sensor_forward_norm
if route_norm >= _FORWARD_MINIMUM_DISPLACEMENT_M:
forward = route_xy / route_norm
if float(np.dot(forward, sensor_forward)) < 0.0:
forward = -forward
forward_source = "smoothed-pose-trajectory-tangent"
else:
forward = sensor_forward
forward_source = "rerun-rfu-sensor-forward-fallback"
up = np.asarray([0.0, 0.0, 1.0], dtype=np.float64)
left = np.cross(up, forward)
left_norm = float(np.linalg.norm(left))
if left_norm <= 1e-9:
raise ValueError("Recorded LAB body left axis is invalid")
left /= left_norm
forward = np.cross(left, up)
forward /= float(np.linalg.norm(forward))
basis = np.column_stack((forward, left, up))
if (
not np.allclose(basis.T @ basis, np.eye(3), atol=1e-7)
or np.linalg.det(basis) < 0.999999
):
raise ValueError("Recorded LAB gravity-stable body basis is invalid")
return basis, forward_source
def _estimate_sensor_height(points: _TimedPoints, poses: _TimedPoses) -> tuple[float, int, float]:
"""Estimate one session mount height from the initial qualified cloud.
@@ -253,17 +318,13 @@ def _estimate_sensor_height(points: _TimedPoints, poses: _TimedPoses) -> tuple[f
estimates: list[float] = []
for point_index in candidates:
pose_index = _latest_index(poses.times_ns, points.times_ns[point_index])
body = _map_points_to_body(
points.values[point_index],
poses.translations[pose_index],
poses.quaternions_xyzw[pose_index],
)
radius = np.linalg.norm(body[:, :2], axis=1)
eligible = body[
delta = points.values[point_index].astype(np.float64) - poses.translations[pose_index]
radius = np.linalg.norm(delta[:, :2], axis=1)
eligible = delta[
(radius >= _HEIGHT_NEAR_MIN_RADIUS_M)
& (radius <= _HEIGHT_NEAR_MAX_RADIUS_M)
& (body[:, 2] >= -2.0)
& (body[:, 2] <= 0.5)
& (delta[:, 2] >= -2.0)
& (delta[:, 2] <= 0.5)
]
if eligible.shape[0] < 100:
continue
@@ -278,12 +339,55 @@ def _estimate_sensor_height(points: _TimedPoints, poses: _TimedPoses) -> tuple[f
return height, len(estimates), mad
def _estimate_local_sensor_height(
points: _TimedPoints,
poses: _TimedPoses,
target_time_ns: int,
fallback_height_m: float,
) -> tuple[float, int, float, str]:
"""Estimate the current gravity-axis height without a fixed camera mount.
RAVNOVES004TREE changes sensor height during the route. A session-wide
constant therefore moves the scene vertically whenever the operator raises
or lowers K1. Use a short source-time window and a conservative near-field
ground quantile; fall back to the sealed session calibration only when the
current cloud has insufficient support.
"""
half_window_ns = round(_LOCAL_HEIGHT_HALF_WINDOW_SECONDS * 1_000_000_000)
first = bisect_right(points.times_ns, max(0, target_time_ns - half_window_ns) - 1)
last = bisect_right(points.times_ns, target_time_ns + half_window_ns)
estimates: list[float] = []
for point_index in range(first, last):
pose_index = _latest_index(poses.times_ns, points.times_ns[point_index])
delta = points.values[point_index].astype(np.float64) - poses.translations[pose_index]
radius = np.linalg.norm(delta[:, :2], axis=1)
eligible = delta[
(radius >= _HEIGHT_NEAR_MIN_RADIUS_M)
& (radius <= _HEIGHT_NEAR_MAX_RADIUS_M)
& (delta[:, 2] >= -2.5)
& (delta[:, 2] <= 0.5)
]
if eligible.shape[0] < 100:
continue
estimate = -float(np.quantile(eligible[:, 2], _LOCAL_HEIGHT_QUANTILE))
if 0.03 <= estimate <= 2.5:
estimates.append(estimate)
if not estimates:
return fallback_height_m, 0, 0.0, "session-source-cloud-fallback"
values = np.asarray(estimates, dtype=np.float64)
height = float(np.median(values))
mad = float(np.median(np.abs(values - height)))
return height, len(estimates), mad, "local-source-cloud-ground-quantile-median"
def _ground_origin_map(
sensor_origin_map: np.ndarray,
basis_map_from_body: np.ndarray,
sensor_height_m: float,
) -> np.ndarray:
return sensor_origin_map - basis_map_from_body[:, 2] * sensor_height_m
# The calibrated height belongs to the map gravity axis. Sensor roll/pitch
# must never tilt the ground origin or the accumulated world cloud.
return sensor_origin_map - np.asarray([0.0, 0.0, sensor_height_m])
def _map_points_to_ground_body(
@@ -329,32 +433,29 @@ def _bounded_local_slam(
return np.ascontiguousarray(local, dtype=np.float32), len(selected), source_count
def canonical_lab_spatial_frame(
recording_path: Path,
generation_sha256: str,
def _canonical_lab_spatial_frame_from_index(
index: _CanonicalSpatialIndex,
target_time_ns: int,
) -> dict[str, object]:
"""Return the current source cloud and bounded Local SLAM on one host time."""
if target_time_ns < 0:
raise ValueError("Recorded LAB target time is invalid")
stat = recording_path.stat()
index = _load_index(
str(recording_path),
stat.st_size,
stat.st_mtime_ns,
generation_sha256,
)
point_index = _latest_index(index.points.times_ns, target_time_ns)
pose_index = _latest_index(index.poses.times_ns, index.points.times_ns[point_index])
trajectory_index = _latest_index(index.trajectories.times_ns, target_time_ns)
translation = index.poses.translations[pose_index]
quaternion = index.poses.quaternions_xyzw[pose_index]
basis_map_from_body = _rotation_map_from_body(quaternion)
sensor_height_m, sensor_height_sample_count, sensor_height_mad_m, height_source = (
_estimate_local_sensor_height(
index.points,
index.poses,
index.points.times_ns[point_index],
index.sensor_height_m,
)
)
basis_map_from_body, forward_source = _gravity_stable_basis_map_from_body(
index.poses,
index.points.times_ns[point_index],
)
ground_origin = _ground_origin_map(
translation,
basis_map_from_body,
index.sensor_height_m,
sensor_height_m,
)
points_body = _map_points_to_ground_body(
index.points.values[point_index],
@@ -368,17 +469,18 @@ def canonical_lab_spatial_frame(
basis_map_from_body,
)
return {
"schema_version": "missioncore.canonical-recorded-lab-spatial-frame/v2",
"schema_version": "missioncore.canonical-recorded-lab-spatial-frame/v3",
"target_time_ns": target_time_ns,
"source_time_ns": index.points.times_ns[point_index],
"pose_time_ns": index.poses.times_ns[pose_index],
"trajectory_time_ns": index.trajectories.times_ns[trajectory_index],
"coordinate_frame": "body-ground",
"sensor_height": {
"meters": index.sensor_height_m,
"source": "initial-source-cloud-lower-quantile-median",
"sample_count": index.sensor_height_sample_count,
"mad_m": index.sensor_height_mad_m,
"meters": sensor_height_m,
"source": height_source,
"sample_count": sensor_height_sample_count,
"mad_m": sensor_height_mad_m,
"session_fallback_meters": index.sensor_height_m,
"authority": "visual-derived",
},
"spatial_profile": {
@@ -392,6 +494,8 @@ def canonical_lab_spatial_frame(
"origin_map_xyz_m": ground_origin.tolist(),
"sensor_origin_map_xyz_m": translation.tolist(),
"basis_map_from_body": basis_map_from_body.tolist(),
"up_source": "rerun-rfu-map-gravity-axis",
"forward_source": forward_source,
},
"source_point_count": int(points_body.shape[0]),
"source_points_body_xyz_m": points_body.tolist(),
@@ -400,3 +504,70 @@ def canonical_lab_spatial_frame(
"local_slam_point_count": int(local_slam.shape[0]),
"local_slam_body_xyz_m": local_slam.tolist(),
}
def canonical_lab_spatial_frame(
recording_path: Path,
generation_sha256: str,
target_time_ns: int,
) -> dict[str, object]:
"""Return the current source cloud and bounded Local SLAM on one media time."""
if target_time_ns < 0:
raise ValueError("Recorded LAB target time is invalid")
stat = recording_path.stat()
index = _load_index(
str(recording_path),
stat.st_size,
stat.st_mtime_ns,
generation_sha256,
)
return _canonical_lab_spatial_frame_from_index(index, target_time_ns)
def canonical_lab_spatial_timeline_samples(
recording_path: Path,
generation_sha256: str,
frame_times_ns: tuple[int, ...],
start_sequence: int,
frame_count: int,
) -> tuple[dict[str, object] | None, ...]:
"""Project only new source increments onto a denser camera timeline.
Camera is roughly 10 Hz in RAVNOVES004TREE while the sealed source cloud is
roughly 2 Hz. Returning the same JSON point array for every camera frame
multiplies transfer and parse cost and makes the viewer chase itself. A row
is populated only when its nearest causal source increment changes; the
canonical viewer retains that spatial frame until the next increment.
"""
if (
start_sequence < 0
or frame_count < 1
or start_sequence >= len(frame_times_ns)
or any(current <= previous for previous, current in zip(frame_times_ns, frame_times_ns[1:]))
):
raise ValueError("Recorded LAB timeline sample request is invalid")
stat = recording_path.stat()
index = _load_index(
str(recording_path),
stat.st_size,
stat.st_mtime_ns,
generation_sha256,
)
stop = min(len(frame_times_ns), start_sequence + frame_count)
samples: list[dict[str, object] | None] = []
for sequence in range(start_sequence, stop):
target_time_ns = frame_times_ns[sequence]
point_index = _latest_index(index.points.times_ns, target_time_ns)
previous_point_index = (
-1
if sequence == 0
else _latest_index(index.points.times_ns, frame_times_ns[sequence - 1])
)
samples.append(
_canonical_lab_spatial_frame_from_index(index, target_time_ns)
if point_index != previous_point_index
else None
)
return tuple(samples)
+15
View File
@@ -360,6 +360,15 @@ def _m48_recorded_camera_playback_source(
return session_recorded_camera_frame_service.playback_source(session_id)
def _canonical_lab_recording_source(session_id: str) -> tuple[Path, str] | None:
"""Resolve one already-published immutable RRD without starting new work."""
snapshot = session_recording_preparation_manager.status(session_id)
if snapshot is None or snapshot.state != "ready" or snapshot.recording is None:
return None
return snapshot.recording.path, snapshot.recording.sha256
def refresh_observation_catalog() -> tuple[str, ...]:
"""Discover completed or recoverable local evidence without copying payloads."""
@@ -1032,6 +1041,12 @@ app.include_router(
/ "lab-v1-vegetation"
/ "results"
),
canonical_recording_provider=_canonical_lab_recording_source,
camera_frame_provider=(
session_recorded_camera_frame_service.extract
if session_recorded_camera_frame_service is not None
else None
),
)
)
app.include_router(
+2 -2
View File
@@ -835,11 +835,11 @@ def build_session_router(
session_id: str,
generation: Annotated[str, Query(min_length=64, max_length=64)],
time_ns: Annotated[int, Query(ge=0, le=MAX_SAFE_INTEGER)],
profile: Literal["source-paced-ground-v2"],
profile: Literal["source-paced-ground-v3"],
) -> JSONResponse:
"""Serve one body-frame sample for the canonical recorded-LAB clock.
The camera timeline owns playback. Spatial evidence is sampled from
The camera media clock owns playback. Spatial evidence is sampled from
the same immutable recording instead of starting a second Rerun clock.
"""
+390 -1
View File
@@ -4,7 +4,10 @@ from __future__ import annotations
import copy
import hashlib
import io
import json
import math
import statistics
import zipfile
from collections.abc import Callable
from functools import lru_cache
@@ -14,6 +17,7 @@ from typing import Any, Final
import numpy as np
from fastapi import APIRouter, HTTPException
from fastapi.responses import FileResponse, JSONResponse, Response
from PIL import Image
from k1link.laboratory.evidence_registry import LaboratoryEvidenceDefinition
from k1link.laboratory.evidence_report import (
@@ -21,9 +25,14 @@ from k1link.laboratory.evidence_report import (
verify_laboratory_evidence_result,
)
from k1link.laboratory.vegetation_shadow_lab import LAB_SCHEMA
from k1link.sessions import RecordedCameraFrame, SessionIntegrityError
from k1link.sessions.canonical_lab_spatial import canonical_lab_spatial_timeline_samples
RootProvider = Callable[[], Path | None]
CanonicalRecordingProvider = Callable[[str], tuple[Path, str] | None]
CameraFrameProvider = Callable[[str, int], RecordedCameraFrame]
_MAX_DOCUMENT_BYTES: Final = 1024 * 1024
_CANONICAL_ROUTE_CHUNK_FRAMES: Final = 8
_DEFINITION: Final = LaboratoryEvidenceDefinition(
work_id="lab-v1-vegetation-shadow",
runtime_relative_root=PurePosixPath("lab-v1-vegetation/results"),
@@ -41,12 +50,17 @@ _BENCHMARK_DEFINITION: Final = LaboratoryEvidenceDefinition(
def build_vegetation_shadow_lab_router(
*, root_provider: RootProvider = lambda: None,
*,
root_provider: RootProvider = lambda: None,
canonical_recording_provider: CanonicalRecordingProvider | None = None,
camera_frame_provider: CameraFrameProvider | None = None,
) -> APIRouter:
return _build_vegetation_lab_router(
prefix="/api/v1/laboratory/vegetation-shadow",
definition=_DEFINITION,
root_provider=root_provider,
canonical_recording_provider=canonical_recording_provider,
camera_frame_provider=camera_frame_provider,
)
@@ -65,6 +79,8 @@ def _build_vegetation_lab_router(
prefix: str,
definition: LaboratoryEvidenceDefinition,
root_provider: RootProvider,
canonical_recording_provider: CanonicalRecordingProvider | None = None,
camera_frame_provider: CameraFrameProvider | None = None,
) -> APIRouter:
router = APIRouter(
prefix=prefix,
@@ -263,6 +279,143 @@ def _build_vegetation_lab_router(
},
)
@router.get("/{result_id}/timeline")
def get_canonical_route_timeline(result_id: str) -> dict[str, object]:
candidate = _resolve_candidate(root_provider, definition, result_id)
manifest = _read_verified(candidate, definition)
route, frame_times_ns = _full_route_context(candidate, manifest)
intervals = [
(current - previous) / 1_000_000_000
for previous, current in zip(frame_times_ns, frame_times_ns[1:])
]
nominal_interval = statistics.median(intervals)
if not math.isfinite(nominal_interval) or nominal_interval <= 0:
raise HTTPException(status_code=503, detail="Full-route timeline cadence is invalid")
return {
"schema_version": "missioncore.recorded-spatial-evidence-timeline/v1",
"result_id": result_id,
"recorded_source": {
"session_id": route["session_id"],
"source_id": route["source_id"],
"representation_id": "registered-map-increment-v1",
"synchronization": "host-arrival-best-effort",
},
"frame_count": len(frame_times_ns),
"frame_times_ns": list(frame_times_ns),
"timeline_start_seconds": frame_times_ns[0] / 1_000_000_000,
"timeline_end_seconds": frame_times_ns[-1] / 1_000_000_000,
"nominal_frame_interval_seconds": nominal_interval,
"nominal_rate_hz": 1.0 / nominal_interval,
"max_chunk_frames": _CANONICAL_ROUTE_CHUNK_FRAMES,
"point_sample_limit": 100_000,
"maximum_source_points_per_frame": 100_000,
"point_delivery": "exact-current-increment",
"world_state_frame_count": len(frame_times_ns),
"superseded_frame_count": 0,
"local_surface_visualization": {
"derivation": "bounded-registered-increment-accumulation",
"window_seconds": 5.0,
"voxel_size_m": 0.12,
"radius_m": 30.0,
"point_limit": 27_000,
"authority": "visual-derived",
},
"image_width": route["width"],
"image_height": route["height"],
"rig": {"length_m": 1.0, "width_m": 0.8, "nominal_sensor_height_m": 0.4},
"corridor": {
"forward_length_m": 8.0,
"rear_margin_m": 0.5,
"occupied_voxel_size_m": 0.45,
"half_width_m": 0.6,
"prediction_horizon_seconds": 8.0,
},
"ground_truth": False,
"authority": "replay-simulated",
"access": "read-only-bounded-recorded-replay",
}
@router.get("/{result_id}/timeline/chunk")
def get_canonical_route_timeline_chunk(
result_id: str,
start: int = 0,
count: int = _CANONICAL_ROUTE_CHUNK_FRAMES,
include_points: bool = True,
) -> dict[str, object]:
if start < 0 or not 1 <= count <= _CANONICAL_ROUTE_CHUNK_FRAMES:
raise HTTPException(status_code=422, detail="Full-route timeline chunk is invalid")
candidate = _resolve_candidate(root_provider, definition, result_id)
manifest = _read_verified(candidate, definition)
route, frame_times_ns = _full_route_context(candidate, manifest)
if start >= len(frame_times_ns):
raise HTTPException(status_code=404, detail="Full-route timeline chunk not found")
if canonical_recording_provider is None:
raise HTTPException(status_code=503, detail="Canonical spatial recording is unavailable")
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:
samples = canonical_lab_spatial_timeline_samples(
recording_path,
generation_sha256,
frame_times_ns,
start,
count,
)
except (OSError, ValueError):
raise HTTPException(status_code=503, detail="Canonical spatial chunk failed") from None
stop = start + len(samples)
frames = [
_canonical_timeline_frame(
result_id=result_id,
endpoint_prefix=prefix,
candidate=candidate,
route=route,
sequence=sequence,
source_time_ns=frame_times_ns[sequence],
spatial=sample,
include_points=include_points,
)
for sequence, sample in zip(range(start, stop), samples, strict=True)
]
return {
"schema_version": "missioncore.recorded-spatial-evidence-chunk/v1",
"result_id": result_id,
"start_sequence": start,
"frame_count": len(frames),
"next_sequence": stop if stop < len(frame_times_ns) else None,
"frames": frames,
"ground_truth": False,
"authority": "replay-simulated",
"access": "read-only-bounded-recorded-replay",
}
@router.get("/{result_id}/timeline/frames/{sequence}/camera")
def get_canonical_route_camera(result_id: str, sequence: int) -> Response:
if camera_frame_provider is None:
raise HTTPException(status_code=503, detail="Recorded camera decoder is unavailable")
candidate = _resolve_candidate(root_provider, definition, result_id)
manifest = _read_verified(candidate, definition)
route, frame_times_ns = _full_route_context(candidate, manifest)
if not 0 <= sequence < len(frame_times_ns):
raise HTTPException(status_code=404, detail="Full-route camera frame not found")
try:
camera = camera_frame_provider(str(route["session_id"]), sequence)
except (OSError, SessionIntegrityError, ValueError):
raise HTTPException(status_code=503, detail="Full-route camera frame unavailable") from None
if camera.width != route["width"] or camera.height != route["height"]:
raise HTTPException(status_code=503, detail="Full-route camera dimensions changed")
return Response(
content=camera.payload,
media_type=camera.media_type,
headers={
"Cache-Control": "private, max-age=31536000, immutable",
"ETag": f'"{camera.sha256}"',
"X-Content-Type-Options": "nosniff",
},
)
@router.get("/{result_id}/route-tgs-anchor/{source_sequence}")
def get_route_tgs_anchor(result_id: str, source_sequence: int) -> JSONResponse:
candidate = _resolve_candidate(root_provider, definition, result_id)
@@ -314,6 +467,242 @@ def _build_vegetation_lab_router(
return router
def _full_route_context(
candidate: Path,
manifest: dict[str, Any],
) -> tuple[dict[str, Any], tuple[int, ...]]:
route = manifest.get("route_full_review")
timeline = route.get("timeline") if isinstance(route, dict) else None
relative_text = timeline.get("path") if isinstance(timeline, dict) else None
if (
not isinstance(route, dict)
or route.get("source_id") != "RAVNOVES004TREE"
or route.get("session_id") != "20260828T130511Z_viewer_live"
or route.get("frame_count") != 6830
or route.get("width") != 800
or route.get("height") != 600
or not isinstance(relative_text, str)
):
raise HTTPException(status_code=404, detail="Full-route canonical timeline not found")
path = candidate.joinpath(*PurePosixPath(relative_text).parts)
try:
payload = path.read_bytes()
if (
len(payload) != timeline.get("byte_length")
or hashlib.sha256(payload).hexdigest() != timeline.get("sha256")
):
raise ValueError("timeline digest changed")
values = np.frombuffer(payload, dtype="<u8")
frame_times_ns = tuple(int(value) for value in values)
except (OSError, ValueError):
raise HTTPException(status_code=503, detail="Full-route timeline verification failed") from None
if (
len(frame_times_ns) != route["frame_count"]
or any(current <= previous for previous, current in zip(frame_times_ns, frame_times_ns[1:]))
):
raise HTTPException(status_code=503, detail="Full-route timeline order changed")
return route, frame_times_ns
def _canonical_timeline_frame(
*,
result_id: str,
endpoint_prefix: str,
candidate: Path,
route: dict[str, Any],
sequence: int,
source_time_ns: int,
spatial: dict[str, object] | None,
include_points: bool,
) -> dict[str, object]:
points = [] if spatial is None or not include_points else spatial["source_points_body_xyz_m"]
point_count = 0 if spatial is None else int(spatial["source_point_count"])
body_frame = None if spatial is None else spatial["body_frame"]
local_slam = [] if spatial is None else spatial["local_slam_body_xyz_m"]
return {
"schema_version": "missioncore.recorded-spatial-evidence-frame/v1",
"sequence": sequence,
"frame_id": f"frame-{sequence:06d}",
"source_time_ns": source_time_ns,
"session_seconds": source_time_ns / 1_000_000_000,
"source_available": spatial is not None,
"spatial_available": spatial is not None,
"world_state_available": True,
"terminal_outcome": "delivered",
"body_frame": body_frame,
"point_cloud_body_xyz_m": points,
"point_cloud_source_count": point_count,
"point_cloud_sample_count": point_count if not include_points else len(points),
"point_cloud_layer": "current-increment",
"local_slam_body_xyz_m": local_slam,
"local_slam_source_frame_count": 0
if spatial is None else spatial["local_slam_source_frame_count"],
"local_slam_source_point_count": 0
if spatial is None else spatial["local_slam_source_point_count"],
"rolling_map_component_count": 0,
"metric_obstacles": [],
"camera_proposals": _semantic_component_proposals(candidate, route, sequence),
"decision_counts": {"threat": 0, "not-threat": 0, "unknown": 0},
"camera_url": (
f"{endpoint_prefix}/{result_id}/timeline/frames/{sequence}/camera"
),
"ground_truth": False,
"authority": "replay-simulated",
}
def _semantic_component_proposals(
candidate: Path,
route: dict[str, Any],
sequence: int,
) -> list[dict[str, object]]:
layers = route.get("layers")
city = layers.get("city") if isinstance(layers, dict) else None
archive = city.get("mask_archive") if isinstance(city, dict) else None
relative = archive.get("path") if isinstance(archive, dict) else None
if not isinstance(relative, str):
return []
archive_path = candidate.joinpath(*PurePosixPath(relative).parts)
try:
stat = archive_path.stat()
except OSError:
return []
return [
dict(proposal)
for proposal in _semantic_component_proposals_cached(
str(archive_path),
stat.st_size,
stat.st_mtime_ns,
sequence,
)
]
@lru_cache(maxsize=256)
def _semantic_component_proposals_cached(
archive_path_text: str,
archive_size: int,
archive_mtime_ns: int,
sequence: int,
) -> tuple[dict[str, object], ...]:
del archive_size, archive_mtime_ns
archive_path = Path(archive_path_text)
member = f"masks/frame-{sequence + 1:06d}.png"
try:
with zipfile.ZipFile(archive_path) as frozen:
payload = frozen.read(member)
with Image.open(io.BytesIO(payload)) as image:
mask = np.asarray(image.convert("L"), dtype=np.uint8)
except (KeyError, OSError, ValueError, zipfile.BadZipFile):
return ()
labels = {
1: "semantic person",
2: "semantic bicycle",
3: "semantic motorcycle",
4: "semantic car",
5: "semantic heavy vehicle",
13: "semantic static obstacle",
14: "semantic animal",
}
proposals: list[dict[str, object]] = []
for class_id, label in labels.items():
minimum_pixels = 80 if class_id == 13 else 24
for component_index, (left, top, right, bottom, pixel_count) in enumerate(
_mask_component_boxes(mask, class_id, minimum_pixels=minimum_pixels)[:12]
):
proposals.append({
"proposal_id": f"semantic-{class_id}-{sequence}-{component_index}",
"bbox_xyxy": [left, top, right, bottom],
"objectness": round(min(0.99, 0.5 + pixel_count / 20_000), 4),
"semantic_hint": label,
"occupied_support": False,
"range_m": None,
"threat_decision": None,
"threat_reason_codes": ["semantic-mask-derived-not-fail-safe-detector"],
})
proposals.sort(
key=lambda proposal: (
-float(proposal["objectness"]),
str(proposal["proposal_id"]),
)
)
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: