feat(perception): add PointPillars visual audit
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi.routing import APIRoute
|
||||
from pytest import raises
|
||||
|
||||
from k1link.web.l3_pointpillars_visual_api import (
|
||||
build_l3_pointpillars_visual_router,
|
||||
)
|
||||
|
||||
|
||||
def _endpoint(router: APIRouter, path: str) -> object:
|
||||
for route in router.routes:
|
||||
if (
|
||||
isinstance(route, APIRoute)
|
||||
and route.path == path
|
||||
and route.methods is not None
|
||||
and "GET" in route.methods
|
||||
):
|
||||
return route.endpoint
|
||||
raise AssertionError(f"GET {path} route is missing")
|
||||
|
||||
|
||||
def _canonical(payload: object) -> bytes:
|
||||
return json.dumps(
|
||||
payload,
|
||||
ensure_ascii=False,
|
||||
separators=(",", ":"),
|
||||
sort_keys=True,
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def _write(path: Path, payload: object) -> dict[str, object]:
|
||||
content = _canonical(payload)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(content)
|
||||
return {
|
||||
"sha256": hashlib.sha256(content).hexdigest(),
|
||||
"byte_length": len(content),
|
||||
}
|
||||
|
||||
|
||||
def _result(root: Path) -> tuple[str, str]:
|
||||
frame_id = "000001"
|
||||
identity = {
|
||||
"source_run_id": f"l3-pointpillars-kitti-{'1' * 64}",
|
||||
"source_frame_results_identity_sha256": "2" * 64,
|
||||
"dataset_source_id": "kitti-3d-object/v2017",
|
||||
"dataset_release_identity_sha256": "3" * 64,
|
||||
"matching": {"metric": "oriented-3d-iou"},
|
||||
"point_sampling": {"maximum_points_per_frame": 12000},
|
||||
"authority": {
|
||||
"read_only": True,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
}
|
||||
identity_sha = hashlib.sha256(_canonical(identity)).hexdigest()
|
||||
result_id = f"l3-pointpillars-visual-audit-{identity_sha}"
|
||||
candidate = root / result_id
|
||||
summary = {
|
||||
"frame_id": frame_id,
|
||||
"inference_ms": 12.5,
|
||||
"prediction_count": 2,
|
||||
"evaluated_prediction_count": 1,
|
||||
"outside_shared_range_count": 1,
|
||||
"truth_count": 1,
|
||||
"true_positive_count": 0,
|
||||
"false_positive_count": 1,
|
||||
"false_negative_count": 1,
|
||||
"truth_classes": ["Car"],
|
||||
}
|
||||
detail = {
|
||||
"schema_version": "missioncore.l3-pointpillars-visual-frame/v1",
|
||||
"frame_id": frame_id,
|
||||
"summary": summary,
|
||||
"points": {
|
||||
"layout": "flat-xyzi",
|
||||
"source_point_count": 1,
|
||||
"shared_range_point_count": 1,
|
||||
"sampled_point_count": 1,
|
||||
"values": [1, 2, 3, 0.5],
|
||||
},
|
||||
"truth_boxes": [],
|
||||
"prediction_boxes": [],
|
||||
}
|
||||
detail_descriptor = _write(candidate / "frames" / f"{frame_id}.json", detail)
|
||||
descriptor = {
|
||||
**summary,
|
||||
"detail_path": f"frames/{frame_id}.json",
|
||||
"detail_sha256": detail_descriptor["sha256"],
|
||||
"detail_byte_length": detail_descriptor["byte_length"],
|
||||
}
|
||||
catalog = {
|
||||
"schema_version": (
|
||||
"missioncore.l3-pointpillars-visual-audit-catalog/v1"
|
||||
),
|
||||
"result_id": result_id,
|
||||
"source_run_id": identity["source_run_id"],
|
||||
"frame_count": 1,
|
||||
"frames": [descriptor],
|
||||
}
|
||||
catalog_descriptor = _write(candidate / "catalog.json", catalog)
|
||||
manifest = {
|
||||
"schema_version": "missioncore.l3-pointpillars-visual-audit/v1",
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha,
|
||||
"identity": identity,
|
||||
"created_at_utc": "2026-07-31T10:00:00Z",
|
||||
"status": "operator-visual-review-required",
|
||||
"source_metrics": {"frame_count": 3769, "aggregates": {}},
|
||||
"catalog": {
|
||||
"path": "catalog.json",
|
||||
"role": "visual-frame-catalog",
|
||||
**catalog_descriptor,
|
||||
},
|
||||
"authority": identity["authority"],
|
||||
}
|
||||
_write(candidate / "manifest.json", manifest)
|
||||
return result_id, frame_id
|
||||
|
||||
|
||||
def test_l3_visual_catalog_and_frame_are_read_only(tmp_path: Path) -> None:
|
||||
result_id, frame_id = _result(tmp_path)
|
||||
router = build_l3_pointpillars_visual_router(root_provider=lambda: tmp_path)
|
||||
catalog_route = _endpoint(
|
||||
router,
|
||||
"/api/v1/laboratory/l3/pointpillars-visual-audits/results",
|
||||
)
|
||||
frame_route = _endpoint(
|
||||
router,
|
||||
(
|
||||
"/api/v1/laboratory/l3/pointpillars-visual-audits/"
|
||||
"{result_id}/frames/{frame_id}"
|
||||
),
|
||||
)
|
||||
|
||||
catalog = catalog_route(limit=1) # type: ignore[operator]
|
||||
assert catalog["configured"] is True
|
||||
assert catalog["invalid_total"] == 0
|
||||
assert catalog["items"][0]["result_id"] == result_id
|
||||
assert catalog["items"][0]["frames"][0]["frame_id"] == frame_id
|
||||
|
||||
frame = frame_route(result_id=result_id, frame_id=frame_id) # type: ignore[operator]
|
||||
assert frame["schema_version"] == "missioncore.l3-pointpillars-visual-frame/v1"
|
||||
assert frame["access"] == "read-only"
|
||||
|
||||
|
||||
def test_l3_visual_frame_fails_closed_after_mutation(tmp_path: Path) -> None:
|
||||
result_id, frame_id = _result(tmp_path)
|
||||
router = build_l3_pointpillars_visual_router(root_provider=lambda: tmp_path)
|
||||
frame_route = _endpoint(
|
||||
router,
|
||||
(
|
||||
"/api/v1/laboratory/l3/pointpillars-visual-audits/"
|
||||
"{result_id}/frames/{frame_id}"
|
||||
),
|
||||
)
|
||||
(tmp_path / result_id / "frames" / f"{frame_id}.json").write_text(
|
||||
"{}",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with raises(Exception) as caught:
|
||||
frame_route(result_id=result_id, frame_id=frame_id) # type: ignore[operator]
|
||||
assert getattr(caught.value, "status_code", None) == 404
|
||||
Reference in New Issue
Block a user