feat(perception): add PointPillars transfer gate
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.datasets import kitti_3d_admission as module
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def _calibration(*, complete: bool = True) -> str:
|
||||
rows = [
|
||||
"P0: " + " ".join(["1"] * 12),
|
||||
"P1: " + " ".join(["1"] * 12),
|
||||
"P2: " + " ".join(["1"] * 12),
|
||||
"P3: " + " ".join(["1"] * 12),
|
||||
"R0_rect: " + " ".join(["1"] * 9),
|
||||
"Tr_velo_to_cam: " + " ".join(["1"] * 12),
|
||||
]
|
||||
if complete:
|
||||
rows.append("Tr_imu_to_velo: " + " ".join(["1"] * 12))
|
||||
return "\n".join(rows) + "\n"
|
||||
|
||||
|
||||
def _label(class_name: str, *, valid: bool = True) -> str:
|
||||
dimensions = "1.5 1.6 3.8" if valid else "0 1.6 3.8"
|
||||
return (
|
||||
f"{class_name} 0 0 0 0 0 10 10 {dimensions} 1 1 10 0\n"
|
||||
)
|
||||
|
||||
|
||||
def _release(
|
||||
root: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
*,
|
||||
labels: dict[str, str] | None = None,
|
||||
complete_calibration: bool = True,
|
||||
invalid_velodyne_frame: bool = False,
|
||||
train_rows: tuple[str, ...] = ("000000",),
|
||||
validation_rows: tuple[str, ...] = ("000001", "000002"),
|
||||
) -> dict[str, Path]:
|
||||
archive_root = root / module.KITTI_3D_RELEASE_ROOT / "archives"
|
||||
split_root = root / module.KITTI_3D_RELEASE_ROOT / "splits" / (
|
||||
f"openpcdet-{module.KITTI_STANDARD_SPLIT_COMMIT}"
|
||||
)
|
||||
archive_root.mkdir(parents=True)
|
||||
split_root.mkdir(parents=True)
|
||||
paths = {
|
||||
module.KITTI_VELODYNE_ARCHIVE: archive_root
|
||||
/ module.KITTI_VELODYNE_ARCHIVE,
|
||||
module.KITTI_LABEL_ARCHIVE: archive_root / module.KITTI_LABEL_ARCHIVE,
|
||||
module.KITTI_CALIB_ARCHIVE: archive_root / module.KITTI_CALIB_ARCHIVE,
|
||||
"train": split_root / "train.txt",
|
||||
"validation": split_root / "val.txt",
|
||||
}
|
||||
with zipfile.ZipFile(paths[module.KITTI_VELODYNE_ARCHIVE], "w") as archive:
|
||||
for frame_id in ("000000", "000001", "000002"):
|
||||
size = 15 if invalid_velodyne_frame and frame_id == "000001" else 16
|
||||
archive.writestr(f"training/velodyne/{frame_id}.bin", b"\x00" * size)
|
||||
for frame_id in ("000000", "000001"):
|
||||
archive.writestr(f"testing/velodyne/{frame_id}.bin", b"\x00" * 16)
|
||||
label_payloads = labels or {
|
||||
"000000": _label("Car"),
|
||||
"000001": _label("Pedestrian") + _label("Car"),
|
||||
"000002": _label("Cyclist"),
|
||||
}
|
||||
with zipfile.ZipFile(paths[module.KITTI_LABEL_ARCHIVE], "w") as archive:
|
||||
for frame_id, payload in label_payloads.items():
|
||||
archive.writestr(f"training/label_2/{frame_id}.txt", payload)
|
||||
with zipfile.ZipFile(paths[module.KITTI_CALIB_ARCHIVE], "w") as archive:
|
||||
for split, frames in {
|
||||
"training": ("000000", "000001", "000002"),
|
||||
"testing": ("000000", "000001"),
|
||||
}.items():
|
||||
for frame_id in frames:
|
||||
archive.writestr(
|
||||
f"{split}/calib/{frame_id}.txt",
|
||||
_calibration(complete=complete_calibration),
|
||||
)
|
||||
paths["train"].write_text("\n".join(train_rows) + "\n", encoding="ascii")
|
||||
paths["validation"].write_text(
|
||||
"\n".join(validation_rows) + "\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
monkeypatch.setattr(module, "KITTI_TRAINING_FRAMES", 3)
|
||||
monkeypatch.setattr(module, "KITTI_TEST_FRAMES", 2)
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"KITTI_ARCHIVE_BYTES",
|
||||
{
|
||||
name: paths[name].stat().st_size
|
||||
for name in (
|
||||
module.KITTI_VELODYNE_ARCHIVE,
|
||||
module.KITTI_LABEL_ARCHIVE,
|
||||
module.KITTI_CALIB_ARCHIVE,
|
||||
)
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"KITTI_SPLIT_COUNTS",
|
||||
{"train": len(train_rows), "validation": len(validation_rows)},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"KITTI_SPLIT_SHA256",
|
||||
{"train": _sha256(paths["train"]), "validation": _sha256(paths["validation"])},
|
||||
)
|
||||
monkeypatch.setattr(module, "_is_worker_dataset_root", lambda _root: True)
|
||||
return paths
|
||||
|
||||
|
||||
def test_admits_archive_only_box_truth_with_path_free_state(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
_release(tmp_path, monkeypatch)
|
||||
|
||||
manifest = module.admit_kitti_3d_object_release(tmp_path)
|
||||
|
||||
assert manifest["status"] == "archive-ready"
|
||||
assert manifest["storage"]["source_archives_extracted"] is False
|
||||
assert manifest["benchmark_contract"] == {
|
||||
"independent_ground_truth": True,
|
||||
"annotations": ["oriented-3d-boxes"],
|
||||
"point_fields": ["x", "y", "z", "intensity"],
|
||||
"eligible_split": "validation",
|
||||
"target_classes": ["Car", "Pedestrian", "Cyclist"],
|
||||
"official_test_submission_authorized": False,
|
||||
"retuning_on_validation_allowed": False,
|
||||
"k1_quality_claim_authorized": False,
|
||||
}
|
||||
assert manifest["alignment"]["validation_target_box_counts"] == {
|
||||
"Car": 1,
|
||||
"Cyclist": 1,
|
||||
"Pedestrian": 1,
|
||||
}
|
||||
serialized = json.dumps(manifest)
|
||||
assert str(tmp_path) not in serialized
|
||||
assert module.read_kitti_3d_admission(tmp_path) == manifest
|
||||
assert module.read_kitti_standard_splits(tmp_path) == {
|
||||
"train": ("000000",),
|
||||
"validation": ("000001", "000002"),
|
||||
}
|
||||
|
||||
|
||||
def test_rejects_tampered_standard_split(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
paths = _release(tmp_path, monkeypatch)
|
||||
paths["validation"].write_text("000002\n000001\n", encoding="ascii")
|
||||
|
||||
with pytest.raises(module.Kitti3DAdmissionError, match="pinned OpenPCDet"):
|
||||
module.admit_kitti_3d_object_release(tmp_path)
|
||||
|
||||
|
||||
def test_rejects_overlapping_split(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
_release(
|
||||
tmp_path,
|
||||
monkeypatch,
|
||||
train_rows=("000000", "000001"),
|
||||
validation_rows=("000001", "000002"),
|
||||
)
|
||||
|
||||
with pytest.raises(module.Kitti3DAdmissionError, match="overlapping or incomplete"):
|
||||
module.admit_kitti_3d_object_release(tmp_path)
|
||||
|
||||
|
||||
def test_rejects_non_xyzi_velodyne_frame(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
_release(tmp_path, monkeypatch, invalid_velodyne_frame=True)
|
||||
|
||||
with pytest.raises(module.Kitti3DAdmissionError, match="not packed XYZI"):
|
||||
module.admit_kitti_3d_object_release(tmp_path)
|
||||
|
||||
|
||||
def test_rejects_invalid_target_box(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
_release(
|
||||
tmp_path,
|
||||
monkeypatch,
|
||||
labels={
|
||||
"000000": _label("Car"),
|
||||
"000001": _label("Pedestrian", valid=False),
|
||||
"000002": _label("Cyclist"),
|
||||
},
|
||||
)
|
||||
|
||||
with pytest.raises(module.Kitti3DAdmissionError, match="invalid dimensions"):
|
||||
module.admit_kitti_3d_object_release(tmp_path)
|
||||
|
||||
|
||||
def test_rejects_missing_calibration_transform(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
_release(tmp_path, monkeypatch, complete_calibration=False)
|
||||
|
||||
with pytest.raises(module.Kitti3DAdmissionError, match="required transforms"):
|
||||
module.admit_kitti_3d_object_release(tmp_path)
|
||||
|
||||
|
||||
def test_read_rejects_tampered_content_identity(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
_release(tmp_path, monkeypatch)
|
||||
module.admit_kitti_3d_object_release(tmp_path)
|
||||
state = tmp_path / "state/kitti-3d-object-v2017.json"
|
||||
payload: dict[str, Any] = json.loads(state.read_text(encoding="utf-8"))
|
||||
payload["identity"]["license"]["spdx"] = "unknown"
|
||||
state.write_text(json.dumps(payload), encoding="utf-8")
|
||||
|
||||
with pytest.raises(module.Kitti3DAdmissionError, match="identity is invalid"):
|
||||
module.read_kitti_3d_admission(tmp_path)
|
||||
@@ -0,0 +1,258 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.compute.kitti_pointpillars_benchmark import (
|
||||
KittiLidarTruth,
|
||||
KittiPointPillarsBenchmarkError,
|
||||
PointPillarsFramePrediction,
|
||||
evaluate_pointpillars_predictions,
|
||||
read_kitti_validation_truth,
|
||||
)
|
||||
from k1link.compute.pointpillars_postprocess import PointPillarsBox
|
||||
|
||||
|
||||
def _calibration() -> str:
|
||||
return "\n".join(
|
||||
[
|
||||
"R0_rect: 1 0 0 0 1 0 0 0 1",
|
||||
"Tr_velo_to_cam: 1 0 0 0 0 1 0 0 0 0 1 0",
|
||||
"",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _label(class_name: str, x_m: float) -> str:
|
||||
return f"{class_name} 0 0 0 0 0 10 10 1.5 2 4 {x_m} 0 0 0\n"
|
||||
|
||||
|
||||
def _truth(
|
||||
frame_id: str,
|
||||
class_name: str,
|
||||
*,
|
||||
x_m: float = 10.0,
|
||||
) -> KittiLidarTruth:
|
||||
return KittiLidarTruth(
|
||||
frame_id=frame_id,
|
||||
benchmark_class=class_name,
|
||||
x_m=x_m,
|
||||
y_m=0.0,
|
||||
z_m=0.0,
|
||||
length_m=4.0,
|
||||
width_m=2.0,
|
||||
height_m=1.5,
|
||||
yaw_rad=0.0,
|
||||
)
|
||||
|
||||
|
||||
def _prediction_box(
|
||||
model_class: str,
|
||||
*,
|
||||
x_m: float = 10.0,
|
||||
score: float = 0.9,
|
||||
) -> PointPillarsBox:
|
||||
return PointPillarsBox(
|
||||
x_m=x_m,
|
||||
y_m=0.0,
|
||||
z_m=0.0,
|
||||
length_m=4.0,
|
||||
width_m=2.0,
|
||||
height_m=1.5,
|
||||
yaw_rad=0.0,
|
||||
class_id={"Vehicle": 0, "Pedestrian": 1, "Cyclist": 2}[model_class],
|
||||
model_class=model_class,
|
||||
score=score,
|
||||
)
|
||||
|
||||
|
||||
def _perfect_fixture() -> tuple[
|
||||
dict[str, tuple[KittiLidarTruth, ...]],
|
||||
tuple[PointPillarsFramePrediction, ...],
|
||||
]:
|
||||
classes = (
|
||||
("000000", "Car", "Vehicle"),
|
||||
("000001", "Pedestrian", "Pedestrian"),
|
||||
("000002", "Cyclist", "Cyclist"),
|
||||
)
|
||||
truth = {
|
||||
frame_id: (_truth(frame_id, benchmark_class),)
|
||||
for frame_id, benchmark_class, _ in classes
|
||||
}
|
||||
predictions = tuple(
|
||||
PointPillarsFramePrediction(
|
||||
frame_id=frame_id,
|
||||
boxes=(_prediction_box(model_class),),
|
||||
inference_ms=50.0 + index,
|
||||
)
|
||||
for index, (frame_id, _, model_class) in enumerate(classes)
|
||||
)
|
||||
return truth, predictions
|
||||
|
||||
|
||||
def test_reads_and_converts_kitti_camera_bottom_centers(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
labels = tmp_path / "labels.zip"
|
||||
calibrations = tmp_path / "calib.zip"
|
||||
with zipfile.ZipFile(labels, "w") as archive:
|
||||
archive.writestr(
|
||||
"training/label_2/000000.txt",
|
||||
_label("Car", 10.0),
|
||||
)
|
||||
archive.writestr(
|
||||
"training/label_2/000001.txt",
|
||||
_label("Pedestrian", 11.0),
|
||||
)
|
||||
archive.writestr(
|
||||
"training/label_2/000002.txt",
|
||||
_label("Cyclist", 12.0),
|
||||
)
|
||||
with zipfile.ZipFile(calibrations, "w") as archive:
|
||||
for frame_id in ("000000", "000001", "000002"):
|
||||
archive.writestr(
|
||||
f"training/calib/{frame_id}.txt",
|
||||
_calibration(),
|
||||
)
|
||||
|
||||
truth = read_kitti_validation_truth(
|
||||
labels_archive=labels,
|
||||
calibrations_archive=calibrations,
|
||||
validation_frame_ids=("000000", "000001", "000002"),
|
||||
)
|
||||
|
||||
car = truth["000000"][0]
|
||||
assert car.x_m == 10.0
|
||||
assert car.z_m == pytest.approx(0.75)
|
||||
assert car.yaw_rad == pytest.approx(-math.pi / 2.0)
|
||||
assert car.length_m == 4.0
|
||||
assert car.width_m == 2.0
|
||||
|
||||
|
||||
def test_perfect_predictions_produce_complete_metrics() -> None:
|
||||
truth, predictions = _perfect_fixture()
|
||||
|
||||
report = evaluate_pointpillars_predictions(
|
||||
truth_by_frame=truth,
|
||||
predictions=predictions,
|
||||
)
|
||||
|
||||
assert report["aggregates"]["bev_map40"] == pytest.approx(1.0)
|
||||
assert report["aggregates"]["3d_map40"] == pytest.approx(1.0)
|
||||
assert report["aggregates"]["false_occupied_rate"] == 0.0
|
||||
assert report["aggregates"]["center_error_m"]["mean"] == 0.0
|
||||
assert report["aggregates"]["range_error_m"]["mean"] == 0.0
|
||||
assert report["aggregates"]["yaw_error_rad"]["mean"] == 0.0
|
||||
assert report["aggregates"]["distance_bucket_recall"]["0-20m"]["recall"] == 1.0
|
||||
assert report["metric_contract"]["evaluation_kind"] == (
|
||||
"public-cross-domain-transfer-probe"
|
||||
)
|
||||
assert report["claim_boundary"]["native_model_accuracy_evaluated"] is False
|
||||
assert report["claim_boundary"]["k1_transfer_evaluated"] is False
|
||||
|
||||
|
||||
def test_false_prediction_reduces_precision_and_counts_false_occupied() -> None:
|
||||
truth, predictions = _perfect_fixture()
|
||||
first = predictions[0]
|
||||
predictions = (
|
||||
PointPillarsFramePrediction(
|
||||
frame_id=first.frame_id,
|
||||
boxes=(
|
||||
_prediction_box("Vehicle", x_m=40.0, score=0.95),
|
||||
*first.boxes,
|
||||
),
|
||||
inference_ms=first.inference_ms,
|
||||
),
|
||||
*predictions[1:],
|
||||
)
|
||||
|
||||
report = evaluate_pointpillars_predictions(
|
||||
truth_by_frame=truth,
|
||||
predictions=predictions,
|
||||
)
|
||||
|
||||
assert report["per_class"]["Car"]["true_positives"] == 1
|
||||
assert report["per_class"]["Car"]["false_positives"] == 1
|
||||
assert report["per_class"]["Car"]["precision"] == pytest.approx(0.5)
|
||||
assert report["aggregates"]["false_occupied_rate"] == pytest.approx(0.25)
|
||||
|
||||
|
||||
def test_predictions_outside_shared_cross_domain_range_are_not_false_positives() -> None:
|
||||
truth, predictions = _perfect_fixture()
|
||||
first = predictions[0]
|
||||
predictions = (
|
||||
PointPillarsFramePrediction(
|
||||
frame_id=first.frame_id,
|
||||
boxes=(
|
||||
_prediction_box("Vehicle", x_m=-10.0, score=0.95),
|
||||
*first.boxes,
|
||||
),
|
||||
inference_ms=first.inference_ms,
|
||||
),
|
||||
*predictions[1:],
|
||||
)
|
||||
|
||||
report = evaluate_pointpillars_predictions(
|
||||
truth_by_frame=truth,
|
||||
predictions=predictions,
|
||||
)
|
||||
|
||||
assert report["per_class"]["Car"]["false_positives"] == 0
|
||||
assert report["aggregates"]["prediction_volume"] == {
|
||||
"model_output_box_count": 4,
|
||||
"evaluated_box_count": 3,
|
||||
"outside_shared_range_count": 1,
|
||||
}
|
||||
|
||||
|
||||
def test_frame_set_must_equal_admitted_validation_split() -> None:
|
||||
truth, predictions = _perfect_fixture()
|
||||
|
||||
with pytest.raises(
|
||||
KittiPointPillarsBenchmarkError,
|
||||
match="do not equal",
|
||||
):
|
||||
evaluate_pointpillars_predictions(
|
||||
truth_by_frame=truth,
|
||||
predictions=predictions[:-1],
|
||||
)
|
||||
|
||||
|
||||
def test_unknown_model_class_is_rejected() -> None:
|
||||
truth, predictions = _perfect_fixture()
|
||||
first = predictions[0]
|
||||
unknown = PointPillarsBox(
|
||||
**{
|
||||
field: getattr(first.boxes[0], field)
|
||||
for field in (
|
||||
"x_m",
|
||||
"y_m",
|
||||
"z_m",
|
||||
"length_m",
|
||||
"width_m",
|
||||
"height_m",
|
||||
"yaw_rad",
|
||||
"class_id",
|
||||
"score",
|
||||
)
|
||||
},
|
||||
model_class="Unknown",
|
||||
)
|
||||
predictions = (
|
||||
PointPillarsFramePrediction(
|
||||
frame_id=first.frame_id,
|
||||
boxes=(unknown,),
|
||||
inference_ms=first.inference_ms,
|
||||
),
|
||||
*predictions[1:],
|
||||
)
|
||||
|
||||
with pytest.raises(KittiPointPillarsBenchmarkError, match="not admitted"):
|
||||
evaluate_pointpillars_predictions(
|
||||
truth_by_frame=truth,
|
||||
predictions=predictions,
|
||||
)
|
||||
@@ -0,0 +1,428 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.compute.l3_pointpillars_admission import (
|
||||
L3PointPillarsAdmissionError,
|
||||
build_l3_pointpillars_admission,
|
||||
read_l3_pointpillars_admission,
|
||||
)
|
||||
|
||||
SHA = "a" * 64
|
||||
TRITON_SHA = "58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794"
|
||||
|
||||
|
||||
def _profile() -> dict[str, Any]:
|
||||
return {
|
||||
"schema_version": "missioncore.l3-pointpillars-benchmark-profile/v1",
|
||||
"profile_id": "l3-pointpillars-public-transfer-probe-v1",
|
||||
"detector": {
|
||||
"family": "nvidia-tao-pointpillars",
|
||||
"upstream_model_id": "nvidia/tao/pointpillarnet",
|
||||
"candidate_frozen": False,
|
||||
"candidate_model_version": None,
|
||||
"candidate_source_sha256": None,
|
||||
"candidate_label_sha256": None,
|
||||
"triton_model_name": "pointpillars",
|
||||
"required_source_format": "onnx",
|
||||
"input_representation": "native-sensor-scan",
|
||||
"input_coordinate_frame": "sensor/lidar",
|
||||
"input_fields": ["x", "y", "z", "intensity"],
|
||||
"batch_size": 1,
|
||||
"maximum_points": 204_800,
|
||||
"point_cloud_range": [
|
||||
-51.20000076293945,
|
||||
-51.20000076293945,
|
||||
-1.399999976158142,
|
||||
51.20000076293945,
|
||||
51.20000076293945,
|
||||
4.400000095367432,
|
||||
],
|
||||
"training_domain": "proprietary-solid-state-lidar",
|
||||
"training_ground_truth_publicly_reproducible": False,
|
||||
"model_classes": ["Vehicle", "Pedestrian", "Cyclist"],
|
||||
"onnx_contract_sha256": (
|
||||
"2fd29cd054ab058c2cfec3dfba305c71e123ef3f04b457d0c64de0c8dac2e1be"
|
||||
),
|
||||
"postprocessing": {
|
||||
"reference_repository": (
|
||||
"https://github.com/NVIDIA-AI-IOT/tao_toolkit_recipes"
|
||||
),
|
||||
"reference_commit": "a540badc47812a17a94e924b537d49ad3969b5a8",
|
||||
"output_row_fields": [
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"length",
|
||||
"width",
|
||||
"height",
|
||||
"yaw",
|
||||
"class_id",
|
||||
"score",
|
||||
],
|
||||
"class_agnostic_nms": True,
|
||||
"nms_iou_threshold": 0.01,
|
||||
"pre_nms_top_n": 4096,
|
||||
"embedded_score_threshold": 0.1,
|
||||
"embedded_contract_source": "onnx-node-attributes",
|
||||
},
|
||||
},
|
||||
"runtime_policy": {
|
||||
"existing_triton_only": True,
|
||||
"second_serving_stack_allowed": False,
|
||||
"engine_built_on_target_required": True,
|
||||
"precision": "strongly-typed",
|
||||
"triton_image": "nvcr.io/nvidia/tritonserver:26.06-py3",
|
||||
"triton_image_digest": TRITON_SHA,
|
||||
},
|
||||
"public_cross_domain_probe": {
|
||||
"required_split": "validation",
|
||||
"required_ground_truth": "oriented-3d-boxes",
|
||||
"independent_ground_truth_required": True,
|
||||
"benchmark_classes": ["Car", "Pedestrian", "Cyclist"],
|
||||
"model_to_benchmark_class_mapping": {
|
||||
"Vehicle": "Car",
|
||||
"Pedestrian": "Pedestrian",
|
||||
"Cyclist": "Cyclist",
|
||||
},
|
||||
"metrics": [
|
||||
"bev-map",
|
||||
"3d-map",
|
||||
"center-error-m",
|
||||
"range-error-m",
|
||||
"yaw-error-rad",
|
||||
"distance-bucket-recall",
|
||||
"false-occupied-rate",
|
||||
"end-to-end-latency-ms",
|
||||
],
|
||||
"metric_contract": {
|
||||
"official_kitti_server_metric": False,
|
||||
"evaluation_kind": "public-cross-domain-transfer-probe",
|
||||
"ap_interpolation": "40-point",
|
||||
"difficulty_filtering": False,
|
||||
"predictions_outside_shared_range_ignored": True,
|
||||
"iou_thresholds": {
|
||||
"Car": 0.7,
|
||||
"Pedestrian": 0.5,
|
||||
"Cyclist": 0.5,
|
||||
},
|
||||
"distance_buckets_m": [[0, 20], [20, 40], [40, 70]],
|
||||
},
|
||||
"native_accuracy_claim_allowed": False,
|
||||
"retuning_allowed": False,
|
||||
},
|
||||
"k1_transfer_stability": {
|
||||
"requires_completed_public_cross_domain_probe": True,
|
||||
"metrics": [
|
||||
"input-admission-rate",
|
||||
"output-schema-valid-rate",
|
||||
"deterministic-replay-rate",
|
||||
"end-to-end-latency-ms",
|
||||
"queue-wait-ms",
|
||||
"drop-rate",
|
||||
],
|
||||
"accuracy_claim_allowed": False,
|
||||
"retuning_allowed": False,
|
||||
},
|
||||
"authority": {
|
||||
"shadow_only": True,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _semantic_dataset(dataset_id: str = "goose-3d/v2025-08-22") -> dict[str, Any]:
|
||||
return {
|
||||
"dataset_id": dataset_id,
|
||||
"installed": True,
|
||||
"release_identity_sha256": SHA,
|
||||
"license": "CC-BY-SA-4.0",
|
||||
"point_fields": ["x", "y", "z", "intensity"],
|
||||
"annotations": ["point-semantic-labels", "point-instance-labels"],
|
||||
"splits": ["validation"],
|
||||
"independent_ground_truth": True,
|
||||
}
|
||||
|
||||
|
||||
def _box_dataset(*, installed: bool = True) -> dict[str, Any]:
|
||||
return {
|
||||
"dataset_id": "kitti-3d-object-detection/v1",
|
||||
"installed": installed,
|
||||
"release_identity_sha256": SHA if installed else None,
|
||||
"license": "CC-BY-NC-SA-3.0",
|
||||
"point_fields": ["x", "y", "z", "intensity"],
|
||||
"annotations": ["oriented-3d-boxes"],
|
||||
"splits": ["validation"],
|
||||
"independent_ground_truth": True,
|
||||
}
|
||||
|
||||
|
||||
def _worker(*, pointpillars: bool = False) -> dict[str, Any]:
|
||||
models: list[dict[str, Any]] = [
|
||||
{
|
||||
"name": "yolox_s",
|
||||
"backend": "onnxruntime",
|
||||
"artifact_sha256": SHA,
|
||||
}
|
||||
]
|
||||
if pointpillars:
|
||||
models.append(
|
||||
{
|
||||
"name": "pointpillars",
|
||||
"upstream_version": "tao-6.26.03-test",
|
||||
"source_model_sha256": "c" * 64,
|
||||
"source_label_sha256": "e" * 64,
|
||||
"source_format": "onnx",
|
||||
"backend": "tensorrt",
|
||||
"precision": "strongly-typed",
|
||||
"artifact_sha256": "b" * 64,
|
||||
"engine_built_on_target": True,
|
||||
"provenance_verified": True,
|
||||
"input_fields": ["x", "y", "z", "intensity"],
|
||||
"maximum_points": 204_800,
|
||||
"point_cloud_range": [
|
||||
-51.20000076293945,
|
||||
-51.20000076293945,
|
||||
-1.399999976158142,
|
||||
51.20000076293945,
|
||||
51.20000076293945,
|
||||
4.400000095367432,
|
||||
],
|
||||
"model_classes": ["Vehicle", "Pedestrian", "Cyclist"],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "output_boxes",
|
||||
"dtype": "FP32",
|
||||
"shape": [1, 393_216, 9],
|
||||
},
|
||||
{"name": "num_boxes", "dtype": "INT32", "shape": [1]},
|
||||
],
|
||||
"representation_smoke": {
|
||||
"status": "engine-executed",
|
||||
"input_artifact_sha256": "d" * 64,
|
||||
"input_point_count": 169_883,
|
||||
"single_query_gpu_compute_ms": 55.0,
|
||||
"accuracy_evaluated": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
}
|
||||
)
|
||||
return {
|
||||
"schema_version": "missioncore.l3-worker-inventory/v1",
|
||||
"host_id": "worker-006",
|
||||
"observed_at_utc": "2026-07-30T20:57:38Z",
|
||||
"serving_stack_count": 1,
|
||||
"staged_models": [],
|
||||
"triton": {
|
||||
"container_name": "ndc-mission-core-triton",
|
||||
"image": "nvcr.io/nvidia/tritonserver:26.06-py3",
|
||||
"image_digest": TRITON_SHA,
|
||||
"healthy": True,
|
||||
"strict_readiness": True,
|
||||
"model_control_mode": "explicit",
|
||||
"model_repository_read_only": True,
|
||||
"models": models,
|
||||
},
|
||||
"gpu": {
|
||||
"name": "NVIDIA GeForce RTX 4090",
|
||||
"driver_version": "610.47",
|
||||
"memory_total_mib": 24_564,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _write(path: Path, value: object) -> None:
|
||||
path.write_text(json.dumps(value), encoding="utf-8")
|
||||
|
||||
|
||||
def _build(
|
||||
tmp_path: Path,
|
||||
*,
|
||||
profile: dict[str, Any] | None = None,
|
||||
datasets: list[dict[str, Any]] | None = None,
|
||||
worker: dict[str, Any] | None = None,
|
||||
):
|
||||
profile_path = tmp_path / "profile.json"
|
||||
datasets_path = tmp_path / "datasets.json"
|
||||
worker_path = tmp_path / "worker.json"
|
||||
_write(profile_path, profile or _profile())
|
||||
_write(
|
||||
datasets_path,
|
||||
{
|
||||
"schema_version": "missioncore.l3-lidar-dataset-inventory/v1",
|
||||
"observed_at_utc": "2026-07-30T20:57:38Z",
|
||||
"datasets": datasets or [_semantic_dataset()],
|
||||
},
|
||||
)
|
||||
_write(worker_path, worker or _worker())
|
||||
return build_l3_pointpillars_admission(
|
||||
profile_path=profile_path,
|
||||
dataset_inventory_path=datasets_path,
|
||||
worker_inventory_path=worker_path,
|
||||
output_root=tmp_path / "results",
|
||||
)
|
||||
|
||||
|
||||
def test_semantic_point_truth_and_missing_model_block_public_probe(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
result = _build(tmp_path)
|
||||
|
||||
assert result.report["status"] == "blocked-foundation-assets"
|
||||
assert result.report["blocker_codes"] == [
|
||||
"public-oriented-3d-box-truth-not-admitted",
|
||||
"pointpillars-compatible-candidate-not-frozen",
|
||||
]
|
||||
assert result.report["next_gate"] == (
|
||||
"admit-public-3d-box-split-and-freeze-compatible-pointpillars-candidate"
|
||||
)
|
||||
assert result.public_transfer_probe_authorized is False
|
||||
finding = result.report["dataset_findings"][0]
|
||||
assert finding["semantic_or_instance_labels_are_not_boxes"] is True
|
||||
assert finding["oriented_3d_box_accuracy_eligible"] is False
|
||||
assert (
|
||||
result.report["decision"]["semantic_point_labels_substitute_for_3d_boxes"]
|
||||
is False
|
||||
)
|
||||
assert result.report["decision"]["k1_transfer_stability_authorized"] is False
|
||||
|
||||
|
||||
def test_box_truth_and_target_built_model_authorize_public_probe_only(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
profile = _profile()
|
||||
profile["detector"]["candidate_frozen"] = True
|
||||
profile["detector"]["candidate_model_version"] = "tao-6.26.03-test"
|
||||
profile["detector"]["candidate_source_sha256"] = "c" * 64
|
||||
profile["detector"]["candidate_label_sha256"] = "e" * 64
|
||||
result = _build(
|
||||
tmp_path,
|
||||
profile=profile,
|
||||
datasets=[_semantic_dataset(), _box_dataset()],
|
||||
worker=_worker(pointpillars=True),
|
||||
)
|
||||
|
||||
assert result.report["status"] == "ready-for-public-cross-domain-probe"
|
||||
assert result.report["blocker_codes"] == []
|
||||
assert result.report["eligible_public_probe_dataset_ids"] == [
|
||||
"kitti-3d-object-detection/v1"
|
||||
]
|
||||
assert result.public_transfer_probe_authorized is True
|
||||
assert result.report["decision"] == {
|
||||
"public_cross_domain_probe_authorized": True,
|
||||
"native_model_accuracy_claim_authorized": False,
|
||||
"k1_transfer_stability_authorized": False,
|
||||
"k1_transfer_quality_claim_authorized": False,
|
||||
"semantic_point_labels_substitute_for_3d_boxes": False,
|
||||
"fine_tuning_allowed": False,
|
||||
"second_serving_stack_allowed": False,
|
||||
"lab_publication_allowed": False,
|
||||
"centerpoint_comparison_allowed": False,
|
||||
}
|
||||
assert result.report["next_gate"] == (
|
||||
"run-public-cross-domain-pointpillars-probe"
|
||||
)
|
||||
|
||||
|
||||
def test_pointpillars_without_target_engine_provenance_remains_blocked(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
profile = _profile()
|
||||
profile["detector"]["candidate_frozen"] = True
|
||||
profile["detector"]["candidate_model_version"] = "tao-6.26.03-test"
|
||||
profile["detector"]["candidate_source_sha256"] = "c" * 64
|
||||
profile["detector"]["candidate_label_sha256"] = "e" * 64
|
||||
worker = _worker(pointpillars=True)
|
||||
worker["triton"]["models"][1]["engine_built_on_target"] = False
|
||||
|
||||
result = _build(
|
||||
tmp_path,
|
||||
profile=profile,
|
||||
datasets=[_box_dataset()],
|
||||
worker=worker,
|
||||
)
|
||||
|
||||
assert result.report["blocker_codes"] == [
|
||||
"pointpillars-target-engine-or-provenance-not-verified"
|
||||
]
|
||||
assert result.report["next_gate"] == "verify-target-engine-and-model-provenance"
|
||||
|
||||
|
||||
def test_verified_staged_engine_is_distinct_from_live_install(tmp_path: Path) -> None:
|
||||
profile = _profile()
|
||||
profile["detector"]["candidate_frozen"] = True
|
||||
profile["detector"]["candidate_model_version"] = "tao-6.26.03-test"
|
||||
profile["detector"]["candidate_source_sha256"] = "c" * 64
|
||||
profile["detector"]["candidate_label_sha256"] = "e" * 64
|
||||
worker = _worker(pointpillars=True)
|
||||
staged = worker["triton"]["models"].pop()
|
||||
worker["staged_models"] = [staged]
|
||||
|
||||
result = _build(
|
||||
tmp_path,
|
||||
profile=profile,
|
||||
datasets=[_box_dataset(installed=False)],
|
||||
worker=worker,
|
||||
)
|
||||
|
||||
assert result.report["blocker_codes"] == [
|
||||
"public-oriented-3d-box-truth-not-admitted",
|
||||
"pointpillars-model-not-installed-live",
|
||||
]
|
||||
assert result.report["detector"]["staged_target_engine_ready"] is True
|
||||
assert result.report["detector"]["model_ready"] is False
|
||||
assert result.report["next_gate"] == (
|
||||
"admit-public-3d-box-split-then-install-staged-pointpillars-model"
|
||||
)
|
||||
|
||||
|
||||
def test_second_serving_stack_is_rejected(tmp_path: Path) -> None:
|
||||
profile = _profile()
|
||||
profile["detector"]["candidate_frozen"] = True
|
||||
profile["detector"]["candidate_model_version"] = "tao-6.26.03-test"
|
||||
profile["detector"]["candidate_source_sha256"] = "c" * 64
|
||||
profile["detector"]["candidate_label_sha256"] = "e" * 64
|
||||
worker = _worker(pointpillars=True)
|
||||
worker["serving_stack_count"] = 2
|
||||
|
||||
result = _build(
|
||||
tmp_path,
|
||||
profile=profile,
|
||||
datasets=[_box_dataset()],
|
||||
worker=worker,
|
||||
)
|
||||
|
||||
assert result.report["blocker_codes"] == [
|
||||
"canonical-triton-runtime-policy-not-satisfied"
|
||||
]
|
||||
assert result.report["runtime_checks"]["second_serving_stack_absent"] is False
|
||||
assert result.report["decision"]["second_serving_stack_allowed"] is False
|
||||
|
||||
|
||||
def test_profile_cannot_skip_the_public_transfer_probe(tmp_path: Path) -> None:
|
||||
profile = copy.deepcopy(_profile())
|
||||
profile["k1_transfer_stability"][
|
||||
"requires_completed_public_cross_domain_probe"
|
||||
] = False
|
||||
|
||||
with pytest.raises(L3PointPillarsAdmissionError):
|
||||
_build(tmp_path, profile=profile)
|
||||
|
||||
|
||||
def test_result_is_content_addressed_and_detects_tampering(tmp_path: Path) -> None:
|
||||
first = _build(tmp_path)
|
||||
second = _build(tmp_path)
|
||||
assert first.result_id == second.result_id
|
||||
|
||||
report_path = first.result_root / "admission-report.json"
|
||||
report = json.loads(report_path.read_text(encoding="utf-8"))
|
||||
report["status"] = "changed"
|
||||
_write(report_path, report)
|
||||
with pytest.raises(L3PointPillarsAdmissionError):
|
||||
read_l3_pointpillars_admission(first.result_root)
|
||||
@@ -0,0 +1,82 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _load_builder() -> ModuleType:
|
||||
path = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "experiments/perception/prepare_l3_pointpillars_worker_package.py"
|
||||
)
|
||||
specification = importlib.util.spec_from_file_location(
|
||||
"l3_pointpillars_worker_package_test",
|
||||
path,
|
||||
)
|
||||
assert specification is not None and specification.loader is not None
|
||||
module = importlib.util.module_from_spec(specification)
|
||||
specification.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_builds_and_reopens_minimal_content_addressed_package(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
builder = _load_builder()
|
||||
repository = Path(__file__).resolve().parents[1]
|
||||
admission = tmp_path / (
|
||||
"l3-pointpillars-admission-" + ("a" * 64)
|
||||
)
|
||||
admission.mkdir()
|
||||
(admission / "manifest.json").write_text("{}\n", encoding="utf-8")
|
||||
(admission / "admission-report.json").write_text("{}\n", encoding="utf-8")
|
||||
|
||||
package = builder.build_l3_worker_package(
|
||||
repository_root=repository,
|
||||
output_root=tmp_path / "packages",
|
||||
admission_result=admission,
|
||||
)
|
||||
reopened = builder.build_l3_worker_package(
|
||||
repository_root=repository,
|
||||
output_root=tmp_path / "packages",
|
||||
admission_result=admission,
|
||||
)
|
||||
manifest = builder.validate_l3_worker_package(package)
|
||||
|
||||
assert reopened == package
|
||||
assert manifest["package_id"] == package.name
|
||||
assert manifest["identity"]["execution_policy"] == {
|
||||
"sequential": True,
|
||||
"parallel_workers": 1,
|
||||
"existing_triton_only": True,
|
||||
"container_creation_allowed": False,
|
||||
"container_restart_allowed": False,
|
||||
"raw_tensor_export_allowed": False,
|
||||
}
|
||||
assert not (package / "runtime/k1link/compute/__pycache__").exists()
|
||||
assert (
|
||||
package / "runtime/k1link/datasets/kitti_3d_admission.py"
|
||||
).is_file()
|
||||
|
||||
|
||||
def test_validation_rejects_modified_member(tmp_path: Path) -> None:
|
||||
builder = _load_builder()
|
||||
repository = Path(__file__).resolve().parents[1]
|
||||
package = builder.build_l3_worker_package(
|
||||
repository_root=repository,
|
||||
output_root=tmp_path / "packages",
|
||||
)
|
||||
target = package / "input/profile.json"
|
||||
profile = json.loads(target.read_text(encoding="utf-8"))
|
||||
profile["profile_id"] = "changed"
|
||||
target.write_text(json.dumps(profile), encoding="utf-8")
|
||||
|
||||
with pytest.raises(
|
||||
builder.L3WorkerPackageError,
|
||||
match="artifact changed",
|
||||
):
|
||||
builder.validate_l3_worker_package(package)
|
||||
@@ -0,0 +1,90 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
|
||||
def _load_worker() -> ModuleType:
|
||||
path = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "experiments/perception/worker/run_l3_pointpillars_public_baseline.py"
|
||||
)
|
||||
specification = importlib.util.spec_from_file_location(
|
||||
"l3_pointpillars_worker_test",
|
||||
path,
|
||||
)
|
||||
assert specification is not None and specification.loader is not None
|
||||
module = importlib.util.module_from_spec(specification)
|
||||
specification.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class _Response:
|
||||
def __init__(self, payload: bytes, header_length: int) -> None:
|
||||
self._payload = payload
|
||||
self.headers = {"Inference-Header-Content-Length": str(header_length)}
|
||||
self.status = 200
|
||||
|
||||
def __enter__(self) -> _Response:
|
||||
return self
|
||||
|
||||
def __exit__(self, *_args: object) -> None:
|
||||
return None
|
||||
|
||||
def read(self) -> bytes:
|
||||
return self._payload
|
||||
|
||||
|
||||
def test_binary_triton_response_parses_both_outputs(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
worker = _load_worker()
|
||||
boxes = np.zeros((1, 393_216, 9), dtype=np.float32)
|
||||
boxes[0, 0] = np.asarray(
|
||||
[10.0, 0.0, 0.0, 4.0, 2.0, 1.5, 0.0, 0.0, 0.9],
|
||||
dtype=np.float32,
|
||||
)
|
||||
count = np.asarray([1], dtype=np.int32)
|
||||
count_bytes = count.tobytes()
|
||||
boxes_bytes = boxes.tobytes()
|
||||
header = json.dumps(
|
||||
{
|
||||
"outputs": [
|
||||
{
|
||||
"name": "num_boxes",
|
||||
"datatype": "INT32",
|
||||
"shape": [1],
|
||||
"parameters": {"binary_data_size": len(count_bytes)},
|
||||
},
|
||||
{
|
||||
"name": "output_boxes",
|
||||
"datatype": "FP32",
|
||||
"shape": [1, 393_216, 9],
|
||||
"parameters": {"binary_data_size": len(boxes_bytes)},
|
||||
},
|
||||
]
|
||||
},
|
||||
separators=(",", ":"),
|
||||
).encode()
|
||||
payload = header + count_bytes + boxes_bytes
|
||||
monkeypatch.setattr(
|
||||
worker.urllib.request,
|
||||
"urlopen",
|
||||
lambda *_args, **_kwargs: _Response(payload, len(header)),
|
||||
)
|
||||
|
||||
output_boxes, output_count, elapsed_ms = worker._infer(
|
||||
"http://127.0.0.1:8000",
|
||||
np.zeros((1, 204_800, 4), dtype=np.float32),
|
||||
np.asarray([1], dtype=np.int32),
|
||||
)
|
||||
|
||||
assert output_boxes.shape == (1, 393_216, 9)
|
||||
assert output_count.tolist() == [1]
|
||||
assert output_boxes[0, 0, 8] == pytest.approx(0.9)
|
||||
assert elapsed_ms > 0.0
|
||||
@@ -0,0 +1,136 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from k1link.compute.pointpillars_postprocess import (
|
||||
PointPillarsBox,
|
||||
PointPillarsPostprocessError,
|
||||
decode_pointpillars_output,
|
||||
oriented_3d_iou,
|
||||
oriented_bev_iou,
|
||||
)
|
||||
|
||||
|
||||
def _outputs(rows: list[list[float]]) -> tuple[np.ndarray, np.ndarray]:
|
||||
boxes = np.zeros((1, 393_216, 9), dtype=np.float32)
|
||||
boxes[0, : len(rows)] = np.asarray(rows, dtype=np.float32)
|
||||
return boxes, np.asarray([len(rows)], dtype=np.int32)
|
||||
|
||||
|
||||
def _row(
|
||||
*,
|
||||
x: float,
|
||||
y: float = 0.0,
|
||||
length: float = 4.0,
|
||||
width: float = 2.0,
|
||||
yaw: float = 0.0,
|
||||
class_id: int = 0,
|
||||
score: float = 0.9,
|
||||
) -> list[float]:
|
||||
return [x, y, 0.0, length, width, 1.5, yaw, float(class_id), score]
|
||||
|
||||
|
||||
def _box(*, yaw: float = 0.0, x: float = 0.0) -> PointPillarsBox:
|
||||
return PointPillarsBox(
|
||||
x_m=x,
|
||||
y_m=0.0,
|
||||
z_m=0.0,
|
||||
length_m=4.0,
|
||||
width_m=2.0,
|
||||
height_m=1.5,
|
||||
yaw_rad=yaw,
|
||||
class_id=0,
|
||||
model_class="Vehicle",
|
||||
score=0.9,
|
||||
)
|
||||
|
||||
|
||||
def test_decodes_native_label_order_and_sorts_by_score() -> None:
|
||||
output_boxes, num_boxes = _outputs(
|
||||
[
|
||||
_row(x=20.0, class_id=2, score=0.6),
|
||||
_row(x=0.0, class_id=0, score=0.9),
|
||||
_row(x=10.0, class_id=1, score=0.8),
|
||||
]
|
||||
)
|
||||
|
||||
decoded = decode_pointpillars_output(output_boxes, num_boxes)
|
||||
|
||||
assert [box.model_class for box in decoded] == [
|
||||
"Vehicle",
|
||||
"Pedestrian",
|
||||
"Cyclist",
|
||||
]
|
||||
assert [box.score for box in decoded] == pytest.approx([0.9, 0.8, 0.6])
|
||||
|
||||
|
||||
def test_nms_reproduces_nvidia_sample_class_agnostic_suppression() -> None:
|
||||
output_boxes, num_boxes = _outputs(
|
||||
[
|
||||
_row(x=0.0, class_id=0, score=0.9),
|
||||
_row(x=0.1, class_id=1, score=0.8),
|
||||
_row(x=20.0, class_id=1, score=0.7),
|
||||
]
|
||||
)
|
||||
|
||||
decoded = decode_pointpillars_output(output_boxes, num_boxes)
|
||||
|
||||
assert [(box.x_m, box.model_class) for box in decoded] == [
|
||||
(0.0, "Vehicle"),
|
||||
(20.0, "Pedestrian"),
|
||||
]
|
||||
|
||||
|
||||
def test_pre_nms_cap_is_applied_after_stable_score_ordering() -> None:
|
||||
output_boxes, num_boxes = _outputs(
|
||||
[
|
||||
_row(x=0.0, score=0.7),
|
||||
_row(x=10.0, score=0.9),
|
||||
_row(x=20.0, score=0.8),
|
||||
]
|
||||
)
|
||||
|
||||
decoded = decode_pointpillars_output(
|
||||
output_boxes,
|
||||
num_boxes,
|
||||
pre_nms_top_n=2,
|
||||
)
|
||||
|
||||
assert [box.x_m for box in decoded] == [10.0, 20.0]
|
||||
|
||||
|
||||
def test_oriented_bev_iou_handles_rotation_and_separation() -> None:
|
||||
assert oriented_bev_iou(_box(), _box()) == pytest.approx(1.0)
|
||||
assert oriented_bev_iou(_box(), _box(yaw=math.pi / 2.0)) == pytest.approx(
|
||||
1.0 / 3.0
|
||||
)
|
||||
assert oriented_bev_iou(_box(), _box(x=20.0)) == 0.0
|
||||
assert oriented_3d_iou(_box(), _box()) == pytest.approx(1.0)
|
||||
assert oriented_3d_iou(_box(), _box(x=20.0)) == 0.0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("row", "message"),
|
||||
[
|
||||
(_row(x=0.0, class_id=3), "class id"),
|
||||
(_row(x=0.0, length=0.0), "dimensions or score"),
|
||||
(_row(x=0.0, score=0.09), "dimensions or score"),
|
||||
(_row(x=0.0, score=1.1), "dimensions or score"),
|
||||
],
|
||||
)
|
||||
def test_invalid_candidate_fails_closed(row: list[float], message: str) -> None:
|
||||
output_boxes, num_boxes = _outputs([row])
|
||||
|
||||
with pytest.raises(PointPillarsPostprocessError, match=message):
|
||||
decode_pointpillars_output(output_boxes, num_boxes)
|
||||
|
||||
|
||||
def test_output_tensor_contract_is_exact() -> None:
|
||||
with pytest.raises(PointPillarsPostprocessError, match="output_boxes contract"):
|
||||
decode_pointpillars_output(
|
||||
np.zeros((1, 1, 9), dtype=np.float32),
|
||||
np.asarray([0], dtype=np.int32),
|
||||
)
|
||||
Reference in New Issue
Block a user