feat(perception): add ground-aware cuboid refusion
This commit is contained in:
@@ -13,7 +13,7 @@ import math
|
||||
import statistics
|
||||
from collections import defaultdict, deque
|
||||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, replace
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -97,6 +97,9 @@ class TrackFusion:
|
||||
status: str
|
||||
cuboid: Cuboid | None
|
||||
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"
|
||||
observed_cuboid: Cuboid | None = None
|
||||
completion_fraction: float | None = None
|
||||
@@ -466,6 +469,157 @@ def _completion_profile(profile: Mapping[str, Any]) -> dict[str, Any]:
|
||||
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:
|
||||
"""Complete visible LiDAR surfaces into provenance-marked, smoothed cuboids.
|
||||
|
||||
@@ -667,24 +821,12 @@ class CuboidCompletionTracker:
|
||||
center_xy: FloatArray,
|
||||
support_lower_z: float,
|
||||
) -> float:
|
||||
ground = self.profile["ground"]
|
||||
radius = float(ground["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)
|
||||
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
|
||||
return _local_ground_z(
|
||||
all_points_map=all_points_map,
|
||||
center_xy=center_xy,
|
||||
support_lower_z=support_lower_z,
|
||||
profile=self.profile["ground"],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
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,
|
||||
) -> tuple[TrackFusion, ...]:
|
||||
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]] = []
|
||||
for track in sorted(tracks, key=lambda value: float(value["score"]), reverse=True):
|
||||
label = str(track["label"])
|
||||
@@ -767,11 +920,21 @@ def fuse_tracks(
|
||||
allowed = np.asarray(association["semantic_ids"][group], dtype=np.uint8)
|
||||
compatible = candidates[np.isin(sampled_semantic[candidates], allowed)]
|
||||
clustered = _depth_cluster(compatible, depths, association)
|
||||
selected = _spatial_cluster(
|
||||
selected_before_ground = _spatial_cluster(
|
||||
source_indices[clustered],
|
||||
points_lidar,
|
||||
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)
|
||||
p10 = None if ranges.size == 0 else float(np.percentile(ranges, 10))
|
||||
median = None if ranges.size == 0 else float(np.median(ranges))
|
||||
@@ -786,6 +949,12 @@ def fuse_tracks(
|
||||
support_coverage_fraction = None
|
||||
if compatible.size == 0:
|
||||
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:
|
||||
status = f"rejected-fewer-than-{minimum}-clustered-points"
|
||||
else:
|
||||
@@ -861,6 +1030,9 @@ def fuse_tracks(
|
||||
status=status,
|
||||
cuboid=cuboid,
|
||||
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,
|
||||
observed_cuboid=observed_cuboid,
|
||||
completion_fraction=completion_fraction,
|
||||
@@ -870,7 +1042,11 @@ def fuse_tracks(
|
||||
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]:
|
||||
@@ -883,6 +1059,9 @@ def fusion_document(item: TrackFusion) -> dict[str, Any]:
|
||||
"candidate_projected_points": item.candidate_points,
|
||||
"semantic_compatible_points": item.semantic_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_median_m": item.distance_median_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,
|
||||
WorldStateProjector,
|
||||
_completion_profile,
|
||||
_object_support_ground_filter_profile,
|
||||
canonical_json,
|
||||
clearance,
|
||||
distance_history,
|
||||
@@ -160,6 +161,14 @@ def read_profile(path: Path) -> tuple[dict[str, Any], str]:
|
||||
if not isinstance(completion, dict):
|
||||
raise RuntimeError("LAB E13 cuboid completion contract is invalid")
|
||||
_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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user