feat(perception): integrate calibrated operator pipeline
Add calibrated K1 projection, recorded and near-live perception qualification, unified Rerun operator layers, bounded replay admission, audited viewer controls, worker experiments, and lab evidence.
This commit is contained in:
@@ -0,0 +1,543 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fuse accepted recorded image masks into calibrated K1 LiDAR observations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
import rerun as rr
|
||||
from PIL import Image, ImageDraw
|
||||
from rerun.components import FillMode
|
||||
|
||||
from k1link.device_plugins.xgrids_k1.analyze.calibrated_overlay import (
|
||||
_extract_camera_frames,
|
||||
_load_calibration_snapshot,
|
||||
_select_camera_anchors,
|
||||
_select_lidar_samples,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.analyze.calibrated_projection import (
|
||||
Kb4ProjectionProfile,
|
||||
map_points_to_lidar,
|
||||
project_map_points_kb4,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.mqtt.capture import read_capture_clock_origin
|
||||
from k1link.sessions import inspect_recorded_media_epoch
|
||||
|
||||
SCHEMA = "missioncore.calibrated-segmentation-fusion-experiment/v1"
|
||||
ACCEPTED_INSTANCE_MODEL = "torchvision/maskrcnn_resnet50_fpn_v2"
|
||||
ACCEPTED_SEMANTIC_MODEL = "microsoft/beit-base-finetuned-ade-640-640"
|
||||
MIN_BOX_POINTS = 4
|
||||
MAX_BOX_SPAN_M = 12.0
|
||||
|
||||
FloatArray = npt.NDArray[np.float64]
|
||||
RgbImage = npt.NDArray[np.uint8]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ObjectFusion:
|
||||
instance_id: int
|
||||
label: str
|
||||
score: float
|
||||
candidate_points: int
|
||||
clustered_points: int
|
||||
distance_p10_m: float | None
|
||||
distance_median_m: float | None
|
||||
box_center_map: tuple[float, float, float] | None
|
||||
box_half_size_map: tuple[float, float, float] | None
|
||||
box_status: str
|
||||
source_indices: npt.NDArray[np.int64]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class FusedFrame:
|
||||
stem: str
|
||||
session_time_seconds: float
|
||||
image_rgb: RgbImage
|
||||
semantic_overlay_rgb: RgbImage
|
||||
fusion_overlay_rgb: RgbImage
|
||||
points_map: FloatArray
|
||||
projected_source_indices: npt.NDArray[np.int64]
|
||||
projected_pixels: FloatArray
|
||||
semantic_colors: RgbImage
|
||||
objects: tuple[ObjectFusion, ...]
|
||||
|
||||
|
||||
def _arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--session", type=Path, required=True)
|
||||
parser.add_argument("--calibration", type=Path, required=True)
|
||||
parser.add_argument("--calibrated-manifest", type=Path, required=True)
|
||||
parser.add_argument("--segmentation", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
parser.add_argument("--ffmpeg", type=Path, required=True)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(value, dict):
|
||||
raise RuntimeError(f"{path.name} is not a JSON object")
|
||||
return value
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _palette(index: int) -> tuple[int, int, int]:
|
||||
digest = hashlib.sha256(f"mission-core-segment-{index}".encode()).digest()
|
||||
return (64 + digest[0] % 176, 64 + digest[1] % 176, 64 + digest[2] % 176)
|
||||
|
||||
|
||||
def _depth_cluster(indices: npt.NDArray[np.int64], depths: FloatArray) -> npt.NDArray[np.int64]:
|
||||
if indices.size < 2:
|
||||
return indices
|
||||
order = np.argsort(depths[indices])
|
||||
ordered = indices[order]
|
||||
ordered_depths = depths[ordered]
|
||||
groups: list[npt.NDArray[np.int64]] = []
|
||||
start = 0
|
||||
for offset, gap in enumerate(np.diff(ordered_depths), start=1):
|
||||
threshold = max(0.6, 0.08 * float(ordered_depths[offset - 1]))
|
||||
if float(gap) > threshold:
|
||||
groups.append(ordered[start:offset])
|
||||
start = offset
|
||||
groups.append(ordered[start:])
|
||||
return min(
|
||||
groups,
|
||||
key=lambda group: (-int(group.size), float(np.median(depths[group]))),
|
||||
)
|
||||
|
||||
|
||||
def _object_fusions(
|
||||
*,
|
||||
instance_map: npt.NDArray[np.uint16],
|
||||
instances: list[dict[str, Any]],
|
||||
pixel_x: npt.NDArray[np.int64],
|
||||
pixel_y: npt.NDArray[np.int64],
|
||||
depths: FloatArray,
|
||||
projected_source_indices: npt.NDArray[np.int64],
|
||||
points_map: FloatArray,
|
||||
points_lidar: FloatArray,
|
||||
) -> tuple[ObjectFusion, ...]:
|
||||
sampled_instances = instance_map[pixel_y, pixel_x]
|
||||
fused: list[ObjectFusion] = []
|
||||
for item in instances:
|
||||
instance_id = int(item["instance_id"])
|
||||
candidates = np.flatnonzero(sampled_instances == instance_id).astype(np.int64)
|
||||
clustered = _depth_cluster(candidates, depths)
|
||||
source_indices = projected_source_indices[clustered]
|
||||
ranges = np.linalg.norm(points_lidar[source_indices], axis=1)
|
||||
distance_p10 = None if ranges.size == 0 else float(np.percentile(ranges, 10.0))
|
||||
distance_median = None if ranges.size == 0 else float(np.median(ranges))
|
||||
center: tuple[float, float, float] | None = None
|
||||
half_size: tuple[float, float, float] | None = None
|
||||
if source_indices.size < MIN_BOX_POINTS:
|
||||
status = f"rejected-fewer-than-{MIN_BOX_POINTS}-clustered-points"
|
||||
else:
|
||||
selected = points_map[source_indices]
|
||||
lower = np.percentile(selected, 5.0, axis=0)
|
||||
upper = np.percentile(selected, 95.0, axis=0)
|
||||
spans = upper - lower
|
||||
if not np.isfinite(spans).all() or np.any(spans > MAX_BOX_SPAN_M):
|
||||
status = "rejected-implausible-span"
|
||||
else:
|
||||
sizes = np.maximum(spans, 0.15)
|
||||
center = tuple(float(value) for value in ((lower + upper) * 0.5))
|
||||
half_size = tuple(float(value) for value in (sizes * 0.5))
|
||||
status = "accepted-diagnostic-axis-aligned"
|
||||
fused.append(
|
||||
ObjectFusion(
|
||||
instance_id=instance_id,
|
||||
label=str(item["label"]),
|
||||
score=float(item["score"]),
|
||||
candidate_points=int(candidates.size),
|
||||
clustered_points=int(clustered.size),
|
||||
distance_p10_m=distance_p10,
|
||||
distance_median_m=distance_median,
|
||||
box_center_map=center,
|
||||
box_half_size_map=half_size,
|
||||
box_status=status,
|
||||
source_indices=source_indices,
|
||||
)
|
||||
)
|
||||
return tuple(fused)
|
||||
|
||||
|
||||
def _fusion_overlay(
|
||||
semantic_overlay: RgbImage,
|
||||
projected_pixels: FloatArray,
|
||||
semantic_colors: RgbImage,
|
||||
objects: tuple[ObjectFusion, ...],
|
||||
instances: list[dict[str, Any]],
|
||||
) -> RgbImage:
|
||||
canvas = Image.fromarray(semantic_overlay).convert("RGBA")
|
||||
draw = ImageDraw.Draw(canvas, "RGBA")
|
||||
for (u, v), color in zip(projected_pixels, semantic_colors, strict=True):
|
||||
red, green, blue = (int(value) for value in color)
|
||||
draw.ellipse((u - 1.5, v - 1.5, u + 1.5, v + 1.5), fill=(red, green, blue, 210))
|
||||
by_id = {item.instance_id: item for item in objects}
|
||||
for item in instances:
|
||||
fused = by_id[int(item["instance_id"])]
|
||||
x1, y1, x2, y2 = (float(value) for value in item["box_xyxy"])
|
||||
color = _palette(500 + fused.instance_id)
|
||||
accepted = fused.box_center_map is not None
|
||||
alpha = 255 if accepted else 135
|
||||
draw.rectangle((x1, y1, x2, y2), outline=(*color, alpha), width=2)
|
||||
distance = (
|
||||
"no LiDAR"
|
||||
if fused.distance_median_m is None
|
||||
else f"{fused.distance_median_m:.1f}m/{fused.clustered_points}pts"
|
||||
)
|
||||
draw.text(
|
||||
(x1 + 2, max(0.0, y1 - 12)),
|
||||
f"{fused.label} {distance}",
|
||||
fill=(255, 255, 255, alpha),
|
||||
stroke_width=2,
|
||||
stroke_fill=(0, 0, 0, alpha),
|
||||
)
|
||||
return np.asarray(canvas.convert("RGB"), dtype=np.uint8)
|
||||
|
||||
|
||||
def _object_document(item: ObjectFusion) -> dict[str, Any]:
|
||||
return {
|
||||
"instance_id": item.instance_id,
|
||||
"label": item.label,
|
||||
"score": round(item.score, 9),
|
||||
"candidate_projected_points": item.candidate_points,
|
||||
"clustered_points": item.clustered_points,
|
||||
"distance_p10_m": item.distance_p10_m,
|
||||
"distance_median_m": item.distance_median_m,
|
||||
"box_status": item.box_status,
|
||||
"box_center_map": item.box_center_map,
|
||||
"box_half_size_map": item.box_half_size_map,
|
||||
}
|
||||
|
||||
|
||||
def _write_rerun(path: Path, experiment_id: str, frames: tuple[FusedFrame, ...]) -> None:
|
||||
recording = rr.RecordingStream("nodedc_k1_calibrated_segmentation", recording_id=experiment_id)
|
||||
recording.set_sinks(rr.FileSink(path, write_footer=True))
|
||||
try:
|
||||
recording.log(
|
||||
"contract",
|
||||
rr.TextDocument(
|
||||
"Recorded-only diagnostic: exact image masks sampled at factory-calibrated "
|
||||
"K1 LiDAR projections; 3D boxes require clustered LiDAR support."
|
||||
),
|
||||
static=True,
|
||||
)
|
||||
for frame in frames:
|
||||
recording.set_time(
|
||||
"session_time",
|
||||
duration=np.timedelta64(round(frame.session_time_seconds * 1_000_000_000), "ns"),
|
||||
)
|
||||
recording.log("camera/raw", rr.Image(frame.image_rgb))
|
||||
recording.log("camera/semantic", rr.Image(frame.semantic_overlay_rgb))
|
||||
recording.log("camera/fusion", rr.Image(frame.fusion_overlay_rgb))
|
||||
recording.log(
|
||||
"camera/projected_lidar_semantic",
|
||||
rr.Points2D(
|
||||
frame.projected_pixels.astype(np.float32),
|
||||
colors=frame.semantic_colors,
|
||||
radii=rr.Radius.ui_points(2.0),
|
||||
),
|
||||
)
|
||||
recording.log(
|
||||
"world/semantic_points",
|
||||
rr.Points3D(
|
||||
frame.points_map[frame.projected_source_indices].astype(np.float32),
|
||||
colors=frame.semantic_colors,
|
||||
radii=rr.Radius.ui_points(2.0),
|
||||
),
|
||||
)
|
||||
boxed = [item for item in frame.objects if item.box_center_map is not None]
|
||||
if boxed:
|
||||
recording.log(
|
||||
"world/boxes3d",
|
||||
rr.Boxes3D(
|
||||
centers=[item.box_center_map for item in boxed],
|
||||
half_sizes=[item.box_half_size_map for item in boxed],
|
||||
colors=[(*_palette(500 + item.instance_id), 96) for item in boxed],
|
||||
fill_mode=FillMode.Solid,
|
||||
labels=[
|
||||
f"{item.label} · {item.distance_median_m:.1f} m · "
|
||||
f"{item.clustered_points} pts"
|
||||
for item in boxed
|
||||
],
|
||||
show_labels=True,
|
||||
),
|
||||
)
|
||||
finally:
|
||||
recording.flush(timeout_sec=30.0)
|
||||
recording.disconnect()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = _arguments()
|
||||
session = args.session.resolve(strict=True)
|
||||
calibration_root = args.calibration.resolve(strict=True)
|
||||
calibrated_manifest = _read_json(args.calibrated_manifest.resolve(strict=True))
|
||||
segmentation_root = args.segmentation.resolve(strict=True)
|
||||
segmentation_manifest = _read_json(segmentation_root / "manifest.redacted.json")
|
||||
ffmpeg = args.ffmpeg.resolve(strict=True)
|
||||
|
||||
models = segmentation_manifest.get("models")
|
||||
acceptance = segmentation_manifest.get("acceptance")
|
||||
if not isinstance(models, dict) or not isinstance(acceptance, dict):
|
||||
raise RuntimeError("segmentation manifest is incomplete")
|
||||
instance_model = models.get("instance")
|
||||
semantic_model = models.get("semantic")
|
||||
if (
|
||||
not isinstance(instance_model, dict)
|
||||
or instance_model.get("id") != ACCEPTED_INSTANCE_MODEL
|
||||
or not isinstance(semantic_model, dict)
|
||||
or semantic_model.get("id") != ACCEPTED_SEMANTIC_MODEL
|
||||
or semantic_model.get("checkpoint_load") != "exact-no-missing-unexpected-or-mismatched-keys"
|
||||
or acceptance.get("recorded_segmentation_artifact") != "generated"
|
||||
):
|
||||
raise RuntimeError("segmentation result is not an admitted strict-load experiment")
|
||||
|
||||
calibration, calibration_identity = _load_calibration_snapshot(calibration_root)
|
||||
calibrated_input = calibrated_manifest.get("input")
|
||||
if (
|
||||
not isinstance(calibrated_input, dict)
|
||||
or calibrated_input.get("session_id") != session.name
|
||||
or calibrated_input.get("calibration_content_identity_sha256") != calibration_identity
|
||||
):
|
||||
raise RuntimeError("calibrated experiment does not bind this session/calibration")
|
||||
source_id = str(calibrated_input["source_id"])
|
||||
offsets = tuple(float(value) for value in calibrated_input["video_offsets_seconds"])
|
||||
profile = Kb4ProjectionProfile.from_factory_calibration(calibration, source_id)
|
||||
|
||||
capture_root = session / "captures" / "mqtt_live"
|
||||
origin = read_capture_clock_origin(capture_root / "mqtt.timeline.origin.json")
|
||||
epoch_root = session / "media" / source_id / "epoch-1"
|
||||
epoch = inspect_recorded_media_epoch(
|
||||
epoch_root,
|
||||
expected_source_name=source_id,
|
||||
origin_epoch_ns=origin.started_at_epoch_ns,
|
||||
origin_monotonic_ns=origin.started_monotonic_ns,
|
||||
)
|
||||
anchors = _select_camera_anchors(
|
||||
epoch_root / "index.jsonl",
|
||||
expected_count=len(epoch.segments),
|
||||
offsets_seconds=offsets,
|
||||
timeline_start_seconds=epoch.timeline_start_seconds,
|
||||
origin_monotonic_ns=origin.started_monotonic_ns,
|
||||
)
|
||||
images = _extract_camera_frames(
|
||||
ffmpeg,
|
||||
init_path=epoch.init_path,
|
||||
segment_paths=tuple(item.path for item in epoch.segments),
|
||||
frame_sequences=tuple(item.sequence for item in anchors),
|
||||
width=profile.width,
|
||||
height=profile.height,
|
||||
)
|
||||
lidar_samples = _select_lidar_samples(
|
||||
capture_root / "mqtt.raw.k1mqtt",
|
||||
anchors,
|
||||
origin_monotonic_ns=origin.started_monotonic_ns,
|
||||
temporal_offset_seconds=float(calibrated_input["temporal_offset_seconds"]),
|
||||
)
|
||||
|
||||
segmentation_inputs = {
|
||||
str(item["name"]): str(item["sha256"])
|
||||
for item in segmentation_manifest["input"]
|
||||
if isinstance(item, dict)
|
||||
}
|
||||
segmentation_frames = segmentation_manifest["frames"]
|
||||
fused_frames: list[FusedFrame] = []
|
||||
for image, anchor, lidar_sample in zip(images, anchors, lidar_samples, strict=True):
|
||||
stem = (
|
||||
f"camera-{anchor.sequence:06d}-"
|
||||
f"session-{round(anchor.session_time_seconds * 1000):09d}ms"
|
||||
)
|
||||
input_name = f"{stem}.png"
|
||||
input_artifact = args.calibrated_manifest.parent / input_name
|
||||
if segmentation_inputs.get(input_name) != _sha256(input_artifact):
|
||||
raise RuntimeError(f"segmentation input identity changed for {input_name}")
|
||||
if _sha256(input_artifact) != hashlib.sha256(image.tobytes()).hexdigest():
|
||||
decoded = np.asarray(Image.open(input_artifact).convert("RGB"), dtype=np.uint8)
|
||||
if not np.array_equal(decoded, image):
|
||||
raise RuntimeError(f"decoded camera pixels changed for {input_name}")
|
||||
|
||||
semantic_map = np.asarray(Image.open(segmentation_root / f"{stem}.semantic.png"))
|
||||
instance_map = np.asarray(Image.open(segmentation_root / f"{stem}.instances.png"))
|
||||
semantic_overlay = np.asarray(
|
||||
Image.open(segmentation_root / f"{stem}.semantic-overlay.png").convert("RGB")
|
||||
)
|
||||
if semantic_map.shape != image.shape[:2] or instance_map.shape != image.shape[:2]:
|
||||
raise RuntimeError("segmentation maps do not match the camera epoch")
|
||||
|
||||
points_map = np.asarray(
|
||||
[
|
||||
point.scaled_xyz(lidar_sample.point_frame.header.scaler)
|
||||
for point in lidar_sample.point_frame.points
|
||||
],
|
||||
dtype=np.float64,
|
||||
)
|
||||
points_lidar = map_points_to_lidar(
|
||||
points_map,
|
||||
position_map_xyz=lidar_sample.pose_frame.position_xyz,
|
||||
orientation_map_from_lidar_xyzw=lidar_sample.pose_frame.orientation_xyzw,
|
||||
)
|
||||
projection = project_map_points_kb4(
|
||||
points_map,
|
||||
position_map_xyz=lidar_sample.pose_frame.position_xyz,
|
||||
orientation_map_from_lidar_xyzw=lidar_sample.pose_frame.orientation_xyzw,
|
||||
profile=profile,
|
||||
)
|
||||
pixel_x = np.clip(
|
||||
np.rint(projection.pixels_xy[:, 0]).astype(np.int64), 0, profile.width - 1
|
||||
)
|
||||
pixel_y = np.clip(
|
||||
np.rint(projection.pixels_xy[:, 1]).astype(np.int64), 0, profile.height - 1
|
||||
)
|
||||
semantic_ids = semantic_map[pixel_y, pixel_x].astype(np.int64)
|
||||
semantic_colors = np.asarray(
|
||||
[_palette(int(value)) for value in semantic_ids], dtype=np.uint8
|
||||
)
|
||||
frame_document = segmentation_frames[stem]
|
||||
instances = frame_document["instance"]["instances"]
|
||||
objects = _object_fusions(
|
||||
instance_map=instance_map.astype(np.uint16),
|
||||
instances=instances,
|
||||
pixel_x=pixel_x,
|
||||
pixel_y=pixel_y,
|
||||
depths=projection.depths_m,
|
||||
projected_source_indices=projection.source_indices,
|
||||
points_map=points_map,
|
||||
points_lidar=points_lidar,
|
||||
)
|
||||
fusion_overlay = _fusion_overlay(
|
||||
semantic_overlay,
|
||||
projection.pixels_xy,
|
||||
semantic_colors,
|
||||
objects,
|
||||
instances,
|
||||
)
|
||||
fused_frames.append(
|
||||
FusedFrame(
|
||||
stem=stem,
|
||||
session_time_seconds=anchor.session_time_seconds,
|
||||
image_rgb=image,
|
||||
semantic_overlay_rgb=semantic_overlay,
|
||||
fusion_overlay_rgb=fusion_overlay,
|
||||
points_map=points_map,
|
||||
projected_source_indices=projection.source_indices,
|
||||
projected_pixels=projection.pixels_xy,
|
||||
semantic_colors=semantic_colors,
|
||||
objects=objects,
|
||||
)
|
||||
)
|
||||
|
||||
created_at = datetime.now(UTC)
|
||||
experiment_id = (
|
||||
f"{created_at.strftime('%Y%m%dT%H%M%SZ')}_k1_segmentation_fusion_{uuid4().hex[:12]}"
|
||||
)
|
||||
private_root = args.output_root.resolve() / "private" / "perception-experiments"
|
||||
private_root.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
output = private_root / experiment_id
|
||||
staging = private_root / f".{experiment_id}.incomplete"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
for frame in fused_frames:
|
||||
Image.fromarray(frame.fusion_overlay_rgb).save(staging / f"{frame.stem}.fusion.png")
|
||||
mosaic_rows = [
|
||||
np.concatenate(
|
||||
(frame.image_rgb, frame.semantic_overlay_rgb, frame.fusion_overlay_rgb),
|
||||
axis=1,
|
||||
)
|
||||
for frame in fused_frames
|
||||
]
|
||||
Image.fromarray(np.concatenate(mosaic_rows, axis=0)).save(staging / "fusion-mosaic.png")
|
||||
_write_rerun(staging / "fusion.rrd", experiment_id, tuple(fused_frames))
|
||||
identity = {
|
||||
"calibrated_generation_sha256": calibrated_manifest["generation_sha256"],
|
||||
"segmentation_generation_sha256": segmentation_manifest["generation_sha256"],
|
||||
"calibration_content_identity_sha256": calibration_identity,
|
||||
"session_id": session.name,
|
||||
"source_id": source_id,
|
||||
}
|
||||
outputs = sorted(staging.iterdir())
|
||||
manifest = {
|
||||
"schema_version": SCHEMA,
|
||||
"experiment_id": experiment_id,
|
||||
"created_at_utc": created_at.isoformat(timespec="milliseconds").replace("+00:00", "Z"),
|
||||
"generation_sha256": hashlib.sha256(
|
||||
json.dumps(identity, sort_keys=True, separators=(",", ":")).encode()
|
||||
).hexdigest(),
|
||||
"input": identity,
|
||||
"fusion": {
|
||||
"pixel_sampling": "nearest-calibrated-projection",
|
||||
"occlusion_policy": "largest-depth-contiguous-cluster-nearest-tiebreak",
|
||||
"distance": "LiDAR-origin Euclidean p10 and median",
|
||||
"box": {
|
||||
"frame": "map",
|
||||
"orientation": "axis-aligned-diagnostic",
|
||||
"minimum_clustered_points": MIN_BOX_POINTS,
|
||||
"robust_bounds": "p05-p95",
|
||||
"maximum_span_m": MAX_BOX_SPAN_M,
|
||||
},
|
||||
},
|
||||
"frames": [
|
||||
{
|
||||
"stem": frame.stem,
|
||||
"session_time_seconds": frame.session_time_seconds,
|
||||
"projected_points": int(frame.projected_source_indices.size),
|
||||
"semantic_class_ids": sorted(
|
||||
{
|
||||
int(value)
|
||||
for value in np.asarray(
|
||||
Image.open(segmentation_root / f"{frame.stem}.semantic.png")
|
||||
).reshape(-1)
|
||||
}
|
||||
),
|
||||
"objects": [_object_document(item) for item in frame.objects],
|
||||
"accepted_boxes": sum(
|
||||
item.box_center_map is not None for item in frame.objects
|
||||
),
|
||||
}
|
||||
for frame in fused_frames
|
||||
],
|
||||
"acceptance": {
|
||||
"recorded_mask_to_lidar_fusion": "generated",
|
||||
"distance": "diagnostic-not-ground-truthed",
|
||||
"boxes3d": "diagnostic-not-tracked-or-ground-truthed",
|
||||
"live": "not-tested",
|
||||
"safety": "not-accepted",
|
||||
},
|
||||
"outputs": [
|
||||
{"name": path.name, "bytes": path.stat().st_size, "sha256": _sha256(path)}
|
||||
for path in outputs
|
||||
],
|
||||
}
|
||||
(staging / "manifest.redacted.json").write_text(
|
||||
json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
os.rename(staging, output)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
print(json.dumps({"experiment_id": experiment_id, "output": str(output)}))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user