feat(perception): add PointPillars transfer gate
This commit is contained in:
@@ -0,0 +1,566 @@
|
||||
"""KITTI-to-LiDAR truth conversion and bounded PointPillars metrics."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import zipfile
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
import numpy as np
|
||||
|
||||
from k1link.compute.pointpillars_postprocess import (
|
||||
POINTPILLARS_MODEL_POINT_CLOUD_RANGE,
|
||||
PointPillarsBox,
|
||||
oriented_3d_iou,
|
||||
oriented_bev_iou,
|
||||
)
|
||||
|
||||
KITTI_BENCHMARK_CLASSES: Final = ("Car", "Pedestrian", "Cyclist")
|
||||
MODEL_TO_KITTI_CLASS: Final = {
|
||||
"Vehicle": "Car",
|
||||
"Pedestrian": "Pedestrian",
|
||||
"Cyclist": "Cyclist",
|
||||
}
|
||||
KITTI_IOU_THRESHOLDS: Final = {
|
||||
"Car": 0.7,
|
||||
"Pedestrian": 0.5,
|
||||
"Cyclist": 0.5,
|
||||
}
|
||||
KITTI_REFERENCE_POINT_CLOUD_RANGE: Final = (
|
||||
0.0,
|
||||
-39.68,
|
||||
-3.0,
|
||||
69.12,
|
||||
39.68,
|
||||
1.0,
|
||||
)
|
||||
CROSS_DOMAIN_EVALUATION_RANGE: Final = tuple(
|
||||
max(
|
||||
KITTI_REFERENCE_POINT_CLOUD_RANGE[index],
|
||||
POINTPILLARS_MODEL_POINT_CLOUD_RANGE[index],
|
||||
)
|
||||
if index < 3
|
||||
else min(
|
||||
KITTI_REFERENCE_POINT_CLOUD_RANGE[index],
|
||||
POINTPILLARS_MODEL_POINT_CLOUD_RANGE[index],
|
||||
)
|
||||
for index in range(6)
|
||||
)
|
||||
DISTANCE_BUCKETS_M: Final = ((0.0, 20.0), (20.0, 40.0), (40.0, 70.0))
|
||||
|
||||
|
||||
class KittiPointPillarsBenchmarkError(RuntimeError):
|
||||
"""KITTI truth or PointPillars predictions violate the frozen contract."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class KittiLidarTruth:
|
||||
frame_id: str
|
||||
benchmark_class: str
|
||||
x_m: float
|
||||
y_m: float
|
||||
z_m: float
|
||||
length_m: float
|
||||
width_m: float
|
||||
height_m: float
|
||||
yaw_rad: float
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PointPillarsFramePrediction:
|
||||
frame_id: str
|
||||
boxes: tuple[PointPillarsBox, ...]
|
||||
inference_ms: float
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class _ScoredPrediction:
|
||||
frame_id: str
|
||||
index: int
|
||||
benchmark_class: str
|
||||
box: PointPillarsBox
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class _MetricEvaluation:
|
||||
average_precision_40: float
|
||||
precision: float
|
||||
recall: float
|
||||
true_positives: int
|
||||
false_positives: int
|
||||
ground_truth_count: int
|
||||
matched_truth: frozenset[tuple[str, int]]
|
||||
matched_pairs: tuple[tuple[PointPillarsBox, KittiLidarTruth], ...]
|
||||
|
||||
|
||||
def read_kitti_validation_truth(
|
||||
*,
|
||||
labels_archive: Path,
|
||||
calibrations_archive: Path,
|
||||
validation_frame_ids: tuple[str, ...],
|
||||
) -> dict[str, tuple[KittiLidarTruth, ...]]:
|
||||
"""Read target cuboids and convert camera-bottom centers into LiDAR centers."""
|
||||
|
||||
if (
|
||||
not validation_frame_ids
|
||||
or len(set(validation_frame_ids)) != len(validation_frame_ids)
|
||||
or any(len(frame_id) != 6 or not frame_id.isdigit() for frame_id in validation_frame_ids)
|
||||
):
|
||||
raise KittiPointPillarsBenchmarkError("KITTI validation frame index is invalid")
|
||||
try:
|
||||
with (
|
||||
zipfile.ZipFile(labels_archive.resolve(strict=True)) as labels_zip,
|
||||
zipfile.ZipFile(calibrations_archive.resolve(strict=True)) as calib_zip,
|
||||
):
|
||||
truth = {
|
||||
frame_id: _read_frame_truth(labels_zip, calib_zip, frame_id)
|
||||
for frame_id in validation_frame_ids
|
||||
}
|
||||
except (OSError, KeyError, UnicodeDecodeError, zipfile.BadZipFile) as exc:
|
||||
raise KittiPointPillarsBenchmarkError(
|
||||
"KITTI validation truth could not be read"
|
||||
) from exc
|
||||
if any(
|
||||
not any(
|
||||
box.benchmark_class == class_name
|
||||
for boxes in truth.values()
|
||||
for box in boxes
|
||||
)
|
||||
for class_name in KITTI_BENCHMARK_CLASSES
|
||||
):
|
||||
raise KittiPointPillarsBenchmarkError("KITTI validation lacks a target class")
|
||||
return truth
|
||||
|
||||
|
||||
def evaluate_pointpillars_predictions(
|
||||
*,
|
||||
truth_by_frame: dict[str, tuple[KittiLidarTruth, ...]],
|
||||
predictions: tuple[PointPillarsFramePrediction, ...],
|
||||
) -> dict[str, Any]:
|
||||
"""Evaluate the frozen model-to-KITTI mapping without retuning."""
|
||||
|
||||
if not truth_by_frame or set(truth_by_frame) != {
|
||||
prediction.frame_id for prediction in predictions
|
||||
}:
|
||||
raise KittiPointPillarsBenchmarkError(
|
||||
"prediction frames do not equal the admitted validation split"
|
||||
)
|
||||
if len(predictions) != len(truth_by_frame):
|
||||
raise KittiPointPillarsBenchmarkError("prediction frames contain duplicates")
|
||||
if any(
|
||||
not math.isfinite(prediction.inference_ms) or prediction.inference_ms <= 0.0
|
||||
for prediction in predictions
|
||||
):
|
||||
raise KittiPointPillarsBenchmarkError("inference latency is invalid")
|
||||
|
||||
all_boxes = tuple(
|
||||
box
|
||||
for prediction in predictions
|
||||
for box in prediction.boxes
|
||||
)
|
||||
evaluated_boxes = tuple(
|
||||
box for box in all_boxes if _center_in_evaluation_range(box.x_m, box.y_m, box.z_m)
|
||||
)
|
||||
scored = tuple(
|
||||
_ScoredPrediction(
|
||||
frame_id=prediction.frame_id,
|
||||
index=index,
|
||||
benchmark_class=_benchmark_class(box),
|
||||
box=box,
|
||||
)
|
||||
for prediction in predictions
|
||||
for index, box in enumerate(prediction.boxes)
|
||||
if _center_in_evaluation_range(box.x_m, box.y_m, box.z_m)
|
||||
)
|
||||
per_class: dict[str, dict[str, Any]] = {}
|
||||
three_d_evaluations: dict[str, _MetricEvaluation] = {}
|
||||
for class_name in KITTI_BENCHMARK_CLASSES:
|
||||
bev = _evaluate_metric(
|
||||
class_name=class_name,
|
||||
truth_by_frame=truth_by_frame,
|
||||
predictions=scored,
|
||||
iou=oriented_bev_iou,
|
||||
)
|
||||
three_d = _evaluate_metric(
|
||||
class_name=class_name,
|
||||
truth_by_frame=truth_by_frame,
|
||||
predictions=scored,
|
||||
iou=oriented_3d_iou,
|
||||
)
|
||||
three_d_evaluations[class_name] = three_d
|
||||
per_class[class_name] = {
|
||||
"iou_threshold": KITTI_IOU_THRESHOLDS[class_name],
|
||||
"bev_ap40": bev.average_precision_40,
|
||||
"3d_ap40": three_d.average_precision_40,
|
||||
"precision": three_d.precision,
|
||||
"recall": three_d.recall,
|
||||
"true_positives": three_d.true_positives,
|
||||
"false_positives": three_d.false_positives,
|
||||
"ground_truth_count": three_d.ground_truth_count,
|
||||
}
|
||||
|
||||
all_three_d_pairs = tuple(
|
||||
pair
|
||||
for class_name in KITTI_BENCHMARK_CLASSES
|
||||
for pair in three_d_evaluations[class_name].matched_pairs
|
||||
)
|
||||
errors = _matched_errors(all_three_d_pairs)
|
||||
total_predictions = sum(
|
||||
evaluation.true_positives + evaluation.false_positives
|
||||
for evaluation in three_d_evaluations.values()
|
||||
)
|
||||
total_false_positives = sum(
|
||||
evaluation.false_positives for evaluation in three_d_evaluations.values()
|
||||
)
|
||||
latencies = np.asarray(
|
||||
[prediction.inference_ms for prediction in predictions],
|
||||
dtype=np.float64,
|
||||
)
|
||||
return {
|
||||
"metric_contract": {
|
||||
"official_kitti_server_metric": False,
|
||||
"evaluation_kind": "public-cross-domain-transfer-probe",
|
||||
"ap_interpolation": "40-point",
|
||||
"difficulty_filtering": False,
|
||||
"retuning_on_validation": False,
|
||||
"model_to_benchmark_class_mapping": MODEL_TO_KITTI_CLASS,
|
||||
"iou_thresholds": KITTI_IOU_THRESHOLDS,
|
||||
"model_training_domain": "proprietary-solid-state-lidar",
|
||||
"model_point_cloud_range": list(
|
||||
POINTPILLARS_MODEL_POINT_CLOUD_RANGE
|
||||
),
|
||||
"dataset_reference_point_cloud_range": list(
|
||||
KITTI_REFERENCE_POINT_CLOUD_RANGE
|
||||
),
|
||||
"shared_evaluation_range": list(CROSS_DOMAIN_EVALUATION_RANGE),
|
||||
"predictions_outside_shared_range_ignored": True,
|
||||
},
|
||||
"frame_count": len(predictions),
|
||||
"per_class": per_class,
|
||||
"aggregates": {
|
||||
"bev_map40": _mean(
|
||||
[per_class[class_name]["bev_ap40"] for class_name in KITTI_BENCHMARK_CLASSES]
|
||||
),
|
||||
"3d_map40": _mean(
|
||||
[per_class[class_name]["3d_ap40"] for class_name in KITTI_BENCHMARK_CLASSES]
|
||||
),
|
||||
"false_occupied_rate": (
|
||||
total_false_positives / total_predictions
|
||||
if total_predictions
|
||||
else 0.0
|
||||
),
|
||||
"prediction_volume": {
|
||||
"model_output_box_count": len(all_boxes),
|
||||
"evaluated_box_count": len(evaluated_boxes),
|
||||
"outside_shared_range_count": len(all_boxes)
|
||||
- len(evaluated_boxes),
|
||||
},
|
||||
**errors,
|
||||
"inference_latency_ms": {
|
||||
"mean": float(np.mean(latencies)),
|
||||
"p50": float(np.percentile(latencies, 50)),
|
||||
"p95": float(np.percentile(latencies, 95)),
|
||||
"maximum": float(np.max(latencies)),
|
||||
},
|
||||
"distance_bucket_recall": _distance_bucket_recall(
|
||||
truth_by_frame,
|
||||
three_d_evaluations,
|
||||
),
|
||||
},
|
||||
"claim_boundary": {
|
||||
"public_cross_domain_transfer_probe": True,
|
||||
"native_model_accuracy_evaluated": False,
|
||||
"k1_transfer_evaluated": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
"camera_first_candidate_replaced": False,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _read_frame_truth(
|
||||
labels_zip: zipfile.ZipFile,
|
||||
calib_zip: zipfile.ZipFile,
|
||||
frame_id: str,
|
||||
) -> tuple[KittiLidarTruth, ...]:
|
||||
calibration = _calibration_transform(
|
||||
calib_zip.read(f"training/calib/{frame_id}.txt").decode("ascii")
|
||||
)
|
||||
labels = labels_zip.read(f"training/label_2/{frame_id}.txt").decode("ascii")
|
||||
result: list[KittiLidarTruth] = []
|
||||
for line in labels.splitlines():
|
||||
fields = line.split()
|
||||
if not fields or fields[0] not in KITTI_BENCHMARK_CLASSES:
|
||||
continue
|
||||
if len(fields) != 15:
|
||||
raise KittiPointPillarsBenchmarkError("KITTI label row is invalid")
|
||||
try:
|
||||
values = np.asarray([float(value) for value in fields[1:]], dtype=np.float64)
|
||||
except ValueError as exc:
|
||||
raise KittiPointPillarsBenchmarkError(
|
||||
"KITTI label row contains invalid numbers"
|
||||
) from exc
|
||||
if not np.isfinite(values).all():
|
||||
raise KittiPointPillarsBenchmarkError(
|
||||
"KITTI label row contains non-finite numbers"
|
||||
)
|
||||
height_m, width_m, length_m = (float(value) for value in values[7:10])
|
||||
if height_m <= 0.0 or width_m <= 0.0 or length_m <= 0.0:
|
||||
raise KittiPointPillarsBenchmarkError("KITTI cuboid dimensions are invalid")
|
||||
camera_bottom_center = np.asarray(
|
||||
[values[10], values[11], values[12], 1.0],
|
||||
dtype=np.float64,
|
||||
)
|
||||
lidar_bottom_center = calibration @ camera_bottom_center
|
||||
x_m, y_m, z_m = (float(value) for value in lidar_bottom_center[:3])
|
||||
z_m += height_m / 2.0
|
||||
if not _center_in_evaluation_range(x_m, y_m, z_m):
|
||||
continue
|
||||
result.append(
|
||||
KittiLidarTruth(
|
||||
frame_id=frame_id,
|
||||
benchmark_class=fields[0],
|
||||
x_m=x_m,
|
||||
y_m=y_m,
|
||||
z_m=z_m,
|
||||
length_m=length_m,
|
||||
width_m=width_m,
|
||||
height_m=height_m,
|
||||
yaw_rad=_wrap_angle(-(float(values[13]) + math.pi / 2.0)),
|
||||
)
|
||||
)
|
||||
return tuple(result)
|
||||
|
||||
|
||||
def _calibration_transform(payload: str) -> np.ndarray:
|
||||
values: dict[str, np.ndarray] = {}
|
||||
for line in payload.splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
key, separator, raw = line.partition(":")
|
||||
if not separator:
|
||||
raise KittiPointPillarsBenchmarkError("KITTI calibration row is invalid")
|
||||
try:
|
||||
row = np.asarray([float(value) for value in raw.split()], dtype=np.float64)
|
||||
except ValueError as exc:
|
||||
raise KittiPointPillarsBenchmarkError(
|
||||
"KITTI calibration contains invalid numbers"
|
||||
) from exc
|
||||
if not np.isfinite(row).all():
|
||||
raise KittiPointPillarsBenchmarkError(
|
||||
"KITTI calibration contains non-finite numbers"
|
||||
)
|
||||
values[key] = row
|
||||
if "R0_rect" not in values or values["R0_rect"].size != 9:
|
||||
raise KittiPointPillarsBenchmarkError("KITTI R0_rect is invalid")
|
||||
if "Tr_velo_to_cam" not in values or values["Tr_velo_to_cam"].size != 12:
|
||||
raise KittiPointPillarsBenchmarkError("KITTI Tr_velo_to_cam is invalid")
|
||||
rectification = np.eye(4, dtype=np.float64)
|
||||
rectification[:3, :3] = values["R0_rect"].reshape(3, 3)
|
||||
lidar_to_camera = np.eye(4, dtype=np.float64)
|
||||
lidar_to_camera[:3, :4] = values["Tr_velo_to_cam"].reshape(3, 4)
|
||||
try:
|
||||
return np.linalg.inv(rectification @ lidar_to_camera)
|
||||
except np.linalg.LinAlgError as exc:
|
||||
raise KittiPointPillarsBenchmarkError(
|
||||
"KITTI calibration transform is singular"
|
||||
) from exc
|
||||
|
||||
|
||||
def _center_in_evaluation_range(x_m: float, y_m: float, z_m: float) -> bool:
|
||||
return (
|
||||
CROSS_DOMAIN_EVALUATION_RANGE[0]
|
||||
<= x_m
|
||||
<= CROSS_DOMAIN_EVALUATION_RANGE[3]
|
||||
and CROSS_DOMAIN_EVALUATION_RANGE[1]
|
||||
<= y_m
|
||||
<= CROSS_DOMAIN_EVALUATION_RANGE[4]
|
||||
and CROSS_DOMAIN_EVALUATION_RANGE[2]
|
||||
<= z_m
|
||||
<= CROSS_DOMAIN_EVALUATION_RANGE[5]
|
||||
)
|
||||
|
||||
|
||||
def _benchmark_class(box: PointPillarsBox) -> str:
|
||||
try:
|
||||
return MODEL_TO_KITTI_CLASS[box.model_class]
|
||||
except KeyError as exc:
|
||||
raise KittiPointPillarsBenchmarkError(
|
||||
"PointPillars model class is not admitted"
|
||||
) from exc
|
||||
|
||||
|
||||
def _truth_as_box(truth: KittiLidarTruth) -> PointPillarsBox:
|
||||
return PointPillarsBox(
|
||||
x_m=truth.x_m,
|
||||
y_m=truth.y_m,
|
||||
z_m=truth.z_m,
|
||||
length_m=truth.length_m,
|
||||
width_m=truth.width_m,
|
||||
height_m=truth.height_m,
|
||||
yaw_rad=truth.yaw_rad,
|
||||
class_id=-1,
|
||||
model_class=truth.benchmark_class,
|
||||
score=1.0,
|
||||
)
|
||||
|
||||
|
||||
def _evaluate_metric(
|
||||
*,
|
||||
class_name: str,
|
||||
truth_by_frame: dict[str, tuple[KittiLidarTruth, ...]],
|
||||
predictions: tuple[_ScoredPrediction, ...],
|
||||
iou: Callable[[PointPillarsBox, PointPillarsBox], float],
|
||||
) -> _MetricEvaluation:
|
||||
truths = {
|
||||
frame_id: tuple(
|
||||
box for box in boxes if box.benchmark_class == class_name
|
||||
)
|
||||
for frame_id, boxes in truth_by_frame.items()
|
||||
}
|
||||
ground_truth_count = sum(len(boxes) for boxes in truths.values())
|
||||
ordered = sorted(
|
||||
(
|
||||
prediction
|
||||
for prediction in predictions
|
||||
if prediction.benchmark_class == class_name
|
||||
),
|
||||
key=lambda prediction: (
|
||||
-prediction.box.score,
|
||||
prediction.frame_id,
|
||||
prediction.index,
|
||||
),
|
||||
)
|
||||
matched: set[tuple[str, int]] = set()
|
||||
matched_pairs: list[tuple[PointPillarsBox, KittiLidarTruth]] = []
|
||||
true_positive_flags: list[int] = []
|
||||
false_positive_flags: list[int] = []
|
||||
threshold = KITTI_IOU_THRESHOLDS[class_name]
|
||||
for prediction in ordered:
|
||||
candidates = truths[prediction.frame_id]
|
||||
best_index = -1
|
||||
best_iou = -1.0
|
||||
for truth_index, truth in enumerate(candidates):
|
||||
if (prediction.frame_id, truth_index) in matched:
|
||||
continue
|
||||
overlap = iou(prediction.box, _truth_as_box(truth))
|
||||
if overlap > best_iou:
|
||||
best_iou = overlap
|
||||
best_index = truth_index
|
||||
if best_index >= 0 and best_iou >= threshold:
|
||||
matched.add((prediction.frame_id, best_index))
|
||||
matched_pairs.append((prediction.box, candidates[best_index]))
|
||||
true_positive_flags.append(1)
|
||||
false_positive_flags.append(0)
|
||||
else:
|
||||
true_positive_flags.append(0)
|
||||
false_positive_flags.append(1)
|
||||
cumulative_true = np.cumsum(true_positive_flags, dtype=np.float64)
|
||||
cumulative_false = np.cumsum(false_positive_flags, dtype=np.float64)
|
||||
precision = np.divide(
|
||||
cumulative_true,
|
||||
np.maximum(cumulative_true + cumulative_false, 1.0),
|
||||
)
|
||||
recall = cumulative_true / max(float(ground_truth_count), 1.0)
|
||||
average_precision = _ap40(precision, recall)
|
||||
return _MetricEvaluation(
|
||||
average_precision_40=average_precision,
|
||||
precision=float(precision[-1]) if precision.size else 0.0,
|
||||
recall=float(recall[-1]) if recall.size else 0.0,
|
||||
true_positives=int(cumulative_true[-1]) if cumulative_true.size else 0,
|
||||
false_positives=int(cumulative_false[-1]) if cumulative_false.size else 0,
|
||||
ground_truth_count=ground_truth_count,
|
||||
matched_truth=frozenset(matched),
|
||||
matched_pairs=tuple(matched_pairs),
|
||||
)
|
||||
|
||||
|
||||
def _ap40(precision: np.ndarray, recall: np.ndarray) -> float:
|
||||
if precision.size == 0:
|
||||
return 0.0
|
||||
interpolated = [
|
||||
float(np.max(precision[recall >= threshold]))
|
||||
if np.any(recall >= threshold)
|
||||
else 0.0
|
||||
for threshold in np.arange(40, dtype=np.float64) / 40.0
|
||||
]
|
||||
return _mean(interpolated)
|
||||
|
||||
|
||||
def _matched_errors(
|
||||
pairs: tuple[tuple[PointPillarsBox, KittiLidarTruth], ...],
|
||||
) -> dict[str, Any]:
|
||||
center_errors = [
|
||||
math.dist(
|
||||
(prediction.x_m, prediction.y_m, prediction.z_m),
|
||||
(truth.x_m, truth.y_m, truth.z_m),
|
||||
)
|
||||
for prediction, truth in pairs
|
||||
]
|
||||
range_errors = [
|
||||
abs(
|
||||
math.hypot(prediction.x_m, prediction.y_m)
|
||||
- math.hypot(truth.x_m, truth.y_m)
|
||||
)
|
||||
for prediction, truth in pairs
|
||||
]
|
||||
yaw_errors = [
|
||||
abs(_wrap_angle(prediction.yaw_rad - truth.yaw_rad))
|
||||
for prediction, truth in pairs
|
||||
]
|
||||
return {
|
||||
"center_error_m": _error_summary(center_errors),
|
||||
"range_error_m": _error_summary(range_errors),
|
||||
"yaw_error_rad": _error_summary(yaw_errors),
|
||||
}
|
||||
|
||||
|
||||
def _error_summary(values: list[float]) -> dict[str, float | int | None]:
|
||||
if not values:
|
||||
return {"count": 0, "mean": None, "p95": None, "maximum": None}
|
||||
array = np.asarray(values, dtype=np.float64)
|
||||
return {
|
||||
"count": len(values),
|
||||
"mean": float(np.mean(array)),
|
||||
"p95": float(np.percentile(array, 95)),
|
||||
"maximum": float(np.max(array)),
|
||||
}
|
||||
|
||||
|
||||
def _distance_bucket_recall(
|
||||
truth_by_frame: dict[str, tuple[KittiLidarTruth, ...]],
|
||||
evaluations: dict[str, _MetricEvaluation],
|
||||
) -> dict[str, dict[str, float | int]]:
|
||||
result: dict[str, dict[str, float | int]] = {}
|
||||
for minimum, maximum in DISTANCE_BUCKETS_M:
|
||||
total = 0
|
||||
matched = 0
|
||||
for frame_id, truths in truth_by_frame.items():
|
||||
by_class_index = {class_name: 0 for class_name in KITTI_BENCHMARK_CLASSES}
|
||||
for truth in truths:
|
||||
index = by_class_index[truth.benchmark_class]
|
||||
by_class_index[truth.benchmark_class] += 1
|
||||
distance = math.hypot(truth.x_m, truth.y_m)
|
||||
if not minimum <= distance < maximum:
|
||||
continue
|
||||
total += 1
|
||||
if (frame_id, index) in evaluations[
|
||||
truth.benchmark_class
|
||||
].matched_truth:
|
||||
matched += 1
|
||||
result[f"{int(minimum)}-{int(maximum)}m"] = {
|
||||
"ground_truth_count": total,
|
||||
"matched_count": matched,
|
||||
"recall": matched / total if total else 0.0,
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def _wrap_angle(value: float) -> float:
|
||||
return (value + math.pi) % (2.0 * math.pi) - math.pi
|
||||
|
||||
|
||||
def _mean(values: list[float]) -> float:
|
||||
return sum(values) / len(values) if values else 0.0
|
||||
@@ -0,0 +1,828 @@
|
||||
"""Fail-closed admission for the L3 NVIDIA PointPillars benchmark.
|
||||
|
||||
The admission deliberately separates three different claims:
|
||||
|
||||
* a detector-quality baseline needs independent oriented 3D box truth;
|
||||
* K1 transfer can measure runtime and representation stability only after that
|
||||
baseline has been accepted;
|
||||
* point-wise semantic or instance labels are valuable evidence, but are not a
|
||||
substitute for 3D cuboid truth.
|
||||
|
||||
The result is immutable and content addressed. It grants no command,
|
||||
navigation, or safety authority and cannot start a worker or install a model.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
from k1link.artifacts import utc_now_iso
|
||||
from k1link.compute.pointpillars_postprocess import (
|
||||
POINTPILLARS_EMBEDDED_SCORE_THRESHOLD,
|
||||
POINTPILLARS_MODEL_POINT_CLOUD_RANGE,
|
||||
)
|
||||
|
||||
L3_PROFILE_SCHEMA: Final = "missioncore.l3-pointpillars-benchmark-profile/v1"
|
||||
L3_DATASET_INVENTORY_SCHEMA: Final = "missioncore.l3-lidar-dataset-inventory/v1"
|
||||
L3_WORKER_INVENTORY_SCHEMA: Final = "missioncore.l3-worker-inventory/v1"
|
||||
L3_ADMISSION_SCHEMA: Final = "missioncore.l3-pointpillars-admission/v1"
|
||||
L3_REPORT_SCHEMA: Final = "missioncore.l3-pointpillars-admission-report/v1"
|
||||
|
||||
L3_REPORT_NAME: Final = "admission-report.json"
|
||||
L3_MANIFEST_NAME: Final = "manifest.json"
|
||||
|
||||
_RESULT_ID = re.compile(r"^l3-pointpillars-admission-[a-f0-9]{64}$")
|
||||
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
|
||||
_IDENTIFIER = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,191}$")
|
||||
|
||||
_BOX_ANNOTATION = "oriented-3d-boxes"
|
||||
_ONNX_CONTRACT_SHA256 = (
|
||||
"2fd29cd054ab058c2cfec3dfba305c71e123ef3f04b457d0c64de0c8dac2e1be"
|
||||
)
|
||||
_POINT_FIELDS = ("x", "y", "z", "intensity")
|
||||
_MODEL_CLASSES = ("Vehicle", "Pedestrian", "Cyclist")
|
||||
_BENCHMARK_CLASSES = ("Car", "Pedestrian", "Cyclist")
|
||||
_BENCHMARK_CLASS_MAPPING = {
|
||||
"Vehicle": "Car",
|
||||
"Pedestrian": "Pedestrian",
|
||||
"Cyclist": "Cyclist",
|
||||
}
|
||||
_POSTPROCESSING_CONTRACT = {
|
||||
"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": POINTPILLARS_EMBEDDED_SCORE_THRESHOLD,
|
||||
"embedded_contract_source": "onnx-node-attributes",
|
||||
}
|
||||
_BENCHMARK_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]],
|
||||
}
|
||||
_POINT_CLOUD_RANGE = POINTPILLARS_MODEL_POINT_CLOUD_RANGE
|
||||
_PUBLIC_TRANSFER_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",
|
||||
)
|
||||
_TRANSFER_METRICS = (
|
||||
"input-admission-rate",
|
||||
"output-schema-valid-rate",
|
||||
"deterministic-replay-rate",
|
||||
"end-to-end-latency-ms",
|
||||
"queue-wait-ms",
|
||||
"drop-rate",
|
||||
)
|
||||
|
||||
|
||||
class L3PointPillarsAdmissionError(RuntimeError):
|
||||
"""An L3 profile, inventory, or immutable result is invalid."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class L3PointPillarsAdmission:
|
||||
result_root: Path
|
||||
result_id: str
|
||||
manifest: dict[str, Any]
|
||||
report: dict[str, Any]
|
||||
|
||||
@property
|
||||
def public_transfer_probe_authorized(self) -> bool:
|
||||
decision = _object(self.report.get("decision"), "L3 decision")
|
||||
return decision.get("public_cross_domain_probe_authorized") is True
|
||||
|
||||
|
||||
def build_l3_pointpillars_admission(
|
||||
*,
|
||||
profile_path: Path,
|
||||
dataset_inventory_path: Path,
|
||||
worker_inventory_path: Path,
|
||||
output_root: Path,
|
||||
) -> L3PointPillarsAdmission:
|
||||
"""Build or reopen one immutable L3 benchmark admission."""
|
||||
|
||||
profile_path = profile_path.resolve(strict=True)
|
||||
dataset_inventory_path = dataset_inventory_path.resolve(strict=True)
|
||||
worker_inventory_path = worker_inventory_path.resolve(strict=True)
|
||||
profile = _read_json(profile_path)
|
||||
dataset_inventory = _read_json(dataset_inventory_path)
|
||||
worker_inventory = _read_json(worker_inventory_path)
|
||||
_validate_profile(profile)
|
||||
_validate_dataset_inventory(dataset_inventory)
|
||||
_validate_worker_inventory(worker_inventory)
|
||||
|
||||
identity = {
|
||||
"schema_version": L3_ADMISSION_SCHEMA,
|
||||
"profile": profile,
|
||||
"profile_sha256": _sha256(profile_path),
|
||||
"dataset_inventory_sha256": _sha256(dataset_inventory_path),
|
||||
"worker_inventory_sha256": _sha256(worker_inventory_path),
|
||||
"producer_sha256": _sha256(Path(__file__)),
|
||||
"authority": _authority(),
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
result_id = f"l3-pointpillars-admission-{identity_sha256}"
|
||||
destination = output_root.expanduser().absolute()
|
||||
destination.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
result_root = destination / result_id
|
||||
if result_root.exists():
|
||||
return read_l3_pointpillars_admission(result_root)
|
||||
|
||||
report = _build_report(
|
||||
result_id=result_id,
|
||||
profile=profile,
|
||||
dataset_inventory=dataset_inventory,
|
||||
worker_inventory=worker_inventory,
|
||||
)
|
||||
staging = destination / f".{result_id}.{os.getpid()}.incomplete"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
_write_json(staging / L3_REPORT_NAME, report)
|
||||
report_artifact = _artifact(
|
||||
staging / L3_REPORT_NAME,
|
||||
"l3-pointpillars-admission-report",
|
||||
)
|
||||
manifest = {
|
||||
"schema_version": L3_ADMISSION_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"status": report["status"],
|
||||
"artifacts": [report_artifact],
|
||||
"created_at_utc": utc_now_iso(),
|
||||
"authority": _authority(),
|
||||
}
|
||||
_write_json(staging / L3_MANIFEST_NAME, manifest)
|
||||
os.replace(staging, result_root)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
return read_l3_pointpillars_admission(result_root)
|
||||
|
||||
|
||||
def read_l3_pointpillars_admission(root: Path) -> L3PointPillarsAdmission:
|
||||
"""Read and fully validate an immutable L3 admission result."""
|
||||
|
||||
resolved = root.expanduser().resolve(strict=True)
|
||||
if not resolved.is_dir() or _RESULT_ID.fullmatch(resolved.name) is None:
|
||||
raise L3PointPillarsAdmissionError("L3 result root is invalid")
|
||||
manifest = _read_json(resolved / L3_MANIFEST_NAME)
|
||||
report = _read_json(resolved / L3_REPORT_NAME)
|
||||
if (
|
||||
manifest.get("schema_version") != L3_ADMISSION_SCHEMA
|
||||
or manifest.get("result_id") != resolved.name
|
||||
or report.get("schema_version") != L3_REPORT_SCHEMA
|
||||
or report.get("result_id") != resolved.name
|
||||
or manifest.get("status") != report.get("status")
|
||||
or manifest.get("authority") != _authority()
|
||||
or report.get("authority") != _authority()
|
||||
):
|
||||
raise L3PointPillarsAdmissionError("L3 manifest and report are inconsistent")
|
||||
identity = _object(manifest.get("identity"), "L3 identity")
|
||||
expected_identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
if (
|
||||
manifest.get("identity_sha256") != expected_identity_sha256
|
||||
or resolved.name != f"l3-pointpillars-admission-{expected_identity_sha256}"
|
||||
):
|
||||
raise L3PointPillarsAdmissionError("L3 result identity is invalid")
|
||||
artifacts = _array(manifest.get("artifacts"), "L3 artifacts")
|
||||
if len(artifacts) != 1:
|
||||
raise L3PointPillarsAdmissionError("L3 result has an invalid artifact set")
|
||||
artifact = _object(artifacts[0], "L3 report artifact")
|
||||
report_path = resolved / L3_REPORT_NAME
|
||||
if (
|
||||
artifact.get("role") != "l3-pointpillars-admission-report"
|
||||
or artifact.get("path") != L3_REPORT_NAME
|
||||
or artifact.get("media_type") != "application/json"
|
||||
or artifact.get("sha256") != _sha256(report_path)
|
||||
or artifact.get("byte_length") != report_path.stat().st_size
|
||||
):
|
||||
raise L3PointPillarsAdmissionError("L3 report artifact is invalid")
|
||||
_validate_report(report)
|
||||
return L3PointPillarsAdmission(
|
||||
result_root=resolved,
|
||||
result_id=resolved.name,
|
||||
manifest=manifest,
|
||||
report=report,
|
||||
)
|
||||
|
||||
|
||||
def _build_report(
|
||||
*,
|
||||
result_id: str,
|
||||
profile: dict[str, Any],
|
||||
dataset_inventory: dict[str, Any],
|
||||
worker_inventory: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
detector = _object(profile.get("detector"), "L3 detector")
|
||||
runtime_policy = _object(profile.get("runtime_policy"), "L3 runtime policy")
|
||||
public_probe = _object(
|
||||
profile.get("public_cross_domain_probe"),
|
||||
"L3 public cross-domain probe",
|
||||
)
|
||||
required_model = _required_string(detector, "triton_model_name")
|
||||
candidate_frozen = detector.get("candidate_frozen") is True
|
||||
required_version = detector.get("candidate_model_version")
|
||||
required_source_sha256 = detector.get("candidate_source_sha256")
|
||||
required_label_sha256 = detector.get("candidate_label_sha256")
|
||||
|
||||
triton = _object(worker_inventory.get("triton"), "L3 Triton inventory")
|
||||
installed_models = [
|
||||
_object(item, "L3 worker model")
|
||||
for item in _array(triton.get("models"), "L3 worker models")
|
||||
]
|
||||
matching_models = [
|
||||
model
|
||||
for model in installed_models
|
||||
if candidate_frozen
|
||||
and model.get("name") == required_model
|
||||
and model.get("upstream_version") == required_version
|
||||
and model.get("source_model_sha256") == required_source_sha256
|
||||
and model.get("source_label_sha256") == required_label_sha256
|
||||
]
|
||||
model_ready = any(_model_ready(model) for model in matching_models)
|
||||
staged_models = [
|
||||
_object(item, "L3 staged worker model")
|
||||
for item in _array(
|
||||
worker_inventory.get("staged_models"),
|
||||
"L3 staged worker models",
|
||||
)
|
||||
]
|
||||
matching_staged_models = [
|
||||
model
|
||||
for model in staged_models
|
||||
if candidate_frozen
|
||||
and model.get("name") == required_model
|
||||
and model.get("upstream_version") == required_version
|
||||
and model.get("source_model_sha256") == required_source_sha256
|
||||
and model.get("source_label_sha256") == required_label_sha256
|
||||
]
|
||||
staged_model_ready = any(_model_ready(model) for model in matching_staged_models)
|
||||
|
||||
datasets = [
|
||||
_object(item, "L3 dataset")
|
||||
for item in _array(dataset_inventory.get("datasets"), "L3 datasets")
|
||||
]
|
||||
dataset_findings = [_dataset_finding(dataset) for dataset in datasets]
|
||||
eligible_datasets = [
|
||||
dataset
|
||||
for dataset in datasets
|
||||
if _dataset_supports_public_probe(dataset, public_probe)
|
||||
]
|
||||
|
||||
runtime_checks = {
|
||||
"canonical_triton_healthy": triton.get("healthy") is True,
|
||||
"canonical_triton_image_pinned": (
|
||||
triton.get("image") == runtime_policy.get("triton_image")
|
||||
and triton.get("image_digest") == runtime_policy.get("triton_image_digest")
|
||||
),
|
||||
"explicit_model_control": triton.get("model_control_mode") == "explicit",
|
||||
"strict_readiness": triton.get("strict_readiness") is True,
|
||||
"model_repository_read_only": triton.get("model_repository_read_only") is True,
|
||||
"second_serving_stack_absent": (
|
||||
worker_inventory.get("serving_stack_count") == 1
|
||||
),
|
||||
}
|
||||
blockers: list[str] = []
|
||||
if not all(runtime_checks.values()):
|
||||
blockers.append("canonical-triton-runtime-policy-not-satisfied")
|
||||
if not eligible_datasets:
|
||||
blockers.append("public-oriented-3d-box-truth-not-admitted")
|
||||
if not candidate_frozen:
|
||||
blockers.append("pointpillars-compatible-candidate-not-frozen")
|
||||
elif not matching_models:
|
||||
blockers.append(
|
||||
"pointpillars-model-not-installed-live"
|
||||
if staged_model_ready
|
||||
else "pointpillars-model-artifact-not-installed"
|
||||
)
|
||||
elif not model_ready:
|
||||
blockers.append("pointpillars-target-engine-or-provenance-not-verified")
|
||||
|
||||
public_probe_authorized = not blockers
|
||||
status = (
|
||||
"ready-for-public-cross-domain-probe"
|
||||
if public_probe_authorized
|
||||
else "blocked-foundation-assets"
|
||||
)
|
||||
next_gate = (
|
||||
"run-public-cross-domain-pointpillars-probe"
|
||||
if public_probe_authorized
|
||||
else _next_gate(blockers)
|
||||
)
|
||||
return {
|
||||
"schema_version": L3_REPORT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"status": status,
|
||||
"profile_id": profile["profile_id"],
|
||||
"observations": {
|
||||
"dataset_inventory_observed_at_utc": dataset_inventory["observed_at_utc"],
|
||||
"worker_inventory_observed_at_utc": worker_inventory["observed_at_utc"],
|
||||
"worker_host_id": worker_inventory["host_id"],
|
||||
},
|
||||
"detector": {
|
||||
"family": detector["family"],
|
||||
"upstream_model_id": detector["upstream_model_id"],
|
||||
"candidate_frozen": candidate_frozen,
|
||||
"candidate_model_version": required_version,
|
||||
"candidate_source_sha256": required_source_sha256,
|
||||
"candidate_label_sha256": required_label_sha256,
|
||||
"triton_model_name": required_model,
|
||||
"required_input_fields": list(_POINT_FIELDS),
|
||||
"maximum_points": detector["maximum_points"],
|
||||
"point_cloud_range": list(_POINT_CLOUD_RANGE),
|
||||
"training_domain": detector["training_domain"],
|
||||
"training_ground_truth_publicly_reproducible": False,
|
||||
"model_classes": list(_MODEL_CLASSES),
|
||||
"benchmark_classes": list(_BENCHMARK_CLASSES),
|
||||
"benchmark_class_mapping": _BENCHMARK_CLASS_MAPPING,
|
||||
"benchmark_metric_contract": _BENCHMARK_METRIC_CONTRACT,
|
||||
"postprocessing": _POSTPROCESSING_CONTRACT,
|
||||
"installed_matching_model_count": len(matching_models),
|
||||
"model_ready": model_ready,
|
||||
"staged_matching_model_count": len(matching_staged_models),
|
||||
"staged_target_engine_ready": staged_model_ready,
|
||||
},
|
||||
"runtime_checks": runtime_checks,
|
||||
"dataset_findings": dataset_findings,
|
||||
"eligible_public_probe_dataset_ids": [
|
||||
_required_string(dataset, "dataset_id") for dataset in eligible_datasets
|
||||
],
|
||||
"blocker_codes": blockers,
|
||||
"decision": {
|
||||
"public_cross_domain_probe_authorized": public_probe_authorized,
|
||||
"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,
|
||||
},
|
||||
"claim_boundaries": {
|
||||
"public_cross_domain_probe_metrics": list(_PUBLIC_TRANSFER_METRICS),
|
||||
"k1_transfer_stability_metrics": list(_TRANSFER_METRICS),
|
||||
"public_probe_is_native_model_accuracy": False,
|
||||
"k1_accuracy_requires_independent_truth": True,
|
||||
"absence_of_detection_means_free_space": False,
|
||||
"point_instance_clusters_are_3d_box_truth": False,
|
||||
},
|
||||
"next_gate": next_gate,
|
||||
"authority": _authority(),
|
||||
}
|
||||
|
||||
|
||||
def _dataset_finding(dataset: dict[str, Any]) -> dict[str, Any]:
|
||||
annotations = set(_string_array(dataset.get("annotations"), "dataset annotations"))
|
||||
installed = dataset.get("installed") is True
|
||||
has_box_truth = _BOX_ANNOTATION in annotations
|
||||
return {
|
||||
"dataset_id": _required_string(dataset, "dataset_id"),
|
||||
"installed": installed,
|
||||
"point_fields": _string_array(dataset.get("point_fields"), "dataset point fields"),
|
||||
"annotations": sorted(annotations),
|
||||
"independent_ground_truth": dataset.get("independent_ground_truth") is True,
|
||||
"oriented_3d_box_accuracy_eligible": (
|
||||
installed
|
||||
and has_box_truth
|
||||
and dataset.get("independent_ground_truth") is True
|
||||
and set(_POINT_FIELDS).issubset(
|
||||
set(_string_array(dataset.get("point_fields"), "dataset point fields"))
|
||||
)
|
||||
),
|
||||
"semantic_or_instance_labels_are_not_boxes": (
|
||||
not has_box_truth
|
||||
and bool(
|
||||
annotations.intersection(
|
||||
{"point-semantic-labels", "point-instance-labels"}
|
||||
)
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _dataset_supports_public_probe(
|
||||
dataset: dict[str, Any],
|
||||
public_probe: dict[str, Any],
|
||||
) -> bool:
|
||||
splits = set(_string_array(dataset.get("splits"), "dataset splits"))
|
||||
point_fields = set(
|
||||
_string_array(dataset.get("point_fields"), "dataset point fields")
|
||||
)
|
||||
annotations = set(
|
||||
_string_array(dataset.get("annotations"), "dataset annotations")
|
||||
)
|
||||
required_split = _required_string(public_probe, "required_split")
|
||||
return (
|
||||
dataset.get("installed") is True
|
||||
and dataset.get("independent_ground_truth") is True
|
||||
and required_split in splits
|
||||
and set(_POINT_FIELDS).issubset(point_fields)
|
||||
and _BOX_ANNOTATION in annotations
|
||||
)
|
||||
|
||||
|
||||
def _model_ready(model: dict[str, Any]) -> bool:
|
||||
smoke = model.get("representation_smoke")
|
||||
return (
|
||||
_valid_sha256(model.get("artifact_sha256"))
|
||||
and model.get("backend") == "tensorrt"
|
||||
and model.get("precision") == "strongly-typed"
|
||||
and model.get("engine_built_on_target") is True
|
||||
and model.get("provenance_verified") is True
|
||||
and model.get("source_format") == "onnx"
|
||||
and model.get("input_fields") == list(_POINT_FIELDS)
|
||||
and model.get("maximum_points") == 204_800
|
||||
and model.get("point_cloud_range") == list(_POINT_CLOUD_RANGE)
|
||||
and model.get("model_classes") == list(_MODEL_CLASSES)
|
||||
and _valid_sha256(model.get("source_label_sha256"))
|
||||
and model.get("outputs")
|
||||
== [
|
||||
{
|
||||
"name": "output_boxes",
|
||||
"dtype": "FP32",
|
||||
"shape": [1, 393_216, 9],
|
||||
},
|
||||
{"name": "num_boxes", "dtype": "INT32", "shape": [1]},
|
||||
]
|
||||
and isinstance(smoke, dict)
|
||||
and smoke.get("status") == "engine-executed"
|
||||
and _valid_sha256(smoke.get("input_artifact_sha256"))
|
||||
and smoke.get("input_point_count") == 169_883
|
||||
and isinstance(smoke.get("single_query_gpu_compute_ms"), float)
|
||||
and 0.0 < smoke["single_query_gpu_compute_ms"] < 1000.0
|
||||
and smoke.get("accuracy_evaluated") is False
|
||||
and smoke.get("navigation_or_safety_accepted") is False
|
||||
)
|
||||
|
||||
|
||||
def _next_gate(blockers: list[str]) -> str:
|
||||
missing_dataset = "public-oriented-3d-box-truth-not-admitted" in blockers
|
||||
missing_candidate = "pointpillars-compatible-candidate-not-frozen" in blockers
|
||||
missing_model = "pointpillars-model-artifact-not-installed" in blockers
|
||||
staged_model = "pointpillars-model-not-installed-live" in blockers
|
||||
if missing_dataset and missing_candidate:
|
||||
return "admit-public-3d-box-split-and-freeze-compatible-pointpillars-candidate"
|
||||
if missing_dataset and missing_model:
|
||||
return "admit-public-3d-box-split-and-build-target-pointpillars-engine"
|
||||
if missing_dataset and staged_model:
|
||||
return "admit-public-3d-box-split-then-install-staged-pointpillars-model"
|
||||
if missing_dataset:
|
||||
return "admit-public-oriented-3d-box-validation-split"
|
||||
if missing_model:
|
||||
return "build-and-admit-target-pointpillars-engine"
|
||||
if staged_model:
|
||||
return "install-staged-pointpillars-model-in-canonical-triton"
|
||||
if missing_candidate:
|
||||
return "freeze-compatible-pointpillars-onnx-candidate"
|
||||
if "pointpillars-target-engine-or-provenance-not-verified" in blockers:
|
||||
return "verify-target-engine-and-model-provenance"
|
||||
return "repair-canonical-triton-runtime-policy"
|
||||
|
||||
|
||||
def _validate_profile(profile: dict[str, Any]) -> None:
|
||||
if profile.get("schema_version") != L3_PROFILE_SCHEMA:
|
||||
raise L3PointPillarsAdmissionError("L3 profile schema is invalid")
|
||||
_safe_identifier(_required_string(profile, "profile_id"), "L3 profile id")
|
||||
detector = _object(profile.get("detector"), "L3 detector")
|
||||
if (
|
||||
detector.get("family") != "nvidia-tao-pointpillars"
|
||||
or detector.get("upstream_model_id") != "nvidia/tao/pointpillarnet"
|
||||
or detector.get("triton_model_name") != "pointpillars"
|
||||
or detector.get("required_source_format") != "onnx"
|
||||
or detector.get("input_representation") != "native-sensor-scan"
|
||||
or detector.get("input_coordinate_frame") != "sensor/lidar"
|
||||
or detector.get("input_fields") != list(_POINT_FIELDS)
|
||||
or detector.get("batch_size") != 1
|
||||
or detector.get("maximum_points") != 204_800
|
||||
or detector.get("point_cloud_range") != list(_POINT_CLOUD_RANGE)
|
||||
or detector.get("training_domain") != "proprietary-solid-state-lidar"
|
||||
or detector.get("training_ground_truth_publicly_reproducible") is not False
|
||||
or detector.get("onnx_contract_sha256") != _ONNX_CONTRACT_SHA256
|
||||
or detector.get("model_classes") != list(_MODEL_CLASSES)
|
||||
):
|
||||
raise L3PointPillarsAdmissionError("L3 detector contract is invalid")
|
||||
candidate_frozen = detector.get("candidate_frozen")
|
||||
candidate_version = detector.get("candidate_model_version")
|
||||
candidate_sha256 = detector.get("candidate_source_sha256")
|
||||
candidate_label_sha256 = detector.get("candidate_label_sha256")
|
||||
if (
|
||||
not isinstance(candidate_frozen, bool)
|
||||
or (
|
||||
candidate_frozen
|
||||
and (
|
||||
not isinstance(candidate_version, str)
|
||||
or not candidate_version
|
||||
or not _valid_sha256(candidate_sha256)
|
||||
or not _valid_sha256(candidate_label_sha256)
|
||||
)
|
||||
)
|
||||
or (
|
||||
not candidate_frozen
|
||||
and (
|
||||
candidate_version is not None
|
||||
or candidate_sha256 is not None
|
||||
or candidate_label_sha256 is not None
|
||||
)
|
||||
)
|
||||
):
|
||||
raise L3PointPillarsAdmissionError("L3 detector candidate freeze is invalid")
|
||||
if detector.get("postprocessing") != _POSTPROCESSING_CONTRACT:
|
||||
raise L3PointPillarsAdmissionError("L3 detector postprocessing is invalid")
|
||||
runtime = _object(profile.get("runtime_policy"), "L3 runtime policy")
|
||||
if (
|
||||
runtime.get("existing_triton_only") is not True
|
||||
or runtime.get("second_serving_stack_allowed") is not False
|
||||
or runtime.get("engine_built_on_target_required") is not True
|
||||
or runtime.get("precision") != "strongly-typed"
|
||||
or runtime.get("triton_image") != "nvcr.io/nvidia/tritonserver:26.06-py3"
|
||||
or not _valid_sha256(runtime.get("triton_image_digest"))
|
||||
):
|
||||
raise L3PointPillarsAdmissionError("L3 runtime policy is invalid")
|
||||
public_probe = _object(
|
||||
profile.get("public_cross_domain_probe"),
|
||||
"L3 public cross-domain probe",
|
||||
)
|
||||
if (
|
||||
public_probe.get("required_split") != "validation"
|
||||
or public_probe.get("required_ground_truth") != _BOX_ANNOTATION
|
||||
or public_probe.get("independent_ground_truth_required") is not True
|
||||
or public_probe.get("benchmark_classes") != list(_BENCHMARK_CLASSES)
|
||||
or public_probe.get("model_to_benchmark_class_mapping")
|
||||
!= _BENCHMARK_CLASS_MAPPING
|
||||
or public_probe.get("metric_contract") != _BENCHMARK_METRIC_CONTRACT
|
||||
or public_probe.get("metrics") != list(_PUBLIC_TRANSFER_METRICS)
|
||||
or public_probe.get("retuning_allowed") is not False
|
||||
or public_probe.get("native_accuracy_claim_allowed") is not False
|
||||
):
|
||||
raise L3PointPillarsAdmissionError(
|
||||
"L3 public cross-domain probe is invalid"
|
||||
)
|
||||
transfer = _object(profile.get("k1_transfer_stability"), "L3 transfer gate")
|
||||
if (
|
||||
transfer.get("requires_completed_public_cross_domain_probe") is not True
|
||||
or transfer.get("metrics") != list(_TRANSFER_METRICS)
|
||||
or transfer.get("accuracy_claim_allowed") is not False
|
||||
or transfer.get("retuning_allowed") is not False
|
||||
):
|
||||
raise L3PointPillarsAdmissionError("L3 K1 transfer gate is invalid")
|
||||
if profile.get("authority") != _authority():
|
||||
raise L3PointPillarsAdmissionError("L3 profile authority is invalid")
|
||||
|
||||
|
||||
def _validate_dataset_inventory(inventory: dict[str, Any]) -> None:
|
||||
if inventory.get("schema_version") != L3_DATASET_INVENTORY_SCHEMA:
|
||||
raise L3PointPillarsAdmissionError("L3 dataset inventory schema is invalid")
|
||||
_utc(_required_string(inventory, "observed_at_utc"), "dataset observation time")
|
||||
datasets = _array(inventory.get("datasets"), "L3 datasets")
|
||||
if not datasets:
|
||||
raise L3PointPillarsAdmissionError("L3 dataset inventory is empty")
|
||||
seen: set[str] = set()
|
||||
for raw_dataset in datasets:
|
||||
dataset = _object(raw_dataset, "L3 dataset")
|
||||
dataset_id = _safe_identifier(
|
||||
_required_string(dataset, "dataset_id"),
|
||||
"L3 dataset id",
|
||||
)
|
||||
if dataset_id in seen:
|
||||
raise L3PointPillarsAdmissionError("L3 dataset inventory has duplicates")
|
||||
seen.add(dataset_id)
|
||||
installed = dataset.get("installed")
|
||||
if not isinstance(installed, bool):
|
||||
raise L3PointPillarsAdmissionError("L3 dataset installed flag is invalid")
|
||||
release_identity = dataset.get("release_identity_sha256")
|
||||
if installed and not _valid_sha256(release_identity):
|
||||
raise L3PointPillarsAdmissionError(
|
||||
"installed L3 dataset lacks a release identity"
|
||||
)
|
||||
if not installed and release_identity is not None:
|
||||
raise L3PointPillarsAdmissionError(
|
||||
"absent L3 dataset cannot claim a release identity"
|
||||
)
|
||||
point_fields = _string_array(
|
||||
dataset.get("point_fields"),
|
||||
"dataset point fields",
|
||||
)
|
||||
if any(field not in _POINT_FIELDS for field in point_fields):
|
||||
raise L3PointPillarsAdmissionError("L3 dataset point field is unknown")
|
||||
_string_array(dataset.get("annotations"), "dataset annotations")
|
||||
_string_array(dataset.get("splits"), "dataset splits")
|
||||
if not isinstance(dataset.get("independent_ground_truth"), bool):
|
||||
raise L3PointPillarsAdmissionError(
|
||||
"L3 dataset truth independence flag is invalid"
|
||||
)
|
||||
_required_string(dataset, "license")
|
||||
|
||||
|
||||
def _validate_worker_inventory(inventory: dict[str, Any]) -> None:
|
||||
if inventory.get("schema_version") != L3_WORKER_INVENTORY_SCHEMA:
|
||||
raise L3PointPillarsAdmissionError("L3 worker inventory schema is invalid")
|
||||
_safe_identifier(_required_string(inventory, "host_id"), "L3 worker host id")
|
||||
_utc(_required_string(inventory, "observed_at_utc"), "worker observation time")
|
||||
serving_stack_count = inventory.get("serving_stack_count")
|
||||
if (
|
||||
isinstance(serving_stack_count, bool)
|
||||
or not isinstance(serving_stack_count, int)
|
||||
or serving_stack_count < 0
|
||||
):
|
||||
raise L3PointPillarsAdmissionError("L3 serving stack count is invalid")
|
||||
triton = _object(inventory.get("triton"), "L3 Triton inventory")
|
||||
_required_string(triton, "container_name")
|
||||
_required_string(triton, "image")
|
||||
if not _valid_sha256(triton.get("image_digest")):
|
||||
raise L3PointPillarsAdmissionError("L3 Triton image digest is invalid")
|
||||
for key in ("healthy", "strict_readiness", "model_repository_read_only"):
|
||||
if not isinstance(triton.get(key), bool):
|
||||
raise L3PointPillarsAdmissionError(f"L3 Triton {key} flag is invalid")
|
||||
_required_string(triton, "model_control_mode")
|
||||
models = _array(triton.get("models"), "L3 worker models")
|
||||
seen: set[str] = set()
|
||||
for raw_model in models:
|
||||
model = _object(raw_model, "L3 worker model")
|
||||
name = _safe_identifier(_required_string(model, "name"), "L3 worker model name")
|
||||
if name in seen:
|
||||
raise L3PointPillarsAdmissionError("L3 worker inventory has duplicate models")
|
||||
seen.add(name)
|
||||
if not _valid_sha256(model.get("artifact_sha256")):
|
||||
raise L3PointPillarsAdmissionError("L3 worker model identity is invalid")
|
||||
_required_string(model, "backend")
|
||||
staged_models = _array(inventory.get("staged_models"), "L3 staged worker models")
|
||||
staged_seen: set[str] = set()
|
||||
for raw_model in staged_models:
|
||||
model = _object(raw_model, "L3 staged worker model")
|
||||
name = _safe_identifier(
|
||||
_required_string(model, "name"),
|
||||
"L3 staged worker model name",
|
||||
)
|
||||
if name in staged_seen:
|
||||
raise L3PointPillarsAdmissionError(
|
||||
"L3 worker inventory has duplicate staged models"
|
||||
)
|
||||
staged_seen.add(name)
|
||||
if not _valid_sha256(model.get("artifact_sha256")):
|
||||
raise L3PointPillarsAdmissionError(
|
||||
"L3 staged worker model identity is invalid"
|
||||
)
|
||||
_required_string(model, "backend")
|
||||
gpu = _object(inventory.get("gpu"), "L3 GPU inventory")
|
||||
_required_string(gpu, "name")
|
||||
_required_string(gpu, "driver_version")
|
||||
memory_mib = gpu.get("memory_total_mib")
|
||||
if (
|
||||
isinstance(memory_mib, bool)
|
||||
or not isinstance(memory_mib, int)
|
||||
or memory_mib <= 0
|
||||
):
|
||||
raise L3PointPillarsAdmissionError("L3 GPU memory declaration is invalid")
|
||||
|
||||
|
||||
def _validate_report(report: dict[str, Any]) -> None:
|
||||
if report.get("schema_version") != L3_REPORT_SCHEMA:
|
||||
raise L3PointPillarsAdmissionError("L3 report schema is invalid")
|
||||
decision = _object(report.get("decision"), "L3 decision")
|
||||
public_probe = decision.get("public_cross_domain_probe_authorized")
|
||||
transfer = decision.get("k1_transfer_stability_authorized")
|
||||
blockers = _string_array(report.get("blocker_codes"), "L3 blockers")
|
||||
if (
|
||||
not isinstance(public_probe, bool)
|
||||
or not isinstance(transfer, bool)
|
||||
or transfer is not False
|
||||
or decision.get("native_model_accuracy_claim_authorized") is not False
|
||||
or decision.get("k1_transfer_quality_claim_authorized") is not False
|
||||
or decision.get("semantic_point_labels_substitute_for_3d_boxes") is not False
|
||||
or decision.get("fine_tuning_allowed") is not False
|
||||
or decision.get("second_serving_stack_allowed") is not False
|
||||
or decision.get("lab_publication_allowed") is not False
|
||||
or decision.get("centerpoint_comparison_allowed") is not False
|
||||
or (public_probe and blockers)
|
||||
or (not public_probe and not blockers)
|
||||
or report.get("authority") != _authority()
|
||||
):
|
||||
raise L3PointPillarsAdmissionError("L3 report decision is invalid")
|
||||
|
||||
|
||||
def _authority() -> dict[str, bool]:
|
||||
return {
|
||||
"shadow_only": True,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
|
||||
def _artifact(path: Path, role: str) -> dict[str, object]:
|
||||
return {
|
||||
"role": role,
|
||||
"path": path.name,
|
||||
"media_type": "application/json",
|
||||
"byte_length": path.stat().st_size,
|
||||
"sha256": _sha256(path),
|
||||
}
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
raise L3PointPillarsAdmissionError(
|
||||
f"cannot read L3 JSON: {path.name}"
|
||||
) from exc
|
||||
return _object(value, f"L3 JSON {path.name}")
|
||||
|
||||
|
||||
def _write_json(path: Path, value: object) -> None:
|
||||
path.write_text(
|
||||
json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _valid_sha256(value: object) -> bool:
|
||||
return isinstance(value, str) and _SHA256.fullmatch(value) is not None
|
||||
|
||||
|
||||
def _safe_identifier(value: str, label: str) -> str:
|
||||
if _IDENTIFIER.fullmatch(value) is None:
|
||||
raise L3PointPillarsAdmissionError(f"{label} is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _utc(value: str, label: str) -> None:
|
||||
if not value.endswith("Z") or "T" not in value:
|
||||
raise L3PointPillarsAdmissionError(f"{label} is invalid")
|
||||
|
||||
|
||||
def _object(value: object, label: str) -> dict[str, Any]:
|
||||
if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
|
||||
raise L3PointPillarsAdmissionError(f"{label} must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def _array(value: object, label: str) -> list[Any]:
|
||||
if not isinstance(value, list):
|
||||
raise L3PointPillarsAdmissionError(f"{label} must be an array")
|
||||
return value
|
||||
|
||||
|
||||
def _string_array(value: object, label: str) -> list[str]:
|
||||
values = _array(value, label)
|
||||
if any(not isinstance(item, str) or not item for item in values):
|
||||
raise L3PointPillarsAdmissionError(f"{label} must contain strings")
|
||||
return values
|
||||
|
||||
|
||||
def _required_string(value: dict[str, Any], key: str) -> str:
|
||||
item = value.get(key)
|
||||
if not isinstance(item, str) or not item.strip():
|
||||
raise L3PointPillarsAdmissionError(f"L3 {key} is invalid")
|
||||
return item
|
||||
@@ -0,0 +1,321 @@
|
||||
"""Validated NVIDIA PointPillars candidate decoding and sample-compatible NMS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Final
|
||||
|
||||
import numpy as np
|
||||
|
||||
POINTPILLARS_MODEL_CLASSES: Final = ("Vehicle", "Pedestrian", "Cyclist")
|
||||
POINTPILLARS_OUTPUT_FIELDS: Final = (
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"length",
|
||||
"width",
|
||||
"height",
|
||||
"yaw",
|
||||
"class_id",
|
||||
"score",
|
||||
)
|
||||
POINTPILLARS_NMS_IOU_THRESHOLD: Final = 0.01
|
||||
POINTPILLARS_PRE_NMS_TOP_N: Final = 4_096
|
||||
POINTPILLARS_EMBEDDED_SCORE_THRESHOLD: Final = 0.1
|
||||
POINTPILLARS_MODEL_POINT_CLOUD_RANGE: Final = (
|
||||
-51.20000076293945,
|
||||
-51.20000076293945,
|
||||
-1.399999976158142,
|
||||
51.20000076293945,
|
||||
51.20000076293945,
|
||||
4.400000095367432,
|
||||
)
|
||||
NVIDIA_REFERENCE_COMMIT: Final = "a540badc47812a17a94e924b537d49ad3969b5a8"
|
||||
_EPSILON: Final = 1e-8
|
||||
|
||||
|
||||
class PointPillarsPostprocessError(RuntimeError):
|
||||
"""PointPillars output does not satisfy the frozen NVIDIA contract."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PointPillarsBox:
|
||||
x_m: float
|
||||
y_m: float
|
||||
z_m: float
|
||||
length_m: float
|
||||
width_m: float
|
||||
height_m: float
|
||||
yaw_rad: float
|
||||
class_id: int
|
||||
model_class: str
|
||||
score: float
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class _Geometry:
|
||||
corners: tuple[tuple[float, float], ...]
|
||||
area: float
|
||||
minimum_x: float
|
||||
maximum_x: float
|
||||
minimum_y: float
|
||||
maximum_y: float
|
||||
|
||||
|
||||
def decode_pointpillars_output(
|
||||
output_boxes: np.ndarray,
|
||||
num_boxes: np.ndarray,
|
||||
*,
|
||||
pre_nms_top_n: int = POINTPILLARS_PRE_NMS_TOP_N,
|
||||
nms_iou_threshold: float = POINTPILLARS_NMS_IOU_THRESHOLD,
|
||||
) -> tuple[PointPillarsBox, ...]:
|
||||
"""Decode validated rows and reproduce the NVIDIA sample's class-agnostic NMS."""
|
||||
|
||||
boxes = np.asarray(output_boxes)
|
||||
counts = np.asarray(num_boxes)
|
||||
if boxes.shape != (1, 393_216, 9) or boxes.dtype != np.float32:
|
||||
raise PointPillarsPostprocessError("PointPillars output_boxes contract changed")
|
||||
if counts.shape != (1,) or counts.dtype != np.int32:
|
||||
raise PointPillarsPostprocessError("PointPillars num_boxes contract changed")
|
||||
count = int(counts[0])
|
||||
if count < 0 or count > boxes.shape[1]:
|
||||
raise PointPillarsPostprocessError("PointPillars candidate count is invalid")
|
||||
if (
|
||||
isinstance(pre_nms_top_n, bool)
|
||||
or not isinstance(pre_nms_top_n, int)
|
||||
or pre_nms_top_n < 1
|
||||
or not math.isfinite(nms_iou_threshold)
|
||||
or not 0.0 <= nms_iou_threshold <= 1.0
|
||||
):
|
||||
raise PointPillarsPostprocessError("PointPillars NMS parameters are invalid")
|
||||
if count == 0:
|
||||
return ()
|
||||
|
||||
candidates = boxes[0, :count]
|
||||
if not np.isfinite(candidates).all():
|
||||
raise PointPillarsPostprocessError("PointPillars candidate is non-finite")
|
||||
decoded = tuple(_decode_row(row) for row in candidates)
|
||||
ordered = tuple(
|
||||
sorted(
|
||||
decoded,
|
||||
key=lambda box: box.score,
|
||||
reverse=True,
|
||||
)[:pre_nms_top_n]
|
||||
)
|
||||
return _class_agnostic_nms(ordered, nms_iou_threshold)
|
||||
|
||||
|
||||
def oriented_bev_iou(one: PointPillarsBox, another: PointPillarsBox) -> float:
|
||||
"""Return BEV IoU for two validated oriented boxes."""
|
||||
|
||||
one_geometry = _geometry(one)
|
||||
another_geometry = _geometry(another)
|
||||
overlap = _intersection_area(one_geometry, another_geometry)
|
||||
denominator = one_geometry.area + another_geometry.area - overlap
|
||||
return overlap / max(denominator, _EPSILON)
|
||||
|
||||
|
||||
def oriented_3d_iou(one: PointPillarsBox, another: PointPillarsBox) -> float:
|
||||
"""Return oriented 3D IoU for two center-based LiDAR-frame boxes."""
|
||||
|
||||
one_geometry = _geometry(one)
|
||||
another_geometry = _geometry(another)
|
||||
bev_overlap = _intersection_area(one_geometry, another_geometry)
|
||||
one_minimum_z = one.z_m - one.height_m / 2.0
|
||||
one_maximum_z = one.z_m + one.height_m / 2.0
|
||||
another_minimum_z = another.z_m - another.height_m / 2.0
|
||||
another_maximum_z = another.z_m + another.height_m / 2.0
|
||||
height_overlap = max(
|
||||
0.0,
|
||||
min(one_maximum_z, another_maximum_z)
|
||||
- max(one_minimum_z, another_minimum_z),
|
||||
)
|
||||
intersection = bev_overlap * height_overlap
|
||||
one_volume = one_geometry.area * one.height_m
|
||||
another_volume = another_geometry.area * another.height_m
|
||||
return intersection / max(one_volume + another_volume - intersection, _EPSILON)
|
||||
|
||||
|
||||
def _decode_row(row: np.ndarray) -> PointPillarsBox:
|
||||
class_value = float(row[7])
|
||||
class_id = int(round(class_value))
|
||||
if (
|
||||
abs(class_value - class_id) > 1e-5
|
||||
or class_id < 0
|
||||
or class_id >= len(POINTPILLARS_MODEL_CLASSES)
|
||||
):
|
||||
raise PointPillarsPostprocessError("PointPillars class id is invalid")
|
||||
length_m, width_m, height_m = (float(row[index]) for index in (3, 4, 5))
|
||||
score = float(row[8])
|
||||
if (
|
||||
length_m <= 0.0
|
||||
or width_m <= 0.0
|
||||
or height_m <= 0.0
|
||||
or score < POINTPILLARS_EMBEDDED_SCORE_THRESHOLD
|
||||
or score > 1.0
|
||||
):
|
||||
raise PointPillarsPostprocessError(
|
||||
"PointPillars dimensions or score are invalid"
|
||||
)
|
||||
x_m, y_m, z_m = (float(row[index]) for index in (0, 1, 2))
|
||||
return PointPillarsBox(
|
||||
x_m=x_m,
|
||||
y_m=y_m,
|
||||
z_m=z_m,
|
||||
length_m=length_m,
|
||||
width_m=width_m,
|
||||
height_m=height_m,
|
||||
yaw_rad=float(row[6]),
|
||||
class_id=class_id,
|
||||
model_class=POINTPILLARS_MODEL_CLASSES[class_id],
|
||||
score=score,
|
||||
)
|
||||
|
||||
|
||||
def _class_agnostic_nms(
|
||||
boxes: tuple[PointPillarsBox, ...],
|
||||
threshold: float,
|
||||
) -> tuple[PointPillarsBox, ...]:
|
||||
geometries = tuple(_geometry(box) for box in boxes)
|
||||
suppressed = [False] * len(boxes)
|
||||
accepted: list[PointPillarsBox] = []
|
||||
for index, box in enumerate(boxes):
|
||||
if suppressed[index]:
|
||||
continue
|
||||
accepted.append(box)
|
||||
geometry = geometries[index]
|
||||
for candidate_index in range(index + 1, len(boxes)):
|
||||
if suppressed[candidate_index]:
|
||||
continue
|
||||
another = geometries[candidate_index]
|
||||
if not _aabbs_overlap(geometry, another):
|
||||
continue
|
||||
overlap = _intersection_area(geometry, another)
|
||||
iou = overlap / max(geometry.area + another.area - overlap, _EPSILON)
|
||||
if iou >= threshold:
|
||||
suppressed[candidate_index] = True
|
||||
return tuple(accepted)
|
||||
|
||||
|
||||
def _geometry(box: PointPillarsBox) -> _Geometry:
|
||||
half_length = box.length_m / 2.0
|
||||
half_width = box.width_m / 2.0
|
||||
cosine = math.cos(box.yaw_rad)
|
||||
sine = math.sin(box.yaw_rad)
|
||||
corners = tuple(
|
||||
(
|
||||
box.x_m + local_x * cosine - local_y * sine,
|
||||
box.y_m + local_x * sine + local_y * cosine,
|
||||
)
|
||||
for local_x, local_y in (
|
||||
(-half_length, -half_width),
|
||||
(half_length, -half_width),
|
||||
(half_length, half_width),
|
||||
(-half_length, half_width),
|
||||
)
|
||||
)
|
||||
x_values = [point[0] for point in corners]
|
||||
y_values = [point[1] for point in corners]
|
||||
return _Geometry(
|
||||
corners=corners,
|
||||
area=box.length_m * box.width_m,
|
||||
minimum_x=min(x_values),
|
||||
maximum_x=max(x_values),
|
||||
minimum_y=min(y_values),
|
||||
maximum_y=max(y_values),
|
||||
)
|
||||
|
||||
|
||||
def _aabbs_overlap(one: _Geometry, another: _Geometry) -> bool:
|
||||
return not (
|
||||
one.maximum_x < another.minimum_x
|
||||
or another.maximum_x < one.minimum_x
|
||||
or one.maximum_y < another.minimum_y
|
||||
or another.maximum_y < one.minimum_y
|
||||
)
|
||||
|
||||
|
||||
def _intersection_area(one: _Geometry, another: _Geometry) -> float:
|
||||
if not _aabbs_overlap(one, another):
|
||||
return 0.0
|
||||
polygon = list(one.corners)
|
||||
clip = another.corners
|
||||
for index, edge_start in enumerate(clip):
|
||||
edge_end = clip[(index + 1) % len(clip)]
|
||||
polygon = _clip_polygon(polygon, edge_start, edge_end)
|
||||
if not polygon:
|
||||
return 0.0
|
||||
return abs(
|
||||
sum(
|
||||
one_point[0] * another_point[1]
|
||||
- another_point[0] * one_point[1]
|
||||
for one_point, another_point in zip(
|
||||
polygon,
|
||||
(*polygon[1:], polygon[0]),
|
||||
strict=True,
|
||||
)
|
||||
)
|
||||
) / 2.0
|
||||
|
||||
|
||||
def _clip_polygon(
|
||||
polygon: list[tuple[float, float]],
|
||||
edge_start: tuple[float, float],
|
||||
edge_end: tuple[float, float],
|
||||
) -> list[tuple[float, float]]:
|
||||
if not polygon:
|
||||
return []
|
||||
result: list[tuple[float, float]] = []
|
||||
previous = polygon[-1]
|
||||
previous_inside = _inside(previous, edge_start, edge_end)
|
||||
for current in polygon:
|
||||
current_inside = _inside(current, edge_start, edge_end)
|
||||
if current_inside:
|
||||
if not previous_inside:
|
||||
result.append(_line_intersection(previous, current, edge_start, edge_end))
|
||||
result.append(current)
|
||||
elif previous_inside:
|
||||
result.append(_line_intersection(previous, current, edge_start, edge_end))
|
||||
previous = current
|
||||
previous_inside = current_inside
|
||||
return result
|
||||
|
||||
|
||||
def _inside(
|
||||
point: tuple[float, float],
|
||||
edge_start: tuple[float, float],
|
||||
edge_end: tuple[float, float],
|
||||
) -> bool:
|
||||
return _cross(
|
||||
edge_end[0] - edge_start[0],
|
||||
edge_end[1] - edge_start[1],
|
||||
point[0] - edge_start[0],
|
||||
point[1] - edge_start[1],
|
||||
) >= -_EPSILON
|
||||
|
||||
|
||||
def _line_intersection(
|
||||
segment_start: tuple[float, float],
|
||||
segment_end: tuple[float, float],
|
||||
edge_start: tuple[float, float],
|
||||
edge_end: tuple[float, float],
|
||||
) -> tuple[float, float]:
|
||||
segment_x = segment_end[0] - segment_start[0]
|
||||
segment_y = segment_end[1] - segment_start[1]
|
||||
edge_x = edge_end[0] - edge_start[0]
|
||||
edge_y = edge_end[1] - edge_start[1]
|
||||
denominator = _cross(segment_x, segment_y, edge_x, edge_y)
|
||||
if abs(denominator) <= _EPSILON:
|
||||
return segment_end
|
||||
offset_x = edge_start[0] - segment_start[0]
|
||||
offset_y = edge_start[1] - segment_start[1]
|
||||
ratio = _cross(offset_x, offset_y, edge_x, edge_y) / denominator
|
||||
return (
|
||||
segment_start[0] + ratio * segment_x,
|
||||
segment_start[1] + ratio * segment_y,
|
||||
)
|
||||
|
||||
|
||||
def _cross(one_x: float, one_y: float, another_x: float, another_y: float) -> float:
|
||||
return one_x * another_y - one_y * another_x
|
||||
Reference in New Issue
Block a user