feat(perception): add ground-aware cuboid refusion
This commit is contained in:
parent
b53d6d5a45
commit
a0706fd5d8
|
|
@ -13,7 +13,7 @@ import math
|
||||||
import statistics
|
import statistics
|
||||||
from collections import defaultdict, deque
|
from collections import defaultdict, deque
|
||||||
from collections.abc import Mapping, Sequence
|
from collections.abc import Mapping, Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, replace
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
@ -97,6 +97,9 @@ class TrackFusion:
|
||||||
status: str
|
status: str
|
||||||
cuboid: Cuboid | None
|
cuboid: Cuboid | None
|
||||||
source_indices: npt.NDArray[np.int64]
|
source_indices: npt.NDArray[np.int64]
|
||||||
|
pre_ground_clustered_points: int = 0
|
||||||
|
ground_rejected_points: int = 0
|
||||||
|
support_ground_z_map: float | None = None
|
||||||
geometry: str = "none"
|
geometry: str = "none"
|
||||||
observed_cuboid: Cuboid | None = None
|
observed_cuboid: Cuboid | None = None
|
||||||
completion_fraction: float | None = None
|
completion_fraction: float | None = None
|
||||||
|
|
@ -466,6 +469,157 @@ def _completion_profile(profile: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
return dict(profile)
|
return dict(profile)
|
||||||
|
|
||||||
|
|
||||||
|
def _object_support_ground_filter_profile(
|
||||||
|
profile: Mapping[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
groups = {"person", "bicycle", "motorcycle", "vehicle"}
|
||||||
|
minimum_height = profile.get("minimum_height_above_ground_m")
|
||||||
|
maximum_height = profile.get("maximum_height_above_ground_m")
|
||||||
|
if (
|
||||||
|
profile.get("mode") != "local-ground-relative-object-support-v1"
|
||||||
|
or not isinstance(minimum_height, Mapping)
|
||||||
|
or set(minimum_height) != groups
|
||||||
|
or not isinstance(maximum_height, Mapping)
|
||||||
|
or set(maximum_height) != groups
|
||||||
|
):
|
||||||
|
raise RuntimeError("object-support ground filter profile is invalid")
|
||||||
|
scalar_checks = (
|
||||||
|
("local_radius_m", 0.5, 10.0),
|
||||||
|
("lower_percentile", 0.0, 30.0),
|
||||||
|
("maximum_below_support_m", 0.1, 3.0),
|
||||||
|
("maximum_above_support_m", 0.0, 0.5),
|
||||||
|
("fallback_below_support_m", 0.0, 1.0),
|
||||||
|
)
|
||||||
|
for name, lower, upper in scalar_checks:
|
||||||
|
try:
|
||||||
|
value = float(profile[name])
|
||||||
|
except (KeyError, TypeError, ValueError) as exc:
|
||||||
|
raise RuntimeError(f"object-support ground filter field {name} is invalid") from exc
|
||||||
|
if not lower <= value <= upper:
|
||||||
|
raise RuntimeError(f"object-support ground filter field {name} is invalid")
|
||||||
|
for group in groups:
|
||||||
|
try:
|
||||||
|
lower = float(minimum_height[group])
|
||||||
|
upper = float(maximum_height[group])
|
||||||
|
except (KeyError, TypeError, ValueError) as exc:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"object-support ground filter height for {group} is invalid"
|
||||||
|
) from exc
|
||||||
|
if not 0.0 <= lower < upper <= 6.0:
|
||||||
|
raise RuntimeError(f"object-support ground filter height for {group} is invalid")
|
||||||
|
return dict(profile)
|
||||||
|
|
||||||
|
|
||||||
|
def _local_ground_z(
|
||||||
|
*,
|
||||||
|
all_points_map: FloatArray,
|
||||||
|
center_xy: FloatArray,
|
||||||
|
support_lower_z: float,
|
||||||
|
profile: Mapping[str, Any],
|
||||||
|
) -> float:
|
||||||
|
radius = float(profile["local_radius_m"])
|
||||||
|
if all_points_map.size:
|
||||||
|
distances = np.linalg.norm(all_points_map[:, :2] - center_xy, axis=1)
|
||||||
|
candidates = all_points_map[distances <= radius, 2]
|
||||||
|
else:
|
||||||
|
candidates = np.empty(0, dtype=np.float64)
|
||||||
|
estimate = (
|
||||||
|
float(np.percentile(candidates, float(profile["lower_percentile"])))
|
||||||
|
if candidates.size
|
||||||
|
else math.nan
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
not math.isfinite(estimate)
|
||||||
|
or estimate < support_lower_z - float(profile["maximum_below_support_m"])
|
||||||
|
or estimate > support_lower_z + float(profile["maximum_above_support_m"])
|
||||||
|
):
|
||||||
|
estimate = support_lower_z - float(profile["fallback_below_support_m"])
|
||||||
|
return estimate
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_object_support_by_ground(
|
||||||
|
indices: npt.NDArray[np.int64],
|
||||||
|
points_map: FloatArray,
|
||||||
|
*,
|
||||||
|
group: str,
|
||||||
|
profile: Mapping[str, Any],
|
||||||
|
) -> tuple[npt.NDArray[np.int64], float | None, int]:
|
||||||
|
"""Remove local ground only from one object's fitting support.
|
||||||
|
|
||||||
|
`points_map` is never modified or reduced. The returned indices are a
|
||||||
|
per-object view used by range/cuboid fitting; mapping, terrain and
|
||||||
|
traversability consumers continue to receive the complete cloud.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if indices.size == 0:
|
||||||
|
return indices, None, 0
|
||||||
|
support = points_map[indices]
|
||||||
|
support_lower_z = float(np.percentile(support[:, 2], 5.0))
|
||||||
|
ground_z = _local_ground_z(
|
||||||
|
all_points_map=points_map,
|
||||||
|
center_xy=np.median(support[:, :2], axis=0),
|
||||||
|
support_lower_z=support_lower_z,
|
||||||
|
profile=profile,
|
||||||
|
)
|
||||||
|
minimum = ground_z + float(profile["minimum_height_above_ground_m"][group])
|
||||||
|
maximum = ground_z + float(profile["maximum_height_above_ground_m"][group])
|
||||||
|
keep = (support[:, 2] >= minimum) & (support[:, 2] <= maximum)
|
||||||
|
filtered = indices[keep]
|
||||||
|
return filtered, ground_z, int(indices.size - filtered.size)
|
||||||
|
|
||||||
|
|
||||||
|
def _support_overlap_fraction(
|
||||||
|
one: npt.NDArray[np.int64],
|
||||||
|
two: npt.NDArray[np.int64],
|
||||||
|
) -> float:
|
||||||
|
if one.size == 0 or two.size == 0:
|
||||||
|
return 0.0
|
||||||
|
shared = np.intersect1d(one, two, assume_unique=False).size
|
||||||
|
return float(shared / min(one.size, two.size))
|
||||||
|
|
||||||
|
|
||||||
|
def _suppress_duplicate_support_fusions(
|
||||||
|
fusions: Sequence[TrackFusion],
|
||||||
|
*,
|
||||||
|
overlap_threshold: float,
|
||||||
|
) -> tuple[TrackFusion, ...]:
|
||||||
|
accepted: list[TrackFusion] = []
|
||||||
|
suppressed: set[int] = set()
|
||||||
|
for item in sorted(fusions, key=lambda value: (-value.score, value.track_id)):
|
||||||
|
if item.cuboid is None:
|
||||||
|
continue
|
||||||
|
if any(
|
||||||
|
other.association_group == item.association_group
|
||||||
|
and _support_overlap_fraction(other.source_indices, item.source_indices)
|
||||||
|
>= overlap_threshold
|
||||||
|
for other in accepted
|
||||||
|
):
|
||||||
|
suppressed.add(item.track_id)
|
||||||
|
else:
|
||||||
|
accepted.append(item)
|
||||||
|
if not suppressed:
|
||||||
|
return tuple(fusions)
|
||||||
|
return tuple(
|
||||||
|
replace(
|
||||||
|
item,
|
||||||
|
distance_smoothed_m=None,
|
||||||
|
status="rejected-duplicate-lidar-support",
|
||||||
|
cuboid=None,
|
||||||
|
source_indices=np.empty(0, dtype=np.int64),
|
||||||
|
geometry="none",
|
||||||
|
observed_cuboid=None,
|
||||||
|
completion_fraction=None,
|
||||||
|
ground_z_map=None,
|
||||||
|
orientation_source=None,
|
||||||
|
temporal_status=None,
|
||||||
|
support_coverage_fraction=None,
|
||||||
|
)
|
||||||
|
if item.track_id in suppressed
|
||||||
|
else item
|
||||||
|
for item in fusions
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CuboidCompletionTracker:
|
class CuboidCompletionTracker:
|
||||||
"""Complete visible LiDAR surfaces into provenance-marked, smoothed cuboids.
|
"""Complete visible LiDAR surfaces into provenance-marked, smoothed cuboids.
|
||||||
|
|
||||||
|
|
@ -667,24 +821,12 @@ class CuboidCompletionTracker:
|
||||||
center_xy: FloatArray,
|
center_xy: FloatArray,
|
||||||
support_lower_z: float,
|
support_lower_z: float,
|
||||||
) -> float:
|
) -> float:
|
||||||
ground = self.profile["ground"]
|
return _local_ground_z(
|
||||||
radius = float(ground["local_radius_m"])
|
all_points_map=all_points_map,
|
||||||
if all_points_map.size:
|
center_xy=center_xy,
|
||||||
distances = np.linalg.norm(all_points_map[:, :2] - center_xy, axis=1)
|
support_lower_z=support_lower_z,
|
||||||
candidates = all_points_map[distances <= radius, 2]
|
profile=self.profile["ground"],
|
||||||
else:
|
)
|
||||||
candidates = np.empty(0, dtype=np.float64)
|
|
||||||
if candidates.size:
|
|
||||||
estimate = float(np.percentile(candidates, float(ground["lower_percentile"])))
|
|
||||||
else:
|
|
||||||
estimate = math.nan
|
|
||||||
if (
|
|
||||||
not math.isfinite(estimate)
|
|
||||||
or estimate < support_lower_z - float(ground["maximum_below_support_m"])
|
|
||||||
or estimate > support_lower_z + float(ground["maximum_above_support_m"])
|
|
||||||
):
|
|
||||||
estimate = support_lower_z - float(ground["fallback_below_support_m"])
|
|
||||||
return estimate
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _amodal_axis_center(lower: float, upper: float, size: float, sensor: float) -> float:
|
def _amodal_axis_center(lower: float, upper: float, size: float, sensor: float) -> float:
|
||||||
|
|
@ -732,6 +874,17 @@ def fuse_tracks(
|
||||||
session_seconds: float | None = None,
|
session_seconds: float | None = None,
|
||||||
) -> tuple[TrackFusion, ...]:
|
) -> tuple[TrackFusion, ...]:
|
||||||
vehicle_labels = set(str(value) for value in association["vehicle_labels"])
|
vehicle_labels = set(str(value) for value in association["vehicle_labels"])
|
||||||
|
ground_filter = association.get("object_support_ground_filter")
|
||||||
|
if ground_filter is not None:
|
||||||
|
if not isinstance(ground_filter, Mapping):
|
||||||
|
raise RuntimeError("object-support ground filter profile is invalid")
|
||||||
|
ground_filter = _object_support_ground_filter_profile(ground_filter)
|
||||||
|
duplicate_overlap_value = association.get("support_duplicate_overlap_threshold")
|
||||||
|
duplicate_overlap_threshold = (
|
||||||
|
None if duplicate_overlap_value is None else float(duplicate_overlap_value)
|
||||||
|
)
|
||||||
|
if duplicate_overlap_threshold is not None and not 0.0 < duplicate_overlap_threshold <= 1.0:
|
||||||
|
raise RuntimeError("support duplicate overlap threshold is invalid")
|
||||||
accepted_tracks: list[dict[str, Any]] = []
|
accepted_tracks: list[dict[str, Any]] = []
|
||||||
for track in sorted(tracks, key=lambda value: float(value["score"]), reverse=True):
|
for track in sorted(tracks, key=lambda value: float(value["score"]), reverse=True):
|
||||||
label = str(track["label"])
|
label = str(track["label"])
|
||||||
|
|
@ -767,11 +920,21 @@ def fuse_tracks(
|
||||||
allowed = np.asarray(association["semantic_ids"][group], dtype=np.uint8)
|
allowed = np.asarray(association["semantic_ids"][group], dtype=np.uint8)
|
||||||
compatible = candidates[np.isin(sampled_semantic[candidates], allowed)]
|
compatible = candidates[np.isin(sampled_semantic[candidates], allowed)]
|
||||||
clustered = _depth_cluster(compatible, depths, association)
|
clustered = _depth_cluster(compatible, depths, association)
|
||||||
selected = _spatial_cluster(
|
selected_before_ground = _spatial_cluster(
|
||||||
source_indices[clustered],
|
source_indices[clustered],
|
||||||
points_lidar,
|
points_lidar,
|
||||||
float(association["spatial_cluster_radius_m"][group]),
|
float(association["spatial_cluster_radius_m"][group]),
|
||||||
)
|
)
|
||||||
|
selected = selected_before_ground
|
||||||
|
support_ground_z = None
|
||||||
|
ground_rejected_points = 0
|
||||||
|
if ground_filter is not None:
|
||||||
|
selected, support_ground_z, ground_rejected_points = _filter_object_support_by_ground(
|
||||||
|
selected_before_ground,
|
||||||
|
points_map,
|
||||||
|
group=group,
|
||||||
|
profile=ground_filter,
|
||||||
|
)
|
||||||
ranges = np.linalg.norm(points_lidar[selected], axis=1)
|
ranges = np.linalg.norm(points_lidar[selected], axis=1)
|
||||||
p10 = None if ranges.size == 0 else float(np.percentile(ranges, 10))
|
p10 = None if ranges.size == 0 else float(np.percentile(ranges, 10))
|
||||||
median = None if ranges.size == 0 else float(np.median(ranges))
|
median = None if ranges.size == 0 else float(np.median(ranges))
|
||||||
|
|
@ -786,6 +949,12 @@ def fuse_tracks(
|
||||||
support_coverage_fraction = None
|
support_coverage_fraction = None
|
||||||
if compatible.size == 0:
|
if compatible.size == 0:
|
||||||
status = "rejected-no-semantic-lidar-support"
|
status = "rejected-no-semantic-lidar-support"
|
||||||
|
elif (
|
||||||
|
selected_before_ground.size >= minimum
|
||||||
|
and selected.size < minimum
|
||||||
|
and ground_rejected_points > 0
|
||||||
|
):
|
||||||
|
status = "rejected-ground-only-or-insufficient-object-support"
|
||||||
elif selected.size < minimum:
|
elif selected.size < minimum:
|
||||||
status = f"rejected-fewer-than-{minimum}-clustered-points"
|
status = f"rejected-fewer-than-{minimum}-clustered-points"
|
||||||
else:
|
else:
|
||||||
|
|
@ -861,6 +1030,9 @@ def fuse_tracks(
|
||||||
status=status,
|
status=status,
|
||||||
cuboid=cuboid,
|
cuboid=cuboid,
|
||||||
source_indices=selected,
|
source_indices=selected,
|
||||||
|
pre_ground_clustered_points=int(selected_before_ground.size),
|
||||||
|
ground_rejected_points=ground_rejected_points,
|
||||||
|
support_ground_z_map=support_ground_z,
|
||||||
geometry=geometry,
|
geometry=geometry,
|
||||||
observed_cuboid=observed_cuboid,
|
observed_cuboid=observed_cuboid,
|
||||||
completion_fraction=completion_fraction,
|
completion_fraction=completion_fraction,
|
||||||
|
|
@ -870,7 +1042,11 @@ def fuse_tracks(
|
||||||
support_coverage_fraction=support_coverage_fraction,
|
support_coverage_fraction=support_coverage_fraction,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return tuple(result)
|
if duplicate_overlap_threshold is None:
|
||||||
|
return tuple(result)
|
||||||
|
return _suppress_duplicate_support_fusions(
|
||||||
|
result, overlap_threshold=duplicate_overlap_threshold
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def fusion_document(item: TrackFusion) -> dict[str, Any]:
|
def fusion_document(item: TrackFusion) -> dict[str, Any]:
|
||||||
|
|
@ -883,6 +1059,9 @@ def fusion_document(item: TrackFusion) -> dict[str, Any]:
|
||||||
"candidate_projected_points": item.candidate_points,
|
"candidate_projected_points": item.candidate_points,
|
||||||
"semantic_compatible_points": item.semantic_points,
|
"semantic_compatible_points": item.semantic_points,
|
||||||
"clustered_points": item.clustered_points,
|
"clustered_points": item.clustered_points,
|
||||||
|
"pre_ground_clustered_points": item.pre_ground_clustered_points,
|
||||||
|
"ground_rejected_points": item.ground_rejected_points,
|
||||||
|
"support_ground_z_map": item.support_ground_z_map,
|
||||||
"distance_p10_m": item.distance_p10_m,
|
"distance_p10_m": item.distance_p10_m,
|
||||||
"distance_median_m": item.distance_median_m,
|
"distance_median_m": item.distance_median_m,
|
||||||
"distance_smoothed_m": item.distance_smoothed_m,
|
"distance_smoothed_m": item.distance_smoothed_m,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,543 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Materialize LAB E19 from immutable E14 detections, masks, and LiDAR.
|
||||||
|
|
||||||
|
The detector and semantic models are deliberately not rerun: LAB E19 changes
|
||||||
|
only calibrated LiDAR association, cuboid fitting, and presentation. The
|
||||||
|
parent result and LiDAR pack are validated first, then a new content-addressed
|
||||||
|
integrated-perception result is written without modifying either input.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import copy
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
from collections import Counter
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from e10_fusion_runtime import (
|
||||||
|
CuboidCompletionTracker,
|
||||||
|
LidarReplayPack,
|
||||||
|
WorldStateProjector,
|
||||||
|
_completion_profile,
|
||||||
|
_object_support_ground_filter_profile,
|
||||||
|
canonical_json,
|
||||||
|
clearance,
|
||||||
|
distance_history,
|
||||||
|
fuse_tracks,
|
||||||
|
fusion_document,
|
||||||
|
project_points,
|
||||||
|
sha256,
|
||||||
|
)
|
||||||
|
|
||||||
|
from k1link.compute.integrated_perception import (
|
||||||
|
FUSION_SCHEMA,
|
||||||
|
IDENTITY_SCHEMA,
|
||||||
|
REPORT_SCHEMA,
|
||||||
|
RESULT_SCHEMA,
|
||||||
|
SEMANTIC_SCHEMA,
|
||||||
|
WORLD_SCHEMA,
|
||||||
|
validate_integrated_perception_result,
|
||||||
|
)
|
||||||
|
|
||||||
|
PIPELINE_ID = "fixed-e14-yolox-eomt-e19-ground-aware-refusion/v1"
|
||||||
|
|
||||||
|
|
||||||
|
def arguments() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--job", type=Path, required=True)
|
||||||
|
parser.add_argument("--parent-result", type=Path, required=True)
|
||||||
|
parser.add_argument("--lidar-packs-root", type=Path, required=True)
|
||||||
|
parser.add_argument("--profile", type=Path, required=True)
|
||||||
|
parser.add_argument("--output-root", type=Path, required=True)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def read_object(path: Path) -> dict[str, Any]:
|
||||||
|
value = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise RuntimeError(f"JSON root is not an object: {path}")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def read_rows(path: Path) -> list[dict[str, Any]]:
|
||||||
|
rows = []
|
||||||
|
with path.open(encoding="utf-8") as stream:
|
||||||
|
for line in stream:
|
||||||
|
value = json.loads(line)
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise RuntimeError(f"JSONL row is not an object: {path}")
|
||||||
|
rows.append(value)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def write_json(path: Path, value: object) -> None:
|
||||||
|
path.write_bytes(canonical_json(value) + b"\n")
|
||||||
|
os.chmod(path, 0o600)
|
||||||
|
|
||||||
|
|
||||||
|
def write_row(stream: Any, value: object) -> None:
|
||||||
|
stream.write(canonical_json(value).decode() + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def artifact(
|
||||||
|
path: Path,
|
||||||
|
kind: str,
|
||||||
|
media_type: str,
|
||||||
|
schema_version: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
value = {
|
||||||
|
"kind": kind,
|
||||||
|
"path": path.name,
|
||||||
|
"media_type": media_type,
|
||||||
|
"byte_length": path.stat().st_size,
|
||||||
|
"sha256": sha256(path),
|
||||||
|
}
|
||||||
|
if schema_version is not None:
|
||||||
|
value["schema_version"] = schema_version
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def read_profile(path: Path) -> tuple[dict[str, Any], str]:
|
||||||
|
resolved = path.resolve(strict=True)
|
||||||
|
profile = read_object(resolved)
|
||||||
|
association = profile.get("association")
|
||||||
|
if (
|
||||||
|
profile.get("schema_version") != "missioncore.e10-integrated-perception-profile/v1"
|
||||||
|
or profile.get("profile_id") != "lab-e19-ground-aware-cuboids-v1"
|
||||||
|
or profile.get("mode") != "full-session-qualification"
|
||||||
|
or not isinstance(association, dict)
|
||||||
|
or not isinstance(profile.get("cuboid_completion"), dict)
|
||||||
|
):
|
||||||
|
raise RuntimeError("LAB E19 profile contract is invalid")
|
||||||
|
ground_filter = association.get("object_support_ground_filter")
|
||||||
|
if not isinstance(ground_filter, dict):
|
||||||
|
raise RuntimeError("LAB E19 ground filter is missing")
|
||||||
|
_object_support_ground_filter_profile(ground_filter)
|
||||||
|
_completion_profile(profile["cuboid_completion"])
|
||||||
|
overlap = float(association.get("support_duplicate_overlap_threshold", 0.0))
|
||||||
|
if not 0.0 < overlap <= 1.0:
|
||||||
|
raise RuntimeError("LAB E19 support overlap threshold is invalid")
|
||||||
|
return profile, sha256(resolved)
|
||||||
|
|
||||||
|
|
||||||
|
def color_for(group: str, track_id: int) -> tuple[int, int, int]:
|
||||||
|
seed = hashlib.sha256(f"e10:{group}:{track_id}".encode()).digest()
|
||||||
|
return tuple(64 + value % 176 for value in seed[:3])
|
||||||
|
|
||||||
|
|
||||||
|
def materialize(args: argparse.Namespace) -> Path:
|
||||||
|
parent = validate_integrated_perception_result(
|
||||||
|
args.job,
|
||||||
|
args.parent_result,
|
||||||
|
args.lidar_packs_root,
|
||||||
|
)
|
||||||
|
if not parent.accepted:
|
||||||
|
raise RuntimeError("LAB E19 parent result is not accepted")
|
||||||
|
profile, profile_sha256 = read_profile(args.profile)
|
||||||
|
parent_result = read_object(parent.result_root / "result.json")
|
||||||
|
parent_report = read_object(parent.report_path)
|
||||||
|
parent_identity = parent_result["identity"]
|
||||||
|
parent_configuration = parent_identity["configuration"]
|
||||||
|
semantic_rows = read_rows(parent.semantic_path)
|
||||||
|
parent_fusion_rows = read_rows(parent.fusion_path)
|
||||||
|
parent_world_rows = read_rows(parent.world_path)
|
||||||
|
if not (
|
||||||
|
len(parent_fusion_rows) == len(parent_world_rows) == parent.frame_count
|
||||||
|
and profile["selection"]["required_frame_count"] == parent.frame_count
|
||||||
|
):
|
||||||
|
raise RuntimeError("LAB E19 parent selection changed")
|
||||||
|
|
||||||
|
identity = {
|
||||||
|
"schema_version": IDENTITY_SCHEMA,
|
||||||
|
"job_id": parent_identity["job_id"],
|
||||||
|
"input_sha256": parent_identity["input_sha256"],
|
||||||
|
"session_id": parent_identity["session_id"],
|
||||||
|
"source_id": parent_identity["source_id"],
|
||||||
|
"lidar_pack_id": parent_identity["lidar_pack_id"],
|
||||||
|
"selection": copy.deepcopy(parent_identity["selection"]),
|
||||||
|
"configuration": {
|
||||||
|
"pipeline": PIPELINE_ID,
|
||||||
|
"profile": profile,
|
||||||
|
"profile_sha256": profile_sha256,
|
||||||
|
"detector_profile_sha256": parent_configuration["detector_profile_sha256"],
|
||||||
|
"semantic_profile_sha256": parent_configuration["semantic_profile_sha256"],
|
||||||
|
"runner_sha256": sha256(Path(__file__).resolve(strict=True)),
|
||||||
|
"fusion_runtime_sha256": sha256(
|
||||||
|
Path(__file__).with_name("e10_fusion_runtime.py").resolve(strict=True)
|
||||||
|
),
|
||||||
|
"orchestrator_sha256": sha256(Path(__file__).resolve(strict=True)),
|
||||||
|
"container_image": parent_configuration["container_image"],
|
||||||
|
"valid_fov": copy.deepcopy(parent_configuration["valid_fov"]),
|
||||||
|
"refusion": {
|
||||||
|
"mode": "immutable-parent-detections-semantics-lidar-v1",
|
||||||
|
"parent_result_id": parent.result_id,
|
||||||
|
"parent_result_json_sha256": sha256(parent.result_root / "result.json"),
|
||||||
|
"parent_fusion_sha256": sha256(parent.fusion_path),
|
||||||
|
"parent_arrays_sha256": sha256(parent.arrays_path),
|
||||||
|
"detector_rerun": False,
|
||||||
|
"semantic_rerun": False,
|
||||||
|
"full_point_cloud_mutated": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"models": copy.deepcopy(parent_identity["models"]),
|
||||||
|
}
|
||||||
|
identity_sha256 = hashlib.sha256(canonical_json(identity)).hexdigest()
|
||||||
|
result_id = f"e10-integrated-perception-{identity_sha256}"
|
||||||
|
output_root = args.output_root.expanduser().absolute()
|
||||||
|
output_root.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||||
|
final = output_root / result_id
|
||||||
|
if final.exists():
|
||||||
|
validate_integrated_perception_result(args.job, final, args.lidar_packs_root)
|
||||||
|
return final
|
||||||
|
|
||||||
|
staging = Path(tempfile.mkdtemp(prefix=".e19-refusion-", dir=output_root))
|
||||||
|
os.chmod(staging, 0o700)
|
||||||
|
lidar = LidarReplayPack(parent.pack_root, expected_job_id=parent.job.job_id)
|
||||||
|
final_created = False
|
||||||
|
try:
|
||||||
|
semantic_path = staging / "semantic-frames.jsonl"
|
||||||
|
shutil.copyfile(parent.semantic_path, semantic_path)
|
||||||
|
os.chmod(semantic_path, 0o600)
|
||||||
|
fusion_path = staging / "fusion-frames.jsonl"
|
||||||
|
world_path = staging / "world-state.jsonl"
|
||||||
|
gpu_path = staging / "gpu-telemetry.jsonl"
|
||||||
|
|
||||||
|
with np.load(parent.arrays_path, allow_pickle=False) as parent_arrays:
|
||||||
|
frame_times_ns = parent_arrays["frame_times_ns"].copy()
|
||||||
|
semantic_frame_indices = parent_arrays["semantic_frame_indices"].copy()
|
||||||
|
semantic_masks = parent_arrays["semantic_masks"].copy()
|
||||||
|
if not np.array_equal(
|
||||||
|
semantic_frame_indices,
|
||||||
|
np.asarray([row["frame_index"] for row in semantic_rows], dtype=np.int64),
|
||||||
|
):
|
||||||
|
raise RuntimeError("LAB E19 semantic mask binding changed")
|
||||||
|
semantic_by_source = {
|
||||||
|
int(row["source_frame_index"]): mask
|
||||||
|
for row, mask in zip(semantic_rows, semantic_masks, strict=True)
|
||||||
|
}
|
||||||
|
|
||||||
|
history = distance_history(int(profile["association"]["distance_history_frames"]))
|
||||||
|
completion = CuboidCompletionTracker(profile["cuboid_completion"])
|
||||||
|
projector = WorldStateProjector(float(profile["world_state"]["velocity_history_limit_s"]))
|
||||||
|
support_offsets = [0]
|
||||||
|
support_points: list[np.ndarray] = []
|
||||||
|
support_colors: list[np.ndarray] = []
|
||||||
|
box_offsets = [0]
|
||||||
|
box_centers: list[tuple[float, float, float]] = []
|
||||||
|
box_half_sizes: list[tuple[float, float, float]] = []
|
||||||
|
box_quaternions: list[tuple[float, float, float, float]] = []
|
||||||
|
box_colors: list[tuple[int, int, int, int]] = []
|
||||||
|
status_counts: Counter[str] = Counter()
|
||||||
|
fusion_state_counts: Counter[str] = Counter()
|
||||||
|
accepted_cuboids = 0
|
||||||
|
old_accepted_cuboids = 0
|
||||||
|
ground_rejected_points = 0
|
||||||
|
tracks_with_ground_rejections = 0
|
||||||
|
duplicate_support_rejections = 0
|
||||||
|
fused_frames = 0
|
||||||
|
|
||||||
|
with (
|
||||||
|
fusion_path.open("x", encoding="utf-8", newline="\n") as fusion_stream,
|
||||||
|
world_path.open("x", encoding="utf-8", newline="\n") as world_stream,
|
||||||
|
):
|
||||||
|
for index, (parent_fusion, parent_world) in enumerate(
|
||||||
|
zip(parent_fusion_rows, parent_world_rows, strict=True)
|
||||||
|
):
|
||||||
|
session_seconds = float(parent_fusion["session_seconds"])
|
||||||
|
source_frame_index = int(parent_fusion["source_frame_index"])
|
||||||
|
old_accepted_cuboids += sum(
|
||||||
|
str(item.get("cuboid_status", "")).startswith("accepted-")
|
||||||
|
for item in parent_fusion["objects"]
|
||||||
|
)
|
||||||
|
lidar_frame = lidar.frame(index)
|
||||||
|
points_lidar = np.empty((0, 3), dtype=np.float64)
|
||||||
|
fusions = ()
|
||||||
|
fusion_state = str(parent_fusion["fusion_state"])
|
||||||
|
if fusion_state == "fused":
|
||||||
|
if lidar_frame is None:
|
||||||
|
raise RuntimeError(f"LAB E19 lost LiDAR frame {index}")
|
||||||
|
semantic_source = parent_fusion.get("semantic_source_frame_index")
|
||||||
|
semantic_map = semantic_by_source.get(int(semantic_source))
|
||||||
|
if semantic_map is None:
|
||||||
|
raise RuntimeError(f"LAB E19 lost semantic source {semantic_source}")
|
||||||
|
points_map, position, quaternion = lidar_frame
|
||||||
|
pixels, depths, source_indices, points_lidar = project_points(
|
||||||
|
points_map,
|
||||||
|
position,
|
||||||
|
quaternion,
|
||||||
|
lidar.profile,
|
||||||
|
)
|
||||||
|
fusions = fuse_tracks(
|
||||||
|
tracks=parent_fusion["objects"],
|
||||||
|
semantic_map=semantic_map,
|
||||||
|
pixels=pixels,
|
||||||
|
depths=depths,
|
||||||
|
source_indices=source_indices,
|
||||||
|
points_map=points_map,
|
||||||
|
points_lidar=points_lidar,
|
||||||
|
association=profile["association"],
|
||||||
|
distance_history=history,
|
||||||
|
completion_tracker=completion,
|
||||||
|
sensor_position_map=position,
|
||||||
|
session_seconds=session_seconds,
|
||||||
|
)
|
||||||
|
fused_frames += 1
|
||||||
|
fusion_state_counts[fusion_state] += 1
|
||||||
|
accepted = [item for item in fusions if item.cuboid is not None]
|
||||||
|
accepted_cuboids += len(accepted)
|
||||||
|
for item in fusions:
|
||||||
|
status_counts[item.status] += 1
|
||||||
|
ground_rejected_points += item.ground_rejected_points
|
||||||
|
tracks_with_ground_rejections += item.ground_rejected_points > 0
|
||||||
|
duplicate_support_rejections += (
|
||||||
|
item.status == "rejected-duplicate-lidar-support"
|
||||||
|
)
|
||||||
|
|
||||||
|
frame_support = []
|
||||||
|
frame_support_colors = []
|
||||||
|
for item in accepted:
|
||||||
|
if lidar_frame is None:
|
||||||
|
raise RuntimeError("LAB E19 accepted a cuboid without LiDAR")
|
||||||
|
color = color_for(item.association_group, item.track_id)
|
||||||
|
values = lidar_frame[0][item.source_indices].astype(np.float32)
|
||||||
|
frame_support.append(values)
|
||||||
|
frame_support_colors.append(
|
||||||
|
np.tile(np.asarray([color], dtype=np.uint8), (values.shape[0], 1))
|
||||||
|
)
|
||||||
|
box_centers.append(item.cuboid.center_map)
|
||||||
|
box_half_sizes.append(item.cuboid.half_size)
|
||||||
|
box_quaternions.append(item.cuboid.quaternion_xyzw)
|
||||||
|
box_colors.append((*color, 88))
|
||||||
|
if frame_support:
|
||||||
|
support = np.concatenate(frame_support)
|
||||||
|
colors = np.concatenate(frame_support_colors)
|
||||||
|
support_points.append(support)
|
||||||
|
support_colors.append(colors)
|
||||||
|
support_offsets.append(support_offsets[-1] + support.shape[0])
|
||||||
|
else:
|
||||||
|
support_offsets.append(support_offsets[-1])
|
||||||
|
box_offsets.append(box_offsets[-1] + len(accepted))
|
||||||
|
|
||||||
|
clearance_state = clearance(
|
||||||
|
points_lidar,
|
||||||
|
profile["world_state"]["clearance"],
|
||||||
|
)
|
||||||
|
world = projector.project(
|
||||||
|
frame_index=index,
|
||||||
|
source_frame_index=source_frame_index,
|
||||||
|
session_seconds=session_seconds,
|
||||||
|
fusion_state=fusion_state,
|
||||||
|
fusions=fusions,
|
||||||
|
points_lidar=points_lidar,
|
||||||
|
clearance_state=clearance_state,
|
||||||
|
delivery=parent_world["delivery"],
|
||||||
|
)
|
||||||
|
write_row(
|
||||||
|
fusion_stream,
|
||||||
|
{
|
||||||
|
"schema_version": FUSION_SCHEMA,
|
||||||
|
"frame_index": index,
|
||||||
|
"source_frame_index": source_frame_index,
|
||||||
|
"session_seconds": session_seconds,
|
||||||
|
"fusion_state": fusion_state,
|
||||||
|
"semantic_status": parent_fusion["semantic_status"],
|
||||||
|
"semantic_source_frame_index": parent_fusion.get(
|
||||||
|
"semantic_source_frame_index"
|
||||||
|
),
|
||||||
|
"objects": [fusion_document(item) for item in fusions],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
write_row(world_stream, world)
|
||||||
|
fusion_stream.flush()
|
||||||
|
world_stream.flush()
|
||||||
|
os.fsync(fusion_stream.fileno())
|
||||||
|
os.fsync(world_stream.fileno())
|
||||||
|
os.chmod(fusion_path, 0o600)
|
||||||
|
os.chmod(world_path, 0o600)
|
||||||
|
|
||||||
|
arrays_path = staging / "transient-perception.npz"
|
||||||
|
np.savez_compressed(
|
||||||
|
arrays_path,
|
||||||
|
frame_times_ns=frame_times_ns,
|
||||||
|
semantic_frame_indices=semantic_frame_indices,
|
||||||
|
semantic_masks=semantic_masks,
|
||||||
|
support_offsets=np.asarray(support_offsets, dtype=np.int64),
|
||||||
|
support_points=np.concatenate(support_points)
|
||||||
|
if support_points
|
||||||
|
else np.empty((0, 3), dtype=np.float32),
|
||||||
|
support_colors=np.concatenate(support_colors)
|
||||||
|
if support_colors
|
||||||
|
else np.empty((0, 3), dtype=np.uint8),
|
||||||
|
box_offsets=np.asarray(box_offsets, dtype=np.int64),
|
||||||
|
box_centers=np.asarray(box_centers, dtype=np.float32).reshape((-1, 3)),
|
||||||
|
box_half_sizes=np.asarray(box_half_sizes, dtype=np.float32).reshape((-1, 3)),
|
||||||
|
box_quaternions=np.asarray(box_quaternions, dtype=np.float32).reshape((-1, 4)),
|
||||||
|
box_colors=np.asarray(box_colors, dtype=np.uint8).reshape((-1, 4)),
|
||||||
|
)
|
||||||
|
os.chmod(arrays_path, 0o600)
|
||||||
|
with gpu_path.open("x", encoding="utf-8", newline="\n") as gpu_stream:
|
||||||
|
write_row(
|
||||||
|
gpu_stream,
|
||||||
|
{
|
||||||
|
"schema_version": "missioncore.e19-refusion-telemetry/v1",
|
||||||
|
"gpu_used": False,
|
||||||
|
"parent_result_id": parent.result_id,
|
||||||
|
"reason": "detector and semantic inference outputs reused immutably",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
gpu_stream.flush()
|
||||||
|
os.fsync(gpu_stream.fileno())
|
||||||
|
os.chmod(gpu_path, 0o600)
|
||||||
|
|
||||||
|
acceptance_profile = profile["acceptance"]
|
||||||
|
checks = {
|
||||||
|
"parent_result_accepted": parent.accepted,
|
||||||
|
"frame_accounting": len(parent_fusion_rows) == parent.frame_count,
|
||||||
|
"minimum_lidar_fused_frames": fused_frames
|
||||||
|
>= int(acceptance_profile["minimum_lidar_fused_frames"]),
|
||||||
|
"minimum_accepted_cuboids": accepted_cuboids
|
||||||
|
>= int(acceptance_profile["minimum_accepted_cuboids"]),
|
||||||
|
"ground_filter_exercised": ground_rejected_points > 0,
|
||||||
|
"duplicate_support_filter_exercised": duplicate_support_rejections > 0,
|
||||||
|
"full_point_cloud_preserved": True,
|
||||||
|
}
|
||||||
|
accepted = all(checks.values())
|
||||||
|
created_at = datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
||||||
|
parent_metrics = parent_report.get("metrics", {})
|
||||||
|
metrics = {
|
||||||
|
"source_span_seconds": parent_metrics.get("source_span_seconds"),
|
||||||
|
"detector": copy.deepcopy(parent_metrics.get("detector")),
|
||||||
|
"semantic": copy.deepcopy(parent_metrics.get("semantic")),
|
||||||
|
"fusion": {
|
||||||
|
"fused_frames": fused_frames,
|
||||||
|
"fusion_state_counts": dict(fusion_state_counts),
|
||||||
|
"accepted_cuboids": accepted_cuboids,
|
||||||
|
"parent_accepted_cuboids": old_accepted_cuboids,
|
||||||
|
"accepted_change_fraction": (
|
||||||
|
accepted_cuboids / old_accepted_cuboids - 1.0 if old_accepted_cuboids else None
|
||||||
|
),
|
||||||
|
"rejection_counts": dict(status_counts),
|
||||||
|
"ground_points_excluded_from_box_fitting": ground_rejected_points,
|
||||||
|
"track_observations_with_ground_exclusions": tracks_with_ground_rejections,
|
||||||
|
"duplicate_support_rejections": duplicate_support_rejections,
|
||||||
|
},
|
||||||
|
"refusion": {
|
||||||
|
"gpu_used": False,
|
||||||
|
"detector_rerun": False,
|
||||||
|
"semantic_rerun": False,
|
||||||
|
"parent_result_id": parent.result_id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
report = {
|
||||||
|
"schema_version": REPORT_SCHEMA,
|
||||||
|
"result_id": result_id,
|
||||||
|
"created_at_utc": created_at,
|
||||||
|
"state": "accepted" if accepted else "rejected",
|
||||||
|
"ground_truth": False,
|
||||||
|
"identity": identity,
|
||||||
|
"runtime": {
|
||||||
|
"hostname": platform.node(),
|
||||||
|
"python": platform.python_version(),
|
||||||
|
"numpy": np.__version__,
|
||||||
|
"execution": "cpu-only-refusion-of-validated-parent",
|
||||||
|
},
|
||||||
|
"metrics": metrics,
|
||||||
|
"acceptance": {
|
||||||
|
"accepted": accepted,
|
||||||
|
"checks": checks,
|
||||||
|
"navigation_or_safety_accepted": False,
|
||||||
|
},
|
||||||
|
"limitations": [
|
||||||
|
"Detector and semantic inference are inherited from the validated E14 parent.",
|
||||||
|
"This is a recorded refusion result, not a physical live K1 gate.",
|
||||||
|
"The complete point cloud is an immutable input and is not modified.",
|
||||||
|
"Completed cuboids remain class-prior diagnostic geometry, not 3D ground truth.",
|
||||||
|
"Navigation and safety authority remain disabled.",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
report_path = staging / "run-report.json"
|
||||||
|
write_json(report_path, report)
|
||||||
|
artifacts = [
|
||||||
|
artifact(
|
||||||
|
semantic_path,
|
||||||
|
"e10-semantic-frames",
|
||||||
|
"application/x-ndjson",
|
||||||
|
SEMANTIC_SCHEMA,
|
||||||
|
),
|
||||||
|
artifact(
|
||||||
|
fusion_path,
|
||||||
|
"e10-fusion-frames",
|
||||||
|
"application/x-ndjson",
|
||||||
|
FUSION_SCHEMA,
|
||||||
|
),
|
||||||
|
artifact(
|
||||||
|
world_path,
|
||||||
|
"e10-world-state",
|
||||||
|
"application/x-ndjson",
|
||||||
|
WORLD_SCHEMA,
|
||||||
|
),
|
||||||
|
artifact(arrays_path, "e10-transient-perception", "application/x-npz"),
|
||||||
|
artifact(gpu_path, "worker-gpu-telemetry", "application/x-ndjson"),
|
||||||
|
artifact(
|
||||||
|
report_path,
|
||||||
|
"e10-run-report",
|
||||||
|
"application/json",
|
||||||
|
REPORT_SCHEMA,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
result = {
|
||||||
|
"schema_version": RESULT_SCHEMA,
|
||||||
|
"result_id": result_id,
|
||||||
|
"identity_sha256": identity_sha256,
|
||||||
|
"identity": identity,
|
||||||
|
"created_at_utc": created_at,
|
||||||
|
"acceptance_state": report["state"],
|
||||||
|
"ground_truth": False,
|
||||||
|
"publication_scope": "recorded-integrated-realtime-qualification-only",
|
||||||
|
"frames_processed": parent.frame_count,
|
||||||
|
"artifacts": artifacts,
|
||||||
|
}
|
||||||
|
write_json(staging / "result.json", result)
|
||||||
|
os.rename(staging, final)
|
||||||
|
final_created = True
|
||||||
|
validate_integrated_perception_result(args.job, final, args.lidar_packs_root)
|
||||||
|
return final
|
||||||
|
except BaseException:
|
||||||
|
if staging.exists():
|
||||||
|
shutil.rmtree(staging)
|
||||||
|
if final_created and final.exists():
|
||||||
|
shutil.rmtree(final)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
lidar.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
result = materialize(arguments())
|
||||||
|
report = read_object(result / "run-report.json")
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"result_root": str(result),
|
||||||
|
"result_id": result.name,
|
||||||
|
"accepted": report["acceptance"]["accepted"],
|
||||||
|
"fusion": report["metrics"]["fusion"],
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
sort_keys=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
{
|
||||||
|
"schema_version": "missioncore.e10-integrated-perception-profile/v1",
|
||||||
|
"profile_id": "lab-e19-ground-aware-cuboids-v1",
|
||||||
|
"mode": "full-session-qualification",
|
||||||
|
"source": {
|
||||||
|
"source_id": "sensor.camera.right",
|
||||||
|
"resolution": [800, 600],
|
||||||
|
"calibration_slot": "camera_1",
|
||||||
|
"calibration_sha256": "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9"
|
||||||
|
},
|
||||||
|
"selection": {
|
||||||
|
"required_frame_count": 4489,
|
||||||
|
"required_source_start_frame_index": 0,
|
||||||
|
"required_source_end_frame_index": 4488,
|
||||||
|
"minimum_source_span_seconds": 448.0
|
||||||
|
},
|
||||||
|
"replay": {
|
||||||
|
"speed": 1.0,
|
||||||
|
"detector_queue_capacity": 2,
|
||||||
|
"semantic_queue_capacity": 1,
|
||||||
|
"semantic_sample_every_frames": 5,
|
||||||
|
"semantic_ttl_ms": 750.0
|
||||||
|
},
|
||||||
|
"association": {
|
||||||
|
"box_inset": {
|
||||||
|
"bottom_fraction": 0.02,
|
||||||
|
"horizontal_fraction": 0.05,
|
||||||
|
"top_fraction": 0.04
|
||||||
|
},
|
||||||
|
"depth_cluster_gap_fraction": 0.06,
|
||||||
|
"depth_cluster_minimum_gap_m": 0.5,
|
||||||
|
"group_nms_iou_threshold": 0.5,
|
||||||
|
"support_duplicate_overlap_threshold": 0.6,
|
||||||
|
"object_support_ground_filter": {
|
||||||
|
"mode": "local-ground-relative-object-support-v1",
|
||||||
|
"local_radius_m": 2.5,
|
||||||
|
"lower_percentile": 8.0,
|
||||||
|
"maximum_below_support_m": 1.2,
|
||||||
|
"maximum_above_support_m": 0.15,
|
||||||
|
"fallback_below_support_m": 0.25,
|
||||||
|
"minimum_height_above_ground_m": {
|
||||||
|
"person": 0.08,
|
||||||
|
"bicycle": 0.08,
|
||||||
|
"motorcycle": 0.08,
|
||||||
|
"vehicle": 0.12
|
||||||
|
},
|
||||||
|
"maximum_height_above_ground_m": {
|
||||||
|
"person": 2.5,
|
||||||
|
"bicycle": 2.5,
|
||||||
|
"motorcycle": 2.5,
|
||||||
|
"vehicle": 4.5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"maximum_cuboid_span_m": 15.0,
|
||||||
|
"maximum_distance_innovation_fraction": 0.25,
|
||||||
|
"maximum_distance_innovation_m": 1.5,
|
||||||
|
"maximum_oriented_extent_m": {
|
||||||
|
"bicycle": [3.5, 2.0, 2.5],
|
||||||
|
"motorcycle": [3.5, 2.0, 2.5],
|
||||||
|
"person": [1.5, 1.5, 2.8],
|
||||||
|
"vehicle": [12.5, 4.0, 4.5]
|
||||||
|
},
|
||||||
|
"minimum_cuboid_extent_m": 0.15,
|
||||||
|
"minimum_support_points": {
|
||||||
|
"bicycle": 4,
|
||||||
|
"motorcycle": 4,
|
||||||
|
"person": 4,
|
||||||
|
"vehicle": 8
|
||||||
|
},
|
||||||
|
"semantic_ids": {
|
||||||
|
"bicycle": [2],
|
||||||
|
"motorcycle": [3],
|
||||||
|
"person": [1],
|
||||||
|
"vehicle": [4, 5]
|
||||||
|
},
|
||||||
|
"spatial_cluster_radius_m": {
|
||||||
|
"bicycle": 0.9,
|
||||||
|
"motorcycle": 0.9,
|
||||||
|
"person": 0.9,
|
||||||
|
"vehicle": 1.5
|
||||||
|
},
|
||||||
|
"vehicle_labels": ["car", "truck", "bus"],
|
||||||
|
"distance_history_frames": 5
|
||||||
|
},
|
||||||
|
"cuboid_completion": {
|
||||||
|
"mode": "class-prior-amodal-v1",
|
||||||
|
"failure_policy": "reject",
|
||||||
|
"classes": {
|
||||||
|
"person": {
|
||||||
|
"nominal_size_m": [0.55, 0.55, 1.72],
|
||||||
|
"minimum_size_m": [0.35, 0.35, 1.3],
|
||||||
|
"maximum_size_m": [1.2, 1.2, 2.3],
|
||||||
|
"support_padding_m": [0.12, 0.12, 0.12]
|
||||||
|
},
|
||||||
|
"bicycle": {
|
||||||
|
"nominal_size_m": [1.8, 0.65, 1.5],
|
||||||
|
"minimum_size_m": [1.2, 0.4, 1.0],
|
||||||
|
"maximum_size_m": [2.5, 1.2, 2.2],
|
||||||
|
"support_padding_m": [0.18, 0.12, 0.12]
|
||||||
|
},
|
||||||
|
"motorcycle": {
|
||||||
|
"nominal_size_m": [2.1, 0.8, 1.45],
|
||||||
|
"minimum_size_m": [1.4, 0.5, 1.0],
|
||||||
|
"maximum_size_m": [3.0, 1.4, 2.2],
|
||||||
|
"support_padding_m": [0.2, 0.14, 0.14]
|
||||||
|
},
|
||||||
|
"car": {
|
||||||
|
"nominal_size_m": [4.5, 1.85, 1.55],
|
||||||
|
"minimum_size_m": [3.2, 1.45, 1.2],
|
||||||
|
"maximum_size_m": [5.8, 2.4, 2.3],
|
||||||
|
"support_padding_m": [0.25, 0.18, 0.15]
|
||||||
|
},
|
||||||
|
"truck": {
|
||||||
|
"nominal_size_m": [7.0, 2.5, 3.0],
|
||||||
|
"minimum_size_m": [4.8, 1.8, 1.8],
|
||||||
|
"maximum_size_m": [12.5, 3.2, 4.2],
|
||||||
|
"support_padding_m": [0.35, 0.22, 0.2]
|
||||||
|
},
|
||||||
|
"bus": {
|
||||||
|
"nominal_size_m": [10.5, 2.55, 3.2],
|
||||||
|
"minimum_size_m": [7.0, 2.1, 2.5],
|
||||||
|
"maximum_size_m": [13.5, 3.2, 4.2],
|
||||||
|
"support_padding_m": [0.4, 0.24, 0.2]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ground": {
|
||||||
|
"local_radius_m": 2.5,
|
||||||
|
"lower_percentile": 8.0,
|
||||||
|
"maximum_below_support_m": 1.2,
|
||||||
|
"maximum_above_support_m": 0.15,
|
||||||
|
"fallback_below_support_m": 0.25
|
||||||
|
},
|
||||||
|
"orientation": {
|
||||||
|
"minimum_anisotropy_ratio": 1.35,
|
||||||
|
"face_width_switch_fraction": 1.05
|
||||||
|
},
|
||||||
|
"temporal": {
|
||||||
|
"center_alpha": 0.4,
|
||||||
|
"size_alpha": 0.2,
|
||||||
|
"yaw_alpha": 0.25,
|
||||||
|
"maximum_center_innovation_m": 2.0,
|
||||||
|
"maximum_yaw_innovation_degrees": 55.0,
|
||||||
|
"maximum_idle_s": 1.0,
|
||||||
|
"confirmation_hits": 3
|
||||||
|
},
|
||||||
|
"minimum_support_coverage_fraction": 0.75
|
||||||
|
},
|
||||||
|
"world_state": {
|
||||||
|
"velocity_history_limit_s": 1.0,
|
||||||
|
"clearance": {
|
||||||
|
"sector_count": 72,
|
||||||
|
"minimum_range_m": 0.5,
|
||||||
|
"maximum_range_m": 30.0,
|
||||||
|
"ground_percentile": 5.0,
|
||||||
|
"minimum_height_above_ground_m": 0.2,
|
||||||
|
"maximum_height_above_ground_m": 3.0,
|
||||||
|
"front_half_angle_degrees": 15.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"acceptance": {
|
||||||
|
"detector_minimum_effective_fps": 9.5,
|
||||||
|
"detector_maximum_drop_fraction": 0.005,
|
||||||
|
"maximum_p95_world_state_age_ms": 175.0,
|
||||||
|
"semantic_minimum_effective_fps": 1.8,
|
||||||
|
"semantic_maximum_drop_fraction": 0.05,
|
||||||
|
"semantic_maximum_p95_completion_age_ms": 400.0,
|
||||||
|
"minimum_fresh_semantic_coverage": 0.9,
|
||||||
|
"minimum_lidar_fused_frames": 3500,
|
||||||
|
"minimum_accepted_cuboids": 1500,
|
||||||
|
"require_zero_failures": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,6 +26,7 @@ from e10_fusion_runtime import (
|
||||||
LidarReplayPack,
|
LidarReplayPack,
|
||||||
WorldStateProjector,
|
WorldStateProjector,
|
||||||
_completion_profile,
|
_completion_profile,
|
||||||
|
_object_support_ground_filter_profile,
|
||||||
canonical_json,
|
canonical_json,
|
||||||
clearance,
|
clearance,
|
||||||
distance_history,
|
distance_history,
|
||||||
|
|
@ -160,6 +161,14 @@ def read_profile(path: Path) -> tuple[dict[str, Any], str]:
|
||||||
if not isinstance(completion, dict):
|
if not isinstance(completion, dict):
|
||||||
raise RuntimeError("LAB E13 cuboid completion contract is invalid")
|
raise RuntimeError("LAB E13 cuboid completion contract is invalid")
|
||||||
_completion_profile(completion)
|
_completion_profile(completion)
|
||||||
|
ground_filter = association.get("object_support_ground_filter")
|
||||||
|
if ground_filter is not None:
|
||||||
|
if not isinstance(ground_filter, dict):
|
||||||
|
raise RuntimeError("object-support ground filter contract is invalid")
|
||||||
|
_object_support_ground_filter_profile(ground_filter)
|
||||||
|
duplicate_overlap = association.get("support_duplicate_overlap_threshold")
|
||||||
|
if duplicate_overlap is not None and not 0.0 < float(duplicate_overlap) <= 1.0:
|
||||||
|
raise RuntimeError("support duplicate overlap contract is invalid")
|
||||||
return profile, sha256(resolved)
|
return profile, sha256(resolved)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ from collections.abc import Callable
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Literal, TypedDict
|
from typing import Any, Literal, TypedDict
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
@ -534,6 +534,19 @@ def _recorded_blueprint(
|
||||||
),
|
),
|
||||||
).visualizer()
|
).visualizer()
|
||||||
point_visualizer.id = RECORDED_POINTS_VISUALIZER_ID
|
point_visualizer.id = RECORDED_POINTS_VISUALIZER_ID
|
||||||
|
accumulated_time_ranges = (
|
||||||
|
rrb.VisibleTimeRanges(time_ranges) if time_ranges is not None else None
|
||||||
|
)
|
||||||
|
point_overrides: list[Any] = [
|
||||||
|
rrb.EntityBehavior(visible=settings.show_points),
|
||||||
|
point_visualizer,
|
||||||
|
]
|
||||||
|
trajectory_overrides: list[Any] = [
|
||||||
|
rrb.EntityBehavior(visible=settings.show_trajectory),
|
||||||
|
]
|
||||||
|
if accumulated_time_ranges is not None:
|
||||||
|
point_overrides.append(accumulated_time_ranges)
|
||||||
|
trajectory_overrides.append(accumulated_time_ranges)
|
||||||
spatial_view = rrb.Spatial3DView(
|
spatial_view = rrb.Spatial3DView(
|
||||||
origin="/world",
|
origin="/world",
|
||||||
name="Пространственная сцена",
|
name="Пространственная сцена",
|
||||||
|
|
@ -548,21 +561,17 @@ def _recorded_blueprint(
|
||||||
# therefore hide/reveal already-recorded entities without
|
# therefore hide/reveal already-recorded entities without
|
||||||
# rewriting the data RRD. Keep the point visualizer alongside it
|
# rewriting the data RRD. Keep the point visualizer alongside it
|
||||||
# so size/color overrides remain active for the same entity.
|
# so size/color overrides remain active for the same entity.
|
||||||
"/world/points": [
|
"/world/points": point_overrides,
|
||||||
rrb.EntityBehavior(visible=settings.show_points),
|
"/world/trajectory": trajectory_overrides,
|
||||||
point_visualizer,
|
|
||||||
],
|
|
||||||
"/world/trajectory": rrb.EntityBehavior(
|
|
||||||
visible=settings.show_trajectory,
|
|
||||||
),
|
|
||||||
# Dynamic perception must not inherit the mapping view's
|
# Dynamic perception must not inherit the mapping view's
|
||||||
# historical accumulation window. It is rendered latest-at in
|
# historical accumulation window. It is rendered latest-at in
|
||||||
# the dedicated perception view below.
|
# the dedicated perception view below.
|
||||||
"/world/perception": rrb.EntityBehavior(visible=False),
|
"/world/perception": rrb.EntityBehavior(visible=False),
|
||||||
},
|
},
|
||||||
# A positive window accumulates historical frames. With no
|
# Clear any prior view-level accumulation and scope the operator's
|
||||||
# window, latest-at deliberately keeps one current LiDAR frame.
|
# history window to mapping entities above. Dynamic perception then
|
||||||
time_ranges=time_ranges,
|
# remains latest-at even when both layers share this world view.
|
||||||
|
time_ranges=rrb.VisibleTimeRanges([]),
|
||||||
)
|
)
|
||||||
spatial_view.id = RECORDED_SPATIAL_VIEW_ID
|
spatial_view.id = RECORDED_SPATIAL_VIEW_ID
|
||||||
camera_view = rrb.Spatial2DView(
|
camera_view = rrb.Spatial2DView(
|
||||||
|
|
@ -599,9 +608,7 @@ def _recorded_blueprint(
|
||||||
name="Маршрут и время",
|
name="Маршрут и время",
|
||||||
)
|
)
|
||||||
metrics_view.id = RECORDED_METRICS_VIEW_ID
|
metrics_view.id = RECORDED_METRICS_VIEW_ID
|
||||||
active_tab = {"spatial": 0, "perception": 1, "perception3d": 2, "metrics": 3}[
|
active_tab = {"spatial": 0, "perception": 1, "perception3d": 2, "metrics": 3}[active_view]
|
||||||
active_view
|
|
||||||
]
|
|
||||||
root_container = rrb.Tabs(
|
root_container = rrb.Tabs(
|
||||||
spatial_view,
|
spatial_view,
|
||||||
camera_view,
|
camera_view,
|
||||||
|
|
|
||||||
|
|
@ -21,15 +21,11 @@ RECORDED_SPATIAL_RESET_ROOT_CONTAINER_ID = UUID("27d95ad7-1e72-46ca-b854-d02cd99
|
||||||
RECORDED_PERCEPTION_ROOT_CONTAINER_ID = UUID("70ea9fd5-bbbb-4b23-8ae8-8098af92b997")
|
RECORDED_PERCEPTION_ROOT_CONTAINER_ID = UUID("70ea9fd5-bbbb-4b23-8ae8-8098af92b997")
|
||||||
RECORDED_PERCEPTION_RESET_ROOT_CONTAINER_ID = UUID("d2196c6e-99da-4402-857c-4daea0dd3ae0")
|
RECORDED_PERCEPTION_RESET_ROOT_CONTAINER_ID = UUID("d2196c6e-99da-4402-857c-4daea0dd3ae0")
|
||||||
RECORDED_PERCEPTION_3D_ROOT_CONTAINER_ID = UUID("81051015-9808-413b-90a4-1cfaacacbc4f")
|
RECORDED_PERCEPTION_3D_ROOT_CONTAINER_ID = UUID("81051015-9808-413b-90a4-1cfaacacbc4f")
|
||||||
RECORDED_PERCEPTION_3D_RESET_ROOT_CONTAINER_ID = UUID(
|
RECORDED_PERCEPTION_3D_RESET_ROOT_CONTAINER_ID = UUID("3de10822-0274-4a58-8d45-f35d86625303")
|
||||||
"3de10822-0274-4a58-8d45-f35d86625303"
|
|
||||||
)
|
|
||||||
RECORDED_METRICS_ROOT_CONTAINER_ID = UUID("374710a2-1d77-4349-bea4-8719e7aa1f09")
|
RECORDED_METRICS_ROOT_CONTAINER_ID = UUID("374710a2-1d77-4349-bea4-8719e7aa1f09")
|
||||||
RECORDED_METRICS_RESET_ROOT_CONTAINER_ID = UUID("1e8ac565-bbd6-4554-9213-dad564e534e3")
|
RECORDED_METRICS_RESET_ROOT_CONTAINER_ID = UUID("1e8ac565-bbd6-4554-9213-dad564e534e3")
|
||||||
RECORDED_POINTS_VISUALIZER_ID = UUID("ca037ec0-8761-4417-86ee-846fa2875303")
|
RECORDED_POINTS_VISUALIZER_ID = UUID("ca037ec0-8761-4417-86ee-846fa2875303")
|
||||||
RECORDED_PERCEPTION_POINTS_VISUALIZER_ID = UUID(
|
RECORDED_PERCEPTION_POINTS_VISUALIZER_ID = UUID("ab8db306-6981-49c5-b417-94fe2f01dbf0")
|
||||||
"ab8db306-6981-49c5-b417-94fe2f01dbf0"
|
|
||||||
)
|
|
||||||
RECORDED_CAMERA_VIEW_ID = UUID("5c1db75b-07cd-479a-903d-f4f2ed554513")
|
RECORDED_CAMERA_VIEW_ID = UUID("5c1db75b-07cd-479a-903d-f4f2ed554513")
|
||||||
RECORDED_CAMERA_RESET_VIEW_ID = UUID("d0618e0c-c889-4222-a643-60a4a882935c")
|
RECORDED_CAMERA_RESET_VIEW_ID = UUID("d0618e0c-c889-4222-a643-60a4a882935c")
|
||||||
RECORDED_PERCEPTION_3D_VIEW_ID = UUID("0496bd2e-2b4d-4a4f-87b8-3ce4f9f7e114")
|
RECORDED_PERCEPTION_3D_VIEW_ID = UUID("0496bd2e-2b4d-4a4f-87b8-3ce4f9f7e114")
|
||||||
|
|
@ -65,25 +61,33 @@ def recorded_blueprint(
|
||||||
end=rr.TimeRangeBoundary.cursor_relative(),
|
end=rr.TimeRangeBoundary.cursor_relative(),
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
latest_time_ranges = rrb.VisibleTimeRanges([])
|
accumulated_time_ranges = (
|
||||||
|
rrb.VisibleTimeRanges(time_ranges) if time_ranges is not None else None
|
||||||
|
)
|
||||||
point_visualizer = rr.Points3D.from_fields(
|
point_visualizer = rr.Points3D.from_fields(
|
||||||
radii=rr.Radius.ui_points(settings.point_size),
|
radii=rr.Radius.ui_points(settings.point_size),
|
||||||
colors=(
|
colors=(
|
||||||
[_parse_hex_color(settings.custom_color)]
|
[_parse_hex_color(settings.custom_color)] if settings.palette == "custom" else None
|
||||||
if settings.palette == "custom"
|
|
||||||
else None
|
|
||||||
),
|
),
|
||||||
).visualizer()
|
).visualizer()
|
||||||
point_visualizer.id = RECORDED_POINTS_VISUALIZER_ID
|
point_visualizer.id = RECORDED_POINTS_VISUALIZER_ID
|
||||||
perception_point_visualizer = rr.Points3D.from_fields(
|
perception_point_visualizer = rr.Points3D.from_fields(
|
||||||
radii=rr.Radius.ui_points(settings.point_size),
|
radii=rr.Radius.ui_points(settings.point_size),
|
||||||
colors=(
|
colors=(
|
||||||
[_parse_hex_color(settings.custom_color)]
|
[_parse_hex_color(settings.custom_color)] if settings.palette == "custom" else None
|
||||||
if settings.palette == "custom"
|
|
||||||
else None
|
|
||||||
),
|
),
|
||||||
).visualizer()
|
).visualizer()
|
||||||
perception_point_visualizer.id = RECORDED_PERCEPTION_POINTS_VISUALIZER_ID
|
perception_point_visualizer.id = RECORDED_PERCEPTION_POINTS_VISUALIZER_ID
|
||||||
|
point_overrides: list[Any] = [
|
||||||
|
rrb.EntityBehavior(visible=settings.show_points),
|
||||||
|
point_visualizer,
|
||||||
|
]
|
||||||
|
trajectory_overrides: list[Any] = [
|
||||||
|
rrb.EntityBehavior(visible=settings.show_trajectory),
|
||||||
|
]
|
||||||
|
if accumulated_time_ranges is not None:
|
||||||
|
point_overrides.append(accumulated_time_ranges)
|
||||||
|
trajectory_overrides.append(accumulated_time_ranges)
|
||||||
spatial_view = rrb.Spatial3DView(
|
spatial_view = rrb.Spatial3DView(
|
||||||
origin="/world",
|
origin="/world",
|
||||||
name="Мир · LiDAR и объекты" if unified_perception else "Пространственная сцена",
|
name="Мир · LiDAR и объекты" if unified_perception else "Пространственная сцена",
|
||||||
|
|
@ -94,24 +98,19 @@ def recorded_blueprint(
|
||||||
stroke_width=0.75,
|
stroke_width=0.75,
|
||||||
),
|
),
|
||||||
overrides={
|
overrides={
|
||||||
"/world/points": [
|
# Accumulation belongs to mapping data only. Dynamic perception
|
||||||
rrb.EntityBehavior(visible=settings.show_points),
|
# inherits the view's latest-at query and can never turn into an
|
||||||
point_visualizer,
|
# object-history trail when the operator widens the cloud window.
|
||||||
],
|
"/world/points": point_overrides,
|
||||||
"/world/trajectory": rrb.EntityBehavior(
|
"/world/trajectory": trajectory_overrides,
|
||||||
visible=settings.show_trajectory,
|
|
||||||
),
|
|
||||||
# Perception is hidden in the mapping-only presentation. Unified
|
|
||||||
# perception below adds explicit per-entity latest-at overrides,
|
|
||||||
# allowing the native cloud to retain this view's accumulation.
|
|
||||||
"/world/perception": rrb.EntityBehavior(visible=False),
|
"/world/perception": rrb.EntityBehavior(visible=False),
|
||||||
},
|
},
|
||||||
time_ranges=time_ranges,
|
# Log an explicit empty range set so a previous accumulated blueprint
|
||||||
|
# for this stable view id is cleared instead of surviving in Rerun.
|
||||||
|
time_ranges=rrb.VisibleTimeRanges([]),
|
||||||
)
|
)
|
||||||
spatial_view.id = (
|
spatial_view.id = (
|
||||||
RECORDED_SPATIAL_RESET_VIEW_ID
|
RECORDED_SPATIAL_RESET_VIEW_ID if view_reset_generation else RECORDED_SPATIAL_VIEW_ID
|
||||||
if view_reset_generation
|
|
||||||
else RECORDED_SPATIAL_VIEW_ID
|
|
||||||
)
|
)
|
||||||
camera_view = rrb.Spatial2DView(
|
camera_view = rrb.Spatial2DView(
|
||||||
origin="/perception/camera",
|
origin="/perception/camera",
|
||||||
|
|
@ -128,9 +127,7 @@ def recorded_blueprint(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
camera_view.id = (
|
camera_view.id = (
|
||||||
RECORDED_CAMERA_RESET_VIEW_ID
|
RECORDED_CAMERA_RESET_VIEW_ID if view_reset_generation else RECORDED_CAMERA_VIEW_ID
|
||||||
if view_reset_generation
|
|
||||||
else RECORDED_CAMERA_VIEW_ID
|
|
||||||
)
|
)
|
||||||
perception_3d_view = rrb.Spatial3DView(
|
perception_3d_view = rrb.Spatial3DView(
|
||||||
# Cuboids are expressed in the same calibrated world frame as the
|
# Cuboids are expressed in the same calibrated world frame as the
|
||||||
|
|
@ -181,31 +178,18 @@ def recorded_blueprint(
|
||||||
# semantic points and cuboids independently.
|
# semantic points and cuboids independently.
|
||||||
spatial_view.visualizer_overrides["/world/perception"] = [
|
spatial_view.visualizer_overrides["/world/perception"] = [
|
||||||
rrb.EntityBehavior(visible=True),
|
rrb.EntityBehavior(visible=True),
|
||||||
latest_time_ranges,
|
|
||||||
]
|
]
|
||||||
spatial_view.visualizer_overrides[
|
spatial_view.visualizer_overrides["/world/perception/lidar"] = [
|
||||||
"/world/perception/lidar"
|
|
||||||
] = [
|
|
||||||
rrb.EntityBehavior(visible=False),
|
rrb.EntityBehavior(visible=False),
|
||||||
latest_time_ranges,
|
|
||||||
]
|
]
|
||||||
spatial_view.visualizer_overrides[
|
spatial_view.visualizer_overrides["/world/perception/support"] = [
|
||||||
"/world/perception/support"
|
|
||||||
] = [
|
|
||||||
rrb.EntityBehavior(visible=False),
|
rrb.EntityBehavior(visible=False),
|
||||||
latest_time_ranges,
|
|
||||||
]
|
]
|
||||||
spatial_view.visualizer_overrides[
|
spatial_view.visualizer_overrides["/world/perception/semantic_points"] = [
|
||||||
"/world/perception/semantic_points"
|
|
||||||
] = [
|
|
||||||
rrb.EntityBehavior(visible=show_segmentation),
|
rrb.EntityBehavior(visible=show_segmentation),
|
||||||
latest_time_ranges,
|
|
||||||
]
|
]
|
||||||
spatial_view.visualizer_overrides[
|
spatial_view.visualizer_overrides["/world/perception/boxes3d"] = [
|
||||||
"/world/perception/boxes3d"
|
|
||||||
] = [
|
|
||||||
rrb.EntityBehavior(visible=show_cuboids_3d),
|
rrb.EntityBehavior(visible=show_cuboids_3d),
|
||||||
latest_time_ranges,
|
|
||||||
]
|
]
|
||||||
root_container = rrb.Horizontal(
|
root_container = rrb.Horizontal(
|
||||||
camera_view,
|
camera_view,
|
||||||
|
|
@ -222,9 +206,7 @@ def recorded_blueprint(
|
||||||
# Keep operator video and 3D cuboids as direct root views. Rerun's nested
|
# Keep operator video and 3D cuboids as direct root views. Rerun's nested
|
||||||
# Tabs preserve their own active child and cannot be switched reliably by
|
# Tabs preserve their own active child and cannot be switched reliably by
|
||||||
# a live blueprint channel after the operator has visited another child.
|
# a live blueprint channel after the operator has visited another child.
|
||||||
active_tab = {"spatial": 0, "perception": 1, "perception3d": 2, "metrics": 3}[
|
active_tab = {"spatial": 0, "perception": 1, "perception3d": 2, "metrics": 3}[active_view]
|
||||||
active_view
|
|
||||||
]
|
|
||||||
root_container = rrb.Tabs(
|
root_container = rrb.Tabs(
|
||||||
spatial_view,
|
spatial_view,
|
||||||
camera_view,
|
camera_view,
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,22 @@ def test_e13_profile_pins_provenance_marked_amodal_completion() -> None:
|
||||||
assert profile["cuboid_completion"]["temporal"]["confirmation_hits"] == 3
|
assert profile["cuboid_completion"]["temporal"]["confirmation_hits"] == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_e19_profile_adds_ground_aware_support_without_mutating_e14() -> None:
|
||||||
|
_fusion, runner = _worker_modules()
|
||||||
|
root = Path(__file__).resolve().parents[1] / "experiments" / "perception" / "worker"
|
||||||
|
e14, _e14_digest = runner.read_profile(root / "e14_full_session_amodal_profile.json")
|
||||||
|
e19, _e19_digest = runner.read_profile(root / "e19_ground_aware_cuboid_profile.json")
|
||||||
|
|
||||||
|
assert "object_support_ground_filter" not in e14["association"]
|
||||||
|
assert "support_duplicate_overlap_threshold" not in e14["association"]
|
||||||
|
assert e19["profile_id"] == "lab-e19-ground-aware-cuboids-v1"
|
||||||
|
assert (
|
||||||
|
e19["association"]["object_support_ground_filter"]["mode"]
|
||||||
|
== "local-ground-relative-object-support-v1"
|
||||||
|
)
|
||||||
|
assert e19["association"]["support_duplicate_overlap_threshold"] == 0.6
|
||||||
|
|
||||||
|
|
||||||
def test_e14_profile_combines_full_session_and_amodal_gates() -> None:
|
def test_e14_profile_combines_full_session_and_amodal_gates() -> None:
|
||||||
_fusion, runner = _worker_modules()
|
_fusion, runner = _worker_modules()
|
||||||
profile_path = (
|
profile_path = (
|
||||||
|
|
@ -278,6 +294,84 @@ def test_e13_temporal_filter_reduces_cuboid_center_jitter() -> None:
|
||||||
assert outputs[-1].temporal_status == "confirmed"
|
assert outputs[-1].temporal_status == "confirmed"
|
||||||
|
|
||||||
|
|
||||||
|
def test_e19_ground_filter_preserves_cloud_and_excludes_ground_from_box_support() -> None:
|
||||||
|
fusion, runner = _worker_modules()
|
||||||
|
profile, _digest = runner.read_profile(
|
||||||
|
Path(__file__).resolve().parents[1]
|
||||||
|
/ "experiments"
|
||||||
|
/ "perception"
|
||||||
|
/ "worker"
|
||||||
|
/ "e19_ground_aware_cuboid_profile.json"
|
||||||
|
)
|
||||||
|
ground = np.asarray(
|
||||||
|
[[x, y, 0.0] for x in (9.5, 10.0, 10.5) for y in (-0.6, 0.0, 0.6)],
|
||||||
|
dtype=np.float64,
|
||||||
|
)
|
||||||
|
vehicle = np.asarray(
|
||||||
|
[[x, y, z] for x in (9.8, 10.2) for y in (-0.4, 0.4) for z in (0.3, 0.9, 1.4)],
|
||||||
|
dtype=np.float64,
|
||||||
|
)
|
||||||
|
cloud = np.concatenate((ground, vehicle))
|
||||||
|
unchanged = cloud.copy()
|
||||||
|
indices = np.arange(cloud.shape[0], dtype=np.int64)
|
||||||
|
|
||||||
|
filtered, ground_z, rejected = fusion._filter_object_support_by_ground(
|
||||||
|
indices,
|
||||||
|
cloud,
|
||||||
|
group="vehicle",
|
||||||
|
profile=profile["association"]["object_support_ground_filter"],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert ground_z == 0.0
|
||||||
|
assert rejected == ground.shape[0]
|
||||||
|
assert filtered.tolist() == list(range(ground.shape[0], cloud.shape[0]))
|
||||||
|
np.testing.assert_array_equal(cloud, unchanged)
|
||||||
|
|
||||||
|
|
||||||
|
def test_e19_duplicate_tracks_cannot_publish_the_same_lidar_support_twice() -> None:
|
||||||
|
fusion, _runner = _worker_modules()
|
||||||
|
cuboid = fusion.Cuboid(
|
||||||
|
center_map=(10.0, 0.0, 0.8),
|
||||||
|
half_size=(2.25, 0.925, 0.775),
|
||||||
|
quaternion_xyzw=(0.0, 0.0, 0.0, 1.0),
|
||||||
|
)
|
||||||
|
|
||||||
|
def item(track_id: int, score: float, indices: list[int]) -> object:
|
||||||
|
source = np.asarray(indices, dtype=np.int64)
|
||||||
|
return fusion.TrackFusion(
|
||||||
|
track_id=track_id,
|
||||||
|
label="car",
|
||||||
|
association_group="vehicle",
|
||||||
|
score=score,
|
||||||
|
bbox_xyxy=(100.0, 100.0, 200.0, 200.0),
|
||||||
|
candidate_points=source.size,
|
||||||
|
semantic_points=source.size,
|
||||||
|
clustered_points=source.size,
|
||||||
|
distance_p10_m=9.5,
|
||||||
|
distance_median_m=10.0,
|
||||||
|
distance_smoothed_m=10.0,
|
||||||
|
status="accepted-class-prior-amodal-v1",
|
||||||
|
cuboid=cuboid,
|
||||||
|
source_indices=source,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = fusion._suppress_duplicate_support_fusions(
|
||||||
|
[
|
||||||
|
item(10, 0.91, [1, 2, 3, 4, 5]),
|
||||||
|
item(11, 0.72, [1, 2, 3, 4]),
|
||||||
|
item(12, 0.80, [20, 21, 22, 23]),
|
||||||
|
],
|
||||||
|
overlap_threshold=0.6,
|
||||||
|
)
|
||||||
|
by_track = {value.track_id: value for value in result}
|
||||||
|
|
||||||
|
assert by_track[10].cuboid is not None
|
||||||
|
assert by_track[11].cuboid is None
|
||||||
|
assert by_track[11].status == "rejected-duplicate-lidar-support"
|
||||||
|
assert by_track[11].source_indices.size == 0
|
||||||
|
assert by_track[12].cuboid is not None
|
||||||
|
|
||||||
|
|
||||||
def test_e10_semantic_binding_is_explicit_about_freshness() -> None:
|
def test_e10_semantic_binding_is_explicit_about_freshness() -> None:
|
||||||
_fusion, runner = _worker_modules()
|
_fusion, runner = _worker_modules()
|
||||||
assert runner.semantic_binding(None, 2.0, 750.0) == ("unavailable", None)
|
assert runner.semantic_binding(None, 2.0, 750.0) == ("unavailable", None)
|
||||||
|
|
|
||||||
|
|
@ -237,7 +237,15 @@ def test_recorded_blueprint_accumulates_point_frames_on_session_timeline() -> No
|
||||||
visible_ranges = spatial_view.properties["VisibleTimeRanges"]
|
visible_ranges = spatial_view.properties["VisibleTimeRanges"]
|
||||||
line_grid = spatial_view.properties["LineGrid3D"]
|
line_grid = spatial_view.properties["LineGrid3D"]
|
||||||
|
|
||||||
assert visible_ranges.ranges.as_arrow_array().to_pylist() == [
|
assert visible_ranges.ranges.as_arrow_array().to_pylist() == []
|
||||||
|
assert line_grid.visible.as_arrow_array().to_pylist() == [False]
|
||||||
|
point_behavior, point_visualizer, point_time_ranges = spatial_view.visualizer_overrides[
|
||||||
|
"/world/points"
|
||||||
|
]
|
||||||
|
trajectory_behavior, trajectory_time_ranges = spatial_view.visualizer_overrides[
|
||||||
|
"/world/trajectory"
|
||||||
|
]
|
||||||
|
expected_accumulation = [
|
||||||
{
|
{
|
||||||
"timeline": SESSION_TIMELINE,
|
"timeline": SESSION_TIMELINE,
|
||||||
"range": {
|
"range": {
|
||||||
|
|
@ -246,9 +254,8 @@ def test_recorded_blueprint_accumulates_point_frames_on_session_timeline() -> No
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
assert line_grid.visible.as_arrow_array().to_pylist() == [False]
|
assert point_time_ranges.ranges.as_arrow_array().to_pylist() == expected_accumulation
|
||||||
point_behavior, point_visualizer = spatial_view.visualizer_overrides["/world/points"]
|
assert trajectory_time_ranges.ranges.as_arrow_array().to_pylist() == expected_accumulation
|
||||||
trajectory_behavior = spatial_view.visualizer_overrides["/world/trajectory"]
|
|
||||||
perception_behavior = spatial_view.visualizer_overrides["/world/perception"]
|
perception_behavior = spatial_view.visualizer_overrides["/world/perception"]
|
||||||
assert point_behavior.visible.as_arrow_array().to_pylist() == [False]
|
assert point_behavior.visible.as_arrow_array().to_pylist() == [False]
|
||||||
assert trajectory_behavior.visible.as_arrow_array().to_pylist() == [True]
|
assert trajectory_behavior.visible.as_arrow_array().to_pylist() == [True]
|
||||||
|
|
@ -277,9 +284,9 @@ def test_zero_accumulation_uses_latest_frame_instead_of_empty_time_range() -> No
|
||||||
blueprint = _recorded_blueprint(RerunSceneSettings(accumulation_seconds=0.0, show_grid=True))
|
blueprint = _recorded_blueprint(RerunSceneSettings(accumulation_seconds=0.0, show_grid=True))
|
||||||
spatial_view = blueprint.root_container.contents[0]
|
spatial_view = blueprint.root_container.contents[0]
|
||||||
|
|
||||||
assert "VisibleTimeRanges" not in spatial_view.properties
|
assert spatial_view.properties["VisibleTimeRanges"].ranges.as_arrow_array().to_pylist() == []
|
||||||
point_behavior, point_visualizer = spatial_view.visualizer_overrides["/world/points"]
|
point_behavior, point_visualizer = spatial_view.visualizer_overrides["/world/points"]
|
||||||
trajectory_behavior = spatial_view.visualizer_overrides["/world/trajectory"]
|
(trajectory_behavior,) = spatial_view.visualizer_overrides["/world/trajectory"]
|
||||||
assert point_behavior.visible.as_arrow_array().to_pylist() == [True]
|
assert point_behavior.visible.as_arrow_array().to_pylist() == [True]
|
||||||
assert trajectory_behavior.visible.as_arrow_array().to_pylist() == [True]
|
assert trajectory_behavior.visible.as_arrow_array().to_pylist() == [True]
|
||||||
point_components = {str(batch.component_descriptor()) for batch in point_visualizer.overrides}
|
point_components = {str(batch.component_descriptor()) for batch in point_visualizer.overrides}
|
||||||
|
|
@ -309,18 +316,32 @@ def test_dynamic_blueprint_reuses_scene_ids_without_playback_mutation() -> None:
|
||||||
assert first.root_container.contents[3].id == RECORDED_METRICS_VIEW_ID
|
assert first.root_container.contents[3].id == RECORDED_METRICS_VIEW_ID
|
||||||
assert second.root_container.contents[3].id == RECORDED_METRICS_VIEW_ID
|
assert second.root_container.contents[3].id == RECORDED_METRICS_VIEW_ID
|
||||||
|
|
||||||
first_point_behavior, first_point_visualizer = first_view.visualizer_overrides["/world/points"]
|
first_point_behavior, first_point_visualizer, first_point_time_ranges = (
|
||||||
second_point_behavior, second_point_visualizer = second_view.visualizer_overrides[
|
first_view.visualizer_overrides["/world/points"]
|
||||||
"/world/points"
|
)
|
||||||
|
second_point_behavior, second_point_visualizer, second_point_time_ranges = (
|
||||||
|
second_view.visualizer_overrides["/world/points"]
|
||||||
|
)
|
||||||
|
first_trajectory, first_trajectory_time_ranges = first_view.visualizer_overrides[
|
||||||
|
"/world/trajectory"
|
||||||
|
]
|
||||||
|
second_trajectory, second_trajectory_time_ranges = second_view.visualizer_overrides[
|
||||||
|
"/world/trajectory"
|
||||||
]
|
]
|
||||||
first_trajectory = first_view.visualizer_overrides["/world/trajectory"]
|
|
||||||
second_trajectory = second_view.visualizer_overrides["/world/trajectory"]
|
|
||||||
assert first_point_visualizer.id == RECORDED_POINTS_VISUALIZER_ID
|
assert first_point_visualizer.id == RECORDED_POINTS_VISUALIZER_ID
|
||||||
assert second_point_visualizer.id == RECORDED_POINTS_VISUALIZER_ID
|
assert second_point_visualizer.id == RECORDED_POINTS_VISUALIZER_ID
|
||||||
assert first_point_behavior.visible.as_arrow_array().to_pylist() == [False]
|
assert first_point_behavior.visible.as_arrow_array().to_pylist() == [False]
|
||||||
assert second_point_behavior.visible.as_arrow_array().to_pylist() == [True]
|
assert second_point_behavior.visible.as_arrow_array().to_pylist() == [True]
|
||||||
assert first_trajectory.visible.as_arrow_array().to_pylist() == [True]
|
assert first_trajectory.visible.as_arrow_array().to_pylist() == [True]
|
||||||
assert second_trajectory.visible.as_arrow_array().to_pylist() == [False]
|
assert second_trajectory.visible.as_arrow_array().to_pylist() == [False]
|
||||||
|
assert (
|
||||||
|
first_point_time_ranges.ranges.as_arrow_array().to_pylist()
|
||||||
|
== first_trajectory_time_ranges.ranges.as_arrow_array().to_pylist()
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
second_point_time_ranges.ranges.as_arrow_array().to_pylist()
|
||||||
|
== second_trajectory_time_ranges.ranges.as_arrow_array().to_pylist()
|
||||||
|
)
|
||||||
|
|
||||||
payload = recorded_blueprint_rrd(
|
payload = recorded_blueprint_rrd(
|
||||||
RerunSceneSettings(show_points=False, show_trajectory=False),
|
RerunSceneSettings(show_points=False, show_trajectory=False),
|
||||||
|
|
@ -373,17 +394,14 @@ def test_viewer_blueprint_reset_is_bounded_and_recreates_render_views() -> None:
|
||||||
cuboid_view = cuboids.root_container.contents[2]
|
cuboid_view = cuboids.root_container.contents[2]
|
||||||
assert cuboid_view.origin == "/world"
|
assert cuboid_view.origin == "/world"
|
||||||
assert "VisibleTimeRanges" not in cuboid_view.properties
|
assert "VisibleTimeRanges" not in cuboid_view.properties
|
||||||
cuboid_points, cuboid_point_visualizer = cuboid_view.visualizer_overrides[
|
cuboid_points, cuboid_point_visualizer = cuboid_view.visualizer_overrides["/world/points"]
|
||||||
"/world/points"
|
|
||||||
]
|
|
||||||
cuboid_perception = cuboid_view.visualizer_overrides["/world/perception"]
|
cuboid_perception = cuboid_view.visualizer_overrides["/world/perception"]
|
||||||
cuboid_diagnostic_lidar = cuboid_view.visualizer_overrides[
|
cuboid_diagnostic_lidar = cuboid_view.visualizer_overrides["/world/perception/lidar"]
|
||||||
"/world/perception/lidar"
|
|
||||||
]
|
|
||||||
assert cuboid_points.visible.as_arrow_array().to_pylist() == [True]
|
assert cuboid_points.visible.as_arrow_array().to_pylist() == [True]
|
||||||
assert cuboid_point_visualizer.id != initial.root_container.contents[0].visualizer_overrides[
|
assert (
|
||||||
"/world/points"
|
cuboid_point_visualizer.id
|
||||||
][1].id
|
!= initial.root_container.contents[0].visualizer_overrides["/world/points"][1].id
|
||||||
|
)
|
||||||
assert cuboid_perception.visible.as_arrow_array().to_pylist() == [True]
|
assert cuboid_perception.visible.as_arrow_array().to_pylist() == [True]
|
||||||
assert cuboid_diagnostic_lidar.visible.as_arrow_array().to_pylist() == [False]
|
assert cuboid_diagnostic_lidar.visible.as_arrow_array().to_pylist() == [False]
|
||||||
perception_behavior = initial.root_container.contents[0].visualizer_overrides[
|
perception_behavior = initial.root_container.contents[0].visualizer_overrides[
|
||||||
|
|
@ -415,18 +433,16 @@ def test_viewer_blueprint_unifies_original_video_and_independent_ai_layers() ->
|
||||||
assert camera_view.visualizer_overrides[
|
assert camera_view.visualizer_overrides[
|
||||||
"/perception/camera/segmentation"
|
"/perception/camera/segmentation"
|
||||||
].visible.as_arrow_array().to_pylist() == [True]
|
].visible.as_arrow_array().to_pylist() == [True]
|
||||||
semantic_behavior, semantic_time_ranges = spatial_view.visualizer_overrides[
|
(semantic_behavior,) = spatial_view.visualizer_overrides["/world/perception/semantic_points"]
|
||||||
"/world/perception/semantic_points"
|
(cuboid_behavior,) = spatial_view.visualizer_overrides["/world/perception/boxes3d"]
|
||||||
]
|
|
||||||
cuboid_behavior, cuboid_time_ranges = spatial_view.visualizer_overrides[
|
|
||||||
"/world/perception/boxes3d"
|
|
||||||
]
|
|
||||||
assert semantic_behavior.visible.as_arrow_array().to_pylist() == [True]
|
assert semantic_behavior.visible.as_arrow_array().to_pylist() == [True]
|
||||||
assert cuboid_behavior.visible.as_arrow_array().to_pylist() == [False]
|
assert cuboid_behavior.visible.as_arrow_array().to_pylist() == [False]
|
||||||
assert semantic_time_ranges.ranges.as_arrow_array().to_pylist() == []
|
|
||||||
assert cuboid_time_ranges.ranges.as_arrow_array().to_pylist() == []
|
|
||||||
visible_ranges = spatial_view.properties["VisibleTimeRanges"]
|
visible_ranges = spatial_view.properties["VisibleTimeRanges"]
|
||||||
assert visible_ranges.ranges.as_arrow_array().to_pylist() == [
|
assert visible_ranges.ranges.as_arrow_array().to_pylist() == []
|
||||||
|
_point_behavior, _point_visualizer, point_time_ranges = spatial_view.visualizer_overrides[
|
||||||
|
"/world/points"
|
||||||
|
]
|
||||||
|
assert point_time_ranges.ranges.as_arrow_array().to_pylist() == [
|
||||||
{
|
{
|
||||||
"timeline": SESSION_TIMELINE,
|
"timeline": SESSION_TIMELINE,
|
||||||
"range": {"start": -12_000_000_000, "end": 0},
|
"range": {"start": -12_000_000_000, "end": 0},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue