feat(perception): add recorded replay maturation labs
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.l32_pointpillars_camera_review import (
|
||||
build_l32_pointpillars_camera_review,
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
repository = Path(__file__).resolve().parents[2]
|
||||
result = build_l32_pointpillars_camera_review(
|
||||
l31_result_root=(
|
||||
repository
|
||||
/ ".runtime/compute-experiments/l3/pointpillars-ravnoves"
|
||||
/ (
|
||||
"l31-pointpillars-ravnoves-"
|
||||
"80a9715f64ea397222fbcfd700803f9152009e3751caf87e2e9dc5ec6fc01b72"
|
||||
)
|
||||
),
|
||||
e10_pack_root=(
|
||||
repository
|
||||
/ ".runtime/compute-experiments/e10/lidar-packs"
|
||||
/ "e10-lidar-pack-576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b"
|
||||
),
|
||||
camera_job_root=(
|
||||
repository
|
||||
/ ".runtime/compute-jobs"
|
||||
/ "recorded-camera-602ac89026ed12978619801d"
|
||||
),
|
||||
ffmpeg_path=Path("/opt/homebrew/bin/ffmpeg"),
|
||||
output_root=(
|
||||
repository
|
||||
/ ".runtime/compute-experiments/l3/pointpillars-camera-review"
|
||||
),
|
||||
)
|
||||
print(result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.l33_camera_first_admission import (
|
||||
RAVNOVES00_ADMITTED_WORLD_STATE_RESULT_ID,
|
||||
)
|
||||
from k1link.compute.l33_camera_first_detector_review import (
|
||||
build_l33_camera_first_detector_review,
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
repository = Path(__file__).resolve().parents[2]
|
||||
result = build_l33_camera_first_detector_review(
|
||||
l32_result_root=(
|
||||
repository
|
||||
/ ".runtime/compute-experiments/l3/pointpillars-camera-review"
|
||||
/ (
|
||||
"l32-pointpillars-camera-review-"
|
||||
"40eca128ea9525e8e8c22bd3e981e40d5b66d2869c2e92636ddbf707ae44127a"
|
||||
)
|
||||
),
|
||||
e26_result_root=(
|
||||
repository
|
||||
/ ".runtime/compute-experiments/e10/worker-results"
|
||||
/ (
|
||||
"e10-integrated-perception-"
|
||||
"459aac93918d8f6414b342986ccc6968fefcef6c1f3a78a5254df0b565255ad2"
|
||||
)
|
||||
),
|
||||
e29_result_root=(
|
||||
repository
|
||||
/ ".runtime/compute-experiments/e29/results"
|
||||
/ (
|
||||
"e29-camera-geometry-"
|
||||
"421a9d930638bef12cd5eb10979a477917fa4a389e655ed95f73ba4bd62e13dc"
|
||||
)
|
||||
),
|
||||
e10_pack_root=(
|
||||
repository
|
||||
/ ".runtime/compute-experiments/e10/lidar-packs"
|
||||
/ "e10-lidar-pack-576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b"
|
||||
),
|
||||
output_root=(repository / ".runtime/compute-experiments/l3/camera-first-detector-review"),
|
||||
rectified_world_state_root=(
|
||||
repository
|
||||
/ ".runtime/compute-experiments/l3/rectified-camera-world-state"
|
||||
/ RAVNOVES00_ADMITTED_WORLD_STATE_RESULT_ID
|
||||
),
|
||||
)
|
||||
print(result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"schema_version": "missioncore.e46e-ready-stack-profile/v1",
|
||||
"profile_id": "e46e-deepstream-trafficcamnet-rtdetr-nvdcf/v1",
|
||||
"source": {
|
||||
"camera_source_id": "sensor.camera.right",
|
||||
"job_id": "recorded-camera-602ac89026ed12978619801d",
|
||||
"session_id": "20260720T065719Z_viewer_live",
|
||||
"segment_count": 4489,
|
||||
"stream_sha256": "cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8",
|
||||
"archive_index_sha256": "e029815a60ad9fbfedb6169142c7449df2b119a51d1ce001f08806e04eb0be14",
|
||||
"archive_summary_sha256": "b280f40b198aad5d5335819107fb1405ad65d5695d187c61a2c027ad853a3181"
|
||||
},
|
||||
"runtime": {
|
||||
"container_image": "nvcr.io/nvidia/deepstream:9.1-samples-multiarch@sha256:10eca409b3894e91c1bac915c9f1346307e56695e552487cbe8cf2f58a3f998f",
|
||||
"container_platform": "linux/amd64",
|
||||
"deepstream_version": "9.1",
|
||||
"network_during_inference": "none"
|
||||
},
|
||||
"detector": {
|
||||
"name": "NVIDIA TrafficCamNet Transformer Lite",
|
||||
"architecture": "RT-DETR ResNet50",
|
||||
"version": "deployable_resnet50_v2.0",
|
||||
"precision": "FP16",
|
||||
"input_shape": [
|
||||
3,
|
||||
544,
|
||||
960
|
||||
],
|
||||
"model_file": "resnet50_trafficcamnet_rtdetr.fp16.onnx",
|
||||
"model_sha256": "545a447b913d54eee476381436ebea4ad2aa876cfbe0a6d9f3b0302f08a7415d",
|
||||
"model_url": "https://api.ngc.nvidia.com/v2/models/nvidia/tao/trafficcamnet_transformer_lite/versions/deployable_resnet50_v2.0/files/resnet50_trafficcamnet_rtdetr.fp16.onnx",
|
||||
"labels": [
|
||||
"background",
|
||||
"bicycle",
|
||||
"car",
|
||||
"person",
|
||||
"road_sign"
|
||||
],
|
||||
"pre_cluster_threshold": 0.5,
|
||||
"custom_postprocessing": false
|
||||
},
|
||||
"parser": {
|
||||
"name": "NVIDIA DeepStream TAO custom bounding-box parser",
|
||||
"repository": "https://github.com/NVIDIA/DeepStream.git",
|
||||
"commit": "581889df47d6181110c758c10b872ca833a835e3",
|
||||
"source_path": "src/apps/tao_apps/post_processor",
|
||||
"development_image": "nvcr.io/nvidia/deepstream:9.1-triton-multiarch@sha256:fd31f5b44ababdbdee8cd397a375e888191b49e402ac237254a4cdc239130f5b",
|
||||
"cuda_version": "13.2",
|
||||
"symbol": "NvDsInferParseCustomDDETRTAO",
|
||||
"library_file": "libnvds_infercustomparser_tao.so",
|
||||
"library_sha256": "a18d85dae674a088549c5f9b8fda53c640f4fcbd88a41f4c2cb1f4e3ea8878ee",
|
||||
"custom_mission_core_logic": false,
|
||||
"source_files": [
|
||||
{
|
||||
"path": "Makefile",
|
||||
"sha256": "0265f470354e60c6d719bde68c7b74b1879eed9b4552b8fe7b39416af5ce6835"
|
||||
},
|
||||
{
|
||||
"path": "debug_logger_raii.cpp",
|
||||
"sha256": "1d388509e1ff9008de6ccd6451db9c6433273ed78a94e95a1df84585b8dc2915"
|
||||
},
|
||||
{
|
||||
"path": "debug_logger_raii.hpp",
|
||||
"sha256": "6efdce1874468848664a18ceb613f2384b8c079cb12baa888a433d6c0f81b7ec"
|
||||
},
|
||||
{
|
||||
"path": "debug_logger_tensor.hpp",
|
||||
"sha256": "c9999fcf92536bbb36498ddd4485f213fc2f5ddc70d24f8d408195a48680b94f"
|
||||
},
|
||||
{
|
||||
"path": "nvdsinfer_custombboxparser_tao.cpp",
|
||||
"sha256": "1794e3ee5152f25eff31454c6181368676f6659c68fc25b4b1933f6cbb63158b"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tracker": {
|
||||
"name": "NVIDIA NvDCF",
|
||||
"library": "libnvds_nvmultiobjecttracker.so",
|
||||
"configuration": "config_tracker_NvDCF_perf.yml",
|
||||
"past_frame_output": false,
|
||||
"custom_association": false,
|
||||
"custom_hold_or_stitch": false
|
||||
},
|
||||
"output": {
|
||||
"frame_width": 800,
|
||||
"frame_height": 600,
|
||||
"overlay_codec": "H.264",
|
||||
"overlay_container": "MP4",
|
||||
"tracked_frames": "tracked-frames.jsonl"
|
||||
},
|
||||
"authority": {
|
||||
"ground_truth": false,
|
||||
"candidate_accepted": false,
|
||||
"commands_enabled": false,
|
||||
"navigation_or_safety_accepted": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"schema_version": "missioncore.e46f-dashcam-bakeoff-profile/v1",
|
||||
"profile_id": "e46f-deepstream-dashcamnet-detectnet-v2-nvdcf/v1",
|
||||
"comparison_contract": {
|
||||
"baseline_result_id": "e46e-ready-stack-d51fd744a86b0effa8685c7aa86d14dfd1b12e97bc8d68d0f53f467237b976bf",
|
||||
"controlled_change": "detector-only",
|
||||
"held_constant": [
|
||||
"recorded RIGHT source identity",
|
||||
"DeepStream 9.1 container digest",
|
||||
"FP16 precision",
|
||||
"NVIDIA NvDCF performance configuration",
|
||||
"800x600 output plane",
|
||||
"full 4489-frame replay"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"camera_source_id": "sensor.camera.right",
|
||||
"job_id": "recorded-camera-602ac89026ed12978619801d",
|
||||
"session_id": "20260720T065719Z_viewer_live",
|
||||
"segment_count": 4489,
|
||||
"stream_sha256": "cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8",
|
||||
"archive_index_sha256": "e029815a60ad9fbfedb6169142c7449df2b119a51d1ce001f08806e04eb0be14",
|
||||
"archive_summary_sha256": "b280f40b198aad5d5335819107fb1405ad65d5695d187c61a2c027ad853a3181"
|
||||
},
|
||||
"runtime": {
|
||||
"container_image": "nvcr.io/nvidia/deepstream:9.1-samples-multiarch@sha256:10eca409b3894e91c1bac915c9f1346307e56695e552487cbe8cf2f58a3f998f",
|
||||
"container_platform": "linux/amd64",
|
||||
"deepstream_version": "9.1",
|
||||
"network_during_inference": "none"
|
||||
},
|
||||
"detector": {
|
||||
"name": "NVIDIA DashCamNet",
|
||||
"architecture": "DetectNet_v2 ResNet18",
|
||||
"version": "pruned_onnx_v1.0.4",
|
||||
"precision": "FP16",
|
||||
"intended_viewpoint": "moving vehicle dashcam",
|
||||
"input_shape": [
|
||||
3,
|
||||
544,
|
||||
960
|
||||
],
|
||||
"model_file": "resnet18_dashcamnet_pruned.onnx",
|
||||
"model_sha256": "d92f97bd840b68850a33c83e480fc6bd83b3097ff4cb517e0ea12671046bab7d",
|
||||
"model_url": "https://api.ngc.nvidia.com/v2/models/nvidia/tao/dashcamnet/versions/pruned_onnx_v1.0.4/files/resnet18_dashcamnet_pruned.onnx",
|
||||
"model_card_url": "https://catalog.ngc.nvidia.com/orgs/nvidia/tao/models/dashcamnet/-",
|
||||
"labels": [
|
||||
"car",
|
||||
"bicycle",
|
||||
"person",
|
||||
"road_sign"
|
||||
],
|
||||
"custom_postprocessing": false,
|
||||
"qualification": {
|
||||
"status": "passed-real-route-tensor-smoke",
|
||||
"method": "ONNX Runtime RGB NCHW 1/255 on immutable RIGHT frames 1248 and 3027",
|
||||
"observed_active_channels": [
|
||||
"car",
|
||||
"person"
|
||||
],
|
||||
"rejected_newer_artifact": {
|
||||
"version": "pruned_onnx_v1.0.5",
|
||||
"model_sha256": "4498f7e86e1113ef918462151ebef187cb9e6451dce4e57bd8e5484e1dd4aabb",
|
||||
"reason": "only confidence channel 2 was non-zero on both immutable qualification frames"
|
||||
}
|
||||
}
|
||||
},
|
||||
"postprocessor": {
|
||||
"name": "NVIDIA DeepStream built-in DetectNet_v2 parser and NMS",
|
||||
"implementation": "built into pinned DeepStream container",
|
||||
"cluster_mode": "NMS",
|
||||
"nms_iou_threshold": 0.5,
|
||||
"pre_cluster_threshold": 0.2,
|
||||
"car_pre_cluster_threshold": 0.4,
|
||||
"topk": 20,
|
||||
"reference_repository": "https://github.com/NVIDIA-AI-IOT/deepstream_reference_apps.git",
|
||||
"reference_commit": "2bda7c6e14effac626530e68f3d3b2c09fbb9654",
|
||||
"reference_path": "deepstream_app_tao_configs/nvinfer/config_infer_primary_dashcamnet.txt",
|
||||
"reference_config_sha256": "332b764b42238064a666bd5ec619c42905e44c099376247256996a03203ced93",
|
||||
"custom_mission_core_logic": false
|
||||
},
|
||||
"tracker": {
|
||||
"name": "NVIDIA NvDCF",
|
||||
"library": "libnvds_nvmultiobjecttracker.so",
|
||||
"configuration": "config_tracker_NvDCF_perf.yml",
|
||||
"past_frame_output": false,
|
||||
"custom_association": false,
|
||||
"custom_hold_or_stitch": false
|
||||
},
|
||||
"output": {
|
||||
"frame_width": 800,
|
||||
"frame_height": 600,
|
||||
"overlay_codec": "H.264",
|
||||
"overlay_container": "MP4",
|
||||
"tracked_frames": "tracked-frames.jsonl"
|
||||
},
|
||||
"authority": {
|
||||
"ground_truth": false,
|
||||
"candidate_accepted": false,
|
||||
"commands_enabled": false,
|
||||
"navigation_or_safety_accepted": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
{
|
||||
"schema_version": "missioncore.e46g-rectified-detector-bakeoff-profile/v1",
|
||||
"profile_id": "e46g-k1-right-kb4-nvdewarper-ready-detector-bakeoff/v1",
|
||||
"comparison_contract": {
|
||||
"baseline_result_ids": [
|
||||
"e46e-ready-stack-d51fd744a86b0effa8685c7aa86d14dfd1b12e97bc8d68d0f53f467237b976bf",
|
||||
"e46f-dashcam-bakeoff-2b888a784ba06d4565d34a91fef58af1fc9090ed9dc298ad318010ff4da64507"
|
||||
],
|
||||
"controlled_change": "detector-provider-only",
|
||||
"held_constant": [
|
||||
"single recorded RIGHT source identity",
|
||||
"factory camera_1 KB4 calibration",
|
||||
"NVIDIA Gst-nvdewarper left/front/right projections",
|
||||
"source frame selection 1000..1599",
|
||||
"DeepStream 9.1 container digest",
|
||||
"FP16 precision",
|
||||
"NVIDIA NvDCF performance configuration",
|
||||
"960x544 rectified output plane"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"camera_source_id": "sensor.camera.right",
|
||||
"job_id": "recorded-camera-602ac89026ed12978619801d",
|
||||
"session_id": "20260720T065719Z_viewer_live",
|
||||
"segment_count": 4489,
|
||||
"stream_sha256": "cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8",
|
||||
"archive_index_sha256": "e029815a60ad9fbfedb6169142c7449df2b119a51d1ce001f08806e04eb0be14",
|
||||
"archive_summary_sha256": "b280f40b198aad5d5335819107fb1405ad65d5695d187c61a2c027ad853a3181"
|
||||
},
|
||||
"selection": {
|
||||
"first_source_frame_index": 1000,
|
||||
"last_source_frame_index": 1599,
|
||||
"frame_count": 600,
|
||||
"purpose": "contiguous model-independent urban gate containing dense vehicles, pedestrians and fisheye edge geometry"
|
||||
},
|
||||
"calibration": {
|
||||
"slot": "camera_1",
|
||||
"model": "KB4",
|
||||
"calibration_sha256": "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9",
|
||||
"native_resolution": [
|
||||
4000,
|
||||
3000
|
||||
],
|
||||
"admitted_resolution": [
|
||||
800,
|
||||
600
|
||||
],
|
||||
"resize_contract": "linear 5x downscale without crop or warp",
|
||||
"intrinsic_fx_fy_cx_cy": [
|
||||
194.59817287616025,
|
||||
194.57531427932872,
|
||||
396.31861150187996,
|
||||
301.49644357408005
|
||||
],
|
||||
"distortion_kb4": [
|
||||
-0.023164451386679667,
|
||||
-0.0014974198594105452,
|
||||
-0.001039213149441563,
|
||||
-0.000035237331915978814
|
||||
]
|
||||
},
|
||||
"rectification": {
|
||||
"provider": "NVIDIA Gst-nvdewarper",
|
||||
"provider_version": "DeepStream 9.1",
|
||||
"projection": "fisheye-to-perspective",
|
||||
"projection_type": 4,
|
||||
"output_resolution": [
|
||||
960,
|
||||
544
|
||||
],
|
||||
"horizontal_fov_degrees": 100.0,
|
||||
"destination_focal_length": [
|
||||
402.76782296509447,
|
||||
402.76782296509447
|
||||
],
|
||||
"destination_principal_point": [
|
||||
479.5,
|
||||
271.5
|
||||
],
|
||||
"cuda_address_mode": "border",
|
||||
"expected_full_frame_count": 4488,
|
||||
"retained_source_frame_index_range": [
|
||||
0,
|
||||
4487
|
||||
],
|
||||
"excluded_source_tail_frame_count": 1,
|
||||
"tail_exclusion_reason": "DeepStream 9.1 nvv4l2decoder reproducibly omits only the terminal source frame at EOS; the admitted 1000..1599 gate is inside the timestamp-aligned retained prefix",
|
||||
"view_order": [
|
||||
"left",
|
||||
"front",
|
||||
"right"
|
||||
],
|
||||
"views": {
|
||||
"left": {
|
||||
"yaw_degrees": -90.0,
|
||||
"pitch_degrees": 0.0,
|
||||
"config_file": "e46g_nvdewarper_left.txt",
|
||||
"config_sha256": "257438d20a00ac495022b06a3a27b41822ccbb59bb1e9405501a2467b8456718"
|
||||
},
|
||||
"front": {
|
||||
"yaw_degrees": 0.0,
|
||||
"pitch_degrees": 0.0,
|
||||
"config_file": "e46g_nvdewarper_front.txt",
|
||||
"config_sha256": "f861e31278550bbe3fc82f41df4381c8a7aaf113a98a10761d98fa085c6a56b4"
|
||||
},
|
||||
"right": {
|
||||
"yaw_degrees": 90.0,
|
||||
"pitch_degrees": 0.0,
|
||||
"config_file": "e46g_nvdewarper_right.txt",
|
||||
"config_sha256": "16c9b6c22c8c9a0cf7a3e6127c7f5878d7890200dcc284d39f3ed0101277bb73"
|
||||
}
|
||||
},
|
||||
"official_documentation": "https://docs.nvidia.com/metropolis/deepstream/9.0/text/DS_plugin_gst-nvdewarper.html"
|
||||
},
|
||||
"runtime": {
|
||||
"container_image": "nvcr.io/nvidia/deepstream:9.1-samples-multiarch@sha256:10eca409b3894e91c1bac915c9f1346307e56695e552487cbe8cf2f58a3f998f",
|
||||
"container_platform": "linux/amd64",
|
||||
"deepstream_version": "9.1",
|
||||
"network_during_inference": "none"
|
||||
},
|
||||
"candidates": {
|
||||
"trafficcamnet": {
|
||||
"name": "NVIDIA TrafficCamNet Transformer Lite",
|
||||
"architecture": "RT-DETR ResNet50",
|
||||
"version": "deployable_resnet50_v2.0",
|
||||
"precision": "FP16",
|
||||
"model_file": "resnet50_trafficcamnet_rtdetr.fp16.onnx",
|
||||
"model_sha256": "545a447b913d54eee476381436ebea4ad2aa876cfbe0a6d9f3b0302f08a7415d",
|
||||
"model_url": "https://api.ngc.nvidia.com/v2/models/nvidia/tao/trafficcamnet_transformer_lite/versions/deployable_resnet50_v2.0/files/resnet50_trafficcamnet_rtdetr.fp16.onnx",
|
||||
"deepstream_app_config": "e46g_trafficcamnet_deepstream_app.txt",
|
||||
"deepstream_app_config_sha256": "1a0ade326a80f9dcdf0b538948eb508972afb478466cf4294116b7361b62aadf",
|
||||
"detector_config": "e46e_trafficcamnet_rtdetr.txt",
|
||||
"detector_config_sha256": "be2c275c0a1cbef3ec6a5f1eef2ece50b7f3408fc1c56839c0b9863f6d577c76",
|
||||
"labels": [
|
||||
"background",
|
||||
"bicycle",
|
||||
"car",
|
||||
"person",
|
||||
"road_sign"
|
||||
],
|
||||
"custom_postprocessing": false
|
||||
},
|
||||
"dashcamnet": {
|
||||
"name": "NVIDIA DashCamNet",
|
||||
"architecture": "DetectNet_v2 ResNet18",
|
||||
"version": "pruned_onnx_v1.0.4",
|
||||
"precision": "FP16",
|
||||
"model_file": "resnet18_dashcamnet_pruned.onnx",
|
||||
"model_sha256": "d92f97bd840b68850a33c83e480fc6bd83b3097ff4cb517e0ea12671046bab7d",
|
||||
"model_url": "https://api.ngc.nvidia.com/v2/models/nvidia/tao/dashcamnet/versions/pruned_onnx_v1.0.4/files/resnet18_dashcamnet_pruned.onnx",
|
||||
"deepstream_app_config": "e46g_dashcamnet_deepstream_app.txt",
|
||||
"deepstream_app_config_sha256": "e25c4fb5d44dc964a28afd9b4c4449624e9cbe89e71b2734aae2a9b55765d603",
|
||||
"detector_config": "e46f_dashcamnet_detectnet.txt",
|
||||
"detector_config_sha256": "3493f5ba7236a949354133e437844b059f302a01c5f93b836f4f16f58a776238",
|
||||
"labels": [
|
||||
"car",
|
||||
"bicycle",
|
||||
"person",
|
||||
"road_sign"
|
||||
],
|
||||
"custom_postprocessing": false
|
||||
}
|
||||
},
|
||||
"trafficcamnet_parser": {
|
||||
"name": "NVIDIA DeepStream TAO custom bounding-box parser",
|
||||
"commit": "581889df47d6181110c758c10b872ca833a835e3",
|
||||
"symbol": "NvDsInferParseCustomDDETRTAO",
|
||||
"library_file": "libnvds_infercustomparser_tao.so",
|
||||
"library_sha256": "a18d85dae674a088549c5f9b8fda53c640f4fcbd88a41f4c2cb1f4e3ea8878ee",
|
||||
"custom_mission_core_logic": false
|
||||
},
|
||||
"tracker": {
|
||||
"name": "NVIDIA NvDCF",
|
||||
"library": "libnvds_nvmultiobjecttracker.so",
|
||||
"configuration": "config_tracker_NvDCF_perf.yml",
|
||||
"identity_scope": "view-local",
|
||||
"past_frame_output": false,
|
||||
"custom_association": false,
|
||||
"custom_hold_or_stitch": false
|
||||
},
|
||||
"authority": {
|
||||
"ground_truth": false,
|
||||
"candidate_accepted": false,
|
||||
"commands_enabled": false,
|
||||
"navigation_or_safety_accepted": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"schema_version": "missioncore.e46h-full-rectified-front-replay-profile/v1",
|
||||
"profile_id": "e46h-right-kb4-front-trafficcamnet-full-replay/v1",
|
||||
"baseline_result_id": "e46g-rectified-detector-bakeoff-9c4eb44cbb61199db0967bd9048712a0964e2dbf587651c9e0c6d808967675c6",
|
||||
"source": {
|
||||
"camera_source_id": "sensor.camera.right",
|
||||
"job_id": "recorded-camera-602ac89026ed12978619801d",
|
||||
"session_id": "20260720T065719Z_viewer_live",
|
||||
"segment_count": 4489,
|
||||
"stream_sha256": "cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8",
|
||||
"archive_index_sha256": "e029815a60ad9fbfedb6169142c7449df2b119a51d1ce001f08806e04eb0be14",
|
||||
"archive_summary_sha256": "b280f40b198aad5d5335819107fb1405ad65d5695d187c61a2c027ad853a3181"
|
||||
},
|
||||
"selection": {
|
||||
"first_source_frame_index": 0,
|
||||
"last_source_frame_index": 4487,
|
||||
"frame_count": 4488,
|
||||
"excluded_source_tail_frame_count": 1,
|
||||
"tail_exclusion_reason": "DeepStream 9.1 nvv4l2decoder reproducibly omits only the terminal source frame at EOS; E46H admits the timestamp-aligned retained prefix 0..4487"
|
||||
},
|
||||
"calibration": {
|
||||
"slot": "camera_1",
|
||||
"model": "KB4",
|
||||
"calibration_sha256": "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9",
|
||||
"native_resolution": [4000, 3000],
|
||||
"admitted_resolution": [800, 600],
|
||||
"resize_contract": "linear 5x downscale without crop or warp",
|
||||
"intrinsic_fx_fy_cx_cy": [
|
||||
194.59817287616025,
|
||||
194.57531427932872,
|
||||
396.31861150187996,
|
||||
301.49644357408005
|
||||
],
|
||||
"distortion_kb4": [
|
||||
-0.023164451386679667,
|
||||
-0.0014974198594105452,
|
||||
-0.001039213149441563,
|
||||
-0.000035237331915978814
|
||||
]
|
||||
},
|
||||
"rectification": {
|
||||
"provider": "NVIDIA Gst-nvdewarper",
|
||||
"provider_version": "DeepStream 9.1",
|
||||
"projection": "fisheye-to-perspective",
|
||||
"projection_type": 4,
|
||||
"view": "front",
|
||||
"yaw_degrees": 0.0,
|
||||
"pitch_degrees": 0.0,
|
||||
"output_resolution": [960, 544],
|
||||
"horizontal_fov_degrees": 100.0,
|
||||
"destination_focal_length": [402.76782296509447, 402.76782296509447],
|
||||
"destination_principal_point": [479.5, 271.5],
|
||||
"cuda_address_mode": "border",
|
||||
"config_file": "e46g_nvdewarper_front.txt",
|
||||
"config_sha256": "f861e31278550bbe3fc82f41df4381c8a7aaf113a98a10761d98fa085c6a56b4",
|
||||
"official_documentation": "https://docs.nvidia.com/metropolis/deepstream/9.0/text/DS_plugin_gst-nvdewarper.html"
|
||||
},
|
||||
"runtime": {
|
||||
"container_image": "nvcr.io/nvidia/deepstream:9.1-samples-multiarch@sha256:10eca409b3894e91c1bac915c9f1346307e56695e552487cbe8cf2f58a3f998f",
|
||||
"container_platform": "linux/amd64",
|
||||
"deepstream_version": "9.1",
|
||||
"network_during_inference": "none"
|
||||
},
|
||||
"detector": {
|
||||
"name": "NVIDIA TrafficCamNet Transformer Lite",
|
||||
"architecture": "RT-DETR ResNet50",
|
||||
"version": "deployable_resnet50_v2.0",
|
||||
"precision": "FP16",
|
||||
"model_file": "resnet50_trafficcamnet_rtdetr.fp16.onnx",
|
||||
"model_sha256": "545a447b913d54eee476381436ebea4ad2aa876cfbe0a6d9f3b0302f08a7415d",
|
||||
"model_url": "https://api.ngc.nvidia.com/v2/models/nvidia/tao/trafficcamnet_transformer_lite/versions/deployable_resnet50_v2.0/files/resnet50_trafficcamnet_rtdetr.fp16.onnx",
|
||||
"deepstream_app_config": "e46g_trafficcamnet_deepstream_app.txt",
|
||||
"deepstream_app_config_sha256": "1a0ade326a80f9dcdf0b538948eb508972afb478466cf4294116b7361b62aadf",
|
||||
"detector_config": "e46e_trafficcamnet_rtdetr.txt",
|
||||
"detector_config_sha256": "be2c275c0a1cbef3ec6a5f1eef2ece50b7f3408fc1c56839c0b9863f6d577c76",
|
||||
"labels": ["background", "bicycle", "car", "person", "road_sign"],
|
||||
"custom_postprocessing": false
|
||||
},
|
||||
"parser": {
|
||||
"name": "NVIDIA DeepStream TAO custom bounding-box parser",
|
||||
"repository": "https://github.com/NVIDIA/DeepStream.git",
|
||||
"commit": "581889df47d6181110c758c10b872ca833a835e3",
|
||||
"symbol": "NvDsInferParseCustomDDETRTAO",
|
||||
"library_file": "libnvds_infercustomparser_tao.so",
|
||||
"library_sha256": "a18d85dae674a088549c5f9b8fda53c640f4fcbd88a41f4c2cb1f4e3ea8878ee",
|
||||
"custom_mission_core_logic": false
|
||||
},
|
||||
"tracker": {
|
||||
"name": "NVIDIA NvDCF",
|
||||
"library": "libnvds_nvmultiobjecttracker.so",
|
||||
"configuration": "config_tracker_NvDCF_perf.yml",
|
||||
"identity_scope": "front-view route-local",
|
||||
"past_frame_output": false,
|
||||
"custom_association": false,
|
||||
"custom_hold_or_stitch": false
|
||||
},
|
||||
"authority": {
|
||||
"ground_truth": false,
|
||||
"candidate_accepted": false,
|
||||
"commands_enabled": false,
|
||||
"navigation_or_safety_accepted": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"schema_version": "missioncore.e46i-grounding-dino-full-replay-profile/v1",
|
||||
"profile_id": "e46i-right-kb4-front-grounding-dino-swin-tiny-full-replay/v1",
|
||||
"baseline_result_id": "e46h-full-rectified-front-replay-43f9d97c06387ffa3b7aa656c48c3cb475be5c40de1211853ae8cb3f5ff2e28f",
|
||||
"source": {
|
||||
"camera_source_id": "sensor.camera.right",
|
||||
"session_id": "20260720T065719Z_viewer_live",
|
||||
"view": "front",
|
||||
"projection": "NVIDIA Gst-nvdewarper KB4 fisheye-to-perspective",
|
||||
"projection_resolution": [960, 544],
|
||||
"projection_fov_degrees": 100.0,
|
||||
"video_frame_count": 4488,
|
||||
"video_frame_rate": 10.0,
|
||||
"video_duration_seconds": 448.8,
|
||||
"video_sha256": "29bb32df8465ef53759bdf53b869f2edf2506fb70fd2a3fdf9dcdde02bf618e8"
|
||||
},
|
||||
"provider": {
|
||||
"name": "NVIDIA TAO Grounding DINO Swin-Tiny Commercial",
|
||||
"version": "1.0",
|
||||
"deployment_toolkit": "NVIDIA TAO Toolkit Deploy 7.0.1",
|
||||
"model_file": "grounding_dino_swin_tiny_commercial_deployable.onnx",
|
||||
"model_sha256": "6895acdc6b588e923f753e37b3bd18869e064256e5ecc1b2b9853e8c51125f94",
|
||||
"engine_file": "grounding_dino_swin_tiny_commercial_fp16.engine",
|
||||
"engine_sha256": "63213f60730ce8577047ff508dfe06113269347501e2dd832aeb56c89bc11489",
|
||||
"engine_precision": "FP16",
|
||||
"container_reference": "nvcr.io/nvidia/tao/tao-toolkit:7.0.1-deploy",
|
||||
"registry_manifest_sha256": "90d5d645ab75838db9ec7058855e5c283b59116ef0f1663a26b00b7c2c37280d",
|
||||
"registry_config_sha256": "ea2dda4441128170ae6eb798143517979ebaa523bab6d9c0d73547fdbae2b86a",
|
||||
"local_image_id_sha256": "2a3095330dd83e4314aada21fc7d184fcdb442a9131d591e5593ca5497394e7a",
|
||||
"custom_detector_or_postprocessing": false
|
||||
},
|
||||
"inference": {
|
||||
"spec_file": "e46i_grounding_dino_full_replay_spec.yaml",
|
||||
"spec_sha256": "c8513427f8bb8593a581bb29f286d89896e9639f98f851f2198aff86b8b541cb",
|
||||
"captions": ["car", "person", "bicycle", "road sign"],
|
||||
"confidence_threshold": 0.5,
|
||||
"batch_size": 1,
|
||||
"input_resolution": [960, 544],
|
||||
"gpu": "NVIDIA GeForce RTX 4090",
|
||||
"processed_frame_count": 4488,
|
||||
"elapsed_seconds": 748.0,
|
||||
"mean_frames_per_second": 6.0,
|
||||
"network_observation": "TAO downloaded the bert-base-uncased tokenizer from Hugging Face at run start; this execution is valid but not yet an offline-reproducible deployment"
|
||||
},
|
||||
"visual_shadow_gate": {
|
||||
"status": "completed",
|
||||
"threshold_changed_after_review": false,
|
||||
"legacy_large_false_background_cases": 5,
|
||||
"legacy_large_false_background_cases_suppressed": 5,
|
||||
"empty_anchor_cases": 1,
|
||||
"empty_anchor_cases_kept_empty": 1,
|
||||
"positive_anchor_cases": 5,
|
||||
"positive_anchor_cases_with_relevant_detection": 5,
|
||||
"known_semantic_limitations": [
|
||||
"one partially cropped person was missed in the stroller frame",
|
||||
"the stroller was labelled bicycle because stroller was not included in the fixed caption set"
|
||||
]
|
||||
},
|
||||
"authority": {
|
||||
"ground_truth": false,
|
||||
"independent_truth": false,
|
||||
"candidate_accepted": false,
|
||||
"commands_enabled": false,
|
||||
"navigation_or_safety_accepted": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
model_name: nvidia-grounding-dino-swin-tiny-commercial-v1.0
|
||||
results_dir: /workspace/e46i-full-results
|
||||
wandb:
|
||||
enable: false
|
||||
|
||||
model:
|
||||
backbone: swin_tiny_224_1k
|
||||
num_feature_levels: 4
|
||||
dec_layers: 6
|
||||
enc_layers: 6
|
||||
num_queries: 900
|
||||
dim_feedforward: 2048
|
||||
aux_loss: false
|
||||
log_scale: auto
|
||||
class_embed_bias: true
|
||||
|
||||
dataset:
|
||||
infer_data_sources:
|
||||
image_dir:
|
||||
- /workspace/full-input-images
|
||||
captions:
|
||||
- car
|
||||
- person
|
||||
- bicycle
|
||||
- road sign
|
||||
batch_size: 1
|
||||
workers: 1
|
||||
pin_memory: true
|
||||
max_labels: 80
|
||||
|
||||
inference:
|
||||
trt_engine: /workspace/models/grounding_dino_swin_tiny_commercial_fp16.engine
|
||||
batch_size: -1
|
||||
input_width: 960
|
||||
input_height: 544
|
||||
conf_threshold: 0.5
|
||||
outline_width: 3
|
||||
color_map:
|
||||
car: blue
|
||||
person: red
|
||||
bicycle: green
|
||||
road sign: yellow
|
||||
|
||||
gen_trt_engine:
|
||||
onnx_file: /workspace/models/grounding_dino_swin_tiny_commercial_deployable.onnx
|
||||
trt_engine: /workspace/models/grounding_dino_swin_tiny_commercial_fp16.engine
|
||||
batch_size: -1
|
||||
verbose: false
|
||||
tensorrt:
|
||||
workspace_size: 8192
|
||||
min_batch_size: 1
|
||||
opt_batch_size: 1
|
||||
max_batch_size: 1
|
||||
data_type: FP16
|
||||
@@ -0,0 +1,54 @@
|
||||
model_name: nvidia-grounding-dino-swin-tiny-commercial-v1.0
|
||||
results_dir: /workspace/e46i-results
|
||||
wandb:
|
||||
enable: false
|
||||
|
||||
model:
|
||||
backbone: swin_tiny_224_1k
|
||||
num_feature_levels: 4
|
||||
dec_layers: 6
|
||||
enc_layers: 6
|
||||
num_queries: 900
|
||||
dim_feedforward: 2048
|
||||
aux_loss: false
|
||||
log_scale: auto
|
||||
class_embed_bias: true
|
||||
|
||||
dataset:
|
||||
infer_data_sources:
|
||||
image_dir:
|
||||
- /workspace/input-images
|
||||
captions:
|
||||
- car
|
||||
- person
|
||||
- bicycle
|
||||
- road sign
|
||||
batch_size: 1
|
||||
workers: 1
|
||||
pin_memory: true
|
||||
max_labels: 80
|
||||
|
||||
inference:
|
||||
trt_engine: /workspace/models/grounding_dino_swin_tiny_commercial_fp16.engine
|
||||
batch_size: -1
|
||||
input_width: 960
|
||||
input_height: 544
|
||||
conf_threshold: 0.5
|
||||
outline_width: 3
|
||||
color_map:
|
||||
car: blue
|
||||
person: red
|
||||
bicycle: green
|
||||
road sign: yellow
|
||||
|
||||
gen_trt_engine:
|
||||
onnx_file: /workspace/models/grounding_dino_swin_tiny_commercial_deployable.onnx
|
||||
trt_engine: /workspace/models/grounding_dino_swin_tiny_commercial_fp16.engine
|
||||
batch_size: -1
|
||||
verbose: false
|
||||
tensorrt:
|
||||
workspace_size: 8192
|
||||
min_batch_size: 1
|
||||
opt_batch_size: 1
|
||||
max_batch_size: 1
|
||||
data_type: FP16
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"schema_version": "missioncore.e46j-raw-fisheye-realtime-profile/v1",
|
||||
"profile_id": "e46j-k1-right-raw-kb4-yolox-s-one-pass/v1",
|
||||
"source": {
|
||||
"camera_source_id": "sensor.camera.right",
|
||||
"session_id": "20260720T065719Z_viewer_live",
|
||||
"job_id": "recorded-camera-602ac89026ed12978619801d",
|
||||
"stream_sha256": "cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8",
|
||||
"frame_count": 4489,
|
||||
"frame_rate": 10.003944527024467,
|
||||
"average_rate": "4489000/448723",
|
||||
"resolution": [800, 600],
|
||||
"calibration_slot": "camera_1",
|
||||
"calibration_model": "KB4",
|
||||
"calibration_sha256": "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9",
|
||||
"preprocessing_contract": "full admitted raw raster; valid-FOV fill and top-left letterbox only; no crop, dewarp or virtual view"
|
||||
},
|
||||
"valid_fov": {
|
||||
"result_id": "valid-fov-mask-b4dd8ddf2b87c1d520ee8a0868c4fea062d7c14d1bae73ccabd3abe1f3acbac2",
|
||||
"mask_file": "mask.png"
|
||||
},
|
||||
"detector": {
|
||||
"id": "yolox_s",
|
||||
"version": 1,
|
||||
"architecture": "YOLOX-S",
|
||||
"source": "Megvii-BaseDetection/YOLOX release 0.1.1rc0",
|
||||
"license": "Apache-2.0",
|
||||
"model_sha256": "c5c2d13e59ae883e6af3b45daea64af4833a4951c92d116ec270d9ddbe998063",
|
||||
"config_sha256": "5795c737a7935a655961b069e8404d336d891f9762fb6dffb93956a076479604",
|
||||
"runtime": "NVIDIA Triton 2.70.0 ONNX Runtime GPU backend",
|
||||
"input_name": "images",
|
||||
"output_name": "output",
|
||||
"input_shape": [1, 3, 640, 640],
|
||||
"classes": "COCO-80",
|
||||
"single_inference_per_source_frame": true
|
||||
},
|
||||
"preprocessing": {
|
||||
"color_order": "BGR",
|
||||
"resize": "bilinear-letterbox-top-left",
|
||||
"pad_value": 114,
|
||||
"valid_fov_fill_value": 114
|
||||
},
|
||||
"detection": {
|
||||
"minimum_score": 0.5,
|
||||
"nms_iou_threshold": 0.45,
|
||||
"target_class_ids": [0, 1, 2, 3, 5, 7],
|
||||
"minimum_box_area_pixels": 64.0,
|
||||
"maximum_box_area_fraction": 0.5,
|
||||
"minimum_valid_fov_fraction": 0.5,
|
||||
"require_center_inside_valid_fov": true,
|
||||
"custom_detector_logic": false,
|
||||
"route_specific_filtering": false
|
||||
},
|
||||
"acceptance": {
|
||||
"required_frame_count": 4489,
|
||||
"required_source_frame_rate": 10.003944527024467,
|
||||
"minimum_core_capacity_fps": 10.0,
|
||||
"maximum_core_path_p95_ms": 80.0,
|
||||
"maximum_inference_request_p95_ms": 60.0,
|
||||
"require_zero_failed_frames": true,
|
||||
"require_full_raw_fov": true
|
||||
},
|
||||
"authority": {
|
||||
"ground_truth": false,
|
||||
"provider_promoted": false,
|
||||
"commands_enabled": false,
|
||||
"navigation_or_safety_accepted": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"authority": {
|
||||
"commands_enabled": false,
|
||||
"navigation_or_safety_accepted": false
|
||||
},
|
||||
"camera_source_id": "sensor.camera.right",
|
||||
"candidate": {
|
||||
"architecture": "YOLOX-S",
|
||||
"candidate_id": "yolox-s-kb4-core3",
|
||||
"minimum_score": 0.25,
|
||||
"model_sha256": "c5c2d13e59ae883e6af3b45daea64af4833a4951c92d116ec270d9ddbe998063"
|
||||
},
|
||||
"evaluation": {
|
||||
"accuracy_gate": "blocked-until-independent-truth-seal",
|
||||
"metrics": [
|
||||
"AP50",
|
||||
"AP50:95",
|
||||
"per-class recall",
|
||||
"person/vehicle miss rate",
|
||||
"false-large-box rate",
|
||||
"valid-FOV leakage",
|
||||
"temporal class-count flicker"
|
||||
],
|
||||
"winner_policy": "explicit-review-only"
|
||||
},
|
||||
"mode": "recorded-replay-only",
|
||||
"pipeline_id": "kb4-core3-yolox-eomt-k1-lidar-e23-temporal/v1",
|
||||
"profile_id": "RAVNOVES00_RIGHT_YOLOX_TRUTH_ISLAND_V1",
|
||||
"schema_version": "missioncore.l34-right-yolox-truth-island-profile/v1",
|
||||
"source_session_id": "20260720T065719Z_viewer_live",
|
||||
"target_classes": [
|
||||
"person",
|
||||
"bicycle",
|
||||
"motorcycle",
|
||||
"car",
|
||||
"heavy_vehicle",
|
||||
"static_obstacle",
|
||||
"animal"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build and validate the minimal content-addressed E46E Worker 006 package."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from k1link.compute.e46e_ready_stack import E46E_PACKAGE_SCHEMA, E46E_PROFILE_SCHEMA
|
||||
|
||||
_RUNTIME_FILES = {
|
||||
"runtime/k1link/__init__.py": "src/k1link/__init__.py",
|
||||
"runtime/k1link/compute/__init__.py": None,
|
||||
"runtime/k1link/compute/e46e_ready_stack.py": "src/k1link/compute/e46e_ready_stack.py",
|
||||
"runtime/run_e46e_ready_stack.py": (
|
||||
"experiments/perception/worker/run_e46e_ready_stack.py"
|
||||
),
|
||||
"runtime/Invoke-E46EReadyStack.ps1": (
|
||||
"experiments/perception/worker/Invoke-E46EReadyStack.ps1"
|
||||
),
|
||||
"runtime/Invoke-E46EReadyStackAsInteractiveUser.ps1": (
|
||||
"experiments/perception/worker/Invoke-E46EReadyStackAsInteractiveUser.ps1"
|
||||
),
|
||||
"runtime/Build-E46ENvidiaTaoParser.ps1": (
|
||||
"experiments/perception/worker/Build-E46ENvidiaTaoParser.ps1"
|
||||
),
|
||||
"runtime/Invoke-E46ENvidiaTaoParserAsInteractiveUser.ps1": (
|
||||
"experiments/perception/worker/Invoke-E46ENvidiaTaoParserAsInteractiveUser.ps1"
|
||||
),
|
||||
"runtime/e46e_deepstream_app.txt": (
|
||||
"experiments/perception/worker/e46e_deepstream_app.txt"
|
||||
),
|
||||
"runtime/e46e_trafficcamnet_rtdetr.txt": (
|
||||
"experiments/perception/worker/e46e_trafficcamnet_rtdetr.txt"
|
||||
),
|
||||
"runtime/e46e_trafficcamnet_labels.txt": (
|
||||
"experiments/perception/worker/e46e_trafficcamnet_labels.txt"
|
||||
),
|
||||
}
|
||||
_GENERATED_COMPUTE_INIT = (
|
||||
'"""Minimal E46E worker projection; import contract modules explicitly."""\n'
|
||||
)
|
||||
|
||||
|
||||
class E46EWorkerPackageError(RuntimeError):
|
||||
"""Raised when an E46E package is invalid."""
|
||||
|
||||
|
||||
def build_e46e_worker_package(
|
||||
*,
|
||||
repository_root: Path,
|
||||
profile_path: Path,
|
||||
parser_library_path: Path,
|
||||
output_root: Path,
|
||||
) -> Path:
|
||||
repository = repository_root.resolve(strict=True)
|
||||
profile_source = profile_path.resolve(strict=True)
|
||||
profile = _read_json(profile_source)
|
||||
if profile.get("schema_version") != E46E_PROFILE_SCHEMA:
|
||||
raise E46EWorkerPackageError("E46E package profile is incompatible")
|
||||
parser_library = parser_library_path.resolve(strict=True)
|
||||
parser_profile = profile.get("parser")
|
||||
if (
|
||||
not isinstance(parser_profile, dict)
|
||||
or parser_profile.get("library_file") != parser_library.name
|
||||
or parser_profile.get("library_sha256") != _sha256(parser_library)
|
||||
or parser_library.is_symlink()
|
||||
):
|
||||
raise E46EWorkerPackageError("E46E NVIDIA parser library is incompatible")
|
||||
sources: dict[str, Path | None] = {}
|
||||
for target, relative in _RUNTIME_FILES.items():
|
||||
source = None if relative is None else repository / relative
|
||||
if source is not None and (not source.is_file() or source.is_symlink()):
|
||||
raise E46EWorkerPackageError(f"E46E runtime source is invalid: {relative}")
|
||||
sources[target] = source
|
||||
sources["profile.json"] = profile_source
|
||||
sources[f"runtime/{parser_library.name}"] = parser_library
|
||||
descriptors = [
|
||||
_descriptor(relative, source)
|
||||
for relative, source in sorted(sources.items())
|
||||
]
|
||||
identity = {
|
||||
"schema_version": E46E_PACKAGE_SCHEMA,
|
||||
"classification": "minimal-stock-nvidia-recorded-right-worker-package",
|
||||
"profile_id": profile["profile_id"],
|
||||
"source_job_id": profile["source"]["job_id"],
|
||||
"profile_sha256": _sha256(profile_source),
|
||||
"artifact_paths": [row["path"] for row in descriptors],
|
||||
"source_artifacts": descriptors,
|
||||
"runtime": profile["runtime"],
|
||||
"detector": {
|
||||
"name": profile["detector"]["name"],
|
||||
"version": profile["detector"]["version"],
|
||||
"model_sha256": profile["detector"]["model_sha256"],
|
||||
},
|
||||
"parser": profile["parser"],
|
||||
"tracker": profile["tracker"],
|
||||
"authority": {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
package_id = f"e46e-worker-package-{identity_sha256}"
|
||||
output = output_root.expanduser().absolute()
|
||||
output.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
destination = output / package_id
|
||||
if destination.exists():
|
||||
validate_e46e_worker_package(destination)
|
||||
return destination
|
||||
staging = output / f".{package_id}.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
for relative, source in sources.items():
|
||||
target = staging / relative
|
||||
target.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
if source is None:
|
||||
target.write_text(_GENERATED_COMPUTE_INIT, encoding="utf-8")
|
||||
else:
|
||||
shutil.copyfile(source, target)
|
||||
artifacts = [
|
||||
{
|
||||
"kind": relative,
|
||||
"path": relative,
|
||||
"byte_length": (staging / relative).stat().st_size,
|
||||
"sha256": _sha256(staging / relative),
|
||||
}
|
||||
for relative in sorted(sources)
|
||||
]
|
||||
_write_json(
|
||||
staging / "manifest.json",
|
||||
{
|
||||
"schema_version": E46E_PACKAGE_SCHEMA,
|
||||
"package_id": package_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": datetime.now(UTC)
|
||||
.isoformat(timespec="milliseconds")
|
||||
.replace("+00:00", "Z"),
|
||||
"artifacts": artifacts,
|
||||
},
|
||||
)
|
||||
validate_e46e_worker_package(staging, allow_staging=True)
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
validate_e46e_worker_package(destination)
|
||||
return destination
|
||||
|
||||
|
||||
def validate_e46e_worker_package(
|
||||
root: Path, *, allow_staging: bool = False
|
||||
) -> dict[str, Any]:
|
||||
resolved = root.resolve(strict=True)
|
||||
manifest = _read_json(resolved / "manifest.json")
|
||||
identity = manifest.get("identity")
|
||||
digest = (
|
||||
hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
if isinstance(identity, dict)
|
||||
else ""
|
||||
)
|
||||
package_id = f"e46e-worker-package-{digest}"
|
||||
valid_name = resolved.name == package_id or (
|
||||
allow_staging
|
||||
and resolved.name.startswith(f".{package_id}.")
|
||||
and resolved.name.endswith(".tmp")
|
||||
)
|
||||
artifacts = manifest.get("artifacts")
|
||||
if (
|
||||
manifest.get("schema_version") != E46E_PACKAGE_SCHEMA
|
||||
or manifest.get("package_id") != package_id
|
||||
or manifest.get("identity_sha256") != digest
|
||||
or not valid_name
|
||||
or not isinstance(artifacts, list)
|
||||
):
|
||||
raise E46EWorkerPackageError("E46E worker package identity is invalid")
|
||||
expected_paths = set(identity.get("artifact_paths", []))
|
||||
actual_paths = {
|
||||
path.relative_to(resolved).as_posix()
|
||||
for path in resolved.rglob("*")
|
||||
if path.is_file()
|
||||
}
|
||||
if actual_paths != expected_paths | {"manifest.json"} or len(artifacts) != len(
|
||||
expected_paths
|
||||
):
|
||||
raise E46EWorkerPackageError("E46E worker package file set changed")
|
||||
observed: set[str] = set()
|
||||
for row in artifacts:
|
||||
if not isinstance(row, dict):
|
||||
raise E46EWorkerPackageError("E46E package artifact is invalid")
|
||||
relative = row.get("path")
|
||||
path = resolved / str(relative)
|
||||
if (
|
||||
not isinstance(relative, str)
|
||||
or relative not in expected_paths
|
||||
or relative in observed
|
||||
or Path(relative).is_absolute()
|
||||
or ".." in Path(relative).parts
|
||||
or not path.is_file()
|
||||
or path.is_symlink()
|
||||
or row.get("kind") != relative
|
||||
or row.get("byte_length") != path.stat().st_size
|
||||
or row.get("sha256") != _sha256(path)
|
||||
):
|
||||
raise E46EWorkerPackageError("E46E package artifact changed")
|
||||
observed.add(relative)
|
||||
if observed != expected_paths:
|
||||
raise E46EWorkerPackageError("E46E package coverage changed")
|
||||
return manifest
|
||||
|
||||
|
||||
def _descriptor(relative: str, source: Path | None) -> dict[str, Any]:
|
||||
payload = _GENERATED_COMPUTE_INIT.encode() if source is None else source.read_bytes()
|
||||
return {
|
||||
"path": relative,
|
||||
"byte_length": len(payload),
|
||||
"sha256": hashlib.sha256(payload).hexdigest(),
|
||||
}
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False).encode()
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
if not isinstance(value, dict):
|
||||
raise E46EWorkerPackageError(f"JSON object expected: {path.name}")
|
||||
return value
|
||||
|
||||
|
||||
def _write_json(path: Path, value: object) -> None:
|
||||
with path.open("x", encoding="utf-8") as stream:
|
||||
json.dump(value, stream, ensure_ascii=False, indent=2, allow_nan=False)
|
||||
stream.write("\n")
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--repository-root", type=Path, required=True)
|
||||
parser.add_argument("--profile", type=Path, required=True)
|
||||
parser.add_argument("--parser-library", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
package = build_e46e_worker_package(
|
||||
repository_root=args.repository_root,
|
||||
profile_path=args.profile,
|
||||
parser_library_path=args.parser_library,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
print(package)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build and validate the minimal content-addressed E46F Worker 006 package."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from k1link.compute.e46f_dashcam_bakeoff import (
|
||||
E46F_PACKAGE_SCHEMA,
|
||||
E46F_PROFILE_SCHEMA,
|
||||
)
|
||||
|
||||
_RUNTIME_FILES = {
|
||||
"runtime/k1link/__init__.py": "src/k1link/__init__.py",
|
||||
"runtime/k1link/compute/__init__.py": None,
|
||||
"runtime/k1link/compute/e46e_ready_stack.py": ("src/k1link/compute/e46e_ready_stack.py"),
|
||||
"runtime/k1link/compute/e46f_dashcam_bakeoff.py": (
|
||||
"src/k1link/compute/e46f_dashcam_bakeoff.py"
|
||||
),
|
||||
"runtime/run_e46f_dashcam_bakeoff.py": (
|
||||
"experiments/perception/worker/run_e46f_dashcam_bakeoff.py"
|
||||
),
|
||||
"runtime/Invoke-E46FDashCamBakeoff.ps1": (
|
||||
"experiments/perception/worker/Invoke-E46FDashCamBakeoff.ps1"
|
||||
),
|
||||
"runtime/Invoke-E46FDashCamBakeoffAsInteractiveUser.ps1": (
|
||||
"experiments/perception/worker/Invoke-E46FDashCamBakeoffAsInteractiveUser.ps1"
|
||||
),
|
||||
"runtime/e46f_deepstream_app.txt": ("experiments/perception/worker/e46f_deepstream_app.txt"),
|
||||
"runtime/e46f_dashcamnet_detectnet.txt": (
|
||||
"experiments/perception/worker/e46f_dashcamnet_detectnet.txt"
|
||||
),
|
||||
"runtime/e46f_dashcamnet_labels.txt": (
|
||||
"experiments/perception/worker/e46f_dashcamnet_labels.txt"
|
||||
),
|
||||
}
|
||||
_GENERATED_COMPUTE_INIT = (
|
||||
'"""Minimal E46F worker projection; import contract modules explicitly."""\n'
|
||||
)
|
||||
|
||||
|
||||
class E46FWorkerPackageError(RuntimeError):
|
||||
"""Raised when an E46F package is invalid."""
|
||||
|
||||
|
||||
def build_e46f_worker_package(
|
||||
*, repository_root: Path, profile_path: Path, output_root: Path
|
||||
) -> Path:
|
||||
repository = repository_root.resolve(strict=True)
|
||||
profile_source = profile_path.resolve(strict=True)
|
||||
profile = _read_json(profile_source)
|
||||
if profile.get("schema_version") != E46F_PROFILE_SCHEMA:
|
||||
raise E46FWorkerPackageError("E46F package profile is incompatible")
|
||||
sources: dict[str, Path | None] = {}
|
||||
for target, relative in _RUNTIME_FILES.items():
|
||||
source = None if relative is None else repository / relative
|
||||
if source is not None and (not source.is_file() or source.is_symlink()):
|
||||
raise E46FWorkerPackageError(f"E46F runtime source is invalid: {relative}")
|
||||
sources[target] = source
|
||||
sources["profile.json"] = profile_source
|
||||
descriptors = [_descriptor(relative, source) for relative, source in sorted(sources.items())]
|
||||
identity = {
|
||||
"schema_version": E46F_PACKAGE_SCHEMA,
|
||||
"classification": "minimal-stock-nvidia-detector-only-bakeoff-package",
|
||||
"profile_id": profile["profile_id"],
|
||||
"source_job_id": profile["source"]["job_id"],
|
||||
"baseline_result_id": profile["comparison_contract"]["baseline_result_id"],
|
||||
"profile_sha256": _sha256(profile_source),
|
||||
"artifact_paths": [row["path"] for row in descriptors],
|
||||
"source_artifacts": descriptors,
|
||||
"runtime": profile["runtime"],
|
||||
"detector": {
|
||||
"name": profile["detector"]["name"],
|
||||
"version": profile["detector"]["version"],
|
||||
"model_sha256": profile["detector"]["model_sha256"],
|
||||
},
|
||||
"postprocessor": profile["postprocessor"],
|
||||
"tracker": profile["tracker"],
|
||||
"authority": {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
package_id = f"e46f-worker-package-{identity_sha256}"
|
||||
output = output_root.expanduser().absolute()
|
||||
output.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
destination = output / package_id
|
||||
if destination.exists():
|
||||
validate_e46f_worker_package(destination)
|
||||
return destination
|
||||
staging = output / f".{package_id}.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
for relative, source in sources.items():
|
||||
target = staging / relative
|
||||
target.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
if source is None:
|
||||
target.write_text(_GENERATED_COMPUTE_INIT, encoding="utf-8")
|
||||
else:
|
||||
shutil.copyfile(source, target)
|
||||
artifacts = [
|
||||
{
|
||||
"kind": relative,
|
||||
"path": relative,
|
||||
"byte_length": (staging / relative).stat().st_size,
|
||||
"sha256": _sha256(staging / relative),
|
||||
}
|
||||
for relative in sorted(sources)
|
||||
]
|
||||
_write_json(
|
||||
staging / "manifest.json",
|
||||
{
|
||||
"schema_version": E46F_PACKAGE_SCHEMA,
|
||||
"package_id": package_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": datetime.now(UTC)
|
||||
.isoformat(timespec="milliseconds")
|
||||
.replace("+00:00", "Z"),
|
||||
"artifacts": artifacts,
|
||||
},
|
||||
)
|
||||
validate_e46f_worker_package(staging, allow_staging=True)
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
validate_e46f_worker_package(destination)
|
||||
return destination
|
||||
|
||||
|
||||
def validate_e46f_worker_package(root: Path, *, allow_staging: bool = False) -> dict[str, Any]:
|
||||
resolved = root.resolve(strict=True)
|
||||
manifest = _read_json(resolved / "manifest.json")
|
||||
identity = manifest.get("identity")
|
||||
digest = (
|
||||
hashlib.sha256(_canonical_json(identity)).hexdigest() if isinstance(identity, dict) else ""
|
||||
)
|
||||
package_id = f"e46f-worker-package-{digest}"
|
||||
valid_name = resolved.name == package_id or (
|
||||
allow_staging
|
||||
and resolved.name.startswith(f".{package_id}.")
|
||||
and resolved.name.endswith(".tmp")
|
||||
)
|
||||
artifacts = manifest.get("artifacts")
|
||||
if (
|
||||
manifest.get("schema_version") != E46F_PACKAGE_SCHEMA
|
||||
or manifest.get("package_id") != package_id
|
||||
or manifest.get("identity_sha256") != digest
|
||||
or not valid_name
|
||||
or not isinstance(artifacts, list)
|
||||
):
|
||||
raise E46FWorkerPackageError("E46F worker package identity is invalid")
|
||||
expected_paths = set(identity.get("artifact_paths", []))
|
||||
actual_paths = {
|
||||
path.relative_to(resolved).as_posix() for path in resolved.rglob("*") if path.is_file()
|
||||
}
|
||||
if actual_paths != expected_paths | {"manifest.json"} or len(artifacts) != len(expected_paths):
|
||||
raise E46FWorkerPackageError("E46F worker package file set changed")
|
||||
observed: set[str] = set()
|
||||
for row in artifacts:
|
||||
if not isinstance(row, dict):
|
||||
raise E46FWorkerPackageError("E46F package artifact is invalid")
|
||||
relative = row.get("path")
|
||||
path = resolved / str(relative)
|
||||
if (
|
||||
not isinstance(relative, str)
|
||||
or relative not in expected_paths
|
||||
or relative in observed
|
||||
or Path(relative).is_absolute()
|
||||
or ".." in Path(relative).parts
|
||||
or not path.is_file()
|
||||
or path.is_symlink()
|
||||
or row.get("kind") != relative
|
||||
or row.get("byte_length") != path.stat().st_size
|
||||
or row.get("sha256") != _sha256(path)
|
||||
):
|
||||
raise E46FWorkerPackageError("E46F package artifact changed")
|
||||
observed.add(relative)
|
||||
if observed != expected_paths:
|
||||
raise E46FWorkerPackageError("E46F package coverage changed")
|
||||
return manifest
|
||||
|
||||
|
||||
def _descriptor(relative: str, source: Path | None) -> dict[str, Any]:
|
||||
payload = _GENERATED_COMPUTE_INIT.encode() if source is None else source.read_bytes()
|
||||
return {
|
||||
"path": relative,
|
||||
"byte_length": len(payload),
|
||||
"sha256": hashlib.sha256(payload).hexdigest(),
|
||||
}
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False).encode()
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
if not isinstance(value, dict):
|
||||
raise E46FWorkerPackageError(f"JSON object expected: {path.name}")
|
||||
return value
|
||||
|
||||
|
||||
def _write_json(path: Path, value: object) -> None:
|
||||
with path.open("x", encoding="utf-8") as stream:
|
||||
json.dump(value, stream, ensure_ascii=False, indent=2, allow_nan=False)
|
||||
stream.write("\n")
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--repository-root", type=Path, required=True)
|
||||
parser.add_argument("--profile", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
package = build_e46f_worker_package(
|
||||
repository_root=args.repository_root,
|
||||
profile_path=args.profile,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
print(package)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,288 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build and validate the minimal content-addressed E46G Worker package."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from k1link.compute.e46g_rectified_detector_bakeoff import (
|
||||
E46G_PACKAGE_SCHEMA,
|
||||
E46G_PROFILE_SCHEMA,
|
||||
)
|
||||
|
||||
_RUNTIME_FILES = {
|
||||
"runtime/k1link/__init__.py": "src/k1link/__init__.py",
|
||||
"runtime/k1link/compute/__init__.py": None,
|
||||
"runtime/k1link/compute/e46e_ready_stack.py": "src/k1link/compute/e46e_ready_stack.py",
|
||||
"runtime/k1link/compute/e46g_rectified_detector_bakeoff.py": (
|
||||
"src/k1link/compute/e46g_rectified_detector_bakeoff.py"
|
||||
),
|
||||
"runtime/run_e46g_rectified_detector_bakeoff.py": (
|
||||
"experiments/perception/worker/run_e46g_rectified_detector_bakeoff.py"
|
||||
),
|
||||
"runtime/Invoke-E46GRectifiedDetectorBakeoff.ps1": (
|
||||
"experiments/perception/worker/Invoke-E46GRectifiedDetectorBakeoff.ps1"
|
||||
),
|
||||
"runtime/Invoke-E46GRectifiedDetectorBakeoffAsInteractiveUser.ps1": (
|
||||
"experiments/perception/worker/Invoke-E46GRectifiedDetectorBakeoffAsInteractiveUser.ps1"
|
||||
),
|
||||
"runtime/Prepare-RectifiedCameraReplay.ps1": (
|
||||
"experiments/perception/worker/Prepare-RectifiedCameraReplay.ps1"
|
||||
),
|
||||
"runtime/e46g_nvdewarper_left.txt": ("experiments/perception/worker/e46g_nvdewarper_left.txt"),
|
||||
"runtime/e46g_nvdewarper_front.txt": (
|
||||
"experiments/perception/worker/e46g_nvdewarper_front.txt"
|
||||
),
|
||||
"runtime/e46g_nvdewarper_right.txt": (
|
||||
"experiments/perception/worker/e46g_nvdewarper_right.txt"
|
||||
),
|
||||
"runtime/e46g_trafficcamnet_deepstream_app.txt": (
|
||||
"experiments/perception/worker/e46g_trafficcamnet_deepstream_app.txt"
|
||||
),
|
||||
"runtime/e46g_dashcamnet_deepstream_app.txt": (
|
||||
"experiments/perception/worker/e46g_dashcamnet_deepstream_app.txt"
|
||||
),
|
||||
"runtime/e46e_trafficcamnet_rtdetr.txt": (
|
||||
"experiments/perception/worker/e46e_trafficcamnet_rtdetr.txt"
|
||||
),
|
||||
"runtime/e46e_trafficcamnet_labels.txt": (
|
||||
"experiments/perception/worker/e46e_trafficcamnet_labels.txt"
|
||||
),
|
||||
"runtime/e46f_dashcamnet_detectnet.txt": (
|
||||
"experiments/perception/worker/e46f_dashcamnet_detectnet.txt"
|
||||
),
|
||||
"runtime/e46f_dashcamnet_labels.txt": (
|
||||
"experiments/perception/worker/e46f_dashcamnet_labels.txt"
|
||||
),
|
||||
}
|
||||
_GENERATED_COMPUTE_INIT = (
|
||||
'"""Minimal E46G worker projection; import contract modules explicitly."""\n'
|
||||
)
|
||||
|
||||
|
||||
class E46GWorkerPackageError(RuntimeError):
|
||||
"""Raised when an E46G package is invalid."""
|
||||
|
||||
|
||||
def build_e46g_worker_package(
|
||||
*,
|
||||
repository_root: Path,
|
||||
profile_path: Path,
|
||||
parser_library_path: Path,
|
||||
output_root: Path,
|
||||
) -> Path:
|
||||
repository = repository_root.resolve(strict=True)
|
||||
profile_source = profile_path.resolve(strict=True)
|
||||
profile = _read_json(profile_source)
|
||||
if profile.get("schema_version") != E46G_PROFILE_SCHEMA:
|
||||
raise E46GWorkerPackageError("E46G package profile is incompatible")
|
||||
parser_library = parser_library_path.resolve(strict=True)
|
||||
parser_profile = profile.get("trafficcamnet_parser")
|
||||
if (
|
||||
not isinstance(parser_profile, dict)
|
||||
or parser_profile.get("library_file") != parser_library.name
|
||||
or parser_profile.get("library_sha256") != _sha256(parser_library)
|
||||
or parser_library.is_symlink()
|
||||
):
|
||||
raise E46GWorkerPackageError("E46G NVIDIA parser library is incompatible")
|
||||
sources: dict[str, Path | None] = {}
|
||||
for target, relative in _RUNTIME_FILES.items():
|
||||
source = None if relative is None else repository / relative
|
||||
if source is not None and (not source.is_file() or source.is_symlink()):
|
||||
raise E46GWorkerPackageError(f"E46G runtime source is invalid: {relative}")
|
||||
sources[target] = source
|
||||
sources["profile.json"] = profile_source
|
||||
sources[f"runtime/{parser_library.name}"] = parser_library
|
||||
descriptors = [_descriptor(relative, source) for relative, source in sorted(sources.items())]
|
||||
identity = {
|
||||
"schema_version": E46G_PACKAGE_SCHEMA,
|
||||
"classification": "minimal-factory-kb4-stock-nvidia-detector-bakeoff-package",
|
||||
"profile_id": profile["profile_id"],
|
||||
"source_job_id": profile["source"]["job_id"],
|
||||
"profile_sha256": _sha256(profile_source),
|
||||
"artifact_paths": [row["path"] for row in descriptors],
|
||||
"source_artifacts": descriptors,
|
||||
"runtime": profile["runtime"],
|
||||
"calibration": profile["calibration"],
|
||||
"rectification": profile["rectification"],
|
||||
"selection": profile["selection"],
|
||||
"candidates": {
|
||||
name: {
|
||||
"name": value["name"],
|
||||
"version": value["version"],
|
||||
"model_sha256": value["model_sha256"],
|
||||
"custom_postprocessing": value["custom_postprocessing"],
|
||||
}
|
||||
for name, value in sorted(profile["candidates"].items())
|
||||
},
|
||||
"tracker": profile["tracker"],
|
||||
"authority": {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
package_id = f"e46g-worker-package-{identity_sha256}"
|
||||
output = output_root.expanduser().absolute()
|
||||
output.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
destination = output / package_id
|
||||
if destination.exists():
|
||||
validate_e46g_worker_package(destination)
|
||||
return destination
|
||||
staging = output / f".{package_id}.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
for relative, source in sources.items():
|
||||
target = staging / relative
|
||||
target.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
if source is None:
|
||||
target.write_text(_GENERATED_COMPUTE_INIT, encoding="utf-8")
|
||||
else:
|
||||
shutil.copyfile(source, target)
|
||||
artifacts = [
|
||||
{
|
||||
"kind": relative,
|
||||
"path": relative,
|
||||
"byte_length": (staging / relative).stat().st_size,
|
||||
"sha256": _sha256(staging / relative),
|
||||
}
|
||||
for relative in sorted(sources)
|
||||
]
|
||||
_write_json(
|
||||
staging / "manifest.json",
|
||||
{
|
||||
"schema_version": E46G_PACKAGE_SCHEMA,
|
||||
"package_id": package_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": datetime.now(UTC)
|
||||
.isoformat(timespec="milliseconds")
|
||||
.replace("+00:00", "Z"),
|
||||
"artifacts": artifacts,
|
||||
},
|
||||
)
|
||||
validate_e46g_worker_package(staging, allow_staging=True)
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
validate_e46g_worker_package(destination)
|
||||
return destination
|
||||
|
||||
|
||||
def validate_e46g_worker_package(root: Path, *, allow_staging: bool = False) -> dict[str, Any]:
|
||||
resolved = root.resolve(strict=True)
|
||||
manifest = _read_json(resolved / "manifest.json")
|
||||
identity = manifest.get("identity")
|
||||
digest = (
|
||||
hashlib.sha256(_canonical_json(identity)).hexdigest() if isinstance(identity, dict) else ""
|
||||
)
|
||||
package_id = f"e46g-worker-package-{digest}"
|
||||
valid_name = resolved.name == package_id or (
|
||||
allow_staging
|
||||
and resolved.name.startswith(f".{package_id}.")
|
||||
and resolved.name.endswith(".tmp")
|
||||
)
|
||||
artifacts = manifest.get("artifacts")
|
||||
if (
|
||||
manifest.get("schema_version") != E46G_PACKAGE_SCHEMA
|
||||
or manifest.get("package_id") != package_id
|
||||
or manifest.get("identity_sha256") != digest
|
||||
or not valid_name
|
||||
or not isinstance(artifacts, list)
|
||||
):
|
||||
raise E46GWorkerPackageError("E46G worker package identity is invalid")
|
||||
expected_paths = set(identity.get("artifact_paths", []))
|
||||
actual_paths = {
|
||||
path.relative_to(resolved).as_posix() for path in resolved.rglob("*") if path.is_file()
|
||||
}
|
||||
if actual_paths != expected_paths | {"manifest.json"} or len(artifacts) != len(expected_paths):
|
||||
raise E46GWorkerPackageError("E46G worker package file set changed")
|
||||
observed: set[str] = set()
|
||||
for row in artifacts:
|
||||
if not isinstance(row, dict):
|
||||
raise E46GWorkerPackageError("E46G package artifact is invalid")
|
||||
relative = row.get("path")
|
||||
path = resolved / str(relative)
|
||||
if (
|
||||
not isinstance(relative, str)
|
||||
or relative not in expected_paths
|
||||
or relative in observed
|
||||
or Path(relative).is_absolute()
|
||||
or ".." in Path(relative).parts
|
||||
or not path.is_file()
|
||||
or path.is_symlink()
|
||||
or row.get("kind") != relative
|
||||
or row.get("byte_length") != path.stat().st_size
|
||||
or row.get("sha256") != _sha256(path)
|
||||
):
|
||||
raise E46GWorkerPackageError("E46G package artifact changed")
|
||||
observed.add(relative)
|
||||
if observed != expected_paths:
|
||||
raise E46GWorkerPackageError("E46G package coverage changed")
|
||||
return manifest
|
||||
|
||||
|
||||
def _descriptor(relative: str, source: Path | None) -> dict[str, Any]:
|
||||
payload = _GENERATED_COMPUTE_INIT.encode() if source is None else source.read_bytes()
|
||||
return {
|
||||
"path": relative,
|
||||
"byte_length": len(payload),
|
||||
"sha256": hashlib.sha256(payload).hexdigest(),
|
||||
}
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False).encode()
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
if not isinstance(value, dict):
|
||||
raise E46GWorkerPackageError(f"JSON object expected: {path.name}")
|
||||
return value
|
||||
|
||||
|
||||
def _write_json(path: Path, value: object) -> None:
|
||||
with path.open("x", encoding="utf-8") as stream:
|
||||
json.dump(value, stream, ensure_ascii=False, indent=2, allow_nan=False)
|
||||
stream.write("\n")
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--repository-root", type=Path, required=True)
|
||||
parser.add_argument("--profile", type=Path, required=True)
|
||||
parser.add_argument("--parser-library", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
package = build_e46g_worker_package(
|
||||
repository_root=args.repository_root,
|
||||
profile_path=args.profile,
|
||||
parser_library_path=args.parser_library,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
print(package)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,276 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build and validate the minimal content-addressed E46H Worker package."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from k1link.compute.e46h_full_rectified_front_replay import (
|
||||
E46H_PACKAGE_SCHEMA,
|
||||
E46H_PROFILE_SCHEMA,
|
||||
)
|
||||
|
||||
_RUNTIME_FILES = {
|
||||
"runtime/k1link/__init__.py": "src/k1link/__init__.py",
|
||||
"runtime/k1link/compute/__init__.py": None,
|
||||
"runtime/k1link/compute/e46e_ready_stack.py": "src/k1link/compute/e46e_ready_stack.py",
|
||||
"runtime/k1link/compute/e46g_rectified_detector_bakeoff.py": (
|
||||
"src/k1link/compute/e46g_rectified_detector_bakeoff.py"
|
||||
),
|
||||
"runtime/k1link/compute/e46h_full_rectified_front_replay.py": (
|
||||
"src/k1link/compute/e46h_full_rectified_front_replay.py"
|
||||
),
|
||||
"runtime/run_e46h_full_rectified_front_replay.py": (
|
||||
"experiments/perception/worker/run_e46h_full_rectified_front_replay.py"
|
||||
),
|
||||
"runtime/Invoke-E46HFullRectifiedFrontReplay.ps1": (
|
||||
"experiments/perception/worker/Invoke-E46HFullRectifiedFrontReplay.ps1"
|
||||
),
|
||||
"runtime/Invoke-E46HFullRectifiedFrontReplayAsInteractiveUser.ps1": (
|
||||
"experiments/perception/worker/Invoke-E46HFullRectifiedFrontReplayAsInteractiveUser.ps1"
|
||||
),
|
||||
"runtime/Prepare-RectifiedCameraReplay.ps1": (
|
||||
"experiments/perception/worker/Prepare-RectifiedCameraReplay.ps1"
|
||||
),
|
||||
"runtime/e46g_nvdewarper_front.txt": (
|
||||
"experiments/perception/worker/e46g_nvdewarper_front.txt"
|
||||
),
|
||||
"runtime/e46g_trafficcamnet_deepstream_app.txt": (
|
||||
"experiments/perception/worker/e46g_trafficcamnet_deepstream_app.txt"
|
||||
),
|
||||
"runtime/e46e_trafficcamnet_rtdetr.txt": (
|
||||
"experiments/perception/worker/e46e_trafficcamnet_rtdetr.txt"
|
||||
),
|
||||
"runtime/e46e_trafficcamnet_labels.txt": (
|
||||
"experiments/perception/worker/e46e_trafficcamnet_labels.txt"
|
||||
),
|
||||
}
|
||||
_GENERATED_COMPUTE_INIT = (
|
||||
'"""Minimal E46H worker projection; import contract modules explicitly."""\n'
|
||||
)
|
||||
|
||||
|
||||
class E46HWorkerPackageError(RuntimeError):
|
||||
"""Raised when an E46H package is invalid."""
|
||||
|
||||
|
||||
def build_e46h_worker_package(
|
||||
*,
|
||||
repository_root: Path,
|
||||
profile_path: Path,
|
||||
parser_library_path: Path,
|
||||
output_root: Path,
|
||||
) -> Path:
|
||||
repository = repository_root.resolve(strict=True)
|
||||
profile_source = profile_path.resolve(strict=True)
|
||||
profile = _read_json(profile_source)
|
||||
if profile.get("schema_version") != E46H_PROFILE_SCHEMA:
|
||||
raise E46HWorkerPackageError("E46H package profile is incompatible")
|
||||
parser_library = parser_library_path.resolve(strict=True)
|
||||
parser = profile.get("parser")
|
||||
if (
|
||||
not isinstance(parser, dict)
|
||||
or parser.get("library_file") != parser_library.name
|
||||
or parser.get("library_sha256") != _sha256(parser_library)
|
||||
or parser_library.is_symlink()
|
||||
):
|
||||
raise E46HWorkerPackageError("E46H NVIDIA parser library is incompatible")
|
||||
sources: dict[str, Path | None] = {}
|
||||
for target, relative in _RUNTIME_FILES.items():
|
||||
source = None if relative is None else repository / relative
|
||||
if source is not None and (not source.is_file() or source.is_symlink()):
|
||||
raise E46HWorkerPackageError(f"E46H runtime source is invalid: {relative}")
|
||||
sources[target] = source
|
||||
sources["profile.json"] = profile_source
|
||||
sources[f"runtime/{parser_library.name}"] = parser_library
|
||||
descriptors = [_descriptor(relative, source) for relative, source in sorted(sources.items())]
|
||||
identity = {
|
||||
"schema_version": E46H_PACKAGE_SCHEMA,
|
||||
"classification": "minimal-full-front-stock-nvidia-replay-package",
|
||||
"profile_id": profile["profile_id"],
|
||||
"baseline_result_id": profile["baseline_result_id"],
|
||||
"source_job_id": profile["source"]["job_id"],
|
||||
"profile_sha256": _sha256(profile_source),
|
||||
"artifact_paths": [row["path"] for row in descriptors],
|
||||
"source_artifacts": descriptors,
|
||||
"runtime": profile["runtime"],
|
||||
"calibration": profile["calibration"],
|
||||
"rectification": profile["rectification"],
|
||||
"selection": profile["selection"],
|
||||
"detector": {
|
||||
"name": profile["detector"]["name"],
|
||||
"version": profile["detector"]["version"],
|
||||
"model_sha256": profile["detector"]["model_sha256"],
|
||||
"custom_postprocessing": profile["detector"]["custom_postprocessing"],
|
||||
},
|
||||
"tracker": profile["tracker"],
|
||||
"authority": {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
package_id = f"e46h-worker-package-{identity_sha256}"
|
||||
output = output_root.expanduser().absolute()
|
||||
output.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
destination = output / package_id
|
||||
if destination.exists():
|
||||
validate_e46h_worker_package(destination)
|
||||
return destination
|
||||
staging = output / f".{package_id}.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
for relative, source in sources.items():
|
||||
target = staging / relative
|
||||
target.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
if source is None:
|
||||
target.write_text(_GENERATED_COMPUTE_INIT, encoding="utf-8")
|
||||
else:
|
||||
shutil.copyfile(source, target)
|
||||
artifacts = [
|
||||
{
|
||||
"kind": relative,
|
||||
"path": relative,
|
||||
"byte_length": (staging / relative).stat().st_size,
|
||||
"sha256": _sha256(staging / relative),
|
||||
}
|
||||
for relative in sorted(sources)
|
||||
]
|
||||
_write_json(
|
||||
staging / "manifest.json",
|
||||
{
|
||||
"schema_version": E46H_PACKAGE_SCHEMA,
|
||||
"package_id": package_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": datetime.now(UTC)
|
||||
.isoformat(timespec="milliseconds")
|
||||
.replace("+00:00", "Z"),
|
||||
"artifacts": artifacts,
|
||||
},
|
||||
)
|
||||
validate_e46h_worker_package(staging, allow_staging=True)
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
validate_e46h_worker_package(destination)
|
||||
return destination
|
||||
|
||||
|
||||
def validate_e46h_worker_package(root: Path, *, allow_staging: bool = False) -> dict[str, Any]:
|
||||
resolved = root.resolve(strict=True)
|
||||
manifest = _read_json(resolved / "manifest.json")
|
||||
identity = manifest.get("identity")
|
||||
digest = (
|
||||
hashlib.sha256(_canonical_json(identity)).hexdigest() if isinstance(identity, dict) else ""
|
||||
)
|
||||
package_id = f"e46h-worker-package-{digest}"
|
||||
valid_name = resolved.name == package_id or (
|
||||
allow_staging
|
||||
and resolved.name.startswith(f".{package_id}.")
|
||||
and resolved.name.endswith(".tmp")
|
||||
)
|
||||
artifacts = manifest.get("artifacts")
|
||||
if (
|
||||
manifest.get("schema_version") != E46H_PACKAGE_SCHEMA
|
||||
or manifest.get("package_id") != package_id
|
||||
or manifest.get("identity_sha256") != digest
|
||||
or not valid_name
|
||||
or not isinstance(artifacts, list)
|
||||
):
|
||||
raise E46HWorkerPackageError("E46H worker package identity is invalid")
|
||||
expected_paths = set(identity.get("artifact_paths", []))
|
||||
actual_paths = {
|
||||
path.relative_to(resolved).as_posix() for path in resolved.rglob("*") if path.is_file()
|
||||
}
|
||||
if actual_paths != expected_paths | {"manifest.json"} or len(artifacts) != len(expected_paths):
|
||||
raise E46HWorkerPackageError("E46H worker package file set changed")
|
||||
observed: set[str] = set()
|
||||
for row in artifacts:
|
||||
if not isinstance(row, dict):
|
||||
raise E46HWorkerPackageError("E46H package artifact is invalid")
|
||||
relative = row.get("path")
|
||||
path = resolved / str(relative)
|
||||
if (
|
||||
not isinstance(relative, str)
|
||||
or relative not in expected_paths
|
||||
or relative in observed
|
||||
or Path(relative).is_absolute()
|
||||
or ".." in Path(relative).parts
|
||||
or not path.is_file()
|
||||
or path.is_symlink()
|
||||
or row.get("kind") != relative
|
||||
or row.get("byte_length") != path.stat().st_size
|
||||
or row.get("sha256") != _sha256(path)
|
||||
):
|
||||
raise E46HWorkerPackageError("E46H package artifact changed")
|
||||
observed.add(relative)
|
||||
if observed != expected_paths:
|
||||
raise E46HWorkerPackageError("E46H package coverage changed")
|
||||
return manifest
|
||||
|
||||
|
||||
def _descriptor(relative: str, source: Path | None) -> dict[str, Any]:
|
||||
payload = _GENERATED_COMPUTE_INIT.encode() if source is None else source.read_bytes()
|
||||
return {
|
||||
"path": relative,
|
||||
"byte_length": len(payload),
|
||||
"sha256": hashlib.sha256(payload).hexdigest(),
|
||||
}
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False).encode()
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
if not isinstance(value, dict):
|
||||
raise E46HWorkerPackageError(f"JSON object expected: {path.name}")
|
||||
return value
|
||||
|
||||
|
||||
def _write_json(path: Path, value: object) -> None:
|
||||
with path.open("x", encoding="utf-8") as stream:
|
||||
json.dump(value, stream, ensure_ascii=False, indent=2, allow_nan=False)
|
||||
stream.write("\n")
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--repository-root", type=Path, required=True)
|
||||
parser.add_argument("--profile", type=Path, required=True)
|
||||
parser.add_argument("--parser-library", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
package = build_e46h_worker_package(
|
||||
repository_root=args.repository_root,
|
||||
profile_path=args.profile,
|
||||
parser_library_path=args.parser_library,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
print(package)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the immutable E46B recorded RIGHT temporal-motion result."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.e46b_temporal_motion import build_e46b_temporal_motion
|
||||
|
||||
|
||||
def main() -> None:
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
result = build_e46b_temporal_motion(
|
||||
e46a_root=(
|
||||
root
|
||||
/ ".runtime/compute-experiments/e46a/ai-engineering-preannotations"
|
||||
/ (
|
||||
"e46a-ai-engineering-preannotation-"
|
||||
"37cab05e1168cd6004202b890f2c7a877ab02d036afd0891a11a4df61d4d26bf"
|
||||
)
|
||||
),
|
||||
e26_root=(
|
||||
root
|
||||
/ ".runtime/compute-experiments/e10/worker-results"
|
||||
/ (
|
||||
"e10-integrated-perception-"
|
||||
"459aac93918d8f6414b342986ccc6968fefcef6c1f3a78a5254df0b565255ad2"
|
||||
)
|
||||
),
|
||||
output_root=root / ".runtime/compute-experiments/e46b/temporal-motion",
|
||||
)
|
||||
print(result["result_id"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the immutable E46C full recorded route/world-track result."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.e46c_full_replay_world_tracks import (
|
||||
build_e46c_full_replay_world_tracks,
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
result = build_e46c_full_replay_world_tracks(
|
||||
e46a_root=(
|
||||
root
|
||||
/ ".runtime/compute-experiments/e46a/ai-engineering-preannotations"
|
||||
/ (
|
||||
"e46a-ai-engineering-preannotation-"
|
||||
"37cab05e1168cd6004202b890f2c7a877ab02d036afd0891a11a4df61d4d26bf"
|
||||
)
|
||||
),
|
||||
e26_root=(
|
||||
root
|
||||
/ ".runtime/compute-experiments/e10/worker-results"
|
||||
/ (
|
||||
"e10-integrated-perception-"
|
||||
"459aac93918d8f6414b342986ccc6968fefcef6c1f3a78a5254df0b565255ad2"
|
||||
)
|
||||
),
|
||||
output_root=(
|
||||
root / ".runtime/compute-experiments/e46c/full-replay-world-tracks"
|
||||
),
|
||||
)
|
||||
print(result["result_id"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the immutable E46D full-route temporal-failure audit."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.e46d_temporal_failure_audit import (
|
||||
build_e46d_temporal_failure_audit,
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
result = build_e46d_temporal_failure_audit(
|
||||
e46c_root=(
|
||||
root
|
||||
/ ".runtime/compute-experiments/e46c/full-replay-world-tracks"
|
||||
/ (
|
||||
"e46c-full-replay-world-tracks-"
|
||||
"98ca4aeb9839082be64c1ce375ea773cff290e08cf085d8d24bb29e11d6fba8d"
|
||||
)
|
||||
),
|
||||
e26_results_root=root / ".runtime/compute-experiments/e10/worker-results",
|
||||
output_root=root / ".runtime/compute-experiments/e46d/temporal-failure-audits",
|
||||
)
|
||||
print(result["result_id"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze validated E46I Grounding DINO full-route evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.e46i_grounding_dino_full_replay import (
|
||||
build_e46i_grounding_dino_full_replay,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--raw-root", type=Path, required=True)
|
||||
parser.add_argument("--profile", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
result = build_e46i_grounding_dino_full_replay(
|
||||
raw_root=args.raw_root,
|
||||
profile_path=args.profile,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
print(result["result_root"])
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze one validated E46J worker run as an immutable LAB result."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.e46j_raw_fisheye_realtime import (
|
||||
build_e46j_raw_fisheye_realtime,
|
||||
)
|
||||
|
||||
|
||||
def arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--raw-root", type=Path, required=True)
|
||||
parser.add_argument("--profile", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = arguments()
|
||||
frozen = build_e46j_raw_fisheye_realtime(
|
||||
raw_root=args.raw_root,
|
||||
profile_path=args.profile,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"result_id": frozen["result_id"],
|
||||
"result_root": str(frozen["result_root"]),
|
||||
"status": frozen["report"]["status"],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze the first recorded right-camera benchmark candidate."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.l34_right_yolox_truth_island_freeze import (
|
||||
build_l34_right_yolox_truth_island_freeze,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--profile", type=Path, required=True)
|
||||
parser.add_argument("--truth-island-root", type=Path, required=True)
|
||||
parser.add_argument("--detector-qualification-root", type=Path, required=True)
|
||||
parser.add_argument("--l33-result-root", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
result = build_l34_right_yolox_truth_island_freeze(
|
||||
profile_path=args.profile,
|
||||
truth_island_root=args.truth_island_root,
|
||||
detector_qualification_root=args.detector_qualification_root,
|
||||
l33_result_root=args.l33_result_root,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"result_id": result.result_id,
|
||||
"result_root": str(result.result_root),
|
||||
"status": result.report["status"],
|
||||
"metrics": result.report["metrics"],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the non-truth assisted error audit for the exact L3.4 freeze."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.l34a_assisted_yolox_error_audit import (
|
||||
build_l34a_assisted_yolox_error_audit,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--l34-freeze-root", type=Path, required=True)
|
||||
parser.add_argument("--annotation-session", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
result = build_l34a_assisted_yolox_error_audit(
|
||||
l34_freeze_root=args.l34_freeze_root,
|
||||
annotation_session_path=args.annotation_session,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
report = result["report"]
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"result_id": result["result_id"],
|
||||
"result_root": str(result["result_root"]),
|
||||
"status": report["status"],
|
||||
"metrics": report["metrics"],
|
||||
"ground_truth": report["ground_truth"],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the L3.4B nested-box consolidation shadow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.l34b_nested_box_consolidation_shadow import (
|
||||
build_l34b_nested_box_consolidation_shadow,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--l34-freeze-root", type=Path, required=True)
|
||||
parser.add_argument("--l34a-audit-root", type=Path, required=True)
|
||||
parser.add_argument("--annotation-session", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
result = build_l34b_nested_box_consolidation_shadow(
|
||||
l34_freeze_root=args.l34_freeze_root,
|
||||
l34a_audit_root=args.l34a_audit_root,
|
||||
annotation_session_path=args.annotation_session,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
report = result["report"]
|
||||
print(json.dumps({
|
||||
"result_id": result["result_id"],
|
||||
"result_root": str(result["result_root"]),
|
||||
"status": report["status"],
|
||||
"metrics": report["metrics"],
|
||||
"ground_truth": report["ground_truth"],
|
||||
}, ensure_ascii=False, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the immutable L3.4C temporal tile-seam stitch shadow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.l34c_tile_seam_stitch_shadow import (
|
||||
build_l34c_tile_seam_stitch_shadow,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--l34-freeze-root", type=Path, required=True)
|
||||
parser.add_argument("--l34a-audit-root", type=Path, required=True)
|
||||
parser.add_argument("--annotation-session", type=Path, required=True)
|
||||
parser.add_argument("--detector-qualification-root", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
result = build_l34c_tile_seam_stitch_shadow(
|
||||
l34_freeze_root=args.l34_freeze_root,
|
||||
l34a_audit_root=args.l34a_audit_root,
|
||||
annotation_session_path=args.annotation_session,
|
||||
detector_qualification_root=args.detector_qualification_root,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
report = result["report"]
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"result_id": result["result_id"],
|
||||
"result_root": str(result["result_root"]),
|
||||
"status": report["status"],
|
||||
"metrics": report["metrics"],
|
||||
"ground_truth": report["ground_truth"],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the immutable L3.4D cumulative post-processing candidate."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.l34d_cumulative_postprocessing_candidate import (
|
||||
build_l34d_cumulative_postprocessing_candidate,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--l34a-audit-root", type=Path, required=True)
|
||||
parser.add_argument("--l34b-shadow-root", type=Path, required=True)
|
||||
parser.add_argument("--l34c-shadow-root", type=Path, required=True)
|
||||
parser.add_argument("--annotation-session", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
result = build_l34d_cumulative_postprocessing_candidate(
|
||||
l34a_audit_root=args.l34a_audit_root,
|
||||
l34b_shadow_root=args.l34b_shadow_root,
|
||||
l34c_shadow_root=args.l34c_shadow_root,
|
||||
annotation_session_path=args.annotation_session,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
report = result["report"]
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"result_id": result["result_id"],
|
||||
"result_root": str(result["result_root"]),
|
||||
"status": report["status"],
|
||||
"metrics": report["metrics"],
|
||||
"ground_truth": report["ground_truth"],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the non-truth L3.4E manual self-review diagnostic."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.l34e_self_review_diagnostic import (
|
||||
build_l34e_self_review_diagnostic,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--l34d-candidate-root", type=Path, required=True)
|
||||
parser.add_argument("--annotation-session", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
result = build_l34e_self_review_diagnostic(
|
||||
l34d_candidate_root=args.l34d_candidate_root,
|
||||
annotation_session_path=args.annotation_session,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
report = result["report"]
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"result_id": result["result_id"],
|
||||
"result_root": str(result["result_root"]),
|
||||
"status": report["status"],
|
||||
"metrics": report["metrics"],
|
||||
"decision": report["decision"],
|
||||
"ground_truth": report["ground_truth"],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Evaluate exact L3.4 predictions after the independent E48 truth seal."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.l35_right_yolox_truth_evaluation import (
|
||||
build_l35_right_yolox_truth_evaluation,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--truth-island-root", type=Path, required=True)
|
||||
parser.add_argument("--truth-seal-root", type=Path, required=True)
|
||||
parser.add_argument("--l34-freeze-root", type=Path, required=True)
|
||||
parser.add_argument("--valid-fov-root", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
result = build_l35_right_yolox_truth_evaluation(
|
||||
truth_island_root=args.truth_island_root,
|
||||
truth_seal_root=args.truth_seal_root,
|
||||
l34_freeze_root=args.l34_freeze_root,
|
||||
valid_fov_root=args.valid_fov_root,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"result_id": result["result_id"],
|
||||
"result_root": str(result["result_root"]),
|
||||
"status": result["report"]["status"],
|
||||
"candidates": result["report"]["candidates"],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,509 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Attach canonical E29 LiDAR geometry to rectified YOLOX ByteTrack objects."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import importlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import resource
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
import types
|
||||
import uuid
|
||||
from collections import Counter
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def _install_compute_import_boundary() -> None:
|
||||
"""Load pure compute modules without importing the application/Rerun graph.
|
||||
|
||||
The product packages still expose legacy eager ``__init__`` imports. A bounded
|
||||
worker replay must not acquire MQTT, Rerun or web dependencies merely to read
|
||||
immutable LiDAR arrays and run calibrated geometry. These namespace packages
|
||||
preserve normal submodule loading while deliberately skipping those facades.
|
||||
"""
|
||||
|
||||
import k1link
|
||||
import k1link.device_plugins
|
||||
|
||||
k1link_root = Path(next(iter(k1link.__path__))).resolve(strict=True)
|
||||
|
||||
def namespace(name: str, path: Path) -> types.ModuleType:
|
||||
module = types.ModuleType(name)
|
||||
module.__package__ = name
|
||||
module.__path__ = [str(path)] # type: ignore[attr-defined]
|
||||
sys.modules[name] = module
|
||||
parent_name, _, child_name = name.rpartition(".")
|
||||
if parent_name:
|
||||
setattr(sys.modules[parent_name], child_name, module)
|
||||
return module
|
||||
|
||||
namespace("k1link.compute", k1link_root / "compute")
|
||||
xgrids = namespace(
|
||||
"k1link.device_plugins.xgrids_k1",
|
||||
k1link_root / "device_plugins" / "xgrids_k1",
|
||||
)
|
||||
analyze = namespace(
|
||||
"k1link.device_plugins.xgrids_k1.analyze",
|
||||
k1link_root / "device_plugins" / "xgrids_k1" / "analyze",
|
||||
)
|
||||
|
||||
replay = types.ModuleType("k1link.compute.lidar_replay")
|
||||
replay.LIDAR_REPLAY_PACK_SCHEMA = "missioncore.lidar-replay-pack/v2"
|
||||
replay.LidarReplayPackV2 = type("LidarReplayPackV2", (), {})
|
||||
sys.modules[replay.__name__] = replay
|
||||
|
||||
projection = importlib.import_module(
|
||||
"k1link.device_plugins.xgrids_k1.analyze.calibrated_projection"
|
||||
)
|
||||
for export in (
|
||||
"CalibratedProjectionError",
|
||||
"Kb4ProjectionProfile",
|
||||
"ProjectedPointCloud",
|
||||
"map_points_to_lidar",
|
||||
"project_map_points_kb4",
|
||||
):
|
||||
setattr(analyze, export, getattr(projection, export))
|
||||
setattr(xgrids, "analyze", analyze)
|
||||
|
||||
|
||||
_install_compute_import_boundary()
|
||||
|
||||
from k1link.compute.lidar_field_review import E10LidarFieldSource
|
||||
from k1link.compute.lidar_local_surface import K1LocalSurfaceV1
|
||||
from k1link.compute.semantic_geometry_fusion import (
|
||||
DEFAULT_CAMERA_GEOMETRY_FUSION_PROFILE,
|
||||
evaluate_camera_geometry_frame,
|
||||
projection_profile_from_source,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.analyze.calibrated_projection import (
|
||||
project_map_points_kb4,
|
||||
)
|
||||
|
||||
|
||||
RESULT_SCHEMA = "missioncore.rectified-camera-lidar-fusion/v1"
|
||||
FRAME_SCHEMA = "missioncore.rectified-camera-lidar-frame/v1"
|
||||
AUTHORITY = {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
LABEL_GROUP = {
|
||||
"person": "person",
|
||||
"bicycle": "two-wheel",
|
||||
"motorcycle": "two-wheel",
|
||||
"car": "vehicle",
|
||||
"bus": "vehicle",
|
||||
"truck": "vehicle",
|
||||
}
|
||||
|
||||
|
||||
def _arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--detector-result", type=Path, required=True)
|
||||
parser.add_argument("--source-pack", type=Path, required=True)
|
||||
parser.add_argument("--local-surface", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def _read_object(path: Path) -> dict[str, Any]:
|
||||
value = json.loads(path.resolve(strict=True).read_text(encoding="utf-8"))
|
||||
if not isinstance(value, dict):
|
||||
raise RuntimeError(f"JSON root is not an object: {path}")
|
||||
return value
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _canonical(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode()
|
||||
|
||||
|
||||
def _distribution(values: list[float]) -> dict[str, float]:
|
||||
ordered = np.asarray(values, dtype=np.float64)
|
||||
return {
|
||||
"minimum": round(float(np.min(ordered)), 6),
|
||||
"mean": round(float(np.mean(ordered)), 6),
|
||||
"p50": round(float(np.percentile(ordered, 50)), 6),
|
||||
"p95": round(float(np.percentile(ordered, 95)), 6),
|
||||
"p99": round(float(np.percentile(ordered, 99)), 6),
|
||||
"maximum": round(float(np.max(ordered)), 6),
|
||||
}
|
||||
|
||||
|
||||
def _process_peak_rss_mib() -> float:
|
||||
value = float(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss)
|
||||
divisor = 1024.0 * 1024.0 if sys.platform == "darwin" else 1024.0
|
||||
return value / divisor
|
||||
|
||||
|
||||
def _track_objects(frame: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
tracks = frame.get("tracks")
|
||||
if not isinstance(tracks, list):
|
||||
raise RuntimeError("Rectified detector frame has no track list")
|
||||
objects: list[dict[str, Any]] = []
|
||||
for track in tracks:
|
||||
if not isinstance(track, dict):
|
||||
raise RuntimeError("Rectified detector track is invalid")
|
||||
label = track.get("label")
|
||||
track_id = track.get("track_id")
|
||||
bbox = track.get("bbox_xyxy")
|
||||
score = track.get("score")
|
||||
if (
|
||||
label not in LABEL_GROUP
|
||||
or not isinstance(track_id, int)
|
||||
or isinstance(track_id, bool)
|
||||
or not isinstance(bbox, list)
|
||||
or len(bbox) != 4
|
||||
or any(
|
||||
not isinstance(value, int | float)
|
||||
or isinstance(value, bool)
|
||||
or not math.isfinite(float(value))
|
||||
for value in bbox
|
||||
)
|
||||
or not isinstance(score, int | float)
|
||||
or isinstance(score, bool)
|
||||
or not math.isfinite(float(score))
|
||||
):
|
||||
raise RuntimeError("Rectified detector track contract changed")
|
||||
objects.append(
|
||||
{
|
||||
"source_track_id": track_id,
|
||||
"track_id": track_id,
|
||||
"label": label,
|
||||
"association_group": LABEL_GROUP[label],
|
||||
"score": float(score),
|
||||
"bbox_xyxy": [float(value) for value in bbox],
|
||||
"cuboid_status": "camera-track-current-no-cuboid",
|
||||
"camera_motion_state": "bytetrack-current",
|
||||
"camera_motion_confidence": None,
|
||||
"motion_state": "camera-track-current",
|
||||
"motion_status": "metric-velocity-not-yet-published",
|
||||
}
|
||||
)
|
||||
return objects
|
||||
|
||||
|
||||
def main() -> None:
|
||||
arguments = _arguments()
|
||||
detector_root = arguments.detector_result.resolve(strict=True)
|
||||
if detector_root.is_symlink():
|
||||
raise RuntimeError("Detector result cannot be a symlink")
|
||||
qualification_path = detector_root / "qualification.json"
|
||||
detector_frames_path = detector_root / "frames.jsonl"
|
||||
qualification = _read_object(qualification_path)
|
||||
if (
|
||||
qualification.get("schema_version")
|
||||
!= "missioncore.rectified-yolox-qualification/v1"
|
||||
or qualification.get("state") != "accepted"
|
||||
or qualification.get("source", {}).get("frame_count") != 4489
|
||||
or qualification.get("source", {}).get("session_id")
|
||||
!= "20260720T065719Z_viewer_live"
|
||||
or qualification.get("pipeline", {}).get("id")
|
||||
!= "k1-kb4-core3-yolox-bytetrack/v1"
|
||||
or qualification.get("acceptance", {}).get("accepted") is not True
|
||||
):
|
||||
raise RuntimeError("Rectified detector qualification is not accepted")
|
||||
|
||||
source = E10LidarFieldSource(arguments.source_pack)
|
||||
surface = K1LocalSurfaceV1(arguments.local_surface)
|
||||
try:
|
||||
if (
|
||||
source.frame_count != 4489
|
||||
or source.identity.get("session_id")
|
||||
!= qualification["source"]["session_id"]
|
||||
or source.identity.get("source_id")
|
||||
!= qualification["source"]["source_id"]
|
||||
or surface.identity.get("source_pack_id") != source.pack_id
|
||||
or surface.identity.get("frame_count") != source.frame_count
|
||||
):
|
||||
raise RuntimeError("Detector, LiDAR and local surface are not source-aligned")
|
||||
identity = {
|
||||
"schema_version": RESULT_SCHEMA,
|
||||
"source": {
|
||||
"session_id": source.identity["session_id"],
|
||||
"source_pack_id": source.pack_id,
|
||||
"source_artifact_sha256": source.manifest["artifact"]["sha256"],
|
||||
"local_surface_model_id": surface.model_id,
|
||||
"local_surface_logical_sha256": surface.identity[
|
||||
"logical_content_sha256"
|
||||
],
|
||||
"detector_qualification_sha256": _sha256(qualification_path),
|
||||
"detector_frames_sha256": _sha256(detector_frames_path),
|
||||
},
|
||||
"pipeline": qualification["pipeline"],
|
||||
"association": DEFAULT_CAMERA_GEOMETRY_FUSION_PROFILE.to_dict(),
|
||||
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
"authority": AUTHORITY,
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical(identity)).hexdigest()
|
||||
result_id = f"rectified-camera-lidar-fusion-{identity_sha256}"
|
||||
destination = arguments.output_root.expanduser().absolute() / result_id
|
||||
if destination.exists():
|
||||
print(json.dumps({"result_id": result_id, "result_root": str(destination)}))
|
||||
return
|
||||
destination.parent.mkdir(parents=True, mode=0o700, exist_ok=True)
|
||||
staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700)
|
||||
started = time.perf_counter()
|
||||
rss_start = _process_peak_rss_mib()
|
||||
arrays = source.arrays
|
||||
offsets = arrays["cloud_offsets"]
|
||||
points_map = arrays["cloud_points_map"]
|
||||
sample_available = arrays["sample_available"]
|
||||
source_frame_indices = arrays["source_frame_indices"]
|
||||
session_seconds = arrays["session_seconds"]
|
||||
pose_positions = arrays["pose_positions_map"]
|
||||
pose_orientations = arrays["pose_quaternions_map_from_lidar"]
|
||||
point_class = surface.arrays["point_class"]
|
||||
point_height = surface.arrays["point_height_m"]
|
||||
surface_frame_valid = surface.arrays["frame_valid"]
|
||||
projection = projection_profile_from_source(source)
|
||||
latencies: list[float] = []
|
||||
status_counts: Counter[str] = Counter()
|
||||
ranged_by_label: Counter[str] = Counter()
|
||||
observations_by_label: Counter[str] = Counter()
|
||||
observation_count = 0
|
||||
ranged_count = 0
|
||||
range_estimate_count = 0
|
||||
unqualified_range_estimates_withheld = 0
|
||||
unqualified_metric_range_count = 0
|
||||
frames_with_tracks = 0
|
||||
frame_count = 0
|
||||
frames_path = staging / "camera-lidar-frames.jsonl"
|
||||
try:
|
||||
with (
|
||||
detector_frames_path.open("r", encoding="utf-8") as detector_stream,
|
||||
frames_path.open("x", encoding="utf-8", newline="\n") as output,
|
||||
):
|
||||
for line in detector_stream:
|
||||
frame_started = time.perf_counter()
|
||||
detector = json.loads(line)
|
||||
if (
|
||||
not isinstance(detector, dict)
|
||||
or detector.get("schema_version")
|
||||
!= "missioncore.rectified-yolox-frame/v1"
|
||||
or detector.get("frame_index") != frame_count
|
||||
):
|
||||
raise RuntimeError("Detector frame order or schema changed")
|
||||
objects = _track_objects(detector)
|
||||
frames_with_tracks += int(bool(objects))
|
||||
start = int(offsets[frame_count])
|
||||
end = int(offsets[frame_count + 1])
|
||||
points = np.asarray(points_map[start:end], dtype=np.float64)
|
||||
source_available = bool(sample_available[frame_count])
|
||||
surface_valid = bool(surface_frame_valid[frame_count])
|
||||
position = np.asarray(pose_positions[frame_count], dtype=np.float64)
|
||||
if source_available and surface_valid:
|
||||
orientation = pose_orientations[frame_count]
|
||||
projected = project_map_points_kb4(
|
||||
points,
|
||||
position_map_xyz=tuple(float(value) for value in position),
|
||||
orientation_map_from_lidar_xyzw=tuple(
|
||||
float(value) for value in orientation
|
||||
),
|
||||
profile=projection,
|
||||
)
|
||||
else:
|
||||
projected = None
|
||||
evaluation = evaluate_camera_geometry_frame(
|
||||
objects=objects,
|
||||
projected=projected,
|
||||
frame_points_map=points,
|
||||
point_class=point_class[start:end],
|
||||
point_height_m=point_height[start:end],
|
||||
sensor_position_map=position,
|
||||
source_available=source_available,
|
||||
surface_valid=surface_valid,
|
||||
profile=DEFAULT_CAMERA_GEOMETRY_FUSION_PROFILE,
|
||||
include_geometry_only=False,
|
||||
)
|
||||
observations = list(evaluation.semantic_observations)
|
||||
observation_count += len(observations)
|
||||
for observation in observations:
|
||||
label = str(observation["label"])
|
||||
observations_by_label[label] += 1
|
||||
status_counts[str(observation["geometry_status"])] += 1
|
||||
estimate_available = bool(
|
||||
observation["range_estimate_available"]
|
||||
)
|
||||
support_qualified = bool(
|
||||
observation["range_support_qualified"]
|
||||
)
|
||||
range_estimate_count += int(estimate_available)
|
||||
unqualified_range_estimates_withheld += int(
|
||||
estimate_available and not support_qualified
|
||||
)
|
||||
unqualified_metric_range_count += int(
|
||||
observation["range_m"] is not None
|
||||
and not support_qualified
|
||||
)
|
||||
if observation["range_m"] is not None:
|
||||
ranged_count += 1
|
||||
ranged_by_label[label] += 1
|
||||
document = {
|
||||
"schema_version": FRAME_SCHEMA,
|
||||
"frame_index": frame_count,
|
||||
"source_frame_index": int(source_frame_indices[frame_count]),
|
||||
"session_seconds": float(session_seconds[frame_count]),
|
||||
"source_available": source_available,
|
||||
"local_surface_valid": surface_valid,
|
||||
"semantic_observations": observations,
|
||||
"policy": {
|
||||
"camera_owns_semantics": True,
|
||||
"lidar_owns_metric_geometry": True,
|
||||
"track_id_preserved": True,
|
||||
"absence_of_points_means_free": False,
|
||||
"commands_enabled": False,
|
||||
},
|
||||
"authority": AUTHORITY,
|
||||
}
|
||||
output.write(_canonical(document).decode() + "\n")
|
||||
frame_count += 1
|
||||
latencies.append((time.perf_counter() - frame_started) * 1000.0)
|
||||
if frame_count % 500 == 0:
|
||||
output.flush()
|
||||
print(f"PHASE=camera-lidar-fusion FRAMES={frame_count}", flush=True)
|
||||
rss_end = _process_peak_rss_mib()
|
||||
distribution = _distribution(latencies)
|
||||
checks = {
|
||||
"complete_frame_accounting": frame_count == source.frame_count,
|
||||
"source_alignment_complete": frame_count == source.frame_count,
|
||||
"canonical_e29_association_reused": True,
|
||||
"camera_semantic_ownership_preserved": True,
|
||||
"track_identity_preserved": True,
|
||||
"absence_of_points_not_free": True,
|
||||
"unqualified_metric_range_absent": unqualified_metric_range_count == 0,
|
||||
"maximum_postprocess_p95_ms_50": distribution["p95"] <= 50.0,
|
||||
"maximum_rss_growth_mib_512": max(0.0, rss_end - rss_start) <= 512.0,
|
||||
}
|
||||
report = {
|
||||
"schema_version": RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"state": "accepted" if all(checks.values()) else "rejected",
|
||||
"identity_sha256": identity_sha256,
|
||||
"created_at_utc": datetime.now(UTC)
|
||||
.isoformat(timespec="milliseconds")
|
||||
.replace("+00:00", "Z"),
|
||||
"metrics": {
|
||||
"frames_processed": frame_count,
|
||||
"frames_with_confirmed_tracks": frames_with_tracks,
|
||||
"semantic_observations": observation_count,
|
||||
"observations_by_label": dict(sorted(observations_by_label.items())),
|
||||
"with_metric_range": ranged_count,
|
||||
"range_fraction": (
|
||||
ranged_count / observation_count if observation_count else 0.0
|
||||
),
|
||||
"qualified_range_fraction": (
|
||||
ranged_count / observation_count if observation_count else 0.0
|
||||
),
|
||||
"with_range_estimate": range_estimate_count,
|
||||
"range_estimate_fraction": (
|
||||
range_estimate_count / observation_count
|
||||
if observation_count else 0.0
|
||||
),
|
||||
"unqualified_range_estimates_withheld": (
|
||||
unqualified_range_estimates_withheld
|
||||
),
|
||||
"ranged_by_label": dict(sorted(ranged_by_label.items())),
|
||||
"geometry_status": dict(sorted(status_counts.items())),
|
||||
"postprocess_latency_ms": distribution,
|
||||
"elapsed_ms": (time.perf_counter() - started) * 1000.0,
|
||||
"rss_growth_mib": max(0.0, rss_end - rss_start),
|
||||
},
|
||||
"acceptance": {"accepted": all(checks.values()), "checks": checks},
|
||||
"decision": {
|
||||
"detector_tracks_connected_to_lidar_range": True,
|
||||
"runtime_promoted": False,
|
||||
"next_gate": "feed observations into existing inline temporal world state",
|
||||
},
|
||||
"limitations": [
|
||||
"RAVNOVES00 has no exhaustive independent object ground truth.",
|
||||
"Metric range is published only with qualified occupied LiDAR support.",
|
||||
"Camera-only objects remain visible without invented range.",
|
||||
],
|
||||
"authority": AUTHORITY,
|
||||
}
|
||||
report_path = staging / "run-report.json"
|
||||
report_path.write_text(
|
||||
json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
manifest = {
|
||||
"schema_version": RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"acceptance_state": report["state"],
|
||||
"ground_truth": False,
|
||||
"artifacts": [
|
||||
{
|
||||
"path": frames_path.name,
|
||||
"role": "camera-lidar-frames",
|
||||
"byte_length": frames_path.stat().st_size,
|
||||
"sha256": _sha256(frames_path),
|
||||
},
|
||||
{
|
||||
"path": report_path.name,
|
||||
"role": "run-report",
|
||||
"byte_length": report_path.stat().st_size,
|
||||
"sha256": _sha256(report_path),
|
||||
},
|
||||
],
|
||||
"authority": AUTHORITY,
|
||||
}
|
||||
(staging / "manifest.json").write_text(
|
||||
json.dumps(manifest, ensure_ascii=False, sort_keys=True, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"result_id": result_id,
|
||||
"result_root": str(destination),
|
||||
"accepted": report["acceptance"]["accepted"],
|
||||
"frames_processed": frame_count,
|
||||
"semantic_observations": observation_count,
|
||||
"with_metric_range": ranged_count,
|
||||
"range_fraction": report["metrics"]["range_fraction"],
|
||||
"with_range_estimate": range_estimate_count,
|
||||
"unqualified_range_estimates_withheld": (
|
||||
unqualified_range_estimates_withheld
|
||||
),
|
||||
"postprocess_p95_ms": distribution["p95"],
|
||||
},
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
finally:
|
||||
source.close()
|
||||
surface.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,631 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compose rectified camera tracks, EoMT semantics and K1 LiDAR world state.
|
||||
|
||||
This is a source-bound replay composition, not a new perception algorithm. It
|
||||
reuses the accepted E10 fusion/cuboid implementation and E23 bounded temporal
|
||||
state while replacing only the raw-fisheye detector input with the accepted
|
||||
three-view KB4 rectified YOLOX/ByteTrack result.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import inspect
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import resource
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
from collections import Counter
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from e10_fusion_runtime import (
|
||||
CuboidCompletionTracker,
|
||||
LidarReplayPack,
|
||||
WorldStateProjector,
|
||||
clearance,
|
||||
distance_history,
|
||||
fuse_tracks,
|
||||
fusion_document,
|
||||
project_points,
|
||||
)
|
||||
from inline_temporal import (
|
||||
TemporalStabilizer,
|
||||
read_inline_profile,
|
||||
stabilize_world_state,
|
||||
)
|
||||
|
||||
|
||||
RESULT_SCHEMA = "missioncore.rectified-camera-world-state/v1"
|
||||
FUSION_FRAME_SCHEMA = "missioncore.rectified-camera-temporal-fusion-frame/v1"
|
||||
PIPELINE_ID = "kb4-core3-yolox-eomt-k1-lidar-e23-temporal/v1"
|
||||
AUTHORITY = {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
|
||||
def _arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--detector-result", type=Path, required=True)
|
||||
parser.add_argument("--semantic-source", type=Path, required=True)
|
||||
parser.add_argument("--lidar-pack", type=Path, required=True)
|
||||
parser.add_argument("--fusion-profile", type=Path, required=True)
|
||||
parser.add_argument("--temporal-profile", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def _read_object(path: Path) -> dict[str, Any]:
|
||||
value = json.loads(path.resolve(strict=True).read_text(encoding="utf-8"))
|
||||
if not isinstance(value, dict):
|
||||
raise RuntimeError(f"JSON root is not an object: {path}")
|
||||
return value
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _canonical(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode()
|
||||
|
||||
|
||||
def _distribution(values: list[float]) -> dict[str, float]:
|
||||
samples = np.asarray(values, dtype=np.float64)
|
||||
if samples.size == 0:
|
||||
return {key: 0.0 for key in ("minimum", "mean", "p50", "p95", "p99", "maximum")}
|
||||
return {
|
||||
"minimum": round(float(np.min(samples)), 6),
|
||||
"mean": round(float(np.mean(samples)), 6),
|
||||
"p50": round(float(np.percentile(samples, 50)), 6),
|
||||
"p95": round(float(np.percentile(samples, 95)), 6),
|
||||
"p99": round(float(np.percentile(samples, 99)), 6),
|
||||
"maximum": round(float(np.max(samples)), 6),
|
||||
}
|
||||
|
||||
|
||||
def _process_peak_rss_mib() -> float:
|
||||
value = float(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss)
|
||||
divisor = 1024.0 * 1024.0 if sys.platform == "darwin" else 1024.0
|
||||
return value / divisor
|
||||
|
||||
|
||||
def _artifact(result: dict[str, Any], kind: str, root: Path) -> Path:
|
||||
artifacts = result.get("artifacts")
|
||||
if not isinstance(artifacts, list):
|
||||
raise RuntimeError("Semantic source artifact list is invalid")
|
||||
matches = [item for item in artifacts if isinstance(item, dict) and item.get("kind") == kind]
|
||||
if len(matches) != 1:
|
||||
raise RuntimeError(f"Semantic source has no unique {kind} artifact")
|
||||
item = matches[0]
|
||||
path = (root / str(item.get("path"))).resolve(strict=True)
|
||||
if (
|
||||
path.is_symlink()
|
||||
or path.parent != root
|
||||
or path.stat().st_size != item.get("byte_length")
|
||||
or _sha256(path) != item.get("sha256")
|
||||
):
|
||||
raise RuntimeError(f"Semantic source {kind} artifact changed")
|
||||
return path
|
||||
|
||||
|
||||
def _validated_sources(arguments: argparse.Namespace) -> tuple[
|
||||
dict[str, Any],
|
||||
Path,
|
||||
dict[str, Any],
|
||||
Path,
|
||||
dict[str, Any],
|
||||
str,
|
||||
]:
|
||||
detector_root = arguments.detector_result.resolve(strict=True)
|
||||
semantic_root = arguments.semantic_source.resolve(strict=True)
|
||||
if detector_root.is_symlink() or semantic_root.is_symlink():
|
||||
raise RuntimeError("Perception source root cannot be a symlink")
|
||||
detector_report_path = detector_root / "qualification.json"
|
||||
detector_frames = detector_root / "frames.jsonl"
|
||||
detector = _read_object(detector_report_path)
|
||||
if (
|
||||
detector.get("schema_version") != "missioncore.rectified-yolox-qualification/v1"
|
||||
or detector.get("state") != "accepted"
|
||||
or detector.get("acceptance", {}).get("accepted") is not True
|
||||
or detector.get("source", {}).get("frame_count") != 4489
|
||||
or detector.get("pipeline", {}).get("id") != "k1-kb4-core3-yolox-bytetrack/v1"
|
||||
):
|
||||
raise RuntimeError("Rectified detector result is not accepted")
|
||||
semantic_report_path = semantic_root / "run-report.json"
|
||||
semantic_result_path = semantic_root / "result.json"
|
||||
semantic_report = _read_object(semantic_report_path)
|
||||
semantic_result = _read_object(semantic_result_path)
|
||||
configuration = semantic_report.get("identity", {}).get("configuration", {})
|
||||
profile = configuration.get("profile")
|
||||
if (
|
||||
semantic_report.get("schema_version")
|
||||
!= "missioncore.e10-integrated-perception-report/v1"
|
||||
or semantic_report.get("state") != "accepted"
|
||||
or semantic_report.get("acceptance", {}).get("accepted") is not True
|
||||
or configuration.get("pipeline")
|
||||
!= "source-paced-yolox-eomt-kb4-lidar-world-state/v1"
|
||||
or not isinstance(profile, dict)
|
||||
or profile.get("selection", {}).get("required_frame_count") != 4489
|
||||
or semantic_report.get("metrics", {}).get("semantic", {}).get("frames_processed")
|
||||
!= 898
|
||||
or semantic_result.get("acceptance_state") != "accepted"
|
||||
):
|
||||
raise RuntimeError("Full-session E10 semantic source is not accepted")
|
||||
semantic_identity = semantic_report.get("identity", {})
|
||||
if (
|
||||
semantic_identity.get("job_id") != detector.get("source", {}).get("job_id")
|
||||
or semantic_identity.get("session_id")
|
||||
!= detector.get("source", {}).get("session_id")
|
||||
or semantic_identity.get("source_id") != detector.get("source", {}).get("source_id")
|
||||
):
|
||||
raise RuntimeError("Detector and semantic source identities disagree")
|
||||
semantic_arrays = _artifact(semantic_result, "e10-transient-perception", semantic_root)
|
||||
temporal, temporal_sha256 = read_inline_profile(arguments.temporal_profile)
|
||||
return detector, detector_frames, semantic_report, semantic_arrays, temporal, temporal_sha256
|
||||
|
||||
|
||||
def _validated_fusion_profile(
|
||||
path: Path,
|
||||
*,
|
||||
semantic_source_profile: dict[str, Any],
|
||||
) -> tuple[dict[str, Any], str]:
|
||||
resolved = path.resolve(strict=True)
|
||||
if resolved.is_symlink() or not resolved.is_file():
|
||||
raise RuntimeError("Fusion profile must be a regular immutable file")
|
||||
profile = _read_object(resolved)
|
||||
association = profile.get("association")
|
||||
cuboid_completion = profile.get("cuboid_completion")
|
||||
if (
|
||||
profile.get("schema_version")
|
||||
!= "missioncore.e10-integrated-perception-profile/v1"
|
||||
or profile.get("profile_id") != "lab-e19-ground-aware-cuboids-v1"
|
||||
or profile.get("source") != semantic_source_profile.get("source")
|
||||
or profile.get("selection") != semantic_source_profile.get("selection")
|
||||
or not isinstance(association, dict)
|
||||
or association.get("support_duplicate_overlap_threshold") != 0.6
|
||||
or association.get("object_support_ground_filter", {}).get("mode")
|
||||
!= "local-ground-relative-object-support-v1"
|
||||
or not isinstance(cuboid_completion, dict)
|
||||
or cuboid_completion.get("mode") != "class-prior-amodal-v1"
|
||||
):
|
||||
raise RuntimeError("Fusion profile is not the canonical E19 ground-aware profile")
|
||||
return profile, _sha256(resolved)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
arguments = _arguments()
|
||||
(
|
||||
detector,
|
||||
detector_frames_path,
|
||||
semantic_report,
|
||||
semantic_arrays_path,
|
||||
temporal_profile,
|
||||
temporal_profile_sha256,
|
||||
) = _validated_sources(arguments)
|
||||
semantic_source_profile = semantic_report["identity"]["configuration"]["profile"]
|
||||
profile, fusion_profile_sha256 = _validated_fusion_profile(
|
||||
arguments.fusion_profile,
|
||||
semantic_source_profile=semantic_source_profile,
|
||||
)
|
||||
job_id = str(detector["source"]["job_id"])
|
||||
lidar = LidarReplayPack(arguments.lidar_pack, expected_job_id=job_id)
|
||||
try:
|
||||
if lidar.frame_count != 4489:
|
||||
raise RuntimeError("LiDAR replay frame count changed")
|
||||
identity = {
|
||||
"schema_version": RESULT_SCHEMA,
|
||||
"pipeline_id": PIPELINE_ID,
|
||||
"source": {
|
||||
"job_id": job_id,
|
||||
"input_sha256": semantic_report["identity"]["input_sha256"],
|
||||
"session_id": detector["source"]["session_id"],
|
||||
"source_id": detector["source"]["source_id"],
|
||||
"detector_qualification_sha256": _sha256(
|
||||
arguments.detector_result / "qualification.json"
|
||||
),
|
||||
"detector_frames_sha256": _sha256(detector_frames_path),
|
||||
"semantic_report_sha256": _sha256(arguments.semantic_source / "run-report.json"),
|
||||
"semantic_arrays_sha256": _sha256(semantic_arrays_path),
|
||||
"lidar_pack_id": lidar.pack_id,
|
||||
},
|
||||
"composition": {
|
||||
"detector": detector["pipeline"],
|
||||
"semantic": semantic_report["identity"]["models"]["semantic"],
|
||||
"fusion_profile": profile["association"],
|
||||
"cuboid_completion_profile": profile["cuboid_completion"],
|
||||
"fusion_profile_sha256": fusion_profile_sha256,
|
||||
"temporal_profile_sha256": temporal_profile_sha256,
|
||||
},
|
||||
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
"fusion_runtime_sha256": _sha256(Path(__file__).with_name("e10_fusion_runtime.py")),
|
||||
"inline_temporal_sha256": _sha256(
|
||||
Path(inspect.getfile(TemporalStabilizer)).resolve(strict=True)
|
||||
),
|
||||
"authority": AUTHORITY,
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical(identity)).hexdigest()
|
||||
result_id = f"rectified-camera-world-state-{identity_sha256}"
|
||||
output_root = arguments.output_root.expanduser().absolute()
|
||||
destination = output_root / result_id
|
||||
if destination.exists():
|
||||
print(json.dumps({"result_id": result_id, "result_root": str(destination)}))
|
||||
return
|
||||
output_root.mkdir(parents=True, exist_ok=True)
|
||||
staging = output_root / f".{result_id}.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700)
|
||||
started = time.perf_counter()
|
||||
rss_start = _process_peak_rss_mib()
|
||||
detector_latencies: list[float] = []
|
||||
projection_latencies: list[float] = []
|
||||
association_latencies: list[float] = []
|
||||
temporal_latencies: list[float] = []
|
||||
composition_latencies: list[float] = []
|
||||
fusion_state_counts: Counter[str] = Counter()
|
||||
rejection_counts: Counter[str] = Counter()
|
||||
ranged_by_label: Counter[str] = Counter()
|
||||
temporal = TemporalStabilizer(temporal_profile)
|
||||
world_memory: dict[int, dict[str, Any]] = {}
|
||||
projector = WorldStateProjector(float(profile["world_state"]["velocity_history_limit_s"]))
|
||||
completion = CuboidCompletionTracker(profile["cuboid_completion"])
|
||||
history = distance_history(int(profile["association"]["distance_history_frames"]))
|
||||
semantic_fresh_frames = 0
|
||||
fused_frames = 0
|
||||
raw_accepted_cuboids = 0
|
||||
temporal_accepted_cuboids = 0
|
||||
world_object_observations = 0
|
||||
frames_processed = 0
|
||||
support_offsets = [0]
|
||||
support_points: list[np.ndarray] = []
|
||||
support_track_ids: list[np.ndarray] = []
|
||||
box_offsets = [0]
|
||||
box_centers: list[list[float]] = []
|
||||
box_half_sizes: list[list[float]] = []
|
||||
box_quaternions: list[list[float]] = []
|
||||
box_track_ids: list[int] = []
|
||||
frames_path = staging / "fusion-frames.jsonl"
|
||||
world_path = staging / "world-state.jsonl"
|
||||
with (
|
||||
np.load(semantic_arrays_path, allow_pickle=False) as semantic_arrays,
|
||||
detector_frames_path.open("r", encoding="utf-8") as detector_stream,
|
||||
frames_path.open("x", encoding="utf-8", newline="\n") as fusion_stream,
|
||||
world_path.open("x", encoding="utf-8", newline="\n") as world_stream,
|
||||
):
|
||||
semantic_indices = semantic_arrays["semantic_frame_indices"]
|
||||
semantic_masks = semantic_arrays["semantic_masks"]
|
||||
if (
|
||||
semantic_indices.shape != (898,)
|
||||
or semantic_masks.shape != (898, 600, 800)
|
||||
or semantic_masks.dtype != np.uint8
|
||||
or int(semantic_indices[0]) != 0
|
||||
or int(semantic_indices[-1]) != 4485
|
||||
or np.any(np.diff(semantic_indices) != 5)
|
||||
):
|
||||
raise RuntimeError("Full-session semantic arrays changed")
|
||||
semantic_slot = 0
|
||||
for expected_frame, line in enumerate(detector_stream):
|
||||
frame_started = time.perf_counter()
|
||||
frame = json.loads(line)
|
||||
if (
|
||||
not isinstance(frame, dict)
|
||||
or frame.get("schema_version") != "missioncore.rectified-yolox-frame/v1"
|
||||
or frame.get("frame_index") != expected_frame
|
||||
):
|
||||
raise RuntimeError("Rectified detector frame stream changed")
|
||||
while (
|
||||
semantic_slot + 1 < semantic_indices.size
|
||||
and int(semantic_indices[semantic_slot + 1]) <= expected_frame
|
||||
):
|
||||
semantic_slot += 1
|
||||
semantic_frame_index = int(semantic_indices[semantic_slot])
|
||||
semantic_age_s = float(
|
||||
lidar.session_seconds[expected_frame]
|
||||
- lidar.session_seconds[semantic_frame_index]
|
||||
)
|
||||
semantic_fresh = 0.0 <= semantic_age_s <= (
|
||||
float(profile["replay"]["semantic_ttl_ms"]) / 1000.0
|
||||
)
|
||||
if semantic_fresh:
|
||||
semantic_fresh_frames += 1
|
||||
lidar_frame = lidar.frame(expected_frame)
|
||||
fusions = ()
|
||||
points_lidar = np.empty((0, 3), dtype=np.float64)
|
||||
fusion_state = "depth-unavailable"
|
||||
projection_ms = 0.0
|
||||
association_ms = 0.0
|
||||
if lidar_frame is not None and semantic_fresh:
|
||||
points_map, position, quaternion = lidar_frame
|
||||
stage_started = time.perf_counter()
|
||||
pixels, depths, source_indices, points_lidar = project_points(
|
||||
points_map, position, quaternion, lidar.profile
|
||||
)
|
||||
projection_ms = (time.perf_counter() - stage_started) * 1000.0
|
||||
stage_started = time.perf_counter()
|
||||
fusions = fuse_tracks(
|
||||
tracks=frame["tracks"],
|
||||
semantic_map=semantic_masks[semantic_slot],
|
||||
pixels=pixels,
|
||||
depths=depths,
|
||||
source_indices=source_indices,
|
||||
points_map=points_map,
|
||||
points_lidar=points_lidar,
|
||||
association=profile["association"],
|
||||
distance_history=history,
|
||||
completion_tracker=completion,
|
||||
sensor_position_map=position,
|
||||
session_seconds=float(lidar.session_seconds[expected_frame]),
|
||||
)
|
||||
association_ms = (time.perf_counter() - stage_started) * 1000.0
|
||||
fusion_state = "fused"
|
||||
fused_frames += 1
|
||||
elif not semantic_fresh:
|
||||
fusion_state = "semantic-stale"
|
||||
fusion_state_counts[fusion_state] += 1
|
||||
raw_objects = [fusion_document(item) for item in fusions]
|
||||
raw_accepted = [item for item in fusions if item.cuboid is not None]
|
||||
raw_accepted_cuboids += len(raw_accepted)
|
||||
for item in fusions:
|
||||
rejection_counts[item.status] += 1
|
||||
if item.distance_smoothed_m is not None:
|
||||
ranged_by_label[item.label] += 1
|
||||
stage_started = time.perf_counter()
|
||||
stabilized = temporal.update(
|
||||
frame_index=expected_frame,
|
||||
session_seconds=float(lidar.session_seconds[expected_frame]),
|
||||
objects=raw_objects,
|
||||
)
|
||||
temporal_ms = (time.perf_counter() - stage_started) * 1000.0
|
||||
temporal_boxes = [
|
||||
item
|
||||
for item in stabilized
|
||||
if str(item.get("cuboid_status", "")).startswith("accepted-")
|
||||
]
|
||||
temporal_accepted_cuboids += len(temporal_boxes)
|
||||
clearance_state = clearance(points_lidar, profile["world_state"]["clearance"])
|
||||
world = projector.project(
|
||||
frame_index=expected_frame,
|
||||
source_frame_index=int(lidar.source_frame_indices[expected_frame]),
|
||||
session_seconds=float(lidar.session_seconds[expected_frame]),
|
||||
fusion_state=fusion_state,
|
||||
fusions=fusions,
|
||||
points_lidar=points_lidar,
|
||||
clearance_state=clearance_state,
|
||||
delivery={
|
||||
"health": "healthy" if fusion_state == "fused" else "degraded",
|
||||
"semantic_status": "fresh" if semantic_fresh else "stale",
|
||||
"semantic_source_age_ms": semantic_age_s * 1000.0,
|
||||
"replay_composition": True,
|
||||
},
|
||||
)
|
||||
world = stabilize_world_state(world, stabilized, world_memory)
|
||||
world_object_observations += int(world["object_count"])
|
||||
support_for_frame: list[np.ndarray] = []
|
||||
support_ids_for_frame: list[np.ndarray] = []
|
||||
if lidar_frame is not None:
|
||||
points_map = lidar_frame[0]
|
||||
for item in raw_accepted:
|
||||
values = points_map[item.source_indices].astype(np.float32)
|
||||
support_for_frame.append(values)
|
||||
support_ids_for_frame.append(
|
||||
np.full(values.shape[0], item.track_id, dtype=np.int32)
|
||||
)
|
||||
if support_for_frame:
|
||||
points = np.concatenate(support_for_frame)
|
||||
ids = np.concatenate(support_ids_for_frame)
|
||||
support_points.append(points)
|
||||
support_track_ids.append(ids)
|
||||
support_offsets.append(support_offsets[-1] + int(points.shape[0]))
|
||||
else:
|
||||
support_offsets.append(support_offsets[-1])
|
||||
for item in temporal_boxes:
|
||||
box_centers.append([float(value) for value in item["cuboid_center_map"]])
|
||||
box_half_sizes.append([float(value) for value in item["cuboid_half_size"]])
|
||||
box_quaternions.append(
|
||||
[float(value) for value in item["cuboid_quaternion_xyzw"]]
|
||||
)
|
||||
box_track_ids.append(int(item["track_id"]))
|
||||
box_offsets.append(box_offsets[-1] + len(temporal_boxes))
|
||||
fusion_stream.write(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": FUSION_FRAME_SCHEMA,
|
||||
"frame_index": expected_frame,
|
||||
"source_frame_index": int(lidar.source_frame_indices[expected_frame]),
|
||||
"session_seconds": float(lidar.session_seconds[expected_frame]),
|
||||
"fusion_state": fusion_state,
|
||||
"semantic_source_frame_index": semantic_frame_index,
|
||||
"objects": stabilized,
|
||||
"authority": AUTHORITY,
|
||||
},
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
world["authority"] = AUTHORITY
|
||||
world_stream.write(
|
||||
json.dumps(world, sort_keys=True, separators=(",", ":"), allow_nan=False)
|
||||
+ "\n"
|
||||
)
|
||||
detector_ms = float(frame["processing_ms"])
|
||||
detector_latencies.append(detector_ms)
|
||||
projection_latencies.append(projection_ms)
|
||||
association_latencies.append(association_ms)
|
||||
temporal_latencies.append(temporal_ms)
|
||||
composition_latencies.append(
|
||||
detector_ms + projection_ms + association_ms + temporal_ms
|
||||
)
|
||||
frames_processed += 1
|
||||
if frames_processed % 500 == 0:
|
||||
print(f"PHASE=rectified-world-state FRAMES={frames_processed}", flush=True)
|
||||
if frames_processed != 4489:
|
||||
raise RuntimeError("Rectified detector frame stream is incomplete")
|
||||
arrays_path = staging / "visual-geometry.npz"
|
||||
np.savez_compressed(
|
||||
arrays_path,
|
||||
frame_times_ns=np.rint(lidar.session_seconds * 1e9).astype(np.int64),
|
||||
sensor_positions_map=np.asarray(lidar.positions, dtype=np.float64),
|
||||
support_offsets=np.asarray(support_offsets, dtype=np.int64),
|
||||
support_points=(
|
||||
np.concatenate(support_points)
|
||||
if support_points
|
||||
else np.empty((0, 3), dtype=np.float32)
|
||||
),
|
||||
support_track_ids=(
|
||||
np.concatenate(support_track_ids)
|
||||
if support_track_ids
|
||||
else np.empty((0,), dtype=np.int32)
|
||||
),
|
||||
box_offsets=np.asarray(box_offsets, dtype=np.int64),
|
||||
box_centers=np.asarray(box_centers, dtype=np.float32).reshape((-1, 3)),
|
||||
box_half_sizes=np.asarray(box_half_sizes, dtype=np.float32).reshape((-1, 3)),
|
||||
box_quaternions=np.asarray(box_quaternions, dtype=np.float32).reshape((-1, 4)),
|
||||
box_track_ids=np.asarray(box_track_ids, dtype=np.int32),
|
||||
)
|
||||
detector_summary = _distribution(detector_latencies)
|
||||
projection_summary = _distribution(projection_latencies)
|
||||
association_summary = _distribution(association_latencies)
|
||||
temporal_summary = _distribution(temporal_latencies)
|
||||
composition_summary = _distribution(composition_latencies)
|
||||
effective_fps = 1000.0 / max(composition_summary["mean"], 1e-9)
|
||||
semantic_metrics = semantic_report["metrics"]["semantic"]
|
||||
checks = {
|
||||
"complete_frame_accounting": frames_processed == 4489,
|
||||
"rectified_detector_accepted": detector["acceptance"]["accepted"] is True,
|
||||
"semantic_source_accepted": semantic_report["acceptance"]["accepted"] is True,
|
||||
"semantic_multirate_fresh_coverage": semantic_fresh_frames / frames_processed >= 0.9,
|
||||
"minimum_lidar_fused_frames": fused_frames >= 3500,
|
||||
"minimum_raw_accepted_cuboids": raw_accepted_cuboids >= 1500,
|
||||
"minimum_temporal_accepted_cuboids": temporal_accepted_cuboids >= 1500,
|
||||
"minimum_effective_composed_fps": effective_fps >= 9.5,
|
||||
"maximum_composed_p95_ms": composition_summary["p95"] <= 175.0,
|
||||
"maximum_temporal_p95_ms": temporal_summary["p95"] <= 5.0,
|
||||
"track_identity_preserved": True,
|
||||
"camera_semantic_ownership_preserved": True,
|
||||
"lidar_metric_geometry_ownership_preserved": True,
|
||||
}
|
||||
accepted = all(checks.values())
|
||||
report = {
|
||||
"schema_version": RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"created_at_utc": datetime.now(UTC).isoformat(timespec="milliseconds").replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"state": "accepted" if accepted else "rejected",
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"metrics": {
|
||||
"frames_processed": frames_processed,
|
||||
"semantic_frames_reused": int(semantic_metrics["frames_processed"]),
|
||||
"semantic_effective_fps": float(semantic_metrics["effective_fps"]),
|
||||
"semantic_fresh_frames": semantic_fresh_frames,
|
||||
"semantic_fresh_fraction": semantic_fresh_frames / frames_processed,
|
||||
"lidar_fused_frames": fused_frames,
|
||||
"fusion_state_counts": dict(sorted(fusion_state_counts.items())),
|
||||
"raw_accepted_cuboids": raw_accepted_cuboids,
|
||||
"temporal_accepted_cuboids": temporal_accepted_cuboids,
|
||||
"world_object_observations": world_object_observations,
|
||||
"ranged_by_label": dict(sorted(ranged_by_label.items())),
|
||||
"rejection_counts": dict(sorted(rejection_counts.items())),
|
||||
"latency_ms": {
|
||||
"rectified_detector": detector_summary,
|
||||
"lidar_projection": projection_summary,
|
||||
"semantic_lidar_association_and_cuboid": association_summary,
|
||||
"temporal_world_state": temporal_summary,
|
||||
"composed_compute": composition_summary,
|
||||
},
|
||||
"effective_composed_fps": effective_fps,
|
||||
"elapsed_ms": (time.perf_counter() - started) * 1000.0,
|
||||
"rss_growth_mib": _process_peak_rss_mib() - rss_start,
|
||||
"temporal_state": temporal.snapshot(),
|
||||
},
|
||||
"acceptance": {"accepted": accepted, "checks": checks},
|
||||
"decision": {
|
||||
"camera_first_world_state_composed": accepted,
|
||||
"runtime_promoted": False,
|
||||
"next_gate": "wire the same bounded composition into the warm worker",
|
||||
},
|
||||
"limitations": [
|
||||
"Recorded source-bound replay is not a live transport proof.",
|
||||
"EoMT runs at the accepted 2 Hz multi-rate cadence, not once per camera frame.",
|
||||
"RAVNOVES00 has no exhaustive independent object ground truth.",
|
||||
"A camera-only object remains visible when LiDAR range is unavailable.",
|
||||
"Completed cuboids include bounded class-size priors for unobserved volume.",
|
||||
],
|
||||
"authority": AUTHORITY,
|
||||
}
|
||||
report_path = staging / "run-report.json"
|
||||
report_path.write_text(
|
||||
json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True, allow_nan=False)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
manifest = {
|
||||
"schema_version": RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": report["created_at_utc"],
|
||||
"classification": "private-derived-perception-qualification",
|
||||
"artifacts": [
|
||||
{
|
||||
"kind": kind,
|
||||
"path": path.name,
|
||||
"byte_length": path.stat().st_size,
|
||||
"sha256": _sha256(path),
|
||||
}
|
||||
for kind, path in (
|
||||
("temporal-fusion-frames", frames_path),
|
||||
("world-state", world_path),
|
||||
("visual-geometry", arrays_path),
|
||||
("run-report", report_path),
|
||||
)
|
||||
],
|
||||
"authority": AUTHORITY,
|
||||
}
|
||||
(staging / "manifest.json").write_text(
|
||||
json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True, allow_nan=False)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
os.replace(staging, destination)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"accepted": accepted,
|
||||
"effective_composed_fps": effective_fps,
|
||||
"fused_frames": fused_frames,
|
||||
"raw_accepted_cuboids": raw_accepted_cuboids,
|
||||
"temporal_accepted_cuboids": temporal_accepted_cuboids,
|
||||
"result_id": result_id,
|
||||
"result_root": str(destination),
|
||||
},
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
finally:
|
||||
lidar.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,135 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SourceRoot,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$OutputRoot,
|
||||
|
||||
[string]$LogPath = ''
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
$sourceCommit = '581889df47d6181110c758c10b872ca833a835e3'
|
||||
$developmentImage = 'nvcr.io/nvidia/deepstream:9.1-triton-multiarch@sha256:fd31f5b44ababdbdee8cd397a375e888191b49e402ac237254a4cdc239130f5b'
|
||||
$runtimeImage = 'nvcr.io/nvidia/deepstream:9.1-samples-multiarch@sha256:10eca409b3894e91c1bac915c9f1346307e56695e552487cbe8cf2f58a3f998f'
|
||||
$libraryName = 'libnvds_infercustomparser_tao.so'
|
||||
$expectedSources = [ordered]@{
|
||||
'Makefile' = '0265f470354e60c6d719bde68c7b74b1879eed9b4552b8fe7b39416af5ce6835'
|
||||
'debug_logger_raii.cpp' = '1d388509e1ff9008de6ccd6451db9c6433273ed78a94e95a1df84585b8dc2915'
|
||||
'debug_logger_raii.hpp' = '6efdce1874468848664a18ceb613f2384b8c079cb12baa888a433d6c0f81b7ec'
|
||||
'debug_logger_tensor.hpp' = 'c9999fcf92536bbb36498ddd4485f213fc2f5ddc70d24f8d408195a48680b94f'
|
||||
'nvdsinfer_custombboxparser_tao.cpp' = '1794e3ee5152f25eff31454c6181368676f6659c68fc25b4b1933f6cbb63158b'
|
||||
}
|
||||
|
||||
function Get-Sha256([string]$Path) {
|
||||
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
}
|
||||
|
||||
function Invoke-Docker([string[]]$Arguments, [string]$Label) {
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
& docker @Arguments
|
||||
$dockerExitCode = $LASTEXITCODE
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
if ($dockerExitCode -ne 0) {
|
||||
throw "$Label failed with exit code $dockerExitCode"
|
||||
}
|
||||
}
|
||||
|
||||
$source = (Resolve-Path -LiteralPath $SourceRoot).Path
|
||||
New-Item -ItemType Directory -Force -Path $OutputRoot | Out-Null
|
||||
$output = (Resolve-Path -LiteralPath $OutputRoot).Path
|
||||
if ($LogPath) {
|
||||
$logParent = Split-Path -Parent $LogPath
|
||||
if ($logParent) { New-Item -ItemType Directory -Force -Path $logParent | Out-Null }
|
||||
Start-Transcript -LiteralPath $LogPath -Append | Out-Null
|
||||
}
|
||||
|
||||
try {
|
||||
$sourceLibrary = Join-Path $source $libraryName
|
||||
if (Test-Path -LiteralPath $sourceLibrary -PathType Leaf) {
|
||||
Remove-Item -LiteralPath $sourceLibrary -Force
|
||||
}
|
||||
$actualSourceFiles = @(Get-ChildItem -LiteralPath $source -File | ForEach-Object { $_.Name })
|
||||
if (@($actualSourceFiles | Where-Object { -not $expectedSources.Contains($_) }).Count -ne 0 -or
|
||||
@($expectedSources.Keys | Where-Object { $actualSourceFiles -notcontains $_ }).Count -ne 0) {
|
||||
throw 'NVIDIA TAO parser source inventory changed.'
|
||||
}
|
||||
foreach ($entry in $expectedSources.GetEnumerator()) {
|
||||
$path = Join-Path $source $entry.Key
|
||||
$actualSha = Get-Sha256 $path
|
||||
if ($actualSha -ne $entry.Value) {
|
||||
throw "NVIDIA TAO parser source changed: $($entry.Key)"
|
||||
}
|
||||
}
|
||||
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
& docker image inspect $developmentImage *> $null
|
||||
$developmentImageCached = $LASTEXITCODE -eq 0
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
if (-not $developmentImageCached) {
|
||||
Write-Host 'Pulling exact NVIDIA DeepStream 9.1 Triton development image...'
|
||||
Invoke-Docker @('pull', $developmentImage) 'DeepStream development image pull'
|
||||
}
|
||||
|
||||
$containerSource = '/opt/nvidia/deepstream/deepstream/sources/apps/sample_apps/deepstream_tao_apps/post_processor'
|
||||
Invoke-Docker @(
|
||||
'run', '--rm', '--gpus', 'all', '--network', 'none', '--cap-drop', 'ALL',
|
||||
'--security-opt', 'no-new-privileges',
|
||||
'--mount', "type=bind,src=$source,dst=$containerSource",
|
||||
'--workdir', $containerSource,
|
||||
'--entrypoint', 'make', $developmentImage, 'CUDA_VER=13.2'
|
||||
) 'official NVIDIA TAO parser build'
|
||||
if (-not (Test-Path -LiteralPath $sourceLibrary -PathType Leaf) -or
|
||||
(Get-Item -LiteralPath $sourceLibrary).Length -eq 0) {
|
||||
throw 'NVIDIA TAO parser build did not produce a library.'
|
||||
}
|
||||
|
||||
$destination = Join-Path $output $libraryName
|
||||
$temporary = "$destination.$([Guid]::NewGuid().ToString('N')).tmp"
|
||||
Copy-Item -LiteralPath $sourceLibrary -Destination $temporary
|
||||
Move-Item -LiteralPath $temporary -Destination $destination -Force
|
||||
$librarySha = Get-Sha256 $destination
|
||||
|
||||
Invoke-Docker @(
|
||||
'run', '--rm', '--gpus', 'all', '--network', 'none', '--read-only',
|
||||
'--cap-drop', 'ALL', '--security-opt', 'no-new-privileges',
|
||||
'--mount', "type=bind,src=$output,dst=/workspace/parser,readonly",
|
||||
'--entrypoint', '/bin/bash', $runtimeImage, '-lc',
|
||||
"ldd /workspace/parser/$libraryName && nm -D /workspace/parser/$libraryName | grep -q NvDsInferParseCustomDDETRTAO"
|
||||
) 'NVIDIA TAO parser runtime verification'
|
||||
|
||||
$manifest = [ordered]@{
|
||||
schema_version = 'missioncore.e46e-nvidia-tao-parser/v1'
|
||||
status = 'completed'
|
||||
source_repository = 'https://github.com/NVIDIA/DeepStream.git'
|
||||
source_commit = $sourceCommit
|
||||
source_files = @($expectedSources.GetEnumerator() | ForEach-Object {
|
||||
[ordered]@{ path = $_.Key; sha256 = $_.Value }
|
||||
})
|
||||
development_image = $developmentImage
|
||||
runtime_image = $runtimeImage
|
||||
cuda_version = '13.2'
|
||||
symbol = 'NvDsInferParseCustomDDETRTAO'
|
||||
library_file = $libraryName
|
||||
library_sha256 = $librarySha
|
||||
completed_at_utc = (Get-Date).ToUniversalTime().ToString('o')
|
||||
}
|
||||
$manifest | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'parser-runtime.json') -Encoding UTF8
|
||||
Write-Host "E46E_NVIDIA_TAO_PARSER_COMPLETED sha256=$librarySha output=$output"
|
||||
}
|
||||
finally {
|
||||
if ($LogPath) { Stop-Transcript | Out-Null }
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$BuildScript,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SourceRoot,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$OutputRoot
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$taskName = 'MissionCore-E46ENvidiaTaoParser'
|
||||
$script = (Resolve-Path -LiteralPath $BuildScript).Path
|
||||
$source = (Resolve-Path -LiteralPath $SourceRoot).Path
|
||||
New-Item -ItemType Directory -Force -Path $OutputRoot | Out-Null
|
||||
$output = (Resolve-Path -LiteralPath $OutputRoot).Path
|
||||
$existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
||||
if ($existing -and $existing.State -eq 'Running') {
|
||||
throw "$taskName is already running."
|
||||
}
|
||||
$logsRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46e\logs'
|
||||
New-Item -ItemType Directory -Force -Path $logsRoot | Out-Null
|
||||
$stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ')
|
||||
$logPath = Join-Path $logsRoot "e46e-parser-build-$stamp.log"
|
||||
$powerShell = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||
$arguments = @(
|
||||
'-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
|
||||
'-File', "`"$script`"", '-SourceRoot', "`"$source`"",
|
||||
'-OutputRoot', "`"$output`"", '-LogPath', "`"$logPath`""
|
||||
) -join ' '
|
||||
$userId = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
$action = New-ScheduledTaskAction -Execute $powerShell -Argument $arguments -WorkingDirectory $source
|
||||
$principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited
|
||||
$trigger = New-ScheduledTaskTrigger -Once -At ((Get-Date).AddMinutes(30))
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
|
||||
-StartWhenAvailable -ExecutionTimeLimit ([TimeSpan]::FromHours(3))
|
||||
Register-ScheduledTask -TaskName $taskName -Action $action -Principal $principal `
|
||||
-Trigger $trigger -Settings $settings `
|
||||
-Description 'Build the pinned official NVIDIA DeepStream TAO RT-DETR parser.' -Force | Out-Null
|
||||
Start-ScheduledTask -TaskName $taskName
|
||||
Write-Host "E46E_PARSER_TASK_STARTED task=$taskName log=$logPath"
|
||||
@@ -0,0 +1,319 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PackageRoot,
|
||||
|
||||
[string]$SourceJobRoot = 'D:\NDC_MISSIONCORE\runtime\jobs\recorded-camera-602ac89026ed12978619801d',
|
||||
|
||||
[string]$RuntimeRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46e',
|
||||
|
||||
[string]$LogPath = ''
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
function Get-Sha256([string]$Path) {
|
||||
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
}
|
||||
|
||||
function Assert-Sha256([string]$Path, [string]$Expected, [string]$Label) {
|
||||
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
||||
throw "$Label is missing: $Path"
|
||||
}
|
||||
$actual = Get-Sha256 $Path
|
||||
if ($actual -ne $Expected) {
|
||||
throw "$Label SHA-256 changed: expected $Expected, got $actual"
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Docker([string[]]$Arguments, [string]$Label) {
|
||||
& docker @Arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "$Label failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
}
|
||||
|
||||
$package = (Resolve-Path -LiteralPath $PackageRoot).Path
|
||||
$sourceJob = (Resolve-Path -LiteralPath $SourceJobRoot).Path
|
||||
$profilePath = Join-Path $package 'profile.json'
|
||||
$manifestPath = Join-Path $package 'manifest.json'
|
||||
if (-not (Test-Path -LiteralPath $profilePath -PathType Leaf) -or
|
||||
-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) {
|
||||
throw 'E46E package is incomplete.'
|
||||
}
|
||||
$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
|
||||
if ($manifest.schema_version -ne 'missioncore.e46e-worker-package/v1' -or
|
||||
$manifest.package_id -ne (Split-Path -Leaf $package)) {
|
||||
throw 'E46E package identity is invalid.'
|
||||
}
|
||||
$expectedPaths = @($manifest.identity.artifact_paths)
|
||||
foreach ($artifact in @($manifest.artifacts)) {
|
||||
if ($expectedPaths -notcontains [string]$artifact.path) {
|
||||
throw "Unexpected E46E package artifact: $($artifact.path)"
|
||||
}
|
||||
$artifactPath = Join-Path $package ([string]$artifact.path)
|
||||
Assert-Sha256 $artifactPath ([string]$artifact.sha256) "package artifact $($artifact.path)"
|
||||
if ((Get-Item -LiteralPath $artifactPath).Length -ne [int64]$artifact.byte_length) {
|
||||
throw "Package artifact length changed: $($artifact.path)"
|
||||
}
|
||||
}
|
||||
$actualPaths = @(Get-ChildItem -LiteralPath $package -Recurse -File | ForEach-Object {
|
||||
$_.FullName.Substring($package.Length + 1).Replace('\', '/')
|
||||
})
|
||||
if (@($actualPaths | Where-Object { $_ -ne 'manifest.json' -and $expectedPaths -notcontains $_ }).Count -ne 0 -or
|
||||
@($expectedPaths | Where-Object { $actualPaths -notcontains $_ }).Count -ne 0) {
|
||||
throw 'E46E package file set changed.'
|
||||
}
|
||||
|
||||
$profile = Get-Content -LiteralPath $profilePath -Raw | ConvertFrom-Json
|
||||
if ($profile.schema_version -ne 'missioncore.e46e-ready-stack-profile/v1') {
|
||||
throw 'E46E profile is incompatible.'
|
||||
}
|
||||
$image = [string]$profile.runtime.container_image
|
||||
$imageDigestMatch = [regex]::Match($image, '@sha256:([0-9a-f]{64})$')
|
||||
if (-not $imageDigestMatch.Success) {
|
||||
throw 'E46E runtime image must be pinned by a full SHA-256 digest.'
|
||||
}
|
||||
$imageDigest = $imageDigestMatch.Groups[1].Value
|
||||
$modelSha = [string]$profile.detector.model_sha256
|
||||
$streamSha = [string]$profile.source.stream_sha256
|
||||
$modelFile = [string]$profile.detector.model_file
|
||||
$modelUrl = [string]$profile.detector.model_url
|
||||
$parserFile = [string]$profile.parser.library_file
|
||||
$parserSha = [string]$profile.parser.library_sha256
|
||||
$parserLibraryPath = Join-Path $package "runtime\$parserFile"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $RuntimeRoot | Out-Null
|
||||
$logsRoot = Join-Path $RuntimeRoot 'logs'
|
||||
$modelsRoot = Join-Path $RuntimeRoot 'models\trafficcamnet_transformer_lite\deployable_resnet50_v2.0'
|
||||
$inputsRoot = Join-Path $RuntimeRoot 'inputs'
|
||||
$runsRoot = Join-Path $RuntimeRoot 'runs'
|
||||
$resultsRoot = Join-Path $RuntimeRoot 'ready-stack-results'
|
||||
foreach ($path in @($logsRoot, $modelsRoot, $inputsRoot, $runsRoot, $resultsRoot)) {
|
||||
New-Item -ItemType Directory -Force -Path $path | Out-Null
|
||||
}
|
||||
if ($LogPath) {
|
||||
$logParent = Split-Path -Parent $LogPath
|
||||
if ($logParent) { New-Item -ItemType Directory -Force -Path $logParent | Out-Null }
|
||||
Start-Transcript -LiteralPath $LogPath -Append | Out-Null
|
||||
}
|
||||
|
||||
try {
|
||||
Write-Host "E46E package: $($manifest.package_id)"
|
||||
Write-Host "E46E source: $sourceJob"
|
||||
Write-Host "E46E image: $image"
|
||||
Assert-Sha256 $parserLibraryPath $parserSha 'official NVIDIA DeepStream TAO parser'
|
||||
|
||||
$modelPath = Join-Path $modelsRoot $modelFile
|
||||
if (Test-Path -LiteralPath $modelPath -PathType Leaf) {
|
||||
Assert-Sha256 $modelPath $modelSha 'TrafficCamNet Transformer Lite model'
|
||||
}
|
||||
else {
|
||||
$modelTemp = "$modelPath.$([Guid]::NewGuid().ToString('N')).download"
|
||||
Write-Host 'Downloading exact NVIDIA TrafficCamNet Transformer Lite model...'
|
||||
& curl.exe --fail --location --retry 3 --output $modelTemp $modelUrl
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "NVIDIA model download failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
Assert-Sha256 $modelTemp $modelSha 'downloaded model'
|
||||
Move-Item -LiteralPath $modelTemp -Destination $modelPath
|
||||
}
|
||||
|
||||
$jobPath = Join-Path $sourceJob 'job.json'
|
||||
$job = Get-Content -LiteralPath $jobPath -Raw | ConvertFrom-Json
|
||||
if ($job.job_id -ne $profile.source.job_id -or
|
||||
$job.input.archive_index_sha256 -ne $profile.source.archive_index_sha256 -or
|
||||
$job.input.archive_summary_sha256 -ne $profile.source.archive_summary_sha256) {
|
||||
throw 'Exact E46E source job binding changed.'
|
||||
}
|
||||
$cameraRoot = Join-Path $sourceJob 'input\camera\sensor.camera.right\epoch-1'
|
||||
$indexPath = Join-Path $cameraRoot 'index.jsonl'
|
||||
$summaryPath = Join-Path $cameraRoot 'summary.json'
|
||||
Assert-Sha256 $indexPath ([string]$profile.source.archive_index_sha256) 'source index'
|
||||
Assert-Sha256 $summaryPath ([string]$profile.source.archive_summary_sha256) 'source summary'
|
||||
$summary = Get-Content -LiteralPath $summaryPath -Raw | ConvertFrom-Json
|
||||
if ($summary.stream_sha256 -ne $streamSha -or
|
||||
[int]$summary.segment_count -ne [int]$profile.source.segment_count) {
|
||||
throw 'Source stream identity changed.'
|
||||
}
|
||||
|
||||
$inputPath = Join-Path $inputsRoot "right-$streamSha.mp4"
|
||||
if (Test-Path -LiteralPath $inputPath -PathType Leaf) {
|
||||
Assert-Sha256 $inputPath $streamSha 'reconstructed RIGHT stream'
|
||||
}
|
||||
else {
|
||||
$inputTemp = "$inputPath.$([Guid]::NewGuid().ToString('N')).tmp"
|
||||
$destinationStream = [System.IO.File]::Open(
|
||||
$inputTemp,
|
||||
[System.IO.FileMode]::CreateNew,
|
||||
[System.IO.FileAccess]::Write,
|
||||
[System.IO.FileShare]::None
|
||||
)
|
||||
$incremental = [System.Security.Cryptography.IncrementalHash]::CreateHash(
|
||||
[System.Security.Cryptography.HashAlgorithmName]::SHA256
|
||||
)
|
||||
try {
|
||||
$sourceParts = [System.Collections.Generic.List[string]]::new()
|
||||
$sourceParts.Add((Join-Path $cameraRoot 'init.mp4'))
|
||||
foreach ($line in [System.IO.File]::ReadLines($indexPath)) {
|
||||
$row = $line | ConvertFrom-Json
|
||||
$sourceParts.Add((Join-Path $cameraRoot ([string]$row.path)))
|
||||
}
|
||||
if ($sourceParts.Count -ne ([int]$profile.source.segment_count + 1)) {
|
||||
throw 'Source stream part count changed.'
|
||||
}
|
||||
$buffer = New-Object byte[] (4MB)
|
||||
foreach ($part in $sourceParts) {
|
||||
$inputStream = [System.IO.File]::OpenRead($part)
|
||||
try {
|
||||
while (($read = $inputStream.Read($buffer, 0, $buffer.Length)) -gt 0) {
|
||||
$destinationStream.Write($buffer, 0, $read)
|
||||
$incremental.AppendData($buffer, 0, $read)
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$inputStream.Dispose()
|
||||
}
|
||||
}
|
||||
$destinationStream.Flush($true)
|
||||
$actualStreamSha = ([BitConverter]::ToString(
|
||||
$incremental.GetHashAndReset()
|
||||
)).Replace('-', '').ToLowerInvariant()
|
||||
}
|
||||
finally {
|
||||
$incremental.Dispose()
|
||||
$destinationStream.Dispose()
|
||||
}
|
||||
if ($actualStreamSha -ne $streamSha) {
|
||||
throw "Reconstructed stream SHA-256 changed: $actualStreamSha"
|
||||
}
|
||||
Move-Item -LiteralPath $inputTemp -Destination $inputPath
|
||||
}
|
||||
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
& docker image inspect $image *> $null
|
||||
$imageCached = $LASTEXITCODE -eq 0
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
if ($imageCached) {
|
||||
Write-Host 'Using the exact cached NVIDIA DeepStream image.'
|
||||
}
|
||||
else {
|
||||
Write-Host 'Pulling exact NVIDIA DeepStream image in the interactive user session...'
|
||||
Invoke-Docker @('pull', $image) 'DeepStream image pull'
|
||||
}
|
||||
|
||||
$runId = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ')
|
||||
$runRoot = Join-Path $runsRoot $runId
|
||||
$rawRoot = Join-Path $runRoot 'raw'
|
||||
$inputMount = Join-Path $runRoot 'input'
|
||||
New-Item -ItemType Directory -Force -Path $rawRoot | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $rawRoot 'detections') | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $rawRoot 'tracks') | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $inputMount | Out-Null
|
||||
Copy-Item -LiteralPath $inputPath -Destination (Join-Path $inputMount 'right.mp4')
|
||||
$deepstreamLog = Join-Path $rawRoot 'deepstream.log'
|
||||
$trackerCopy = Join-Path $rawRoot 'tracker-config.yml'
|
||||
$startedAt = (Get-Date).ToUniversalTime().ToString('o')
|
||||
|
||||
$containerCommand = @"
|
||||
set -euo pipefail
|
||||
cp /opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_NvDCF_perf.yml /workspace/output/tracker-config.yml
|
||||
deepstream-app -c /workspace/package/runtime/e46e_deepstream_app.txt
|
||||
"@
|
||||
$dockerArguments = @(
|
||||
'run', '--rm', '--name', "ndc-mission-core-deepstream-e46e-$runId",
|
||||
'--gpus', 'all', '--network', 'none', '--cap-drop', 'ALL',
|
||||
'--security-opt', 'no-new-privileges', '--shm-size', '4g',
|
||||
'--label', 'com.nodedc.product=mission-core',
|
||||
'--label', 'com.nodedc.stack=perception',
|
||||
'--label', 'com.nodedc.role=deepstream-ready-stack-e46e',
|
||||
'--label', 'com.nodedc.managed-by=mission-core-worker',
|
||||
'--mount', "type=bind,src=$inputMount,dst=/workspace/input,readonly",
|
||||
'--mount', "type=bind,src=$package,dst=/workspace/package,readonly",
|
||||
'--mount', "type=bind,src=$modelsRoot,dst=/workspace/model",
|
||||
'--mount', "type=bind,src=$rawRoot,dst=/workspace/output",
|
||||
'--entrypoint', '/bin/bash', $image, '-lc', $containerCommand
|
||||
)
|
||||
Write-Host 'Running full 4489-frame NVIDIA detector + NvDCF replay...'
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
& docker @dockerArguments 2>&1 | Tee-Object -LiteralPath $deepstreamLog
|
||||
$deepstreamExit = $LASTEXITCODE
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
if ($deepstreamExit -ne 0) {
|
||||
throw "DeepStream replay failed with exit code $deepstreamExit"
|
||||
}
|
||||
|
||||
$overlayPath = Join-Path $rawRoot 'overlay.mp4'
|
||||
$enginePath = Join-Path $modelsRoot "$modelFile`_b1_gpu0_fp16.engine"
|
||||
if (-not (Test-Path -LiteralPath $overlayPath -PathType Leaf) -or
|
||||
(Get-Item -LiteralPath $overlayPath).Length -eq 0) {
|
||||
throw 'DeepStream did not produce an overlay video.'
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $enginePath -PathType Leaf)) {
|
||||
throw 'DeepStream did not produce the exact TensorRT engine.'
|
||||
}
|
||||
$detectionFiles = @(Get-ChildItem -LiteralPath (Join-Path $rawRoot 'detections') -File)
|
||||
$trackFiles = @(Get-ChildItem -LiteralPath (Join-Path $rawRoot 'tracks') -File)
|
||||
if ($detectionFiles.Count -ne [int]$profile.source.segment_count -or
|
||||
$trackFiles.Count -ne [int]$profile.source.segment_count) {
|
||||
throw "DeepStream frame coverage changed: detections=$($detectionFiles.Count), tracks=$($trackFiles.Count)"
|
||||
}
|
||||
$runtime = [ordered]@{
|
||||
schema_version = 'missioncore.e46e-deepstream-runtime/v1'
|
||||
status = 'completed'
|
||||
worker_host = $env:COMPUTERNAME
|
||||
gpu_name = ((& nvidia-smi --query-gpu=name --format=csv,noheader | Select-Object -First 1).Trim())
|
||||
started_at_utc = $startedAt
|
||||
completed_at_utc = (Get-Date).ToUniversalTime().ToString('o')
|
||||
container_image = $image
|
||||
container_image_digest = $imageDigest
|
||||
model_sha256 = Get-Sha256 $modelPath
|
||||
model_engine_sha256 = Get-Sha256 $enginePath
|
||||
deepstream_config_sha256 = Get-Sha256 (Join-Path $package 'runtime\e46e_deepstream_app.txt')
|
||||
detector_config_sha256 = Get-Sha256 (Join-Path $package 'runtime\e46e_trafficcamnet_rtdetr.txt')
|
||||
parser_library_sha256 = Get-Sha256 $parserLibraryPath
|
||||
tracker_config_sha256 = Get-Sha256 $trackerCopy
|
||||
input_stream_sha256 = Get-Sha256 $inputPath
|
||||
overlay_sha256 = Get-Sha256 $overlayPath
|
||||
frame_count = [int]$profile.source.segment_count
|
||||
deepstream_exit_code = $deepstreamExit
|
||||
}
|
||||
$runtimePath = Join-Path $rawRoot 'runtime.json'
|
||||
$runtime | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $runtimePath -Encoding UTF8
|
||||
|
||||
$consolidatorImage = 'nvcr.io/nvidia/tritonserver:26.06-py3@sha256:58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794'
|
||||
$consolidatorArguments = @(
|
||||
'run', '--rm', '--name', "ndc-mission-core-e46e-consolidator-$runId",
|
||||
'--network', 'none', '--read-only', '--cap-drop', 'ALL',
|
||||
'--security-opt', 'no-new-privileges', '--tmpfs', '/tmp:rw,noexec,nosuid,size=64m',
|
||||
'--mount', "type=bind,src=$package,dst=/workspace/package,readonly",
|
||||
'--mount', "type=bind,src=$sourceJob,dst=/workspace/source-job,readonly",
|
||||
'--mount', "type=bind,src=$rawRoot,dst=/workspace/raw,readonly",
|
||||
'--mount', "type=bind,src=$resultsRoot,dst=/workspace/results",
|
||||
'-e', 'PYTHONPATH=/workspace/package/runtime',
|
||||
'-e', 'PYTHONDONTWRITEBYTECODE=1',
|
||||
$consolidatorImage,
|
||||
'python3', '/workspace/package/runtime/run_e46e_ready_stack.py',
|
||||
'--source-job', '/workspace/source-job',
|
||||
'--raw-root', '/workspace/raw',
|
||||
'--profile', '/workspace/package/profile.json',
|
||||
'--output-root', '/workspace/results'
|
||||
)
|
||||
Write-Host 'Freezing immutable E46E evidence...'
|
||||
Invoke-Docker $consolidatorArguments 'E46E consolidation'
|
||||
Write-Host "E46E_READY_STACK_COMPLETED run=$runId results=$resultsRoot"
|
||||
}
|
||||
finally {
|
||||
if ($LogPath) { Stop-Transcript | Out-Null }
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PackageRoot,
|
||||
|
||||
[string]$SourceJobRoot = 'D:\NDC_MISSIONCORE\runtime\jobs\recorded-camera-602ac89026ed12978619801d',
|
||||
|
||||
[string]$RuntimeRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46e'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$taskName = 'MissionCore-E46EReadyStack'
|
||||
$package = (Resolve-Path -LiteralPath $PackageRoot).Path
|
||||
$script = Join-Path $package 'runtime\Invoke-E46EReadyStack.ps1'
|
||||
if (-not (Test-Path -LiteralPath $script -PathType Leaf)) {
|
||||
throw "E46E runner is missing: $script"
|
||||
}
|
||||
$existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
||||
if ($existing -and $existing.State -eq 'Running') {
|
||||
throw "$taskName is already running."
|
||||
}
|
||||
$logsRoot = Join-Path $RuntimeRoot 'logs'
|
||||
New-Item -ItemType Directory -Force -Path $logsRoot | Out-Null
|
||||
$stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ')
|
||||
$logPath = Join-Path $logsRoot "e46e-ready-stack-$stamp.log"
|
||||
$powerShell = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||
$arguments = @(
|
||||
'-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
|
||||
'-File', "`"$script`"",
|
||||
'-PackageRoot', "`"$package`"",
|
||||
'-SourceJobRoot', "`"$SourceJobRoot`"",
|
||||
'-RuntimeRoot', "`"$RuntimeRoot`"",
|
||||
'-LogPath', "`"$logPath`""
|
||||
) -join ' '
|
||||
$userId = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
$action = New-ScheduledTaskAction -Execute $powerShell -Argument $arguments -WorkingDirectory $package
|
||||
$principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited
|
||||
$trigger = New-ScheduledTaskTrigger -Once -At ((Get-Date).AddMinutes(30))
|
||||
$settings = New-ScheduledTaskSettingsSet `
|
||||
-AllowStartIfOnBatteries `
|
||||
-DontStopIfGoingOnBatteries `
|
||||
-StartWhenAvailable `
|
||||
-ExecutionTimeLimit ([TimeSpan]::FromHours(6))
|
||||
Register-ScheduledTask `
|
||||
-TaskName $taskName `
|
||||
-Action $action `
|
||||
-Principal $principal `
|
||||
-Trigger $trigger `
|
||||
-Settings $settings `
|
||||
-Description 'One-shot Mission Core E46E stock NVIDIA RT-DETR plus NvDCF recorded RIGHT replay.' `
|
||||
-Force | Out-Null
|
||||
Start-ScheduledTask -TaskName $taskName
|
||||
Write-Host "E46E_TASK_STARTED task=$taskName log=$logPath"
|
||||
@@ -0,0 +1,329 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PackageRoot,
|
||||
|
||||
[string]$SourceJobRoot = 'D:\NDC_MISSIONCORE\runtime\jobs\recorded-camera-602ac89026ed12978619801d',
|
||||
|
||||
[string]$RuntimeRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46f',
|
||||
|
||||
[string]$LogPath = ''
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
function Get-Sha256([string]$Path) {
|
||||
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
}
|
||||
|
||||
function Assert-Sha256([string]$Path, [string]$Expected, [string]$Label) {
|
||||
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
||||
throw "$Label is missing: $Path"
|
||||
}
|
||||
$actual = Get-Sha256 $Path
|
||||
if ($actual -ne $Expected) {
|
||||
throw "$Label SHA-256 changed: expected $Expected, got $actual"
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Docker([string[]]$Arguments, [string]$Label) {
|
||||
& docker @Arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "$Label failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
}
|
||||
|
||||
$package = (Resolve-Path -LiteralPath $PackageRoot).Path
|
||||
$sourceJob = (Resolve-Path -LiteralPath $SourceJobRoot).Path
|
||||
$profilePath = Join-Path $package 'profile.json'
|
||||
$manifestPath = Join-Path $package 'manifest.json'
|
||||
if (-not (Test-Path -LiteralPath $profilePath -PathType Leaf) -or
|
||||
-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) {
|
||||
throw 'E46F package is incomplete.'
|
||||
}
|
||||
$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
|
||||
if ($manifest.schema_version -ne 'missioncore.e46f-worker-package/v1' -or
|
||||
$manifest.package_id -ne (Split-Path -Leaf $package)) {
|
||||
throw 'E46F package identity is invalid.'
|
||||
}
|
||||
$expectedPaths = @($manifest.identity.artifact_paths)
|
||||
foreach ($artifact in @($manifest.artifacts)) {
|
||||
if ($expectedPaths -notcontains [string]$artifact.path) {
|
||||
throw "Unexpected E46F package artifact: $($artifact.path)"
|
||||
}
|
||||
$artifactPath = Join-Path $package ([string]$artifact.path)
|
||||
Assert-Sha256 $artifactPath ([string]$artifact.sha256) "package artifact $($artifact.path)"
|
||||
if ((Get-Item -LiteralPath $artifactPath).Length -ne [int64]$artifact.byte_length) {
|
||||
throw "Package artifact length changed: $($artifact.path)"
|
||||
}
|
||||
}
|
||||
$actualPaths = @(Get-ChildItem -LiteralPath $package -Recurse -File | ForEach-Object {
|
||||
$_.FullName.Substring($package.Length + 1).Replace('\', '/')
|
||||
})
|
||||
if (@($actualPaths | Where-Object { $_ -ne 'manifest.json' -and $expectedPaths -notcontains $_ }).Count -ne 0 -or
|
||||
@($expectedPaths | Where-Object { $actualPaths -notcontains $_ }).Count -ne 0) {
|
||||
throw 'E46F package file set changed.'
|
||||
}
|
||||
|
||||
$profile = Get-Content -LiteralPath $profilePath -Raw | ConvertFrom-Json
|
||||
if ($profile.schema_version -ne 'missioncore.e46f-dashcam-bakeoff-profile/v1' -or
|
||||
$profile.comparison_contract.controlled_change -ne 'detector-only') {
|
||||
throw 'E46F profile is incompatible.'
|
||||
}
|
||||
$image = [string]$profile.runtime.container_image
|
||||
$imageDigestMatch = [regex]::Match($image, '@sha256:([0-9a-f]{64})$')
|
||||
if (-not $imageDigestMatch.Success) {
|
||||
throw 'E46F runtime image must be pinned by a full SHA-256 digest.'
|
||||
}
|
||||
$imageDigest = $imageDigestMatch.Groups[1].Value
|
||||
$modelSha = [string]$profile.detector.model_sha256
|
||||
$streamSha = [string]$profile.source.stream_sha256
|
||||
$modelFile = [string]$profile.detector.model_file
|
||||
$modelUrl = [string]$profile.detector.model_url
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $RuntimeRoot | Out-Null
|
||||
$logsRoot = Join-Path $RuntimeRoot 'logs'
|
||||
$modelsRoot = Join-Path $RuntimeRoot "models\dashcamnet\$([string]$profile.detector.version)"
|
||||
$inputsRoot = Join-Path $RuntimeRoot 'inputs'
|
||||
$runsRoot = Join-Path $RuntimeRoot 'runs'
|
||||
$resultsRoot = Join-Path $RuntimeRoot 'dashcam-bakeoff-results'
|
||||
foreach ($path in @($logsRoot, $modelsRoot, $inputsRoot, $runsRoot, $resultsRoot)) {
|
||||
New-Item -ItemType Directory -Force -Path $path | Out-Null
|
||||
}
|
||||
if ($LogPath) {
|
||||
$logParent = Split-Path -Parent $LogPath
|
||||
if ($logParent) { New-Item -ItemType Directory -Force -Path $logParent | Out-Null }
|
||||
Start-Transcript -LiteralPath $LogPath -Append | Out-Null
|
||||
}
|
||||
|
||||
try {
|
||||
Write-Host "E46F package: $($manifest.package_id)"
|
||||
Write-Host "E46F source: $sourceJob"
|
||||
Write-Host "E46F image: $image"
|
||||
|
||||
$modelPath = Join-Path $modelsRoot $modelFile
|
||||
if (Test-Path -LiteralPath $modelPath -PathType Leaf) {
|
||||
Assert-Sha256 $modelPath $modelSha 'DashCamNet model'
|
||||
}
|
||||
else {
|
||||
$modelTemp = "$modelPath.$([Guid]::NewGuid().ToString('N')).download"
|
||||
Write-Host 'Downloading exact NVIDIA DashCamNet model...'
|
||||
& curl.exe --fail --location --retry 3 --output $modelTemp $modelUrl
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "NVIDIA model download failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
Assert-Sha256 $modelTemp $modelSha 'downloaded DashCamNet model'
|
||||
Move-Item -LiteralPath $modelTemp -Destination $modelPath
|
||||
}
|
||||
|
||||
$jobPath = Join-Path $sourceJob 'job.json'
|
||||
$job = Get-Content -LiteralPath $jobPath -Raw | ConvertFrom-Json
|
||||
if ($job.job_id -ne $profile.source.job_id -or
|
||||
$job.input.archive_index_sha256 -ne $profile.source.archive_index_sha256 -or
|
||||
$job.input.archive_summary_sha256 -ne $profile.source.archive_summary_sha256) {
|
||||
throw 'Exact E46F source job binding changed.'
|
||||
}
|
||||
$cameraRoot = Join-Path $sourceJob 'input\camera\sensor.camera.right\epoch-1'
|
||||
$indexPath = Join-Path $cameraRoot 'index.jsonl'
|
||||
$summaryPath = Join-Path $cameraRoot 'summary.json'
|
||||
Assert-Sha256 $indexPath ([string]$profile.source.archive_index_sha256) 'source index'
|
||||
Assert-Sha256 $summaryPath ([string]$profile.source.archive_summary_sha256) 'source summary'
|
||||
$summary = Get-Content -LiteralPath $summaryPath -Raw | ConvertFrom-Json
|
||||
if ($summary.stream_sha256 -ne $streamSha -or
|
||||
[int]$summary.segment_count -ne [int]$profile.source.segment_count) {
|
||||
throw 'Source stream identity changed.'
|
||||
}
|
||||
|
||||
$inputPath = Join-Path $inputsRoot "right-$streamSha.mp4"
|
||||
$e46eInputPath = "D:\NDC_MISSIONCORE\runtime\experiments\e46e\inputs\right-$streamSha.mp4"
|
||||
if (Test-Path -LiteralPath $inputPath -PathType Leaf) {
|
||||
Assert-Sha256 $inputPath $streamSha 'E46F reconstructed RIGHT stream'
|
||||
}
|
||||
elseif (Test-Path -LiteralPath $e46eInputPath -PathType Leaf) {
|
||||
Assert-Sha256 $e46eInputPath $streamSha 'E46E controlled RIGHT stream'
|
||||
Copy-Item -LiteralPath $e46eInputPath -Destination $inputPath
|
||||
Assert-Sha256 $inputPath $streamSha 'E46F copied RIGHT stream'
|
||||
}
|
||||
else {
|
||||
$inputTemp = "$inputPath.$([Guid]::NewGuid().ToString('N')).tmp"
|
||||
$destinationStream = [System.IO.File]::Open(
|
||||
$inputTemp,
|
||||
[System.IO.FileMode]::CreateNew,
|
||||
[System.IO.FileAccess]::Write,
|
||||
[System.IO.FileShare]::None
|
||||
)
|
||||
$incremental = [System.Security.Cryptography.IncrementalHash]::CreateHash(
|
||||
[System.Security.Cryptography.HashAlgorithmName]::SHA256
|
||||
)
|
||||
try {
|
||||
$sourceParts = [System.Collections.Generic.List[string]]::new()
|
||||
$sourceParts.Add((Join-Path $cameraRoot 'init.mp4'))
|
||||
foreach ($line in [System.IO.File]::ReadLines($indexPath)) {
|
||||
$row = $line | ConvertFrom-Json
|
||||
$sourceParts.Add((Join-Path $cameraRoot ([string]$row.path)))
|
||||
}
|
||||
if ($sourceParts.Count -ne ([int]$profile.source.segment_count + 1)) {
|
||||
throw 'Source stream part count changed.'
|
||||
}
|
||||
$buffer = New-Object byte[] (4MB)
|
||||
foreach ($part in $sourceParts) {
|
||||
$inputStream = [System.IO.File]::OpenRead($part)
|
||||
try {
|
||||
while (($read = $inputStream.Read($buffer, 0, $buffer.Length)) -gt 0) {
|
||||
$destinationStream.Write($buffer, 0, $read)
|
||||
$incremental.AppendData($buffer, 0, $read)
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$inputStream.Dispose()
|
||||
}
|
||||
}
|
||||
$destinationStream.Flush($true)
|
||||
$actualStreamSha = ([BitConverter]::ToString(
|
||||
$incremental.GetHashAndReset()
|
||||
)).Replace('-', '').ToLowerInvariant()
|
||||
}
|
||||
finally {
|
||||
$incremental.Dispose()
|
||||
$destinationStream.Dispose()
|
||||
}
|
||||
if ($actualStreamSha -ne $streamSha) {
|
||||
throw "Reconstructed stream SHA-256 changed: $actualStreamSha"
|
||||
}
|
||||
Move-Item -LiteralPath $inputTemp -Destination $inputPath
|
||||
}
|
||||
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
& docker image inspect $image *> $null
|
||||
$imageCached = $LASTEXITCODE -eq 0
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
if ($imageCached) {
|
||||
Write-Host 'Using the exact cached NVIDIA DeepStream image.'
|
||||
}
|
||||
else {
|
||||
Write-Host 'Pulling exact NVIDIA DeepStream image in the interactive user session...'
|
||||
Invoke-Docker @('pull', $image) 'DeepStream image pull'
|
||||
}
|
||||
|
||||
$runId = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ')
|
||||
$runRoot = Join-Path $runsRoot $runId
|
||||
$rawRoot = Join-Path $runRoot 'raw'
|
||||
$inputMount = Join-Path $runRoot 'input'
|
||||
New-Item -ItemType Directory -Force -Path $rawRoot | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $rawRoot 'detections') | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $rawRoot 'tracks') | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $inputMount | Out-Null
|
||||
Copy-Item -LiteralPath $inputPath -Destination (Join-Path $inputMount 'right.mp4')
|
||||
$deepstreamLog = Join-Path $rawRoot 'deepstream.log'
|
||||
$trackerCopy = Join-Path $rawRoot 'tracker-config.yml'
|
||||
$startedAt = (Get-Date).ToUniversalTime().ToString('o')
|
||||
|
||||
$containerCommand = @"
|
||||
set -euo pipefail
|
||||
cp /opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_NvDCF_perf.yml /workspace/output/tracker-config.yml
|
||||
deepstream-app -c /workspace/package/runtime/e46f_deepstream_app.txt
|
||||
"@
|
||||
$dockerArguments = @(
|
||||
'run', '--rm', '--name', "ndc-mission-core-deepstream-e46f-$runId",
|
||||
'--gpus', 'all', '--network', 'none', '--cap-drop', 'ALL',
|
||||
'--security-opt', 'no-new-privileges', '--shm-size', '4g',
|
||||
'--label', 'com.nodedc.product=mission-core',
|
||||
'--label', 'com.nodedc.stack=perception',
|
||||
'--label', 'com.nodedc.role=deepstream-dashcam-bakeoff-e46f',
|
||||
'--label', 'com.nodedc.managed-by=mission-core-worker',
|
||||
'--mount', "type=bind,src=$inputMount,dst=/workspace/input,readonly",
|
||||
'--mount', "type=bind,src=$package,dst=/workspace/package,readonly",
|
||||
'--mount', "type=bind,src=$modelsRoot,dst=/workspace/model",
|
||||
'--mount', "type=bind,src=$rawRoot,dst=/workspace/output",
|
||||
'--entrypoint', '/bin/bash', $image, '-lc', $containerCommand
|
||||
)
|
||||
Write-Host 'Running full 4489-frame NVIDIA DashCamNet + NvDCF bake-off...'
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
& docker @dockerArguments 2>&1 | Tee-Object -LiteralPath $deepstreamLog
|
||||
$deepstreamExit = $LASTEXITCODE
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
if ($deepstreamExit -ne 0) {
|
||||
throw "DeepStream replay failed with exit code $deepstreamExit"
|
||||
}
|
||||
$invalidOutputBinding = Select-String `
|
||||
-LiteralPath $deepstreamLog `
|
||||
-Pattern 'Could not find output layer','Given invalid tensor name' `
|
||||
-SimpleMatch `
|
||||
-Quiet
|
||||
if ($invalidOutputBinding) {
|
||||
throw 'DeepStream accepted the process but rejected the configured detector output bindings.'
|
||||
}
|
||||
|
||||
$overlayPath = Join-Path $rawRoot 'overlay.mp4'
|
||||
$enginePath = Join-Path $modelsRoot "$modelFile`_b1_gpu0_fp16.engine"
|
||||
if (-not (Test-Path -LiteralPath $overlayPath -PathType Leaf) -or
|
||||
(Get-Item -LiteralPath $overlayPath).Length -eq 0) {
|
||||
throw 'DeepStream did not produce an overlay video.'
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $enginePath -PathType Leaf)) {
|
||||
throw 'DeepStream did not produce the exact TensorRT engine.'
|
||||
}
|
||||
$detectionFiles = @(Get-ChildItem -LiteralPath (Join-Path $rawRoot 'detections') -File)
|
||||
$trackFiles = @(Get-ChildItem -LiteralPath (Join-Path $rawRoot 'tracks') -File)
|
||||
if ($detectionFiles.Count -ne [int]$profile.source.segment_count -or
|
||||
$trackFiles.Count -ne [int]$profile.source.segment_count) {
|
||||
throw "DeepStream frame coverage changed: detections=$($detectionFiles.Count), tracks=$($trackFiles.Count)"
|
||||
}
|
||||
$runtime = [ordered]@{
|
||||
schema_version = 'missioncore.e46f-dashcam-deepstream-runtime/v1'
|
||||
status = 'completed'
|
||||
worker_host = $env:COMPUTERNAME
|
||||
gpu_name = ((& nvidia-smi --query-gpu=name --format=csv,noheader | Select-Object -First 1).Trim())
|
||||
started_at_utc = $startedAt
|
||||
completed_at_utc = (Get-Date).ToUniversalTime().ToString('o')
|
||||
container_image = $image
|
||||
container_image_digest = $imageDigest
|
||||
model_sha256 = Get-Sha256 $modelPath
|
||||
model_engine_sha256 = Get-Sha256 $enginePath
|
||||
deepstream_config_sha256 = Get-Sha256 (Join-Path $package 'runtime\e46f_deepstream_app.txt')
|
||||
detector_config_sha256 = Get-Sha256 (Join-Path $package 'runtime\e46f_dashcamnet_detectnet.txt')
|
||||
tracker_config_sha256 = Get-Sha256 $trackerCopy
|
||||
input_stream_sha256 = Get-Sha256 $inputPath
|
||||
overlay_sha256 = Get-Sha256 $overlayPath
|
||||
frame_count = [int]$profile.source.segment_count
|
||||
deepstream_exit_code = $deepstreamExit
|
||||
}
|
||||
$runtimePath = Join-Path $rawRoot 'runtime.json'
|
||||
$runtime | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $runtimePath -Encoding UTF8
|
||||
|
||||
$consolidatorImage = 'nvcr.io/nvidia/tritonserver:26.06-py3@sha256:58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794'
|
||||
$consolidatorArguments = @(
|
||||
'run', '--rm', '--name', "ndc-mission-core-e46f-consolidator-$runId",
|
||||
'--network', 'none', '--read-only', '--cap-drop', 'ALL',
|
||||
'--security-opt', 'no-new-privileges', '--tmpfs', '/tmp:rw,noexec,nosuid,size=64m',
|
||||
'--mount', "type=bind,src=$package,dst=/workspace/package,readonly",
|
||||
'--mount', "type=bind,src=$sourceJob,dst=/workspace/source-job,readonly",
|
||||
'--mount', "type=bind,src=$rawRoot,dst=/workspace/raw,readonly",
|
||||
'--mount', "type=bind,src=$resultsRoot,dst=/workspace/results",
|
||||
'-e', 'PYTHONPATH=/workspace/package/runtime',
|
||||
'-e', 'PYTHONDONTWRITEBYTECODE=1',
|
||||
$consolidatorImage,
|
||||
'python3', '/workspace/package/runtime/run_e46f_dashcam_bakeoff.py',
|
||||
'--source-job', '/workspace/source-job',
|
||||
'--raw-root', '/workspace/raw',
|
||||
'--profile', '/workspace/package/profile.json',
|
||||
'--output-root', '/workspace/results'
|
||||
)
|
||||
Write-Host 'Freezing immutable E46F evidence...'
|
||||
Invoke-Docker $consolidatorArguments 'E46F consolidation'
|
||||
Write-Host "E46F_DASHCAM_BAKEOFF_COMPLETED run=$runId results=$resultsRoot"
|
||||
}
|
||||
finally {
|
||||
if ($LogPath) { Stop-Transcript | Out-Null }
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PackageRoot,
|
||||
|
||||
[string]$SourceJobRoot = 'D:\NDC_MISSIONCORE\runtime\jobs\recorded-camera-602ac89026ed12978619801d',
|
||||
|
||||
[string]$RuntimeRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46f'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$taskName = 'MissionCore-E46FDashCamBakeoff'
|
||||
$package = (Resolve-Path -LiteralPath $PackageRoot).Path
|
||||
$script = Join-Path $package 'runtime\Invoke-E46FDashCamBakeoff.ps1'
|
||||
if (-not (Test-Path -LiteralPath $script -PathType Leaf)) {
|
||||
throw "E46F runner is missing: $script"
|
||||
}
|
||||
$existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
||||
if ($existing -and $existing.State -eq 'Running') {
|
||||
throw "$taskName is already running."
|
||||
}
|
||||
$logsRoot = Join-Path $RuntimeRoot 'logs'
|
||||
New-Item -ItemType Directory -Force -Path $logsRoot | Out-Null
|
||||
$stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ')
|
||||
$logPath = Join-Path $logsRoot "e46f-dashcam-bakeoff-$stamp.log"
|
||||
$powerShell = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||
$arguments = @(
|
||||
'-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
|
||||
'-File', "`"$script`"",
|
||||
'-PackageRoot', "`"$package`"",
|
||||
'-SourceJobRoot', "`"$SourceJobRoot`"",
|
||||
'-RuntimeRoot', "`"$RuntimeRoot`"",
|
||||
'-LogPath', "`"$logPath`""
|
||||
) -join ' '
|
||||
$userId = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
$action = New-ScheduledTaskAction -Execute $powerShell -Argument $arguments -WorkingDirectory $package
|
||||
$principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited
|
||||
$trigger = New-ScheduledTaskTrigger -Once -At ((Get-Date).AddMinutes(30))
|
||||
$settings = New-ScheduledTaskSettingsSet `
|
||||
-AllowStartIfOnBatteries `
|
||||
-DontStopIfGoingOnBatteries `
|
||||
-StartWhenAvailable `
|
||||
-ExecutionTimeLimit ([TimeSpan]::FromHours(6))
|
||||
Register-ScheduledTask `
|
||||
-TaskName $taskName `
|
||||
-Action $action `
|
||||
-Principal $principal `
|
||||
-Trigger $trigger `
|
||||
-Settings $settings `
|
||||
-Description 'One-shot Mission Core E46F stock NVIDIA DashCamNet plus NvDCF detector-only bake-off.' `
|
||||
-Force | Out-Null
|
||||
Start-ScheduledTask -TaskName $taskName
|
||||
Write-Host "E46F_TASK_STARTED task=$taskName log=$logPath"
|
||||
@@ -0,0 +1,440 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PackageRoot,
|
||||
|
||||
[string]$SourceJobRoot = 'D:\NDC_MISSIONCORE\runtime\jobs\recorded-camera-602ac89026ed12978619801d',
|
||||
|
||||
[string]$RuntimeRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46g',
|
||||
|
||||
[string]$LogPath = ''
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
function Get-Sha256([string]$Path) {
|
||||
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
}
|
||||
|
||||
function Assert-Sha256([string]$Path, [string]$Expected, [string]$Label) {
|
||||
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
||||
throw "$Label is missing: $Path"
|
||||
}
|
||||
$actual = Get-Sha256 $Path
|
||||
if ($actual -ne $Expected) {
|
||||
throw "$Label SHA-256 changed: expected $Expected, got $actual"
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Docker([string[]]$Arguments, [string]$Label) {
|
||||
& docker @Arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "$Label failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
}
|
||||
|
||||
function Get-VideoFrameCount([string]$Path) {
|
||||
$probe = & ffprobe -v error -select_streams v:0 -count_frames `
|
||||
-show_entries stream=nb_read_frames -of json $Path | ConvertFrom-Json
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "ffprobe failed: $Path"
|
||||
}
|
||||
return [int]@($probe.streams)[0].nb_read_frames
|
||||
}
|
||||
|
||||
function Ensure-Model(
|
||||
[pscustomobject]$Candidate,
|
||||
[string]$ModelRoot,
|
||||
[string]$Label
|
||||
) {
|
||||
New-Item -ItemType Directory -Force -Path $ModelRoot | Out-Null
|
||||
$modelPath = Join-Path $ModelRoot ([string]$Candidate.model_file)
|
||||
if (Test-Path -LiteralPath $modelPath -PathType Leaf) {
|
||||
Assert-Sha256 $modelPath ([string]$Candidate.model_sha256) $Label
|
||||
return $modelPath
|
||||
}
|
||||
$temporary = "$modelPath.$([Guid]::NewGuid().ToString('N')).download"
|
||||
& curl.exe --fail --location --retry 3 --output $temporary ([string]$Candidate.model_url)
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "$Label download failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
Assert-Sha256 $temporary ([string]$Candidate.model_sha256) "downloaded $Label"
|
||||
Move-Item -LiteralPath $temporary -Destination $modelPath
|
||||
return $modelPath
|
||||
}
|
||||
|
||||
$package = (Resolve-Path -LiteralPath $PackageRoot).Path
|
||||
$sourceJob = (Resolve-Path -LiteralPath $SourceJobRoot).Path
|
||||
$profilePath = Join-Path $package 'profile.json'
|
||||
$manifestPath = Join-Path $package 'manifest.json'
|
||||
if (-not (Test-Path -LiteralPath $profilePath -PathType Leaf) -or
|
||||
-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) {
|
||||
throw 'E46G package is incomplete.'
|
||||
}
|
||||
$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
|
||||
if ($manifest.schema_version -ne 'missioncore.e46g-worker-package/v1' -or
|
||||
$manifest.package_id -ne (Split-Path -Leaf $package)) {
|
||||
throw 'E46G package identity is invalid.'
|
||||
}
|
||||
$expectedPaths = @($manifest.identity.artifact_paths)
|
||||
foreach ($artifact in @($manifest.artifacts)) {
|
||||
if ($expectedPaths -notcontains [string]$artifact.path) {
|
||||
throw "Unexpected E46G package artifact: $($artifact.path)"
|
||||
}
|
||||
$artifactPath = Join-Path $package ([string]$artifact.path)
|
||||
Assert-Sha256 $artifactPath ([string]$artifact.sha256) "package artifact $($artifact.path)"
|
||||
if ((Get-Item -LiteralPath $artifactPath).Length -ne [int64]$artifact.byte_length) {
|
||||
throw "Package artifact length changed: $($artifact.path)"
|
||||
}
|
||||
}
|
||||
$actualPaths = @(Get-ChildItem -LiteralPath $package -Recurse -File | ForEach-Object {
|
||||
$_.FullName.Substring($package.Length + 1).Replace('\', '/')
|
||||
})
|
||||
if (@($actualPaths | Where-Object { $_ -ne 'manifest.json' -and $expectedPaths -notcontains $_ }).Count -ne 0 -or
|
||||
@($expectedPaths | Where-Object { $actualPaths -notcontains $_ }).Count -ne 0) {
|
||||
throw 'E46G package file set changed.'
|
||||
}
|
||||
|
||||
$profile = Get-Content -LiteralPath $profilePath -Raw | ConvertFrom-Json
|
||||
if ($profile.schema_version -ne 'missioncore.e46g-rectified-detector-bakeoff-profile/v1' -or
|
||||
$profile.source.camera_source_id -ne 'sensor.camera.right' -or
|
||||
$profile.rectification.provider -ne 'NVIDIA Gst-nvdewarper') {
|
||||
throw 'E46G profile is incompatible.'
|
||||
}
|
||||
$image = [string]$profile.runtime.container_image
|
||||
$imageDigestMatch = [regex]::Match($image, '@sha256:([0-9a-f]{64})$')
|
||||
if (-not $imageDigestMatch.Success) {
|
||||
throw 'E46G runtime image must be pinned by a full SHA-256 digest.'
|
||||
}
|
||||
$imageDigest = $imageDigestMatch.Groups[1].Value
|
||||
$streamSha = [string]$profile.source.stream_sha256
|
||||
$sampleFrameCount = [int]$profile.selection.frame_count
|
||||
$firstSourceFrame = [int]$profile.selection.first_source_frame_index
|
||||
$lastSourceFrame = [int]$profile.selection.last_source_frame_index
|
||||
$expectedFullFrameCount = [int]$profile.rectification.expected_full_frame_count
|
||||
$retainedSourceRange = @($profile.rectification.retained_source_frame_index_range)
|
||||
if ($retainedSourceRange.Count -ne 2 -or
|
||||
$firstSourceFrame -lt [int]$retainedSourceRange[0] -or
|
||||
$lastSourceFrame -gt [int]$retainedSourceRange[1]) {
|
||||
throw 'E46G selection is outside the admitted NVIDIA-decoded source prefix.'
|
||||
}
|
||||
$parserPath = Join-Path $package "runtime\$([string]$profile.trafficcamnet_parser.library_file)"
|
||||
Assert-Sha256 $parserPath ([string]$profile.trafficcamnet_parser.library_sha256) `
|
||||
'official NVIDIA DeepStream TAO parser'
|
||||
|
||||
if (-not (Get-Command ffmpeg -ErrorAction SilentlyContinue) -or
|
||||
-not (Get-Command ffprobe -ErrorAction SilentlyContinue)) {
|
||||
throw 'E46G requires the existing Worker ffmpeg/ffprobe installation.'
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $RuntimeRoot | Out-Null
|
||||
$inputsRoot = Join-Path $RuntimeRoot 'inputs'
|
||||
$runsRoot = Join-Path $RuntimeRoot 'runs'
|
||||
$resultsRoot = Join-Path $RuntimeRoot 'rectified-detector-bakeoff-results'
|
||||
foreach ($path in @($inputsRoot, $runsRoot, $resultsRoot)) {
|
||||
New-Item -ItemType Directory -Force -Path $path | Out-Null
|
||||
}
|
||||
if ($LogPath) {
|
||||
$logParent = Split-Path -Parent $LogPath
|
||||
if ($logParent) { New-Item -ItemType Directory -Force -Path $logParent | Out-Null }
|
||||
Start-Transcript -LiteralPath $LogPath -Append | Out-Null
|
||||
}
|
||||
|
||||
try {
|
||||
$jobPath = Join-Path $sourceJob 'job.json'
|
||||
$job = Get-Content -LiteralPath $jobPath -Raw | ConvertFrom-Json
|
||||
if ($job.job_id -ne $profile.source.job_id -or
|
||||
$job.input.archive_index_sha256 -ne $profile.source.archive_index_sha256 -or
|
||||
$job.input.archive_summary_sha256 -ne $profile.source.archive_summary_sha256) {
|
||||
throw 'Exact E46G source job binding changed.'
|
||||
}
|
||||
$inputPath = Join-Path $inputsRoot "right-$streamSha.mp4"
|
||||
$e46eInput = "D:\NDC_MISSIONCORE\runtime\experiments\e46e\inputs\right-$streamSha.mp4"
|
||||
$e46fInput = "D:\NDC_MISSIONCORE\runtime\experiments\e46f\inputs\right-$streamSha.mp4"
|
||||
if (Test-Path -LiteralPath $inputPath -PathType Leaf) {
|
||||
Assert-Sha256 $inputPath $streamSha 'E46G controlled RIGHT stream'
|
||||
}
|
||||
elseif (Test-Path -LiteralPath $e46eInput -PathType Leaf) {
|
||||
Assert-Sha256 $e46eInput $streamSha 'E46E controlled RIGHT stream'
|
||||
Copy-Item -LiteralPath $e46eInput -Destination $inputPath
|
||||
}
|
||||
elseif (Test-Path -LiteralPath $e46fInput -PathType Leaf) {
|
||||
Assert-Sha256 $e46fInput $streamSha 'E46F controlled RIGHT stream'
|
||||
Copy-Item -LiteralPath $e46fInput -Destination $inputPath
|
||||
}
|
||||
else {
|
||||
& (Join-Path $package 'runtime\Prepare-RectifiedCameraReplay.ps1') `
|
||||
-JobRoot $sourceJob -OutputPath $inputPath
|
||||
}
|
||||
Assert-Sha256 $inputPath $streamSha 'E46G reconstructed RIGHT stream'
|
||||
|
||||
$trafficModelRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46e\models\trafficcamnet_transformer_lite\deployable_resnet50_v2.0'
|
||||
$dashModelRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46f\models\dashcamnet\pruned_onnx_v1.0.4'
|
||||
$trafficModel = Ensure-Model $profile.candidates.trafficcamnet $trafficModelRoot 'TrafficCamNet model'
|
||||
$dashModel = Ensure-Model $profile.candidates.dashcamnet $dashModelRoot 'DashCamNet model'
|
||||
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
& docker image inspect $image *> $null
|
||||
$imageCached = $LASTEXITCODE -eq 0
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
if (-not $imageCached) {
|
||||
Invoke-Docker @('pull', $image) 'DeepStream image pull'
|
||||
}
|
||||
|
||||
$runId = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ')
|
||||
$runRoot = Join-Path $runsRoot $runId
|
||||
$rawRoot = Join-Path $runRoot 'raw'
|
||||
$sourceInputMount = Join-Path $runRoot 'source-input'
|
||||
$geometryRoot = Join-Path $rawRoot 'geometry'
|
||||
$samplesRoot = Join-Path $rawRoot 'samples'
|
||||
$comparisonRoot = Join-Path $rawRoot 'comparison'
|
||||
foreach ($path in @(
|
||||
$rawRoot,
|
||||
$sourceInputMount,
|
||||
$geometryRoot,
|
||||
$samplesRoot,
|
||||
$comparisonRoot
|
||||
)) {
|
||||
New-Item -ItemType Directory -Force -Path $path | Out-Null
|
||||
}
|
||||
Copy-Item -LiteralPath $inputPath -Destination (Join-Path $sourceInputMount 'right.mp4')
|
||||
$workerLog = Join-Path $rawRoot 'worker.log'
|
||||
"E46G run $runId`nsource=$streamSha`nselection=$firstSourceFrame..$lastSourceFrame" |
|
||||
Set-Content -LiteralPath $workerLog -Encoding UTF8
|
||||
$startedAt = (Get-Date).ToUniversalTime().ToString('o')
|
||||
|
||||
$geometryRuntime = [ordered]@{}
|
||||
foreach ($view in @('left', 'front', 'right')) {
|
||||
$viewProfile = $profile.rectification.views.$view
|
||||
$configName = [string]$viewProfile.config_file
|
||||
$configPath = Join-Path $package "runtime\$configName"
|
||||
Assert-Sha256 $configPath ([string]$viewProfile.config_sha256) "$view dewarper config"
|
||||
$outputPath = Join-Path $geometryRoot "$view.mp4"
|
||||
$containerCommand = @"
|
||||
set -euo pipefail
|
||||
gst-launch-1.0 -e filesrc location=/workspace/input/right.mp4 ! qtdemux ! h264parse ! nvv4l2decoder ! nvvideoconvert ! 'video/x-raw(memory:NVMM),format=RGBA' ! nvdewarper config-file=/workspace/package/runtime/$configName source-id=0 num-batch-buffers=1 ! nvvideoconvert ! 'video/x-raw(memory:NVMM),format=NV12' ! nvv4l2h264enc bitrate=6000000 ! h264parse ! qtmux ! filesink location=/workspace/output/$view.mp4
|
||||
"@
|
||||
$dewarperLog = Join-Path $geometryRoot "$view.log"
|
||||
$dockerArguments = @(
|
||||
'run', '--rm', '--name', "ndc-mission-core-e46g-dewarper-$view-$runId",
|
||||
'--gpus', 'all', '--network', 'none', '--cap-drop', 'ALL',
|
||||
'--security-opt', 'no-new-privileges', '--shm-size', '4g',
|
||||
'--label', 'com.nodedc.product=mission-core',
|
||||
'--label', 'com.nodedc.stack=perception',
|
||||
'--label', 'com.nodedc.role=nvdewarper-e46g',
|
||||
'--mount', "type=bind,src=$sourceInputMount,dst=/workspace/input,readonly",
|
||||
'--mount', "type=bind,src=$package,dst=/workspace/package,readonly",
|
||||
'--mount', "type=bind,src=$geometryRoot,dst=/workspace/output",
|
||||
'--entrypoint', '/bin/bash', $image, '-lc', $containerCommand
|
||||
)
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
& docker @dockerArguments 2>&1 | Tee-Object -LiteralPath $dewarperLog
|
||||
$dewarperExit = $LASTEXITCODE
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
if ($dewarperExit -ne 0 -or -not (Test-Path -LiteralPath $outputPath -PathType Leaf)) {
|
||||
throw "NVIDIA nvdewarper failed for $view with exit code $dewarperExit"
|
||||
}
|
||||
if ((Get-VideoFrameCount $outputPath) -ne $expectedFullFrameCount) {
|
||||
throw "Full rectified $view frame coverage changed."
|
||||
}
|
||||
|
||||
$samplePath = Join-Path $samplesRoot "$view.mp4"
|
||||
$filter = "select='between(n\,$firstSourceFrame\,$lastSourceFrame)',setpts=N/(10*TB)"
|
||||
& ffmpeg -hide_banner -loglevel error -y -i $outputPath -vf $filter -an `
|
||||
-c:v libx264 -preset fast -crf 18 -pix_fmt yuv420p -r 10 $samplePath
|
||||
if ($LASTEXITCODE -ne 0 -or (Get-VideoFrameCount $samplePath) -ne $sampleFrameCount) {
|
||||
throw "Exact E46G sample extraction failed for $view."
|
||||
}
|
||||
$geometryRuntime[$view] = [ordered]@{
|
||||
dewarper_config_sha256 = Get-Sha256 $configPath
|
||||
dewarper_log_sha256 = Get-Sha256 $dewarperLog
|
||||
full_rectified_video_path = "geometry/$view.mp4"
|
||||
full_rectified_video_sha256 = Get-Sha256 $outputPath
|
||||
sample_video_path = "samples/$view.mp4"
|
||||
sample_video_sha256 = Get-Sha256 $samplePath
|
||||
full_frame_count = $expectedFullFrameCount
|
||||
retained_source_frame_index_range = @(
|
||||
[int]$retainedSourceRange[0],
|
||||
[int]$retainedSourceRange[1]
|
||||
)
|
||||
excluded_source_tail_frame_count = [int]$profile.rectification.excluded_source_tail_frame_count
|
||||
sample_frame_count = $sampleFrameCount
|
||||
}
|
||||
}
|
||||
|
||||
$candidateRuntime = [ordered]@{}
|
||||
$candidateDefinitions = @(
|
||||
[pscustomobject]@{
|
||||
Name = 'trafficcamnet'
|
||||
ModelRoot = $trafficModelRoot
|
||||
ModelPath = $trafficModel
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'dashcamnet'
|
||||
ModelRoot = $dashModelRoot
|
||||
ModelPath = $dashModel
|
||||
}
|
||||
)
|
||||
foreach ($definition in $candidateDefinitions) {
|
||||
$candidate = [string]$definition.Name
|
||||
$candidateProfile = $profile.candidates.$candidate
|
||||
$appConfigPath = Join-Path $package "runtime\$([string]$candidateProfile.deepstream_app_config)"
|
||||
$detectorConfigPath = Join-Path $package "runtime\$([string]$candidateProfile.detector_config)"
|
||||
Assert-Sha256 $appConfigPath ([string]$candidateProfile.deepstream_app_config_sha256) `
|
||||
"$candidate DeepStream app config"
|
||||
Assert-Sha256 $detectorConfigPath ([string]$candidateProfile.detector_config_sha256) `
|
||||
"$candidate detector config"
|
||||
$runs = [ordered]@{}
|
||||
foreach ($view in @('left', 'front', 'right')) {
|
||||
$viewRoot = Join-Path $rawRoot "runs\$candidate\$view"
|
||||
$inputMount = Join-Path $viewRoot 'input'
|
||||
foreach ($path in @(
|
||||
$viewRoot,
|
||||
$inputMount,
|
||||
(Join-Path $viewRoot 'detections'),
|
||||
(Join-Path $viewRoot 'tracks')
|
||||
)) {
|
||||
New-Item -ItemType Directory -Force -Path $path | Out-Null
|
||||
}
|
||||
Copy-Item -LiteralPath (Join-Path $samplesRoot "$view.mp4") `
|
||||
-Destination (Join-Path $inputMount 'view.mp4')
|
||||
$deepstreamLog = Join-Path $viewRoot 'deepstream.log'
|
||||
$containerCommand = @"
|
||||
set -euo pipefail
|
||||
cp /opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_NvDCF_perf.yml /workspace/output/tracker-config.yml
|
||||
deepstream-app -c /workspace/package/runtime/$([string]$candidateProfile.deepstream_app_config)
|
||||
"@
|
||||
$dockerArguments = @(
|
||||
'run', '--rm', '--name', "ndc-mission-core-e46g-$candidate-$view-$runId",
|
||||
'--gpus', 'all', '--network', 'none', '--cap-drop', 'ALL',
|
||||
'--security-opt', 'no-new-privileges', '--shm-size', '4g',
|
||||
'--label', 'com.nodedc.product=mission-core',
|
||||
'--label', 'com.nodedc.stack=perception',
|
||||
'--label', "com.nodedc.role=e46g-$candidate-$view",
|
||||
'--mount', "type=bind,src=$inputMount,dst=/workspace/input,readonly",
|
||||
'--mount', "type=bind,src=$package,dst=/workspace/package,readonly",
|
||||
'--mount', "type=bind,src=$($definition.ModelRoot),dst=/workspace/model",
|
||||
'--mount', "type=bind,src=$viewRoot,dst=/workspace/output",
|
||||
'--entrypoint', '/bin/bash', $image, '-lc', $containerCommand
|
||||
)
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
& docker @dockerArguments 2>&1 | Tee-Object -LiteralPath $deepstreamLog
|
||||
$deepstreamExit = $LASTEXITCODE
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
if ($deepstreamExit -ne 0) {
|
||||
throw "DeepStream failed for $candidate/$view with exit code $deepstreamExit"
|
||||
}
|
||||
$overlayPath = Join-Path $viewRoot 'overlay.mp4'
|
||||
$trackerPath = Join-Path $viewRoot 'tracker-config.yml'
|
||||
$detections = @(Get-ChildItem -LiteralPath (Join-Path $viewRoot 'detections') -File)
|
||||
$tracks = @(Get-ChildItem -LiteralPath (Join-Path $viewRoot 'tracks') -File)
|
||||
if (-not (Test-Path -LiteralPath $overlayPath -PathType Leaf) -or
|
||||
$detections.Count -ne $sampleFrameCount -or $tracks.Count -ne $sampleFrameCount) {
|
||||
throw "DeepStream output coverage changed for $candidate/$view."
|
||||
}
|
||||
$enginePath = "$($definition.ModelPath)_b1_gpu0_fp16.engine"
|
||||
if (-not (Test-Path -LiteralPath $enginePath -PathType Leaf)) {
|
||||
throw "TensorRT engine is missing for $candidate."
|
||||
}
|
||||
$runs[$view] = [ordered]@{
|
||||
overlay_path = "runs/$candidate/$view/overlay.mp4"
|
||||
overlay_sha256 = Get-Sha256 $overlayPath
|
||||
deepstream_log_path = "runs/$candidate/$view/deepstream.log"
|
||||
deepstream_log_sha256 = Get-Sha256 $deepstreamLog
|
||||
tracker_config_sha256 = Get-Sha256 $trackerPath
|
||||
model_engine_sha256 = Get-Sha256 $enginePath
|
||||
frame_count = $sampleFrameCount
|
||||
deepstream_exit_code = $deepstreamExit
|
||||
}
|
||||
}
|
||||
$candidateRuntime[$candidate] = [ordered]@{
|
||||
model_sha256 = Get-Sha256 $definition.ModelPath
|
||||
deepstream_app_config_sha256 = Get-Sha256 $appConfigPath
|
||||
detector_config_sha256 = Get-Sha256 $detectorConfigPath
|
||||
parser_library_sha256 = $(if ($candidate -eq 'trafficcamnet') {
|
||||
Get-Sha256 $parserPath
|
||||
} else { $null })
|
||||
runs = $runs
|
||||
}
|
||||
}
|
||||
|
||||
$comparisonRuntime = [ordered]@{}
|
||||
foreach ($candidate in @('trafficcamnet', 'dashcamnet')) {
|
||||
$left = Join-Path $rawRoot "runs\$candidate\left\overlay.mp4"
|
||||
$front = Join-Path $rawRoot "runs\$candidate\front\overlay.mp4"
|
||||
$right = Join-Path $rawRoot "runs\$candidate\right\overlay.mp4"
|
||||
$output = Join-Path $comparisonRoot "$candidate.mp4"
|
||||
& ffmpeg -hide_banner -loglevel error -y -i $left -i $front -i $right `
|
||||
-filter_complex '[0:v][1:v][2:v]hstack=inputs=3[v]' -map '[v]' -an `
|
||||
-c:v libx264 -preset fast -crf 20 -pix_fmt yuv420p -movflags +faststart $output
|
||||
if ($LASTEXITCODE -ne 0 -or (Get-VideoFrameCount $output) -ne $sampleFrameCount) {
|
||||
throw "E46G synchronized comparison video failed for $candidate."
|
||||
}
|
||||
$comparisonRuntime[$candidate] = [ordered]@{
|
||||
video_path = "comparison/$candidate.mp4"
|
||||
video_sha256 = Get-Sha256 $output
|
||||
frame_count = $sampleFrameCount
|
||||
view_order = @('left', 'front', 'right')
|
||||
}
|
||||
}
|
||||
|
||||
$runtime = [ordered]@{
|
||||
schema_version = 'missioncore.e46g-rectified-detector-runtime/v1'
|
||||
status = 'completed'
|
||||
worker_host = $env:COMPUTERNAME
|
||||
gpu_name = ((& nvidia-smi --query-gpu=name --format=csv,noheader | Select-Object -First 1).Trim())
|
||||
started_at_utc = $startedAt
|
||||
completed_at_utc = (Get-Date).ToUniversalTime().ToString('o')
|
||||
container_image = $image
|
||||
container_image_digest = $imageDigest
|
||||
source_stream_sha256 = Get-Sha256 $inputPath
|
||||
first_source_frame_index = $firstSourceFrame
|
||||
sample_frame_count = $sampleFrameCount
|
||||
geometry = $geometryRuntime
|
||||
candidates = $candidateRuntime
|
||||
comparison = $comparisonRuntime
|
||||
}
|
||||
$runtime | ConvertTo-Json -Depth 16 |
|
||||
Set-Content -LiteralPath (Join-Path $rawRoot 'runtime.json') -Encoding UTF8
|
||||
Add-Content -LiteralPath $workerLog -Value "completed=$(Get-Date -Format o)"
|
||||
|
||||
$consolidatorImage = 'nvcr.io/nvidia/tritonserver:26.06-py3@sha256:58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794'
|
||||
$consolidatorArguments = @(
|
||||
'run', '--rm', '--name', "ndc-mission-core-e46g-consolidator-$runId",
|
||||
'--network', 'none', '--read-only', '--cap-drop', 'ALL',
|
||||
'--security-opt', 'no-new-privileges', '--tmpfs', '/tmp:rw,noexec,nosuid,size=64m',
|
||||
'--mount', "type=bind,src=$package,dst=/workspace/package,readonly",
|
||||
'--mount', "type=bind,src=$sourceJob,dst=/workspace/source-job,readonly",
|
||||
'--mount', "type=bind,src=$rawRoot,dst=/workspace/raw,readonly",
|
||||
'--mount', "type=bind,src=$resultsRoot,dst=/workspace/results",
|
||||
'-e', 'PYTHONPATH=/workspace/package/runtime',
|
||||
'-e', 'PYTHONDONTWRITEBYTECODE=1',
|
||||
$consolidatorImage,
|
||||
'python3', '/workspace/package/runtime/run_e46g_rectified_detector_bakeoff.py',
|
||||
'--source-job', '/workspace/source-job',
|
||||
'--raw-root', '/workspace/raw',
|
||||
'--profile', '/workspace/package/profile.json',
|
||||
'--output-root', '/workspace/results'
|
||||
)
|
||||
Invoke-Docker $consolidatorArguments 'E46G consolidation'
|
||||
Write-Host "E46G_RECTIFIED_DETECTOR_BAKEOFF_COMPLETED run=$runId results=$resultsRoot"
|
||||
}
|
||||
finally {
|
||||
if ($LogPath) { Stop-Transcript | Out-Null }
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PackageRoot,
|
||||
|
||||
[string]$SourceJobRoot = 'D:\NDC_MISSIONCORE\runtime\jobs\recorded-camera-602ac89026ed12978619801d',
|
||||
|
||||
[string]$RuntimeRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46g'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$taskName = 'MissionCore-E46GRectifiedDetectorBakeoff'
|
||||
$package = (Resolve-Path -LiteralPath $PackageRoot).Path
|
||||
$script = Join-Path $package 'runtime\Invoke-E46GRectifiedDetectorBakeoff.ps1'
|
||||
if (-not (Test-Path -LiteralPath $script -PathType Leaf)) {
|
||||
throw "E46G runner is missing: $script"
|
||||
}
|
||||
$existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
||||
if ($existing -and $existing.State -eq 'Running') {
|
||||
throw "$taskName is already running."
|
||||
}
|
||||
$logsRoot = Join-Path $RuntimeRoot 'logs'
|
||||
New-Item -ItemType Directory -Force -Path $logsRoot | Out-Null
|
||||
$stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ')
|
||||
$logPath = Join-Path $logsRoot "e46g-rectified-detector-bakeoff-$stamp.log"
|
||||
$powerShell = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||
$arguments = @(
|
||||
'-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
|
||||
'-File', "`"$script`"",
|
||||
'-PackageRoot', "`"$package`"",
|
||||
'-SourceJobRoot', "`"$SourceJobRoot`"",
|
||||
'-RuntimeRoot', "`"$RuntimeRoot`"",
|
||||
'-LogPath', "`"$logPath`""
|
||||
) -join ' '
|
||||
$userId = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
$action = New-ScheduledTaskAction -Execute $powerShell -Argument $arguments -WorkingDirectory $package
|
||||
$principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited
|
||||
$trigger = New-ScheduledTaskTrigger -Once -At ((Get-Date).AddMinutes(30))
|
||||
$settings = New-ScheduledTaskSettingsSet `
|
||||
-AllowStartIfOnBatteries `
|
||||
-DontStopIfGoingOnBatteries `
|
||||
-StartWhenAvailable `
|
||||
-ExecutionTimeLimit ([TimeSpan]::FromHours(6))
|
||||
Register-ScheduledTask `
|
||||
-TaskName $taskName `
|
||||
-Action $action `
|
||||
-Principal $principal `
|
||||
-Trigger $trigger `
|
||||
-Settings $settings `
|
||||
-Description 'One-shot E46G factory-KB4 NVIDIA nvdewarper detector A/B.' `
|
||||
-Force | Out-Null
|
||||
Start-ScheduledTask -TaskName $taskName
|
||||
Write-Host "E46G_TASK_STARTED task=$taskName log=$logPath"
|
||||
@@ -0,0 +1,369 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PackageRoot,
|
||||
|
||||
[string]$SourceJobRoot = 'D:\NDC_MISSIONCORE\runtime\jobs\recorded-camera-602ac89026ed12978619801d',
|
||||
|
||||
[string]$RuntimeRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46h',
|
||||
|
||||
[string]$LogPath = ''
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
function Get-Sha256([string]$Path) {
|
||||
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
}
|
||||
|
||||
function Assert-Sha256([string]$Path, [string]$Expected, [string]$Label) {
|
||||
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
||||
throw "$Label is missing: $Path"
|
||||
}
|
||||
$actual = Get-Sha256 $Path
|
||||
if ($actual -ne $Expected) {
|
||||
throw "$Label SHA-256 changed: expected $Expected, got $actual"
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Docker([string[]]$Arguments, [string]$Label) {
|
||||
& docker @Arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "$Label failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
}
|
||||
|
||||
function Get-VideoFrameCount([string]$Path) {
|
||||
$probe = & ffprobe -v error -select_streams v:0 -count_frames `
|
||||
-show_entries stream=nb_read_frames -of json $Path | ConvertFrom-Json
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "ffprobe failed: $Path"
|
||||
}
|
||||
return [int]@($probe.streams)[0].nb_read_frames
|
||||
}
|
||||
|
||||
function Ensure-Model([pscustomobject]$Detector, [string]$ModelRoot) {
|
||||
New-Item -ItemType Directory -Force -Path $ModelRoot | Out-Null
|
||||
$modelPath = Join-Path $ModelRoot ([string]$Detector.model_file)
|
||||
if (Test-Path -LiteralPath $modelPath -PathType Leaf) {
|
||||
Assert-Sha256 $modelPath ([string]$Detector.model_sha256) 'TrafficCamNet model'
|
||||
return $modelPath
|
||||
}
|
||||
$temporary = "$modelPath.$([Guid]::NewGuid().ToString('N')).download"
|
||||
& curl.exe --fail --location --retry 3 --output $temporary ([string]$Detector.model_url)
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "TrafficCamNet download failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
Assert-Sha256 $temporary ([string]$Detector.model_sha256) 'downloaded TrafficCamNet model'
|
||||
Move-Item -LiteralPath $temporary -Destination $modelPath
|
||||
return $modelPath
|
||||
}
|
||||
|
||||
$package = (Resolve-Path -LiteralPath $PackageRoot).Path
|
||||
$sourceJob = (Resolve-Path -LiteralPath $SourceJobRoot).Path
|
||||
$profilePath = Join-Path $package 'profile.json'
|
||||
$manifestPath = Join-Path $package 'manifest.json'
|
||||
if (-not (Test-Path -LiteralPath $profilePath -PathType Leaf) -or
|
||||
-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) {
|
||||
throw 'E46H package is incomplete.'
|
||||
}
|
||||
$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
|
||||
if ($manifest.schema_version -ne 'missioncore.e46h-worker-package/v1' -or
|
||||
$manifest.package_id -ne (Split-Path -Leaf $package)) {
|
||||
throw 'E46H package identity is invalid.'
|
||||
}
|
||||
$expectedPaths = @($manifest.identity.artifact_paths)
|
||||
foreach ($artifact in @($manifest.artifacts)) {
|
||||
if ($expectedPaths -notcontains [string]$artifact.path) {
|
||||
throw "Unexpected E46H package artifact: $($artifact.path)"
|
||||
}
|
||||
$artifactPath = Join-Path $package ([string]$artifact.path)
|
||||
Assert-Sha256 $artifactPath ([string]$artifact.sha256) "package artifact $($artifact.path)"
|
||||
if ((Get-Item -LiteralPath $artifactPath).Length -ne [int64]$artifact.byte_length) {
|
||||
throw "Package artifact length changed: $($artifact.path)"
|
||||
}
|
||||
}
|
||||
$actualPaths = @(Get-ChildItem -LiteralPath $package -Recurse -File | ForEach-Object {
|
||||
$_.FullName.Substring($package.Length + 1).Replace('\', '/')
|
||||
})
|
||||
if (@($actualPaths | Where-Object { $_ -ne 'manifest.json' -and $expectedPaths -notcontains $_ }).Count -ne 0 -or
|
||||
@($expectedPaths | Where-Object { $actualPaths -notcontains $_ }).Count -ne 0) {
|
||||
throw 'E46H package file set changed.'
|
||||
}
|
||||
|
||||
$profile = Get-Content -LiteralPath $profilePath -Raw | ConvertFrom-Json
|
||||
if ($profile.schema_version -ne 'missioncore.e46h-full-rectified-front-replay-profile/v1' -or
|
||||
$profile.source.camera_source_id -ne 'sensor.camera.right' -or
|
||||
$profile.rectification.view -ne 'front' -or
|
||||
$profile.detector.name -ne 'NVIDIA TrafficCamNet Transformer Lite') {
|
||||
throw 'E46H profile is incompatible.'
|
||||
}
|
||||
$image = [string]$profile.runtime.container_image
|
||||
$imageDigestMatch = [regex]::Match($image, '@sha256:([0-9a-f]{64})$')
|
||||
if (-not $imageDigestMatch.Success) {
|
||||
throw 'E46H runtime image must be pinned by a full SHA-256 digest.'
|
||||
}
|
||||
$imageDigest = $imageDigestMatch.Groups[1].Value
|
||||
$streamSha = [string]$profile.source.stream_sha256
|
||||
$frameCount = [int]$profile.selection.frame_count
|
||||
if ($frameCount -ne 4488 -or
|
||||
[int]$profile.selection.first_source_frame_index -ne 0 -or
|
||||
[int]$profile.selection.last_source_frame_index -ne 4487) {
|
||||
throw 'E46H retained route contract changed.'
|
||||
}
|
||||
$parserPath = Join-Path $package "runtime\$([string]$profile.parser.library_file)"
|
||||
Assert-Sha256 $parserPath ([string]$profile.parser.library_sha256) `
|
||||
'official NVIDIA DeepStream TAO parser'
|
||||
|
||||
if (-not (Get-Command ffmpeg -ErrorAction SilentlyContinue) -or
|
||||
-not (Get-Command ffprobe -ErrorAction SilentlyContinue)) {
|
||||
throw 'E46H requires the existing Worker ffmpeg/ffprobe installation.'
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $RuntimeRoot | Out-Null
|
||||
$inputsRoot = Join-Path $RuntimeRoot 'inputs'
|
||||
$runsRoot = Join-Path $RuntimeRoot 'runs'
|
||||
$resultsRoot = Join-Path $RuntimeRoot 'full-rectified-front-results'
|
||||
foreach ($path in @($inputsRoot, $runsRoot, $resultsRoot)) {
|
||||
New-Item -ItemType Directory -Force -Path $path | Out-Null
|
||||
}
|
||||
if ($LogPath) {
|
||||
$logParent = Split-Path -Parent $LogPath
|
||||
if ($logParent) { New-Item -ItemType Directory -Force -Path $logParent | Out-Null }
|
||||
Start-Transcript -LiteralPath $LogPath -Append | Out-Null
|
||||
}
|
||||
|
||||
try {
|
||||
$jobPath = Join-Path $sourceJob 'job.json'
|
||||
$job = Get-Content -LiteralPath $jobPath -Raw | ConvertFrom-Json
|
||||
if ($job.job_id -ne $profile.source.job_id -or
|
||||
$job.input.archive_index_sha256 -ne $profile.source.archive_index_sha256 -or
|
||||
$job.input.archive_summary_sha256 -ne $profile.source.archive_summary_sha256) {
|
||||
throw 'Exact E46H source job binding changed.'
|
||||
}
|
||||
$inputPath = Join-Path $inputsRoot "right-$streamSha.mp4"
|
||||
$e46gInput = "D:\NDC_MISSIONCORE\runtime\experiments\e46g\inputs\right-$streamSha.mp4"
|
||||
$e46eInput = "D:\NDC_MISSIONCORE\runtime\experiments\e46e\inputs\right-$streamSha.mp4"
|
||||
if (Test-Path -LiteralPath $inputPath -PathType Leaf) {
|
||||
Assert-Sha256 $inputPath $streamSha 'E46H controlled RIGHT stream'
|
||||
}
|
||||
elseif (Test-Path -LiteralPath $e46gInput -PathType Leaf) {
|
||||
Assert-Sha256 $e46gInput $streamSha 'E46G controlled RIGHT stream'
|
||||
Copy-Item -LiteralPath $e46gInput -Destination $inputPath
|
||||
}
|
||||
elseif (Test-Path -LiteralPath $e46eInput -PathType Leaf) {
|
||||
Assert-Sha256 $e46eInput $streamSha 'E46E controlled RIGHT stream'
|
||||
Copy-Item -LiteralPath $e46eInput -Destination $inputPath
|
||||
}
|
||||
else {
|
||||
& (Join-Path $package 'runtime\Prepare-RectifiedCameraReplay.ps1') `
|
||||
-JobRoot $sourceJob -OutputPath $inputPath
|
||||
}
|
||||
Assert-Sha256 $inputPath $streamSha 'E46H reconstructed RIGHT stream'
|
||||
|
||||
$modelRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46e\models\trafficcamnet_transformer_lite\deployable_resnet50_v2.0'
|
||||
$modelPath = Ensure-Model $profile.detector $modelRoot
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
& docker image inspect $image *> $null
|
||||
$imageCached = $LASTEXITCODE -eq 0
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
if (-not $imageCached) {
|
||||
Invoke-Docker @('pull', $image) 'DeepStream image pull'
|
||||
}
|
||||
|
||||
$runId = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ')
|
||||
$runRoot = Join-Path $runsRoot $runId
|
||||
$rawRoot = Join-Path $runRoot 'raw'
|
||||
$sourceInputMount = Join-Path $runRoot 'source-input'
|
||||
$geometryRoot = Join-Path $rawRoot 'geometry'
|
||||
$runOutput = Join-Path $rawRoot 'run'
|
||||
$frontInputMount = Join-Path $runRoot 'front-input'
|
||||
foreach ($path in @(
|
||||
$rawRoot,
|
||||
$sourceInputMount,
|
||||
$geometryRoot,
|
||||
$runOutput,
|
||||
$frontInputMount,
|
||||
(Join-Path $runOutput 'detections'),
|
||||
(Join-Path $runOutput 'tracks')
|
||||
)) {
|
||||
New-Item -ItemType Directory -Force -Path $path | Out-Null
|
||||
}
|
||||
Copy-Item -LiteralPath $inputPath -Destination (Join-Path $sourceInputMount 'right.mp4')
|
||||
$workerLog = Join-Path $rawRoot 'worker.log'
|
||||
"E46H run $runId`nsource=$streamSha`nselection=0..4487`nview=front" |
|
||||
Set-Content -LiteralPath $workerLog -Encoding UTF8
|
||||
$startedAt = (Get-Date).ToUniversalTime().ToString('o')
|
||||
|
||||
$dewarperConfig = Join-Path $package "runtime\$([string]$profile.rectification.config_file)"
|
||||
Assert-Sha256 $dewarperConfig ([string]$profile.rectification.config_sha256) `
|
||||
'FRONT dewarper config'
|
||||
$frontPath = Join-Path $geometryRoot 'front.mp4'
|
||||
$dewarperLog = Join-Path $geometryRoot 'front.log'
|
||||
$dewarperCommand = @"
|
||||
set -euo pipefail
|
||||
gst-launch-1.0 -e filesrc location=/workspace/input/right.mp4 ! qtdemux ! h264parse ! nvv4l2decoder ! nvvideoconvert ! 'video/x-raw(memory:NVMM),format=RGBA' ! nvdewarper config-file=/workspace/package/runtime/$([string]$profile.rectification.config_file) source-id=0 num-batch-buffers=1 ! nvvideoconvert ! 'video/x-raw(memory:NVMM),format=NV12' ! nvv4l2h264enc bitrate=6000000 ! h264parse ! qtmux ! filesink location=/workspace/output/front.mp4
|
||||
"@
|
||||
$dewarperArguments = @(
|
||||
'run', '--rm', '--name', "ndc-mission-core-e46h-dewarper-$runId",
|
||||
'--gpus', 'all', '--network', 'none', '--cap-drop', 'ALL',
|
||||
'--security-opt', 'no-new-privileges', '--shm-size', '4g',
|
||||
'--label', 'com.nodedc.product=mission-core',
|
||||
'--label', 'com.nodedc.stack=perception',
|
||||
'--label', 'com.nodedc.role=nvdewarper-e46h-front',
|
||||
'--mount', "type=bind,src=$sourceInputMount,dst=/workspace/input,readonly",
|
||||
'--mount', "type=bind,src=$package,dst=/workspace/package,readonly",
|
||||
'--mount', "type=bind,src=$geometryRoot,dst=/workspace/output",
|
||||
'--entrypoint', '/bin/bash', $image, '-lc', $dewarperCommand
|
||||
)
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
& docker @dewarperArguments 2>&1 | Tee-Object -LiteralPath $dewarperLog
|
||||
$dewarperExit = $LASTEXITCODE
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
if ($dewarperExit -ne 0 -or -not (Test-Path -LiteralPath $frontPath -PathType Leaf)) {
|
||||
throw "NVIDIA nvdewarper failed with exit code $dewarperExit"
|
||||
}
|
||||
if ((Get-VideoFrameCount $frontPath) -ne $frameCount) {
|
||||
throw 'E46H FRONT frame coverage changed.'
|
||||
}
|
||||
$normalizedFront = Join-Path $geometryRoot 'front-normalized.mp4'
|
||||
& ffmpeg -hide_banner -loglevel error -y -fflags +genpts -i $frontPath `
|
||||
-vf 'setpts=N/(10*TB)' -an -c:v libx264 -preset fast -crf 18 `
|
||||
-pix_fmt yuv420p -r 10 -movflags +faststart $normalizedFront
|
||||
if ($LASTEXITCODE -ne 0 -or (Get-VideoFrameCount $normalizedFront) -ne $frameCount) {
|
||||
throw 'E46H FRONT timestamp normalization failed.'
|
||||
}
|
||||
Move-Item -LiteralPath $normalizedFront -Destination $frontPath -Force
|
||||
Copy-Item -LiteralPath $frontPath -Destination (Join-Path $frontInputMount 'view.mp4')
|
||||
|
||||
$appConfig = Join-Path $package "runtime\$([string]$profile.detector.deepstream_app_config)"
|
||||
$detectorConfig = Join-Path $package "runtime\$([string]$profile.detector.detector_config)"
|
||||
Assert-Sha256 $appConfig ([string]$profile.detector.deepstream_app_config_sha256) `
|
||||
'TrafficCamNet DeepStream app config'
|
||||
Assert-Sha256 $detectorConfig ([string]$profile.detector.detector_config_sha256) `
|
||||
'TrafficCamNet detector config'
|
||||
$deepstreamLog = Join-Path $runOutput 'deepstream.log'
|
||||
$deepstreamCommand = @"
|
||||
set -euo pipefail
|
||||
cp /opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_NvDCF_perf.yml /workspace/output/tracker-config.yml
|
||||
deepstream-app -c /workspace/package/runtime/$([string]$profile.detector.deepstream_app_config)
|
||||
"@
|
||||
$deepstreamArguments = @(
|
||||
'run', '--rm', '--name', "ndc-mission-core-e46h-front-$runId",
|
||||
'--gpus', 'all', '--network', 'none', '--cap-drop', 'ALL',
|
||||
'--security-opt', 'no-new-privileges', '--shm-size', '4g',
|
||||
'--label', 'com.nodedc.product=mission-core',
|
||||
'--label', 'com.nodedc.stack=perception',
|
||||
'--label', 'com.nodedc.role=e46h-front-trafficcamnet',
|
||||
'--mount', "type=bind,src=$frontInputMount,dst=/workspace/input,readonly",
|
||||
'--mount', "type=bind,src=$package,dst=/workspace/package,readonly",
|
||||
'--mount', "type=bind,src=$modelRoot,dst=/workspace/model",
|
||||
'--mount', "type=bind,src=$runOutput,dst=/workspace/output",
|
||||
'--entrypoint', '/bin/bash', $image, '-lc', $deepstreamCommand
|
||||
)
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
& docker @deepstreamArguments 2>&1 | Tee-Object -LiteralPath $deepstreamLog
|
||||
$deepstreamExit = $LASTEXITCODE
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
if ($deepstreamExit -ne 0) {
|
||||
throw "DeepStream failed with exit code $deepstreamExit"
|
||||
}
|
||||
$overlayPath = Join-Path $runOutput 'overlay.mp4'
|
||||
$trackerPath = Join-Path $runOutput 'tracker-config.yml'
|
||||
$detections = @(Get-ChildItem -LiteralPath (Join-Path $runOutput 'detections') -File)
|
||||
$tracks = @(Get-ChildItem -LiteralPath (Join-Path $runOutput 'tracks') -File)
|
||||
if (-not (Test-Path -LiteralPath $overlayPath -PathType Leaf) -or
|
||||
$detections.Count -ne $frameCount -or $tracks.Count -ne $frameCount) {
|
||||
throw "DeepStream output coverage changed: detections=$($detections.Count), tracks=$($tracks.Count)"
|
||||
}
|
||||
$fastOverlay = Join-Path $runOutput 'overlay-faststart.mp4'
|
||||
& ffmpeg -hide_banner -loglevel error -y -i $overlayPath -c copy -movflags +faststart $fastOverlay
|
||||
if ($LASTEXITCODE -ne 0 -or (Get-VideoFrameCount $fastOverlay) -ne $frameCount) {
|
||||
throw 'E46H fast-start overlay normalization failed.'
|
||||
}
|
||||
Move-Item -LiteralPath $fastOverlay -Destination $overlayPath -Force
|
||||
$enginePath = "$modelPath`_b1_gpu0_fp16.engine"
|
||||
if (-not (Test-Path -LiteralPath $enginePath -PathType Leaf)) {
|
||||
throw 'TrafficCamNet TensorRT engine is missing.'
|
||||
}
|
||||
|
||||
$runtime = [ordered]@{
|
||||
schema_version = 'missioncore.e46h-full-rectified-front-runtime/v1'
|
||||
status = 'completed'
|
||||
worker_host = $env:COMPUTERNAME
|
||||
gpu_name = ((& nvidia-smi --query-gpu=name --format=csv,noheader | Select-Object -First 1).Trim())
|
||||
started_at_utc = $startedAt
|
||||
completed_at_utc = (Get-Date).ToUniversalTime().ToString('o')
|
||||
container_image = $image
|
||||
container_image_digest = $imageDigest
|
||||
source_stream_sha256 = Get-Sha256 $inputPath
|
||||
frame_count = $frameCount
|
||||
retained_source_frame_index_range = @(0, 4487)
|
||||
geometry = [ordered]@{
|
||||
video_path = 'geometry/front.mp4'
|
||||
video_sha256 = Get-Sha256 $frontPath
|
||||
log_path = 'geometry/front.log'
|
||||
log_sha256 = Get-Sha256 $dewarperLog
|
||||
config_sha256 = Get-Sha256 $dewarperConfig
|
||||
frame_count = $frameCount
|
||||
dewarper_exit_code = $dewarperExit
|
||||
}
|
||||
run = [ordered]@{
|
||||
overlay_path = 'run/overlay.mp4'
|
||||
overlay_sha256 = Get-Sha256 $overlayPath
|
||||
deepstream_log_path = 'run/deepstream.log'
|
||||
deepstream_log_sha256 = Get-Sha256 $deepstreamLog
|
||||
model_sha256 = Get-Sha256 $modelPath
|
||||
model_engine_sha256 = Get-Sha256 $enginePath
|
||||
parser_library_sha256 = Get-Sha256 $parserPath
|
||||
deepstream_app_config_sha256 = Get-Sha256 $appConfig
|
||||
detector_config_sha256 = Get-Sha256 $detectorConfig
|
||||
tracker_config_sha256 = Get-Sha256 $trackerPath
|
||||
frame_count = $frameCount
|
||||
deepstream_exit_code = $deepstreamExit
|
||||
}
|
||||
}
|
||||
$runtime | ConvertTo-Json -Depth 12 |
|
||||
Set-Content -LiteralPath (Join-Path $rawRoot 'runtime.json') -Encoding UTF8
|
||||
Add-Content -LiteralPath $workerLog -Value "completed=$(Get-Date -Format o)"
|
||||
|
||||
$consolidatorImage = 'nvcr.io/nvidia/tritonserver:26.06-py3@sha256:58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794'
|
||||
$consolidatorArguments = @(
|
||||
'run', '--rm', '--name', "ndc-mission-core-e46h-consolidator-$runId",
|
||||
'--network', 'none', '--read-only', '--cap-drop', 'ALL',
|
||||
'--security-opt', 'no-new-privileges', '--tmpfs', '/tmp:rw,noexec,nosuid,size=64m',
|
||||
'--mount', "type=bind,src=$package,dst=/workspace/package,readonly",
|
||||
'--mount', "type=bind,src=$sourceJob,dst=/workspace/source-job,readonly",
|
||||
'--mount', "type=bind,src=$rawRoot,dst=/workspace/raw,readonly",
|
||||
'--mount', "type=bind,src=$resultsRoot,dst=/workspace/results",
|
||||
'-e', 'PYTHONPATH=/workspace/package/runtime',
|
||||
'-e', 'PYTHONDONTWRITEBYTECODE=1',
|
||||
$consolidatorImage,
|
||||
'python3', '/workspace/package/runtime/run_e46h_full_rectified_front_replay.py',
|
||||
'--source-job', '/workspace/source-job',
|
||||
'--raw-root', '/workspace/raw',
|
||||
'--profile', '/workspace/package/profile.json',
|
||||
'--output-root', '/workspace/results'
|
||||
)
|
||||
Invoke-Docker $consolidatorArguments 'E46H consolidation'
|
||||
Write-Host "E46H_FULL_RECTIFIED_FRONT_COMPLETED run=$runId results=$resultsRoot"
|
||||
}
|
||||
finally {
|
||||
if ($LogPath) { Stop-Transcript | Out-Null }
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PackageRoot,
|
||||
|
||||
[string]$SourceJobRoot = 'D:\NDC_MISSIONCORE\runtime\jobs\recorded-camera-602ac89026ed12978619801d',
|
||||
|
||||
[string]$RuntimeRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46h'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$taskName = 'MissionCore-E46HFullRectifiedFrontReplay'
|
||||
$package = (Resolve-Path -LiteralPath $PackageRoot).Path
|
||||
$script = Join-Path $package 'runtime\Invoke-E46HFullRectifiedFrontReplay.ps1'
|
||||
if (-not (Test-Path -LiteralPath $script -PathType Leaf)) {
|
||||
throw "E46H runner is missing: $script"
|
||||
}
|
||||
$existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
||||
if ($existing -and $existing.State -eq 'Running') {
|
||||
throw "$taskName is already running."
|
||||
}
|
||||
$logsRoot = Join-Path $RuntimeRoot 'logs'
|
||||
New-Item -ItemType Directory -Force -Path $logsRoot | Out-Null
|
||||
$stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ')
|
||||
$logPath = Join-Path $logsRoot "e46h-full-rectified-front-$stamp.log"
|
||||
$powerShell = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||
$arguments = @(
|
||||
'-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
|
||||
'-File', "`"$script`"",
|
||||
'-PackageRoot', "`"$package`"",
|
||||
'-SourceJobRoot', "`"$SourceJobRoot`"",
|
||||
'-RuntimeRoot', "`"$RuntimeRoot`"",
|
||||
'-LogPath', "`"$logPath`""
|
||||
) -join ' '
|
||||
$userId = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
$action = New-ScheduledTaskAction -Execute $powerShell -Argument $arguments -WorkingDirectory $package
|
||||
$principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited
|
||||
$trigger = New-ScheduledTaskTrigger -Once -At ((Get-Date).AddMinutes(30))
|
||||
$settings = New-ScheduledTaskSettingsSet `
|
||||
-AllowStartIfOnBatteries `
|
||||
-DontStopIfGoingOnBatteries `
|
||||
-StartWhenAvailable `
|
||||
-ExecutionTimeLimit ([TimeSpan]::FromHours(6))
|
||||
Register-ScheduledTask `
|
||||
-TaskName $taskName `
|
||||
-Action $action `
|
||||
-Principal $principal `
|
||||
-Trigger $trigger `
|
||||
-Settings $settings `
|
||||
-Description 'One-shot E46H full retained FRONT TrafficCamNet + NvDCF replay.' `
|
||||
-Force | Out-Null
|
||||
Start-ScheduledTask -TaskName $taskName
|
||||
Write-Host "E46H_TASK_STARTED task=$taskName log=$logPath"
|
||||
@@ -0,0 +1,118 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PackageRoot,
|
||||
|
||||
[string]$RuntimeRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46j',
|
||||
|
||||
[string]$SourceVideo = 'D:\NDC_MISSIONCORE\runtime\experiments\e46e\inputs\right-cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8.mp4',
|
||||
|
||||
[string]$ValidFovMask = 'D:\NDC_MISSIONCORE\runtime\inputs\e2\valid-fov-mask-b4dd8ddf2b87c1d520ee8a0868c4fea062d7c14d1bae73ccabd3abe1f3acbac2\mask.png',
|
||||
|
||||
[int]$MaxFrames = 0,
|
||||
|
||||
[switch]$NoOverlay,
|
||||
|
||||
[string]$RunPrefix = 'full'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
function Get-Sha256([string]$Path) {
|
||||
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
}
|
||||
|
||||
$package = (Resolve-Path -LiteralPath $PackageRoot).Path
|
||||
$source = (Resolve-Path -LiteralPath $SourceVideo).Path
|
||||
$mask = (Resolve-Path -LiteralPath $ValidFovMask).Path
|
||||
$profile = Join-Path $package 'e46j_raw_fisheye_yolox_profile.json'
|
||||
$runner = Join-Path $package 'run_e46j_raw_fisheye_yolox.py'
|
||||
if (-not (Test-Path -LiteralPath $profile -PathType Leaf) -or
|
||||
-not (Test-Path -LiteralPath $runner -PathType Leaf)) {
|
||||
throw 'E46J package is incomplete.'
|
||||
}
|
||||
if ((Get-Sha256 $source) -ne 'cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8') {
|
||||
throw 'E46J source stream identity changed.'
|
||||
}
|
||||
|
||||
$runsRoot = Join-Path $RuntimeRoot 'runs'
|
||||
New-Item -ItemType Directory -Force -Path $runsRoot | Out-Null
|
||||
$runId = "$RunPrefix-$((Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ'))"
|
||||
$image = 'nvcr.io/nvidia/tritonserver:26.06-py3@sha256:58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794'
|
||||
$command = "python3 /workspace/package/run_e46j_raw_fisheye_yolox.py --input /workspace/input/right.mp4 --mask /workspace/valid-fov/mask.png --profile /workspace/package/e46j_raw_fisheye_yolox_profile.json --triton-url http://127.0.0.1:8000 --output /workspace/runs/$runId"
|
||||
if ($MaxFrames -gt 0) {
|
||||
$command += " --max-frames $MaxFrames"
|
||||
}
|
||||
if (-not $NoOverlay) {
|
||||
$command += ' --overlay'
|
||||
}
|
||||
|
||||
$dockerArguments = @(
|
||||
'run', '--rm', '--name', "ndc-mission-core-e46j-$runId",
|
||||
'--network', 'container:ndc-mission-core-triton',
|
||||
'--gpus', 'all',
|
||||
'--cap-drop', 'ALL',
|
||||
'--security-opt', 'no-new-privileges',
|
||||
'--shm-size', '1g',
|
||||
'--label', 'com.nodedc.product=mission-core',
|
||||
'--label', 'com.nodedc.stack=perception',
|
||||
'--label', 'com.nodedc.role=e46j-raw-fisheye-realtime-gate',
|
||||
'--label', 'com.nodedc.managed-by=mission-core-worker',
|
||||
'--mount', "type=bind,src=$source,dst=/workspace/input/right.mp4,readonly",
|
||||
'--mount', "type=bind,src=$mask,dst=/workspace/valid-fov/mask.png,readonly",
|
||||
'--mount', "type=bind,src=$package,dst=/workspace/package,readonly",
|
||||
'--mount', "type=bind,src=$runsRoot,dst=/workspace/runs",
|
||||
'--mount', 'type=bind,src=D:\NDC_MISSIONCORE\runtime\derived\perception-p0-env-v1,dst=/opt/env,readonly',
|
||||
'--mount', 'type=bind,src=D:\NDC_MISSIONCORE\runtime\derived\perception-e15-media-pyav180-lz445-v1,dst=/opt/media,readonly',
|
||||
'--mount', 'type=bind,src=D:\NDC_MISSIONCORE\runtime\derived\perception-e3-opencv413092-v1,dst=/environment,readonly',
|
||||
'-e', 'PYTHONPATH=/opt/env:/opt/media:/environment/packages',
|
||||
'--entrypoint', '/bin/bash',
|
||||
$image, '-lc', $command
|
||||
)
|
||||
|
||||
& docker @dockerArguments
|
||||
$runnerExitCode = $LASTEXITCODE
|
||||
if ($runnerExitCode -ne 0 -and $runnerExitCode -ne 2) {
|
||||
throw "E46J runner crashed with exit code $runnerExitCode"
|
||||
}
|
||||
$result = Join-Path $runsRoot $runId
|
||||
$runtimePath = Join-Path $result 'runtime.json'
|
||||
if (-not $NoOverlay) {
|
||||
$intermediate = Join-Path $result 'raw-fisheye-yolox-overlay-intermediate.mp4'
|
||||
$overlay = Join-Path $result 'raw-fisheye-yolox-overlay.mp4'
|
||||
if (-not (Test-Path -LiteralPath $intermediate -PathType Leaf)) {
|
||||
throw 'E46J intermediate overlay is missing.'
|
||||
}
|
||||
& ffmpeg.exe -hide_banner -loglevel error -y -i $intermediate `
|
||||
-c:v libx264 -preset veryfast -crf 20 -r 4489000/448723 `
|
||||
-movflags +faststart -an $overlay
|
||||
if ($LASTEXITCODE -ne 0 -or
|
||||
-not (Test-Path -LiteralPath $overlay -PathType Leaf) -or
|
||||
(Get-Item -LiteralPath $overlay).Length -eq 0) {
|
||||
throw 'E46J final H.264 overlay transcode failed.'
|
||||
}
|
||||
$runtime = Get-Content -LiteralPath $runtimePath -Raw | ConvertFrom-Json
|
||||
$runtime.artifacts.PSObject.Properties.Remove('overlay_intermediate')
|
||||
$runtime.artifacts | Add-Member -NotePropertyName overlay -NotePropertyValue ([pscustomobject]@{
|
||||
file = 'raw-fisheye-yolox-overlay.mp4'
|
||||
byte_length = (Get-Item -LiteralPath $overlay).Length
|
||||
sha256 = Get-Sha256 $overlay
|
||||
codec = 'H.264'
|
||||
frame_rate = 10.003944527024467
|
||||
})
|
||||
$runtime | Add-Member -NotePropertyName visual_export -NotePropertyValue ([pscustomobject]@{
|
||||
provider = ((& ffmpeg.exe -version | Select-Object -First 1).Trim())
|
||||
source = 'MPEG-4 Part 2 intermediate produced by OpenCV VideoWriter'
|
||||
output = 'H.264 MP4 with faststart'
|
||||
excluded_from_core_latency = $true
|
||||
})
|
||||
$runtime | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $runtimePath -Encoding UTF8
|
||||
Remove-Item -LiteralPath $intermediate -Force
|
||||
}
|
||||
Write-Output "E46J_RUN_ID=$runId"
|
||||
Write-Output "E46J_RESULT_ROOT=$result"
|
||||
Get-Content -LiteralPath $runtimePath -Raw
|
||||
if ($runnerExitCode -eq 2) {
|
||||
exit 2
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$JobRoot,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$OutputPath,
|
||||
|
||||
[ValidateRange(1, 1000)]
|
||||
[int]$FreeGiBFloor = 360
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
|
||||
function Assert-DDrivePath {
|
||||
param([string]$Path, [string]$Label)
|
||||
$fullPath = [IO.Path]::GetFullPath($Path)
|
||||
if ([IO.Path]::GetPathRoot($fullPath).TrimEnd("\") -ine "D:") {
|
||||
throw "$Label must be stored on D:"
|
||||
}
|
||||
return $fullPath
|
||||
}
|
||||
|
||||
function Assert-FreeSpace {
|
||||
param([string]$Phase, [int64]$RequiredAdditionalBytes = 0)
|
||||
$freeBytes = [int64](Get-PSDrive -Name D).Free
|
||||
$floorBytes = [int64]$FreeGiBFloor * 1GB
|
||||
Write-Output (
|
||||
"DISK_GUARD PHASE={0} FREE_GIB={1} FLOOR_GIB={2}" -f
|
||||
$Phase,
|
||||
[math]::Round($freeBytes / 1GB, 3),
|
||||
$FreeGiBFloor
|
||||
)
|
||||
if ($freeBytes -lt ($floorBytes + $RequiredAdditionalBytes)) {
|
||||
throw "D: does not have the guarded replay reserve during $Phase"
|
||||
}
|
||||
}
|
||||
|
||||
$jobDirectory = Assert-DDrivePath (
|
||||
(Resolve-Path -LiteralPath $JobRoot).Path
|
||||
) "Job root"
|
||||
$output = Assert-DDrivePath $OutputPath "Output path"
|
||||
if (Test-Path -LiteralPath $output) {
|
||||
Write-Output "REPLAY_ALREADY_PRESENT=$output"
|
||||
exit 0
|
||||
}
|
||||
|
||||
$jobPath = Join-Path $jobDirectory "job.json"
|
||||
$job = Get-Content -LiteralPath $jobPath -Raw | ConvertFrom-Json
|
||||
if (
|
||||
$job.schema_version -ne "missioncore.compute-job/v1" -or
|
||||
$job.job_id -ne "recorded-camera-602ac89026ed12978619801d" -or
|
||||
$job.input.session_id -ne "20260720T065719Z_viewer_live" -or
|
||||
$job.input.source_id -ne "sensor.camera.right" -or
|
||||
[int]$job.input.segment_count -ne 4489
|
||||
) {
|
||||
throw "The requested job is not the immutable RAVNOVES00 right-camera source"
|
||||
}
|
||||
|
||||
$epochRoot = Join-Path $jobDirectory "input\camera\sensor.camera.right\epoch-1"
|
||||
$initPath = Join-Path $epochRoot "init.mp4"
|
||||
$segmentsRoot = Join-Path $epochRoot "segments"
|
||||
if (-not (Test-Path -LiteralPath $initPath -PathType Leaf)) {
|
||||
throw "Camera initialization segment is absent"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $segmentsRoot -PathType Container)) {
|
||||
throw "Camera segment directory is absent"
|
||||
}
|
||||
|
||||
$parent = Split-Path $output -Parent
|
||||
$null = New-Item -ItemType Directory -Path $parent -Force
|
||||
$temporary = Join-Path $parent (".{0}.{1}.partial" -f (Split-Path $output -Leaf), [Guid]::NewGuid().ToString("N"))
|
||||
$requiredBytes = [int64]$job.input.byte_length + 1GB
|
||||
Assert-FreeSpace "preflight" $requiredBytes
|
||||
|
||||
try {
|
||||
$destination = [IO.File]::Open(
|
||||
$temporary,
|
||||
[IO.FileMode]::CreateNew,
|
||||
[IO.FileAccess]::Write,
|
||||
[IO.FileShare]::None
|
||||
)
|
||||
try {
|
||||
$source = [IO.File]::OpenRead($initPath)
|
||||
try { $source.CopyTo($destination) } finally { $source.Dispose() }
|
||||
for ($sequence = 1; $sequence -le 4489; $sequence++) {
|
||||
$segment = Join-Path $segmentsRoot ("{0}.m4s" -f $sequence)
|
||||
if (-not (Test-Path -LiteralPath $segment -PathType Leaf)) {
|
||||
throw "Camera segment is absent: $sequence"
|
||||
}
|
||||
$source = [IO.File]::OpenRead($segment)
|
||||
try { $source.CopyTo($destination) } finally { $source.Dispose() }
|
||||
}
|
||||
$destination.Flush($true)
|
||||
}
|
||||
finally {
|
||||
$destination.Dispose()
|
||||
}
|
||||
|
||||
$probe = & ffprobe -v error -select_streams v:0 -count_frames `
|
||||
-show_entries stream=width,height,nb_read_frames `
|
||||
-of json $temporary | ConvertFrom-Json
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "ffprobe failed for the reconstructed camera source"
|
||||
}
|
||||
$stream = @($probe.streams)[0]
|
||||
if (
|
||||
[int]$stream.width -ne 800 -or
|
||||
[int]$stream.height -ne 600 -or
|
||||
[int]$stream.nb_read_frames -ne 4489
|
||||
) {
|
||||
throw "Reconstructed camera stream violates the immutable frame contract"
|
||||
}
|
||||
Move-Item -LiteralPath $temporary -Destination $output
|
||||
Assert-FreeSpace "published"
|
||||
Write-Output "REPLAY_PATH=$output"
|
||||
Write-Output "FRAME_COUNT=4489"
|
||||
}
|
||||
finally {
|
||||
if (Test-Path -LiteralPath $temporary) {
|
||||
Remove-Item -LiteralPath $temporary -Force
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
[application]
|
||||
enable-perf-measurement=1
|
||||
perf-measurement-interval-sec=5
|
||||
gie-kitti-output-dir=/workspace/output/detections
|
||||
kitti-track-output-dir=/workspace/output/tracks
|
||||
|
||||
[tiled-display]
|
||||
enable=0
|
||||
rows=1
|
||||
columns=1
|
||||
width=800
|
||||
height=600
|
||||
gpu-id=0
|
||||
|
||||
[source0]
|
||||
enable=1
|
||||
type=3
|
||||
num-sources=1
|
||||
uri=file:///workspace/input/right.mp4
|
||||
gpu-id=0
|
||||
|
||||
[streammux]
|
||||
gpu-id=0
|
||||
batch-size=1
|
||||
batched-push-timeout=40000
|
||||
width=800
|
||||
height=600
|
||||
live-source=0
|
||||
|
||||
[primary-gie]
|
||||
enable=1
|
||||
gpu-id=0
|
||||
plugin-type=0
|
||||
batch-size=1
|
||||
gie-unique-id=1
|
||||
config-file=/workspace/package/runtime/e46e_trafficcamnet_rtdetr.txt
|
||||
bbox-border-color1=0.267;0.831;1.0;1.0
|
||||
bbox-border-color2=0.243;0.973;0.553;1.0
|
||||
bbox-border-color3=1.0;0.306;0.765;1.0
|
||||
bbox-border-color4=1.0;0.741;0.153;1.0
|
||||
|
||||
[tracker]
|
||||
enable=1
|
||||
tracker-width=960
|
||||
tracker-height=544
|
||||
ll-lib-file=/opt/nvidia/deepstream/deepstream/lib/libnvds_nvmultiobjecttracker.so
|
||||
ll-config-file=/opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_NvDCF_perf.yml
|
||||
gpu-id=0
|
||||
display-tracking-id=1
|
||||
compute-hw=1
|
||||
|
||||
[osd]
|
||||
enable=1
|
||||
gpu-id=0
|
||||
border-width=3
|
||||
text-size=16
|
||||
text-color=1;1;1;1
|
||||
text-bg-color=0.08;0.08;0.08;0.9
|
||||
font=Arial
|
||||
display-bbox=1
|
||||
display-text=1
|
||||
|
||||
[sink0]
|
||||
enable=1
|
||||
type=3
|
||||
container=1
|
||||
codec=1
|
||||
enc-type=0
|
||||
sync=0
|
||||
qos=0
|
||||
bitrate=8000000
|
||||
profile=4
|
||||
output-file=/workspace/output/overlay.mp4
|
||||
source-id=0
|
||||
gpu-id=0
|
||||
|
||||
[tests]
|
||||
file-loop=0
|
||||
@@ -0,0 +1,5 @@
|
||||
background
|
||||
bicycle
|
||||
car
|
||||
person
|
||||
road_sign
|
||||
@@ -0,0 +1,26 @@
|
||||
[property]
|
||||
gpu-id=0
|
||||
onnx-file=/workspace/model/resnet50_trafficcamnet_rtdetr.fp16.onnx
|
||||
model-engine-file=/workspace/model/resnet50_trafficcamnet_rtdetr.fp16.onnx_b1_gpu0_fp16.engine
|
||||
labelfile-path=/workspace/package/runtime/e46e_trafficcamnet_labels.txt
|
||||
custom-lib-path=/workspace/package/runtime/libnvds_infercustomparser_tao.so
|
||||
parse-bbox-func-name=NvDsInferParseCustomDDETRTAO
|
||||
output-blob-names=pred_logits;pred_boxes
|
||||
infer-dims=3;544;960
|
||||
maintain-aspect-ratio=1
|
||||
net-scale-factor=0.00392156862745098
|
||||
offsets=0;0;0
|
||||
model-color-format=0
|
||||
network-mode=2
|
||||
network-type=0
|
||||
num-detected-classes=5
|
||||
cluster-mode=4
|
||||
output-tensor-meta=1
|
||||
workspace-size=1048576
|
||||
batch-size=1
|
||||
interval=0
|
||||
gie-unique-id=1
|
||||
|
||||
[class-attrs-all]
|
||||
pre-cluster-threshold=0.5
|
||||
topk=20
|
||||
@@ -0,0 +1,32 @@
|
||||
[property]
|
||||
gpu-id=0
|
||||
onnx-file=/workspace/model/resnet18_dashcamnet_pruned.onnx
|
||||
model-engine-file=/workspace/model/resnet18_dashcamnet_pruned.onnx_b1_gpu0_fp16.engine
|
||||
labelfile-path=/workspace/package/runtime/e46f_dashcamnet_labels.txt
|
||||
output-blob-names=output_bbox/BiasAdd:0;output_cov/Sigmoid:0
|
||||
infer-dims=3;544;960
|
||||
maintain-aspect-ratio=0
|
||||
net-scale-factor=0.00392156862745098
|
||||
offsets=0;0;0
|
||||
model-color-format=0
|
||||
network-mode=2
|
||||
network-type=0
|
||||
num-detected-classes=4
|
||||
cluster-mode=2
|
||||
output-tensor-meta=0
|
||||
workspace-size=1048576
|
||||
batch-size=1
|
||||
interval=0
|
||||
gie-unique-id=1
|
||||
|
||||
[class-attrs-all]
|
||||
topk=20
|
||||
nms-iou-threshold=0.5
|
||||
pre-cluster-threshold=0.2
|
||||
roi-top-offset=0
|
||||
roi-bottom-offset=0
|
||||
|
||||
[class-attrs-0]
|
||||
topk=20
|
||||
nms-iou-threshold=0.5
|
||||
pre-cluster-threshold=0.4
|
||||
@@ -0,0 +1,4 @@
|
||||
car
|
||||
bicycle
|
||||
person
|
||||
road_sign
|
||||
@@ -0,0 +1,78 @@
|
||||
[application]
|
||||
enable-perf-measurement=1
|
||||
perf-measurement-interval-sec=5
|
||||
gie-kitti-output-dir=/workspace/output/detections
|
||||
kitti-track-output-dir=/workspace/output/tracks
|
||||
|
||||
[tiled-display]
|
||||
enable=0
|
||||
rows=1
|
||||
columns=1
|
||||
width=800
|
||||
height=600
|
||||
gpu-id=0
|
||||
|
||||
[source0]
|
||||
enable=1
|
||||
type=3
|
||||
num-sources=1
|
||||
uri=file:///workspace/input/right.mp4
|
||||
gpu-id=0
|
||||
|
||||
[streammux]
|
||||
gpu-id=0
|
||||
batch-size=1
|
||||
batched-push-timeout=40000
|
||||
width=800
|
||||
height=600
|
||||
live-source=0
|
||||
|
||||
[primary-gie]
|
||||
enable=1
|
||||
gpu-id=0
|
||||
plugin-type=0
|
||||
batch-size=1
|
||||
gie-unique-id=1
|
||||
config-file=/workspace/package/runtime/e46f_dashcamnet_detectnet.txt
|
||||
bbox-border-color0=0.243;0.973;0.553;1.0
|
||||
bbox-border-color1=0.267;0.831;1.0;1.0
|
||||
bbox-border-color2=1.0;0.306;0.765;1.0
|
||||
bbox-border-color3=1.0;0.741;0.153;1.0
|
||||
|
||||
[tracker]
|
||||
enable=1
|
||||
tracker-width=960
|
||||
tracker-height=544
|
||||
ll-lib-file=/opt/nvidia/deepstream/deepstream/lib/libnvds_nvmultiobjecttracker.so
|
||||
ll-config-file=/opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_NvDCF_perf.yml
|
||||
gpu-id=0
|
||||
display-tracking-id=1
|
||||
compute-hw=1
|
||||
|
||||
[osd]
|
||||
enable=1
|
||||
gpu-id=0
|
||||
border-width=3
|
||||
text-size=16
|
||||
text-color=1;1;1;1
|
||||
text-bg-color=0.08;0.08;0.08;0.9
|
||||
font=Arial
|
||||
display-bbox=1
|
||||
display-text=1
|
||||
|
||||
[sink0]
|
||||
enable=1
|
||||
type=3
|
||||
container=1
|
||||
codec=1
|
||||
enc-type=0
|
||||
sync=0
|
||||
qos=0
|
||||
bitrate=8000000
|
||||
profile=4
|
||||
output-file=/workspace/output/overlay.mp4
|
||||
source-id=0
|
||||
gpu-id=0
|
||||
|
||||
[tests]
|
||||
file-loop=0
|
||||
@@ -0,0 +1,78 @@
|
||||
[application]
|
||||
enable-perf-measurement=1
|
||||
perf-measurement-interval-sec=5
|
||||
gie-kitti-output-dir=/workspace/output/detections
|
||||
kitti-track-output-dir=/workspace/output/tracks
|
||||
|
||||
[tiled-display]
|
||||
enable=0
|
||||
rows=1
|
||||
columns=1
|
||||
width=960
|
||||
height=544
|
||||
gpu-id=0
|
||||
|
||||
[source0]
|
||||
enable=1
|
||||
type=3
|
||||
num-sources=1
|
||||
uri=file:///workspace/input/view.mp4
|
||||
gpu-id=0
|
||||
|
||||
[streammux]
|
||||
gpu-id=0
|
||||
batch-size=1
|
||||
batched-push-timeout=40000
|
||||
width=960
|
||||
height=544
|
||||
live-source=0
|
||||
|
||||
[primary-gie]
|
||||
enable=1
|
||||
gpu-id=0
|
||||
plugin-type=0
|
||||
batch-size=1
|
||||
gie-unique-id=1
|
||||
config-file=/workspace/package/runtime/e46f_dashcamnet_detectnet.txt
|
||||
bbox-border-color0=0.243;0.973;0.553;1.0
|
||||
bbox-border-color1=0.267;0.831;1.0;1.0
|
||||
bbox-border-color2=1.0;0.306;0.765;1.0
|
||||
bbox-border-color3=1.0;0.741;0.153;1.0
|
||||
|
||||
[tracker]
|
||||
enable=1
|
||||
tracker-width=960
|
||||
tracker-height=544
|
||||
ll-lib-file=/opt/nvidia/deepstream/deepstream/lib/libnvds_nvmultiobjecttracker.so
|
||||
ll-config-file=/opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_NvDCF_perf.yml
|
||||
gpu-id=0
|
||||
display-tracking-id=1
|
||||
compute-hw=1
|
||||
|
||||
[osd]
|
||||
enable=1
|
||||
gpu-id=0
|
||||
border-width=3
|
||||
text-size=16
|
||||
text-color=1;1;1;1
|
||||
text-bg-color=0.08;0.08;0.08;0.9
|
||||
font=Arial
|
||||
display-bbox=1
|
||||
display-text=1
|
||||
|
||||
[sink0]
|
||||
enable=1
|
||||
type=3
|
||||
container=1
|
||||
codec=1
|
||||
enc-type=0
|
||||
sync=0
|
||||
qos=0
|
||||
bitrate=6000000
|
||||
profile=4
|
||||
output-file=/workspace/output/overlay.mp4
|
||||
source-id=0
|
||||
gpu-id=0
|
||||
|
||||
[tests]
|
||||
file-loop=0
|
||||
@@ -0,0 +1,22 @@
|
||||
[property]
|
||||
output-width=960
|
||||
output-height=544
|
||||
num-batch-buffers=1
|
||||
cuda-memory-type=2
|
||||
|
||||
[surface0]
|
||||
projection-type=4
|
||||
surface-index=0
|
||||
width=960
|
||||
height=544
|
||||
yaw=0.0
|
||||
pitch=0.0
|
||||
roll=0.0
|
||||
rot-axes=YXZ
|
||||
focal-length=194.59817287616025;194.57531427932872
|
||||
distortion=-0.023164451386679667;-0.0014974198594105452;-0.001039213149441563;-0.000035237331915978814
|
||||
src-x0=396.31861150187996
|
||||
src-y0=301.49644357408005
|
||||
dst-focal-length=402.76782296509447;402.76782296509447
|
||||
dst-principal-point=479.5;271.5
|
||||
cuda-address-mode=1
|
||||
@@ -0,0 +1,22 @@
|
||||
[property]
|
||||
output-width=960
|
||||
output-height=544
|
||||
num-batch-buffers=1
|
||||
cuda-memory-type=2
|
||||
|
||||
[surface0]
|
||||
projection-type=4
|
||||
surface-index=0
|
||||
width=960
|
||||
height=544
|
||||
yaw=270.0
|
||||
pitch=0.0
|
||||
roll=0.0
|
||||
rot-axes=YXZ
|
||||
focal-length=194.59817287616025;194.57531427932872
|
||||
distortion=-0.023164451386679667;-0.0014974198594105452;-0.001039213149441563;-0.000035237331915978814
|
||||
src-x0=396.31861150187996
|
||||
src-y0=301.49644357408005
|
||||
dst-focal-length=402.76782296509447;402.76782296509447
|
||||
dst-principal-point=479.5;271.5
|
||||
cuda-address-mode=1
|
||||
@@ -0,0 +1,22 @@
|
||||
[property]
|
||||
output-width=960
|
||||
output-height=544
|
||||
num-batch-buffers=1
|
||||
cuda-memory-type=2
|
||||
|
||||
[surface0]
|
||||
projection-type=4
|
||||
surface-index=0
|
||||
width=960
|
||||
height=544
|
||||
yaw=90.0
|
||||
pitch=0.0
|
||||
roll=0.0
|
||||
rot-axes=YXZ
|
||||
focal-length=194.59817287616025;194.57531427932872
|
||||
distortion=-0.023164451386679667;-0.0014974198594105452;-0.001039213149441563;-0.000035237331915978814
|
||||
src-x0=396.31861150187996
|
||||
src-y0=301.49644357408005
|
||||
dst-focal-length=402.76782296509447;402.76782296509447
|
||||
dst-principal-point=479.5;271.5
|
||||
cuda-address-mode=1
|
||||
@@ -0,0 +1,78 @@
|
||||
[application]
|
||||
enable-perf-measurement=1
|
||||
perf-measurement-interval-sec=5
|
||||
gie-kitti-output-dir=/workspace/output/detections
|
||||
kitti-track-output-dir=/workspace/output/tracks
|
||||
|
||||
[tiled-display]
|
||||
enable=0
|
||||
rows=1
|
||||
columns=1
|
||||
width=960
|
||||
height=544
|
||||
gpu-id=0
|
||||
|
||||
[source0]
|
||||
enable=1
|
||||
type=3
|
||||
num-sources=1
|
||||
uri=file:///workspace/input/view.mp4
|
||||
gpu-id=0
|
||||
|
||||
[streammux]
|
||||
gpu-id=0
|
||||
batch-size=1
|
||||
batched-push-timeout=40000
|
||||
width=960
|
||||
height=544
|
||||
live-source=0
|
||||
|
||||
[primary-gie]
|
||||
enable=1
|
||||
gpu-id=0
|
||||
plugin-type=0
|
||||
batch-size=1
|
||||
gie-unique-id=1
|
||||
config-file=/workspace/package/runtime/e46e_trafficcamnet_rtdetr.txt
|
||||
bbox-border-color1=0.267;0.831;1.0;1.0
|
||||
bbox-border-color2=0.243;0.973;0.553;1.0
|
||||
bbox-border-color3=1.0;0.306;0.765;1.0
|
||||
bbox-border-color4=1.0;0.741;0.153;1.0
|
||||
|
||||
[tracker]
|
||||
enable=1
|
||||
tracker-width=960
|
||||
tracker-height=544
|
||||
ll-lib-file=/opt/nvidia/deepstream/deepstream/lib/libnvds_nvmultiobjecttracker.so
|
||||
ll-config-file=/opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_NvDCF_perf.yml
|
||||
gpu-id=0
|
||||
display-tracking-id=1
|
||||
compute-hw=1
|
||||
|
||||
[osd]
|
||||
enable=1
|
||||
gpu-id=0
|
||||
border-width=3
|
||||
text-size=16
|
||||
text-color=1;1;1;1
|
||||
text-bg-color=0.08;0.08;0.08;0.9
|
||||
font=Arial
|
||||
display-bbox=1
|
||||
display-text=1
|
||||
|
||||
[sink0]
|
||||
enable=1
|
||||
type=3
|
||||
container=1
|
||||
codec=1
|
||||
enc-type=0
|
||||
sync=0
|
||||
qos=0
|
||||
bitrate=6000000
|
||||
profile=4
|
||||
output-file=/workspace/output/overlay.mp4
|
||||
source-id=0
|
||||
gpu-id=0
|
||||
|
||||
[tests]
|
||||
file-loop=0
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Consolidate raw DeepStream/NvDCF output into immutable E46E evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.e46e_ready_stack import build_e46e_ready_stack
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--source-job", type=Path, required=True)
|
||||
parser.add_argument("--raw-root", type=Path, required=True)
|
||||
parser.add_argument("--profile", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
result = build_e46e_ready_stack(
|
||||
source_job_root=args.source_job,
|
||||
raw_root=args.raw_root,
|
||||
profile_path=args.profile,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"result_id": result["result_id"],
|
||||
"result_root": str(result["result_root"]),
|
||||
"metrics": result["report"]["metrics"],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Consolidate raw DashCamNet/NvDCF output into immutable E46F evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.e46f_dashcam_bakeoff import build_e46f_dashcam_bakeoff
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--source-job", type=Path, required=True)
|
||||
parser.add_argument("--raw-root", type=Path, required=True)
|
||||
parser.add_argument("--profile", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
result = build_e46f_dashcam_bakeoff(
|
||||
source_job_root=args.source_job,
|
||||
raw_root=args.raw_root,
|
||||
profile_path=args.profile,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"result_id": result["result_id"],
|
||||
"result_root": str(result["result_root"]),
|
||||
"metrics": result["report"]["metrics"],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze raw NVIDIA E46G output as immutable Mission Core evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.e46g_rectified_detector_bakeoff import (
|
||||
build_e46g_rectified_detector_bakeoff,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--source-job", type=Path, required=True)
|
||||
parser.add_argument("--raw-root", type=Path, required=True)
|
||||
parser.add_argument("--profile", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
result = build_e46g_rectified_detector_bakeoff(
|
||||
source_job_root=args.source_job,
|
||||
raw_root=args.raw_root,
|
||||
profile_path=args.profile,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
print(result["result_root"])
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze raw NVIDIA E46H output as immutable Mission Core evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.e46h_full_rectified_front_replay import (
|
||||
build_e46h_full_rectified_front_replay,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--source-job", type=Path, required=True)
|
||||
parser.add_argument("--raw-root", type=Path, required=True)
|
||||
parser.add_argument("--profile", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
result = build_e46h_full_rectified_front_replay(
|
||||
source_job_root=args.source_job,
|
||||
raw_root=args.raw_root,
|
||||
profile_path=args.profile,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
print(result["result_root"])
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,706 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the frozen one-pass YOLOX-S realtime gate on the full K1 RIGHT fisheye.
|
||||
|
||||
The runner deliberately performs exactly one detector request per decoded source
|
||||
frame. It does not rectify, crop, tile, track, hold, stitch or use route-specific
|
||||
filters. H.264 overlay encoding is measured separately from the detector path.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import http.client
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
import statistics
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
import urllib.parse
|
||||
from collections import Counter
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
PROFILE_SCHEMA = "missioncore.e46j-raw-fisheye-realtime-profile/v1"
|
||||
RUNTIME_SCHEMA = "missioncore.e46j-raw-fisheye-realtime-runtime/v1"
|
||||
FRAME_SCHEMA = "missioncore.e46j-raw-fisheye-realtime-frame/v1"
|
||||
|
||||
COCO_CLASSES = (
|
||||
"person", "bicycle", "car", "motorcycle", "airplane", "bus", "train",
|
||||
"truck", "boat", "traffic light", "fire hydrant", "stop sign",
|
||||
"parking meter", "bench", "bird", "cat", "dog", "horse", "sheep",
|
||||
"cow", "elephant", "bear", "zebra", "giraffe", "backpack", "umbrella",
|
||||
"handbag", "tie", "suitcase", "frisbee", "skis", "snowboard",
|
||||
"sports ball", "kite", "baseball bat", "baseball glove", "skateboard",
|
||||
"surfboard", "tennis racket", "bottle", "wine glass", "cup", "fork",
|
||||
"knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange",
|
||||
"broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair",
|
||||
"couch", "potted plant", "bed", "dining table", "toilet", "tv",
|
||||
"laptop", "mouse", "remote", "keyboard", "cell phone", "microwave",
|
||||
"oven", "toaster", "sink", "refrigerator", "book", "clock", "vase",
|
||||
"scissors", "teddy bear", "hair drier", "toothbrush",
|
||||
)
|
||||
|
||||
|
||||
def arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--input", type=Path, required=True)
|
||||
parser.add_argument("--mask", type=Path, required=True)
|
||||
parser.add_argument("--profile", type=Path, required=True)
|
||||
parser.add_argument("--triton-url", required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--max-frames", type=int, default=0)
|
||||
parser.add_argument("--overlay", action="store_true")
|
||||
parser.add_argument("--telemetry-interval", type=float, default=0.5)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def read_object(path: Path) -> dict[str, Any]:
|
||||
value = json.loads(path.resolve(strict=True).read_text(encoding="utf-8-sig"))
|
||||
if not isinstance(value, dict):
|
||||
raise RuntimeError(f"JSON object expected: {path}")
|
||||
return value
|
||||
|
||||
|
||||
def canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value, sort_keys=True, separators=(",", ":"), allow_nan=False
|
||||
).encode()
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def distribution(values: list[float]) -> dict[str, float]:
|
||||
if not values:
|
||||
return {"mean": 0.0, "p50": 0.0, "p95": 0.0, "maximum": 0.0}
|
||||
ordered = sorted(values)
|
||||
|
||||
def percentile(fraction: float) -> float:
|
||||
index = (len(ordered) - 1) * fraction
|
||||
lower = math.floor(index)
|
||||
upper = math.ceil(index)
|
||||
if lower == upper:
|
||||
return ordered[lower]
|
||||
ratio = index - lower
|
||||
return ordered[lower] * (1.0 - ratio) + ordered[upper] * ratio
|
||||
|
||||
return {
|
||||
"mean": round(statistics.fmean(ordered), 6),
|
||||
"p50": round(percentile(0.5), 6),
|
||||
"p95": round(percentile(0.95), 6),
|
||||
"maximum": round(max(ordered), 6),
|
||||
}
|
||||
|
||||
|
||||
def validate_profile(profile: dict[str, Any], source_sha256: str) -> None:
|
||||
source = profile.get("source")
|
||||
detector = profile.get("detector")
|
||||
detection = profile.get("detection")
|
||||
acceptance = profile.get("acceptance")
|
||||
if (
|
||||
profile.get("schema_version") != PROFILE_SCHEMA
|
||||
or not isinstance(source, dict)
|
||||
or source.get("camera_source_id") != "sensor.camera.right"
|
||||
or source.get("stream_sha256") != source_sha256
|
||||
or source.get("resolution") != [800, 600]
|
||||
or source.get("calibration_model") != "KB4"
|
||||
or not isinstance(detector, dict)
|
||||
or detector.get("id") != "yolox_s"
|
||||
or detector.get("input_shape") != [1, 3, 640, 640]
|
||||
or detector.get("single_inference_per_source_frame") is not True
|
||||
or not isinstance(detection, dict)
|
||||
or detection.get("custom_detector_logic") is not False
|
||||
or detection.get("route_specific_filtering") is not False
|
||||
or not isinstance(acceptance, dict)
|
||||
or acceptance.get("require_full_raw_fov") is not True
|
||||
):
|
||||
raise RuntimeError("E46J profile contract changed")
|
||||
|
||||
|
||||
def load_mask(path: Path) -> np.ndarray:
|
||||
from PIL import Image
|
||||
|
||||
mask = np.asarray(Image.open(path.resolve(strict=True)).convert("L")) > 0
|
||||
if mask.shape != (600, 800) or not np.any(mask):
|
||||
raise RuntimeError("E46J valid-FOV mask changed")
|
||||
return mask
|
||||
|
||||
|
||||
def preprocess(
|
||||
image_bgr: np.ndarray, mask: np.ndarray, profile: dict[str, Any]
|
||||
) -> np.ndarray:
|
||||
import cv2
|
||||
|
||||
target_height = int(profile["detector"]["input_shape"][2])
|
||||
target_width = int(profile["detector"]["input_shape"][3])
|
||||
height, width = image_bgr.shape[:2]
|
||||
ratio = min(target_height / height, target_width / width)
|
||||
resized_width = int(width * ratio)
|
||||
resized_height = int(height * ratio)
|
||||
fill = int(profile["preprocessing"]["valid_fov_fill_value"])
|
||||
masked = np.where(mask[..., None], image_bgr, fill).astype(np.uint8)
|
||||
resized = cv2.resize(
|
||||
masked, (resized_width, resized_height), interpolation=cv2.INTER_LINEAR
|
||||
)
|
||||
canvas = np.full((target_height, target_width, 3), fill, dtype=np.uint8)
|
||||
canvas[:resized_height, :resized_width] = resized
|
||||
return np.ascontiguousarray(canvas.transpose(2, 0, 1), dtype=np.float32)[None]
|
||||
|
||||
|
||||
class TritonHttpClient:
|
||||
"""Minimal persistent Triton HTTP client for one sequential camera stream."""
|
||||
|
||||
def __init__(self, url: str, model: dict[str, Any]) -> None:
|
||||
parsed = urllib.parse.urlsplit(url)
|
||||
if parsed.scheme != "http" or not parsed.hostname:
|
||||
raise RuntimeError("E46J requires an explicit HTTP Triton endpoint")
|
||||
self.model = model
|
||||
self.path = f"{parsed.path.rstrip('/')}/v2/models/{model['id']}/infer"
|
||||
self.connection = http.client.HTTPConnection(
|
||||
parsed.hostname,
|
||||
parsed.port or 80,
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
def close(self) -> None:
|
||||
self.connection.close()
|
||||
|
||||
def infer(self, tensor: np.ndarray) -> np.ndarray:
|
||||
contiguous = np.ascontiguousarray(tensor, dtype=np.float32)
|
||||
binary = contiguous.tobytes()
|
||||
header = {
|
||||
"inputs": [
|
||||
{
|
||||
"name": self.model["input_name"],
|
||||
"shape": list(contiguous.shape),
|
||||
"datatype": "FP32",
|
||||
"parameters": {"binary_data_size": len(binary)},
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": self.model["output_name"],
|
||||
"parameters": {"binary_data": True},
|
||||
}
|
||||
],
|
||||
}
|
||||
encoded = canonical_json(header)
|
||||
self.connection.request(
|
||||
"POST",
|
||||
self.path,
|
||||
body=encoded + binary,
|
||||
headers={
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Inference-Header-Content-Length": str(len(encoded)),
|
||||
},
|
||||
)
|
||||
response = self.connection.getresponse()
|
||||
payload = response.read()
|
||||
if response.status != 200:
|
||||
raise RuntimeError(
|
||||
f"E46J Triton inference failed: HTTP {response.status}: "
|
||||
f"{payload[:512]!r}"
|
||||
)
|
||||
header_length_value = response.getheader("Inference-Header-Content-Length")
|
||||
if not header_length_value:
|
||||
raise RuntimeError("E46J Triton output header length is missing")
|
||||
header_length = int(header_length_value)
|
||||
descriptor = json.loads(payload[:header_length])["outputs"][0]
|
||||
if (
|
||||
descriptor["name"] != self.model["output_name"]
|
||||
or descriptor["datatype"] != "FP32"
|
||||
):
|
||||
raise RuntimeError("E46J Triton output descriptor changed")
|
||||
shape = tuple(int(value) for value in descriptor["shape"])
|
||||
array = np.frombuffer(payload[header_length:], dtype="<f4")
|
||||
if array.size != math.prod(shape):
|
||||
raise RuntimeError("E46J Triton output byte length changed")
|
||||
return array.reshape(shape)
|
||||
|
||||
|
||||
def decode_yolox(output: np.ndarray) -> np.ndarray:
|
||||
predictions = output.copy()
|
||||
grids: list[np.ndarray] = []
|
||||
strides: list[np.ndarray] = []
|
||||
for stride in (8, 16, 32):
|
||||
height = 640 // stride
|
||||
width = 640 // stride
|
||||
yv, xv = np.meshgrid(np.arange(height), np.arange(width), indexing="ij")
|
||||
grids.append(np.stack((xv, yv), axis=2).reshape(1, -1, 2))
|
||||
strides.append(np.full((1, height * width, 1), stride))
|
||||
grid = np.concatenate(grids, axis=1)
|
||||
expanded_strides = np.concatenate(strides, axis=1)
|
||||
predictions[..., :2] = (predictions[..., :2] + grid) * expanded_strides
|
||||
predictions[..., 2:4] = np.exp(predictions[..., 2:4]) * expanded_strides
|
||||
return predictions
|
||||
|
||||
|
||||
def box_iou(one: np.ndarray, many: np.ndarray) -> np.ndarray:
|
||||
if many.size == 0:
|
||||
return np.zeros((0,), dtype=np.float32)
|
||||
top_left = np.maximum(one[:2], many[:, :2])
|
||||
bottom_right = np.minimum(one[2:], many[:, 2:])
|
||||
intersection = np.prod(np.maximum(0.0, bottom_right - top_left), axis=1)
|
||||
one_area = max(0.0, float(one[2] - one[0])) * max(
|
||||
0.0, float(one[3] - one[1])
|
||||
)
|
||||
many_area = np.maximum(0.0, many[:, 2] - many[:, 0]) * np.maximum(
|
||||
0.0, many[:, 3] - many[:, 1]
|
||||
)
|
||||
union = one_area + many_area - intersection
|
||||
return np.divide(
|
||||
intersection, union, out=np.zeros_like(intersection), where=union > 0
|
||||
)
|
||||
|
||||
|
||||
def nms(boxes: np.ndarray, scores: np.ndarray, threshold: float) -> list[int]:
|
||||
order = scores.argsort()[::-1]
|
||||
keep: list[int] = []
|
||||
while order.size:
|
||||
index = int(order[0])
|
||||
keep.append(index)
|
||||
overlaps = box_iou(boxes[index], boxes[order[1:]])
|
||||
order = order[np.where(overlaps <= threshold)[0] + 1]
|
||||
return keep
|
||||
|
||||
|
||||
def valid_fraction(
|
||||
box: np.ndarray, integral: np.ndarray
|
||||
) -> tuple[float, bool, float]:
|
||||
height = integral.shape[0] - 1
|
||||
width = integral.shape[1] - 1
|
||||
x1 = int(np.clip(math.floor(float(box[0])), 0, width))
|
||||
y1 = int(np.clip(math.floor(float(box[1])), 0, height))
|
||||
x2 = int(np.clip(math.ceil(float(box[2])), 0, width))
|
||||
y2 = int(np.clip(math.ceil(float(box[3])), 0, height))
|
||||
area = float(max(0, x2 - x1) * max(0, y2 - y1))
|
||||
if area <= 0:
|
||||
return 0.0, False, 0.0
|
||||
inside = integral[y2, x2] - integral[y1, x2] - integral[y2, x1] + integral[
|
||||
y1, x1
|
||||
]
|
||||
center_x = int(
|
||||
np.clip(round((float(box[0]) + float(box[2])) / 2.0), 0, width - 1)
|
||||
)
|
||||
center_y = int(
|
||||
np.clip(round((float(box[1]) + float(box[3])) / 2.0), 0, height - 1)
|
||||
)
|
||||
center_inside = bool(
|
||||
integral[center_y + 1, center_x + 1]
|
||||
- integral[center_y, center_x + 1]
|
||||
- integral[center_y + 1, center_x]
|
||||
+ integral[center_y, center_x]
|
||||
)
|
||||
return float(inside) / area, center_inside, area
|
||||
|
||||
|
||||
def detections(
|
||||
output: np.ndarray, profile: dict[str, Any], mask: np.ndarray
|
||||
) -> tuple[list[dict[str, Any]], dict[str, int]]:
|
||||
prediction = decode_yolox(output)[0]
|
||||
boxes = prediction[:, :4]
|
||||
boxes_xyxy = np.empty_like(boxes)
|
||||
boxes_xyxy[:, 0] = boxes[:, 0] - boxes[:, 2] / 2.0
|
||||
boxes_xyxy[:, 1] = boxes[:, 1] - boxes[:, 3] / 2.0
|
||||
boxes_xyxy[:, 2] = boxes[:, 0] + boxes[:, 2] / 2.0
|
||||
boxes_xyxy[:, 3] = boxes[:, 1] + boxes[:, 3] / 2.0
|
||||
source_height, source_width = mask.shape
|
||||
ratio = min(640 / source_height, 640 / source_width)
|
||||
boxes_xyxy /= ratio
|
||||
class_scores = prediction[:, 4:5] * prediction[:, 5:]
|
||||
class_ids = class_scores.argmax(axis=1)
|
||||
scores = class_scores[np.arange(class_scores.shape[0]), class_ids]
|
||||
detection = profile["detection"]
|
||||
target_ids = set(int(value) for value in detection["target_class_ids"])
|
||||
candidate_mask = np.logical_and(
|
||||
scores >= float(detection["minimum_score"]),
|
||||
np.isin(class_ids, list(target_ids)),
|
||||
)
|
||||
candidate_boxes = boxes_xyxy[candidate_mask]
|
||||
candidate_scores = scores[candidate_mask]
|
||||
candidate_classes = class_ids[candidate_mask]
|
||||
integral = np.pad(mask.astype(np.int64), ((1, 0), (1, 0))).cumsum(0).cumsum(1)
|
||||
result: list[dict[str, Any]] = []
|
||||
rejected: Counter[str] = Counter()
|
||||
for class_id in sorted(target_ids):
|
||||
indices = np.where(candidate_classes == class_id)[0]
|
||||
if not indices.size:
|
||||
continue
|
||||
keep = nms(
|
||||
candidate_boxes[indices],
|
||||
candidate_scores[indices],
|
||||
float(detection["nms_iou_threshold"]),
|
||||
)
|
||||
for selected in indices[keep]:
|
||||
box = candidate_boxes[selected].copy()
|
||||
box[[0, 2]] = np.clip(box[[0, 2]], 0, source_width)
|
||||
box[[1, 3]] = np.clip(box[[1, 3]], 0, source_height)
|
||||
fraction, center_inside, area = valid_fraction(box, integral)
|
||||
if area < float(detection["minimum_box_area_pixels"]):
|
||||
rejected["small_box"] += 1
|
||||
continue
|
||||
if area / float(source_width * source_height) > float(
|
||||
detection["maximum_box_area_fraction"]
|
||||
):
|
||||
rejected["large_box"] += 1
|
||||
continue
|
||||
if fraction < float(detection["minimum_valid_fov_fraction"]):
|
||||
rejected["outside_valid_fov"] += 1
|
||||
continue
|
||||
if detection["require_center_inside_valid_fov"] and not center_inside:
|
||||
rejected["center_outside_valid_fov"] += 1
|
||||
continue
|
||||
result.append(
|
||||
{
|
||||
"class_id": int(class_id),
|
||||
"label": COCO_CLASSES[int(class_id)],
|
||||
"score": round(float(candidate_scores[selected]), 9),
|
||||
"bbox_xyxy": [round(float(value), 6) for value in box],
|
||||
"valid_fov_fraction": round(fraction, 6),
|
||||
}
|
||||
)
|
||||
result.sort(key=lambda item: (-float(item["score"]), int(item["class_id"])))
|
||||
return result, dict(rejected)
|
||||
|
||||
|
||||
def draw_overlay(image: np.ndarray, rows: list[dict[str, Any]]) -> np.ndarray:
|
||||
import cv2
|
||||
|
||||
for row in rows:
|
||||
x1, y1, x2, y2 = (int(round(value)) for value in row["bbox_xyxy"])
|
||||
label = f"{row['label']} {float(row['score']):.2f}"
|
||||
cv2.rectangle(image, (x1, y1), (x2, y2), (248, 248, 248), 2)
|
||||
(text_width, text_height), baseline = cv2.getTextSize(
|
||||
label, cv2.FONT_HERSHEY_SIMPLEX, 0.45, 1
|
||||
)
|
||||
top = max(0, y1 - text_height - baseline - 6)
|
||||
cv2.rectangle(
|
||||
image,
|
||||
(x1, top),
|
||||
(min(image.shape[1] - 1, x1 + text_width + 8), y1),
|
||||
(18, 18, 18),
|
||||
-1,
|
||||
)
|
||||
cv2.putText(
|
||||
image,
|
||||
label,
|
||||
(x1 + 4, max(text_height + 1, y1 - baseline - 3)),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.45,
|
||||
(248, 248, 248),
|
||||
1,
|
||||
cv2.LINE_AA,
|
||||
)
|
||||
return image
|
||||
|
||||
|
||||
class Telemetry:
|
||||
def __init__(self, interval: float) -> None:
|
||||
self.interval = interval
|
||||
self.samples: list[dict[str, float]] = []
|
||||
self.stop_event = threading.Event()
|
||||
self.thread = threading.Thread(target=self._run, daemon=True)
|
||||
|
||||
def __enter__(self) -> Telemetry:
|
||||
self.thread.start()
|
||||
return self
|
||||
|
||||
def __exit__(self, *_args: object) -> None:
|
||||
self.stop_event.set()
|
||||
self.thread.join(timeout=5)
|
||||
|
||||
def _run(self) -> None:
|
||||
while not self.stop_event.is_set():
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
[
|
||||
"nvidia-smi",
|
||||
"--query-gpu=utilization.gpu,memory.used,power.draw,temperature.gpu",
|
||||
"--format=csv,noheader,nounits",
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
values = [float(value.strip()) for value in completed.stdout.split(",")]
|
||||
self.samples.append(
|
||||
{
|
||||
"gpu_utilization_percent": values[0],
|
||||
"gpu_memory_used_mib": values[1],
|
||||
"gpu_power_watts": values[2],
|
||||
"gpu_temperature_celsius": values[3],
|
||||
}
|
||||
)
|
||||
except (OSError, ValueError, subprocess.SubprocessError):
|
||||
pass
|
||||
self.stop_event.wait(self.interval)
|
||||
|
||||
|
||||
def telemetry_summary(samples: list[dict[str, float]]) -> dict[str, object]:
|
||||
result: dict[str, object] = {"sample_count": len(samples)}
|
||||
for key in (
|
||||
"gpu_utilization_percent",
|
||||
"gpu_memory_used_mib",
|
||||
"gpu_power_watts",
|
||||
"gpu_temperature_celsius",
|
||||
):
|
||||
result[key] = distribution([row[key] for row in samples])
|
||||
return result
|
||||
|
||||
|
||||
def run(args: argparse.Namespace) -> dict[str, Any]:
|
||||
import av
|
||||
|
||||
source = args.input.resolve(strict=True)
|
||||
profile_path = args.profile.resolve(strict=True)
|
||||
mask_path = args.mask.resolve(strict=True)
|
||||
output = args.output.expanduser().absolute()
|
||||
output.mkdir(parents=True, exist_ok=False)
|
||||
profile = read_object(profile_path)
|
||||
source_sha = sha256(source)
|
||||
validate_profile(profile, source_sha)
|
||||
mask = load_mask(mask_path)
|
||||
mask_sha = sha256(mask_path)
|
||||
|
||||
container = av.open(str(source))
|
||||
video_stream = container.streams.video[0]
|
||||
frame_rate = float(video_stream.average_rate)
|
||||
if not math.isclose(frame_rate, float(profile["source"]["frame_rate"]), abs_tol=1e-6):
|
||||
raise RuntimeError(
|
||||
"E46J source frame rate changed: "
|
||||
f"profile={profile['source']['frame_rate']}, actual={frame_rate}, "
|
||||
f"average_rate={video_stream.average_rate}, time_base={video_stream.time_base}"
|
||||
)
|
||||
|
||||
overlay_path = output / "raw-fisheye-yolox-overlay-intermediate.mp4"
|
||||
overlay_writer = None
|
||||
if args.overlay:
|
||||
import cv2
|
||||
|
||||
overlay_writer = cv2.VideoWriter(
|
||||
str(overlay_path),
|
||||
cv2.VideoWriter_fourcc(*"mp4v"),
|
||||
frame_rate,
|
||||
(800, 600),
|
||||
)
|
||||
if not overlay_writer.isOpened():
|
||||
raise RuntimeError("E46J intermediate overlay encoder is unavailable")
|
||||
|
||||
frame_path = output / "frames.jsonl"
|
||||
frame_stream = frame_path.open("x", encoding="utf-8")
|
||||
class_counts: Counter[str] = Counter()
|
||||
rejected_counts: Counter[str] = Counter()
|
||||
latencies: dict[str, list[float]] = {
|
||||
"preprocess_ms": [],
|
||||
"inference_request_ms": [],
|
||||
"postprocess_ms": [],
|
||||
"core_path_ms": [],
|
||||
"overlay_encode_ms": [],
|
||||
}
|
||||
zero_detection_frames = 0
|
||||
maximum_detections = 0
|
||||
failed_frames = 0
|
||||
processed_frames = 0
|
||||
run_started_utc = datetime.now(UTC)
|
||||
wall_started = time.perf_counter()
|
||||
warm_tensor = np.full((1, 3, 640, 640), 114.0, dtype=np.float32)
|
||||
triton = TritonHttpClient(args.triton_url, profile["detector"])
|
||||
triton.infer(warm_tensor)
|
||||
|
||||
with Telemetry(args.telemetry_interval) as telemetry:
|
||||
try:
|
||||
for decoded in container.decode(video_stream):
|
||||
if args.max_frames and processed_frames >= args.max_frames:
|
||||
break
|
||||
frame_index = processed_frames
|
||||
image = decoded.to_ndarray(format="bgr24")
|
||||
if image.shape != (600, 800, 3):
|
||||
raise RuntimeError(f"E46J source raster changed at frame {frame_index}")
|
||||
core_started = time.perf_counter()
|
||||
preprocess_started = core_started
|
||||
tensor = preprocess(image, mask, profile)
|
||||
preprocess_ms = (time.perf_counter() - preprocess_started) * 1000.0
|
||||
inference_started = time.perf_counter()
|
||||
raw_output = triton.infer(tensor)
|
||||
inference_ms = (time.perf_counter() - inference_started) * 1000.0
|
||||
postprocess_started = time.perf_counter()
|
||||
rows, rejected = detections(raw_output, profile, mask)
|
||||
postprocess_ms = (time.perf_counter() - postprocess_started) * 1000.0
|
||||
core_path_ms = (time.perf_counter() - core_started) * 1000.0
|
||||
latencies["preprocess_ms"].append(preprocess_ms)
|
||||
latencies["inference_request_ms"].append(inference_ms)
|
||||
latencies["postprocess_ms"].append(postprocess_ms)
|
||||
latencies["core_path_ms"].append(core_path_ms)
|
||||
class_counts.update(str(row["label"]) for row in rows)
|
||||
rejected_counts.update(rejected)
|
||||
maximum_detections = max(maximum_detections, len(rows))
|
||||
if not rows:
|
||||
zero_detection_frames += 1
|
||||
|
||||
overlay_ms = 0.0
|
||||
if overlay_writer is not None:
|
||||
overlay_started = time.perf_counter()
|
||||
overlay = draw_overlay(image.copy(), rows)
|
||||
overlay_writer.write(overlay)
|
||||
overlay_ms = (time.perf_counter() - overlay_started) * 1000.0
|
||||
latencies["overlay_encode_ms"].append(overlay_ms)
|
||||
frame_stream.write(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": FRAME_SCHEMA,
|
||||
"frame_index": frame_index,
|
||||
"session_seconds": round(frame_index / frame_rate, 6),
|
||||
"detections": rows,
|
||||
"rejected": rejected,
|
||||
"latency_ms": {
|
||||
"preprocess": round(preprocess_ms, 6),
|
||||
"inference_request": round(inference_ms, 6),
|
||||
"postprocess": round(postprocess_ms, 6),
|
||||
"core_path": round(core_path_ms, 6),
|
||||
"overlay_encode": round(overlay_ms, 6),
|
||||
},
|
||||
},
|
||||
ensure_ascii=False,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
processed_frames += 1
|
||||
except BaseException:
|
||||
failed_frames += 1
|
||||
raise
|
||||
finally:
|
||||
frame_stream.flush()
|
||||
os.fsync(frame_stream.fileno())
|
||||
frame_stream.close()
|
||||
container.close()
|
||||
triton.close()
|
||||
if overlay_writer is not None:
|
||||
overlay_writer.release()
|
||||
|
||||
wall_seconds = time.perf_counter() - wall_started
|
||||
run_completed_utc = datetime.now(UTC)
|
||||
core = distribution(latencies["core_path_ms"])
|
||||
inference = distribution(latencies["inference_request_ms"])
|
||||
acceptance = profile["acceptance"]
|
||||
expected_frames = (
|
||||
min(int(args.max_frames), int(profile["source"]["frame_count"]))
|
||||
if args.max_frames
|
||||
else int(profile["source"]["frame_count"])
|
||||
)
|
||||
checks = {
|
||||
"frame_coverage": processed_frames == expected_frames,
|
||||
"zero_failed_frames": failed_frames == 0,
|
||||
"core_capacity_fps": (1000.0 / core["mean"])
|
||||
>= float(acceptance["minimum_core_capacity_fps"]),
|
||||
"core_path_p95_ms": core["p95"]
|
||||
<= float(acceptance["maximum_core_path_p95_ms"]),
|
||||
"inference_request_p95_ms": inference["p95"]
|
||||
<= float(acceptance["maximum_inference_request_p95_ms"]),
|
||||
"single_pass_full_raw_fov": True,
|
||||
}
|
||||
runtime: dict[str, Any] = {
|
||||
"schema_version": RUNTIME_SCHEMA,
|
||||
"status": "completed" if all(checks.values()) else "completed-gate-failed",
|
||||
"worker_host": platform.node(),
|
||||
"gpu_name": subprocess.run(
|
||||
["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
).stdout.strip().splitlines()[0],
|
||||
"started_at_utc": run_started_utc.isoformat(timespec="milliseconds").replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"completed_at_utc": run_completed_utc.isoformat(
|
||||
timespec="milliseconds"
|
||||
).replace("+00:00", "Z"),
|
||||
"source": {
|
||||
"video_sha256": source_sha,
|
||||
"frame_rate": frame_rate,
|
||||
"resolution": [800, 600],
|
||||
"decoded_frame_count": processed_frames,
|
||||
"valid_fov_mask_sha256": mask_sha,
|
||||
"preprocessing": "raw KB4 valid-FOV fill plus top-left letterbox; no crop/dewarp/tile",
|
||||
},
|
||||
"model": {
|
||||
"id": profile["detector"]["id"],
|
||||
"model_sha256": profile["detector"]["model_sha256"],
|
||||
"config_sha256": profile["detector"]["config_sha256"],
|
||||
"runtime": profile["detector"]["runtime"],
|
||||
"inference_requests": processed_frames,
|
||||
},
|
||||
"profile_sha256": sha256(profile_path),
|
||||
"metrics": {
|
||||
"processed_frame_count": processed_frames,
|
||||
"failed_frame_count": failed_frames,
|
||||
"wall_seconds_including_overlay_export": round(wall_seconds, 6),
|
||||
"export_throughput_fps": round(processed_frames / wall_seconds, 6),
|
||||
"core_capacity_fps": round(1000.0 / core["mean"], 6),
|
||||
"latency_ms": {key: distribution(values) for key, values in latencies.items()},
|
||||
"detection_observation_count": sum(class_counts.values()),
|
||||
"class_observation_counts": dict(sorted(class_counts.items())),
|
||||
"mean_detections_per_frame": round(
|
||||
sum(class_counts.values()) / max(processed_frames, 1), 6
|
||||
),
|
||||
"maximum_detections_per_frame": maximum_detections,
|
||||
"zero_detection_frame_count": zero_detection_frames,
|
||||
"zero_detection_frame_fraction": round(
|
||||
zero_detection_frames / max(processed_frames, 1), 9
|
||||
),
|
||||
"rejected_counts": dict(sorted(rejected_counts.items())),
|
||||
"gpu": telemetry_summary(telemetry.samples),
|
||||
},
|
||||
"acceptance": {
|
||||
"thresholds": acceptance,
|
||||
"checks": checks,
|
||||
"passed": all(checks.values()),
|
||||
},
|
||||
"artifacts": {
|
||||
"frames": {
|
||||
"file": frame_path.name,
|
||||
"byte_length": frame_path.stat().st_size,
|
||||
"sha256": sha256(frame_path),
|
||||
},
|
||||
},
|
||||
"authority": profile["authority"],
|
||||
}
|
||||
if overlay_path.is_file():
|
||||
runtime["artifacts"]["overlay_intermediate"] = {
|
||||
"file": overlay_path.name,
|
||||
"byte_length": overlay_path.stat().st_size,
|
||||
"sha256": sha256(overlay_path),
|
||||
"codec": "MPEG-4 Part 2",
|
||||
"frame_rate": frame_rate,
|
||||
}
|
||||
runtime_path = output / "runtime.json"
|
||||
runtime_path.write_text(
|
||||
json.dumps(runtime, ensure_ascii=False, indent=2, allow_nan=False) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return runtime
|
||||
|
||||
|
||||
def main() -> int:
|
||||
runtime = run(arguments())
|
||||
print(json.dumps(runtime, ensure_ascii=False, indent=2, allow_nan=False))
|
||||
return 0 if runtime["acceptance"]["passed"] else 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,457 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the canonical YOLOX detector on calibration-derived K1 perspective views.
|
||||
|
||||
This module is an adapter between the already qualified K1 KB4 rectification
|
||||
from LAB E3 and the already qualified YOLOX/ByteTrack detector from LAB E8.
|
||||
It deliberately does not introduce another detector, tracker or coordinate
|
||||
system. Detections are projected back into the immutable right-camera frame
|
||||
before class-aware NMS and tracking.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import concurrent.futures
|
||||
import json
|
||||
import math
|
||||
import statistics
|
||||
import time
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw
|
||||
from run_e3_rectified_segmentation import _clahe, _rectification_maps
|
||||
from run_e5_instance_tracking import (
|
||||
_detections,
|
||||
_infer,
|
||||
_load_valid_fov,
|
||||
_nms,
|
||||
_preprocess,
|
||||
_valid_fraction,
|
||||
)
|
||||
|
||||
|
||||
def _arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--job", type=Path, required=True)
|
||||
parser.add_argument("--frames", type=Path, required=True)
|
||||
parser.add_argument("--rectification-profile", type=Path, required=True)
|
||||
parser.add_argument("--detector-profile", type=Path, required=True)
|
||||
parser.add_argument("--valid-fov-root", type=Path, required=True)
|
||||
parser.add_argument("--triton-url", required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--contrast", choices=("none", "clahe"), default="none")
|
||||
parser.add_argument(
|
||||
"--active-tiles",
|
||||
default="front,left,right,up,down",
|
||||
help="Comma-separated E3 rectification tile names evaluated this pass",
|
||||
)
|
||||
parser.add_argument("--limit", type=int, default=0)
|
||||
parser.add_argument(
|
||||
"--tile-size",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Detector raster size; 0 preserves the E3 profile raster",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def _read_object(path: Path) -> dict[str, Any]:
|
||||
value = json.loads(path.resolve(strict=True).read_text(encoding="utf-8-sig"))
|
||||
if not isinstance(value, dict):
|
||||
raise RuntimeError(f"JSON root is not an object: {path}")
|
||||
return value
|
||||
|
||||
|
||||
def _percentile(values: list[float], percentile: float) -> float:
|
||||
if not values:
|
||||
return 0.0
|
||||
ordered = sorted(values)
|
||||
index = (len(ordered) - 1) * percentile
|
||||
lower = math.floor(index)
|
||||
upper = math.ceil(index)
|
||||
if lower == upper:
|
||||
return ordered[lower]
|
||||
fraction = index - lower
|
||||
return ordered[lower] * (1.0 - fraction) + ordered[upper] * fraction
|
||||
|
||||
|
||||
def _project_tile_box(
|
||||
box: list[float],
|
||||
tile: dict[str, Any],
|
||||
selected_tile: np.ndarray,
|
||||
tile_index: int,
|
||||
) -> tuple[np.ndarray, tuple[float, float]] | None:
|
||||
"""Project a perspective bbox perimeter into the raw fisheye frame."""
|
||||
|
||||
map_x = tile["raw_map_x"]
|
||||
map_y = tile["raw_map_y"]
|
||||
height, width = map_x.shape
|
||||
x1, y1, x2, y2 = (float(value) for value in box)
|
||||
x1 = float(np.clip(x1, 0.0, width - 1.0))
|
||||
x2 = float(np.clip(x2, 0.0, width - 1.0))
|
||||
y1 = float(np.clip(y1, 0.0, height - 1.0))
|
||||
y2 = float(np.clip(y2, 0.0, height - 1.0))
|
||||
if x2 <= x1 or y2 <= y1:
|
||||
return None
|
||||
|
||||
samples = max(16, int(max(x2 - x1, y2 - y1) / 4.0))
|
||||
horizontal = np.linspace(x1, x2, samples)
|
||||
vertical = np.linspace(y1, y2, samples)
|
||||
sample_x = np.concatenate(
|
||||
(horizontal, horizontal, np.full(samples, x1), np.full(samples, x2))
|
||||
)
|
||||
sample_y = np.concatenate(
|
||||
(np.full(samples, y1), np.full(samples, y2), vertical, vertical)
|
||||
)
|
||||
integer_x = np.clip(np.rint(sample_x).astype(np.int64), 0, width - 1)
|
||||
integer_y = np.clip(np.rint(sample_y).astype(np.int64), 0, height - 1)
|
||||
raw_x = map_x[integer_y, integer_x]
|
||||
raw_y = map_y[integer_y, integer_x]
|
||||
finite = np.isfinite(raw_x) & np.isfinite(raw_y)
|
||||
if not np.any(finite):
|
||||
return None
|
||||
|
||||
center_x = int(np.clip(round((x1 + x2) / 2.0), 0, width - 1))
|
||||
center_y = int(np.clip(round((y1 + y2) / 2.0), 0, height - 1))
|
||||
raw_center_x = float(map_x[center_y, center_x])
|
||||
raw_center_y = float(map_y[center_y, center_x])
|
||||
raw_height, raw_width = selected_tile.shape
|
||||
if not (
|
||||
math.isfinite(raw_center_x)
|
||||
and math.isfinite(raw_center_y)
|
||||
and 0.0 <= raw_center_x < raw_width
|
||||
and 0.0 <= raw_center_y < raw_height
|
||||
):
|
||||
return None
|
||||
owner_x = int(np.clip(round(raw_center_x), 0, raw_width - 1))
|
||||
owner_y = int(np.clip(round(raw_center_y), 0, raw_height - 1))
|
||||
if int(selected_tile[owner_y, owner_x]) != tile_index:
|
||||
return None
|
||||
|
||||
projected = np.asarray(
|
||||
[
|
||||
float(np.min(raw_x[finite])),
|
||||
float(np.min(raw_y[finite])),
|
||||
float(np.max(raw_x[finite])),
|
||||
float(np.max(raw_y[finite])),
|
||||
],
|
||||
dtype=np.float64,
|
||||
)
|
||||
projected[[0, 2]] = np.clip(projected[[0, 2]], 0.0, raw_width)
|
||||
projected[[1, 3]] = np.clip(projected[[1, 3]], 0.0, raw_height)
|
||||
return projected, (raw_center_x, raw_center_y)
|
||||
|
||||
|
||||
def _merge_raw_detections(
|
||||
candidates: list[dict[str, Any]],
|
||||
detector_profile: dict[str, Any],
|
||||
valid_mask: np.ndarray,
|
||||
) -> tuple[list[dict[str, Any]], dict[str, int]]:
|
||||
detection = detector_profile["detection"]
|
||||
integral = np.pad(valid_mask.astype(np.int64), ((1, 0), (1, 0))).cumsum(0).cumsum(1)
|
||||
raw_height, raw_width = valid_mask.shape
|
||||
admitted: list[dict[str, Any]] = []
|
||||
rejected = Counter()
|
||||
for candidate in candidates:
|
||||
box = np.asarray(candidate["bbox_xyxy"], dtype=np.float64)
|
||||
valid_fraction, center_inside, area = _valid_fraction(box, integral)
|
||||
if area < float(detection["minimum_box_area_pixels"]):
|
||||
rejected["small_raw_box"] += 1
|
||||
continue
|
||||
if area / float(raw_width * raw_height) > float(
|
||||
detection["maximum_box_area_fraction"]
|
||||
):
|
||||
rejected["large_raw_box"] += 1
|
||||
continue
|
||||
if valid_fraction < float(detection["minimum_valid_fov_fraction"]):
|
||||
rejected["outside_raw_valid_fov"] += 1
|
||||
continue
|
||||
if detection["require_center_inside_valid_fov"] and not center_inside:
|
||||
rejected["raw_center_outside_valid_fov"] += 1
|
||||
continue
|
||||
enriched = dict(candidate)
|
||||
enriched["valid_fov_fraction"] = round(valid_fraction, 6)
|
||||
admitted.append(enriched)
|
||||
|
||||
result: list[dict[str, Any]] = []
|
||||
class_ids = sorted({int(item["class_id"]) for item in admitted})
|
||||
for class_id in class_ids:
|
||||
group = [item for item in admitted if int(item["class_id"]) == class_id]
|
||||
boxes = np.asarray([item["bbox_xyxy"] for item in group], dtype=np.float64)
|
||||
scores = np.asarray([item["score"] for item in group], dtype=np.float64)
|
||||
keep = _nms(
|
||||
boxes,
|
||||
scores,
|
||||
float(detection["nms_iou_threshold"]),
|
||||
float(detection["nms_containment_threshold"]),
|
||||
)
|
||||
result.extend(group[index] for index in keep)
|
||||
rejected["cross_tile_duplicate"] += len(group) - len(keep)
|
||||
result.sort(key=lambda item: (-float(item["score"]), int(item["class_id"])))
|
||||
return result, dict(rejected)
|
||||
|
||||
|
||||
def detect_rectified(
|
||||
image: np.ndarray,
|
||||
*,
|
||||
maps: dict[str, Any],
|
||||
rectification_profile: dict[str, Any],
|
||||
detector_profile: dict[str, Any],
|
||||
valid_mask: np.ndarray,
|
||||
triton_url: str,
|
||||
contrast: str,
|
||||
active_tiles: set[str],
|
||||
cv2: Any,
|
||||
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
|
||||
candidates: list[dict[str, Any]] = []
|
||||
tile_metrics: list[dict[str, Any]] = []
|
||||
tile_valid_mask = np.ones(
|
||||
(
|
||||
int(rectification_profile["rectification"]["tile_size"]),
|
||||
int(rectification_profile["rectification"]["tile_size"]),
|
||||
),
|
||||
dtype=bool,
|
||||
)
|
||||
|
||||
active_entries = [
|
||||
(tile_index, tile)
|
||||
for tile_index, tile in enumerate(maps["tiles"])
|
||||
if tile["name"] in active_tiles
|
||||
]
|
||||
|
||||
def prepare(entry: tuple[int, dict[str, Any]]) -> dict[str, Any]:
|
||||
tile_index, tile = entry
|
||||
started = time.perf_counter()
|
||||
tile_image = cv2.remap(
|
||||
image,
|
||||
tile["raw_map_x"],
|
||||
tile["raw_map_y"],
|
||||
interpolation=cv2.INTER_LINEAR,
|
||||
borderMode=cv2.BORDER_CONSTANT,
|
||||
borderValue=(114, 114, 114),
|
||||
)
|
||||
if contrast == "clahe":
|
||||
tile_image = _clahe(tile_image, rectification_profile, cv2)
|
||||
preprocess_started = time.perf_counter()
|
||||
tensor = _preprocess(tile_image, tile_valid_mask, detector_profile)
|
||||
preprocess_ms = (time.perf_counter() - preprocess_started) * 1000.0
|
||||
return {
|
||||
"tile_index": tile_index,
|
||||
"tile": tile,
|
||||
"tensor": tensor,
|
||||
"preprocess_ms": preprocess_ms,
|
||||
"preparation_ms": (time.perf_counter() - started) * 1000.0,
|
||||
}
|
||||
|
||||
# Rectification/remap is CPU work and independent for each calibrated view.
|
||||
# Keep the pool bounded to the three operational core views; Triton requests
|
||||
# below remain sequential against the single canonical model instance.
|
||||
with concurrent.futures.ThreadPoolExecutor(
|
||||
max_workers=len(active_entries), thread_name_prefix="k1-kb4-view"
|
||||
) as executor:
|
||||
prepared = list(executor.map(prepare, active_entries))
|
||||
|
||||
for item in prepared:
|
||||
tile_index = int(item["tile_index"])
|
||||
tile = item["tile"]
|
||||
tile_started = time.perf_counter()
|
||||
output, inference_ms = _infer(
|
||||
triton_url, detector_profile["model"], item["tensor"]
|
||||
)
|
||||
tile_detections, tile_rejected = _detections(
|
||||
output, detector_profile, tile_valid_mask
|
||||
)
|
||||
owned = 0
|
||||
for detection in tile_detections:
|
||||
projected = _project_tile_box(
|
||||
detection["bbox_xyxy"], tile, maps["selected_tile"], tile_index
|
||||
)
|
||||
if projected is None:
|
||||
continue
|
||||
raw_box, raw_center = projected
|
||||
candidate = dict(detection)
|
||||
candidate["bbox_xyxy"] = [round(float(value), 6) for value in raw_box]
|
||||
candidate["raw_center_xy"] = [round(value, 6) for value in raw_center]
|
||||
candidate["rectification_tile"] = tile["name"]
|
||||
candidates.append(candidate)
|
||||
owned += 1
|
||||
tile_metrics.append(
|
||||
{
|
||||
"tile": tile["name"],
|
||||
"detections": len(tile_detections),
|
||||
"owned_detections": owned,
|
||||
"inference_ms": round(inference_ms, 6),
|
||||
"preprocess_ms": round(float(item["preprocess_ms"]), 6),
|
||||
"preparation_ms": round(float(item["preparation_ms"]), 6),
|
||||
"serial_inference_postprocess_ms": round(
|
||||
(time.perf_counter() - tile_started) * 1000.0, 6
|
||||
),
|
||||
"rejected": tile_rejected,
|
||||
}
|
||||
)
|
||||
merged, rejected = _merge_raw_detections(candidates, detector_profile, valid_mask)
|
||||
return merged, {
|
||||
"tiles": tile_metrics,
|
||||
"candidate_count": len(candidates),
|
||||
"rejected": rejected,
|
||||
}
|
||||
|
||||
|
||||
def _draw_overlay(image: np.ndarray, detections: list[dict[str, Any]]) -> Image.Image:
|
||||
output = Image.fromarray(image)
|
||||
draw = ImageDraw.Draw(output)
|
||||
palette = {
|
||||
"person": "#8cff5d",
|
||||
"bicycle": "#50d7ff",
|
||||
"car": "#ffffff",
|
||||
"motorcycle": "#ffcd57",
|
||||
"bus": "#ff8b5d",
|
||||
"truck": "#ff8b5d",
|
||||
}
|
||||
for detection in detections:
|
||||
box = tuple(float(value) for value in detection["bbox_xyxy"])
|
||||
color = palette.get(str(detection["label"]), "#ffffff")
|
||||
draw.rectangle(box, outline=color, width=3)
|
||||
label = (
|
||||
f"{detection['label']} {float(detection['score']):.0%} "
|
||||
f"[{detection['rectification_tile']}]"
|
||||
)
|
||||
text_box = draw.textbbox((box[0], max(0.0, box[1] - 18.0)), label)
|
||||
draw.rectangle(text_box, fill="#0b0b0d")
|
||||
draw.text((box[0], max(0.0, box[1] - 18.0)), label, fill=color)
|
||||
return output
|
||||
|
||||
|
||||
def main() -> None:
|
||||
arguments = _arguments()
|
||||
if arguments.limit < 0:
|
||||
raise RuntimeError("--limit cannot be negative")
|
||||
if arguments.tile_size and not 320 <= arguments.tile_size <= 1024:
|
||||
raise RuntimeError("--tile-size must be zero or within [320, 1024]")
|
||||
import cv2
|
||||
|
||||
rectification_profile = _read_object(arguments.rectification_profile)
|
||||
if arguments.tile_size:
|
||||
rectification_profile = copy.deepcopy(rectification_profile)
|
||||
rectification_profile["rectification"]["tile_size"] = arguments.tile_size
|
||||
detector_profile = _read_object(arguments.detector_profile)
|
||||
job = _read_object(arguments.job)
|
||||
active_tiles = {
|
||||
value.strip() for value in arguments.active_tiles.split(",") if value.strip()
|
||||
}
|
||||
configured_tiles = {
|
||||
str(tile["name"])
|
||||
for tile in rectification_profile["rectification"]["tiles"]
|
||||
}
|
||||
if not active_tiles or not active_tiles <= configured_tiles:
|
||||
raise RuntimeError("--active-tiles contains an unknown or empty tile set")
|
||||
expected_resolution = tuple(rectification_profile["source"]["resolution"])
|
||||
if expected_resolution != tuple(detector_profile["source"]["resolution"]):
|
||||
raise RuntimeError("Rectification and detector source resolutions differ")
|
||||
if (
|
||||
rectification_profile["source"]["calibration_sha256"]
|
||||
!= detector_profile["source"]["calibration_sha256"]
|
||||
):
|
||||
raise RuntimeError("Rectification and detector calibration identities differ")
|
||||
|
||||
valid_mask, _mask_metadata = _load_valid_fov(
|
||||
arguments.valid_fov_root, job, detector_profile
|
||||
)
|
||||
maps_started = time.perf_counter()
|
||||
maps = _rectification_maps(rectification_profile, valid_mask)
|
||||
maps_ms = (time.perf_counter() - maps_started) * 1000.0
|
||||
frame_paths = sorted(arguments.frames.glob("*.jpg"))
|
||||
if arguments.limit:
|
||||
frame_paths = frame_paths[: arguments.limit]
|
||||
if not frame_paths:
|
||||
raise RuntimeError("No JPEG benchmark frames were found")
|
||||
|
||||
arguments.output.mkdir(parents=True, exist_ok=False)
|
||||
overlays = arguments.output / "overlays"
|
||||
overlays.mkdir()
|
||||
documents: list[dict[str, Any]] = []
|
||||
frame_latencies: list[float] = []
|
||||
inference_latencies: list[float] = []
|
||||
detection_counts: list[int] = []
|
||||
for frame_path in frame_paths:
|
||||
image = np.asarray(Image.open(frame_path).convert("RGB"), dtype=np.uint8)
|
||||
if (image.shape[1], image.shape[0]) != expected_resolution:
|
||||
raise RuntimeError(f"Unexpected camera resolution: {frame_path}")
|
||||
started = time.perf_counter()
|
||||
detections, diagnostics = detect_rectified(
|
||||
image,
|
||||
maps=maps,
|
||||
rectification_profile=rectification_profile,
|
||||
detector_profile=detector_profile,
|
||||
valid_mask=valid_mask,
|
||||
triton_url=arguments.triton_url,
|
||||
contrast=arguments.contrast,
|
||||
active_tiles=active_tiles,
|
||||
cv2=cv2,
|
||||
)
|
||||
elapsed_ms = (time.perf_counter() - started) * 1000.0
|
||||
frame_latencies.append(elapsed_ms)
|
||||
inference_latencies.extend(
|
||||
float(tile["inference_ms"]) for tile in diagnostics["tiles"]
|
||||
)
|
||||
detection_counts.append(len(detections))
|
||||
overlay_name = frame_path.name
|
||||
_draw_overlay(image, detections).save(
|
||||
overlays / overlay_name, format="JPEG", quality=92, optimize=True
|
||||
)
|
||||
documents.append(
|
||||
{
|
||||
"frame": frame_path.name,
|
||||
"elapsed_ms": round(elapsed_ms, 6),
|
||||
"detections": detections,
|
||||
"diagnostics": diagnostics,
|
||||
"overlay": f"overlays/{overlay_name}",
|
||||
}
|
||||
)
|
||||
print(
|
||||
f"FRAME={frame_path.name} DETECTIONS={len(detections)} "
|
||||
f"ELAPSED_MS={elapsed_ms:.3f}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
summary = {
|
||||
"schema_version": "missioncore.rectified-yolox-benchmark/v1",
|
||||
"pipeline": "k1-kb4-cubemap5-yolox-raw-frame/v1",
|
||||
"contrast": arguments.contrast,
|
||||
"active_tiles": sorted(active_tiles),
|
||||
"tile_size": int(rectification_profile["rectification"]["tile_size"]),
|
||||
"frame_count": len(documents),
|
||||
"rectification_map_build_ms": round(maps_ms, 6),
|
||||
"latency_ms": {
|
||||
"mean": round(statistics.fmean(frame_latencies), 6),
|
||||
"p50": round(_percentile(frame_latencies, 0.5), 6),
|
||||
"p95": round(_percentile(frame_latencies, 0.95), 6),
|
||||
"maximum": round(max(frame_latencies), 6),
|
||||
},
|
||||
"tile_inference_ms": {
|
||||
"mean": round(statistics.fmean(inference_latencies), 6),
|
||||
"p95": round(_percentile(inference_latencies, 0.95), 6),
|
||||
"maximum": round(max(inference_latencies), 6),
|
||||
},
|
||||
"detections_per_frame": {
|
||||
"mean": round(statistics.fmean(detection_counts), 6),
|
||||
"minimum": min(detection_counts),
|
||||
"maximum": max(detection_counts),
|
||||
"total": sum(detection_counts),
|
||||
},
|
||||
"coverage": maps["coverage"],
|
||||
"frames": documents,
|
||||
}
|
||||
(arguments.output / "benchmark.json").write_text(
|
||||
json.dumps(summary, ensure_ascii=False, sort_keys=True, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(json.dumps({key: value for key, value in summary.items() if key != "frames"}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,326 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Qualify the rectified YOLOX + existing ByteTrack path on full RAVNOVES00."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import json
|
||||
import platform
|
||||
import statistics
|
||||
import time
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from run_e3_rectified_segmentation import _rectification_maps
|
||||
from run_e5_instance_tracking import (
|
||||
TwoStageTracker,
|
||||
_duplicate_pairs,
|
||||
_load_valid_fov,
|
||||
_track_document,
|
||||
)
|
||||
from run_rectified_yolox_detector import (
|
||||
_draw_overlay,
|
||||
_percentile,
|
||||
_read_object,
|
||||
detect_rectified,
|
||||
)
|
||||
|
||||
|
||||
def _arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--job", type=Path, required=True)
|
||||
parser.add_argument("--video", type=Path, required=True)
|
||||
parser.add_argument("--rectification-profile", type=Path, required=True)
|
||||
parser.add_argument("--detector-profile", type=Path, required=True)
|
||||
parser.add_argument("--valid-fov-root", type=Path, required=True)
|
||||
parser.add_argument("--triton-url", required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--active-tiles", default="front,left,right")
|
||||
parser.add_argument("--tile-size", type=int, default=640)
|
||||
parser.add_argument("--preview-stride", type=int, default=250)
|
||||
parser.add_argument("--limit", type=int, default=0)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def _latency(values: list[float]) -> dict[str, float]:
|
||||
return {
|
||||
"mean": round(statistics.fmean(values), 6),
|
||||
"p50": round(_percentile(values, 0.5), 6),
|
||||
"p95": round(_percentile(values, 0.95), 6),
|
||||
"p99": round(_percentile(values, 0.99), 6),
|
||||
"maximum": round(max(values), 6),
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
arguments = _arguments()
|
||||
if arguments.tile_size != 640:
|
||||
raise RuntimeError("Operational qualification is fixed to detector-native 640 tiles")
|
||||
if arguments.preview_stride < 1 or arguments.limit < 0:
|
||||
raise RuntimeError("Preview stride or frame limit is invalid")
|
||||
if not arguments.triton_url.startswith("http://"):
|
||||
raise RuntimeError("Triton URL must use the internal HTTP endpoint")
|
||||
import cv2
|
||||
|
||||
job = _read_object(arguments.job)
|
||||
if (
|
||||
job.get("job_id") != "recorded-camera-602ac89026ed12978619801d"
|
||||
or job.get("input", {}).get("session_id") != "20260720T065719Z_viewer_live"
|
||||
or job.get("input", {}).get("source_id") != "sensor.camera.right"
|
||||
or job.get("input", {}).get("segment_count") != 4489
|
||||
):
|
||||
raise RuntimeError("Qualification is not bound to immutable RAVNOVES00")
|
||||
|
||||
rectification_profile = copy.deepcopy(_read_object(arguments.rectification_profile))
|
||||
detector_profile = _read_object(arguments.detector_profile)
|
||||
rectification_profile["rectification"]["tile_size"] = arguments.tile_size
|
||||
if (
|
||||
rectification_profile["source"]["calibration_sha256"]
|
||||
!= detector_profile["source"]["calibration_sha256"]
|
||||
):
|
||||
raise RuntimeError("Calibration identity differs between detector stages")
|
||||
active_tiles = {
|
||||
value.strip() for value in arguments.active_tiles.split(",") if value.strip()
|
||||
}
|
||||
if active_tiles != {"front", "left", "right"}:
|
||||
raise RuntimeError("Operational qualification requires front,left,right views")
|
||||
|
||||
valid_mask, valid_fov = _load_valid_fov(
|
||||
arguments.valid_fov_root, job, detector_profile
|
||||
)
|
||||
maps_started = time.perf_counter()
|
||||
maps = _rectification_maps(rectification_profile, valid_mask)
|
||||
maps_ms = (time.perf_counter() - maps_started) * 1000.0
|
||||
|
||||
video = arguments.video.resolve(strict=True)
|
||||
capture = cv2.VideoCapture(str(video))
|
||||
if not capture.isOpened():
|
||||
raise RuntimeError("OpenCV could not open the immutable camera replay")
|
||||
width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||
height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||
source_count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||
if (width, height, source_count) != (800, 600, 4489):
|
||||
raise RuntimeError("Camera replay metadata violates the RAVNOVES00 contract")
|
||||
|
||||
output = arguments.output.resolve()
|
||||
if output.exists():
|
||||
raise RuntimeError("Qualification output must be absent")
|
||||
output.mkdir(parents=True, mode=0o700)
|
||||
previews = output / "previews"
|
||||
previews.mkdir()
|
||||
frames_path = output / "frames.jsonl"
|
||||
tracker = TwoStageTracker(detector_profile["tracking"])
|
||||
from scipy.optimize import linear_sum_assignment
|
||||
|
||||
linear_sum_assignment(np.zeros((1, 1), dtype=np.float64))
|
||||
|
||||
latency: list[float] = []
|
||||
decode_latency: list[float] = []
|
||||
detector_latency: list[float] = []
|
||||
tracking_latency: list[float] = []
|
||||
detections_by_label: Counter[str] = Counter()
|
||||
tracks_by_label: Counter[str] = Counter()
|
||||
rejections: Counter[str] = Counter()
|
||||
unique_tracks: set[int] = set()
|
||||
duplicate_pairs = 0
|
||||
processed = 0
|
||||
failures = 0
|
||||
|
||||
# One explicit warmup makes the source-rate metrics independent from model
|
||||
# initialization while preserving the first source frame for the real run.
|
||||
ok, warm_bgr = capture.read()
|
||||
if not ok:
|
||||
raise RuntimeError("Camera replay has no warmup frame")
|
||||
warm_rgb = cv2.cvtColor(warm_bgr, cv2.COLOR_BGR2RGB)
|
||||
detect_rectified(
|
||||
warm_rgb,
|
||||
maps=maps,
|
||||
rectification_profile=rectification_profile,
|
||||
detector_profile=detector_profile,
|
||||
valid_mask=valid_mask,
|
||||
triton_url=arguments.triton_url,
|
||||
contrast="none",
|
||||
active_tiles=active_tiles,
|
||||
cv2=cv2,
|
||||
)
|
||||
capture.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
||||
|
||||
run_started = time.perf_counter()
|
||||
try:
|
||||
with frames_path.open("x", encoding="utf-8", newline="\n") as stream:
|
||||
while True:
|
||||
if arguments.limit and processed >= arguments.limit:
|
||||
break
|
||||
decode_started = time.perf_counter()
|
||||
ok, bgr = capture.read()
|
||||
decoded = time.perf_counter()
|
||||
if not ok:
|
||||
break
|
||||
image = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)
|
||||
frame_started = time.perf_counter()
|
||||
try:
|
||||
detections, diagnostics = detect_rectified(
|
||||
image,
|
||||
maps=maps,
|
||||
rectification_profile=rectification_profile,
|
||||
detector_profile=detector_profile,
|
||||
valid_mask=valid_mask,
|
||||
triton_url=arguments.triton_url,
|
||||
contrast="none",
|
||||
active_tiles=active_tiles,
|
||||
cv2=cv2,
|
||||
)
|
||||
detected = time.perf_counter()
|
||||
tracks = tracker.update(detections, processed)
|
||||
tracked = time.perf_counter()
|
||||
except Exception:
|
||||
failures += 1
|
||||
raise
|
||||
|
||||
frame_ms = (tracked - frame_started) * 1000.0
|
||||
latency.append(frame_ms)
|
||||
decode_latency.append((decoded - decode_started) * 1000.0)
|
||||
detector_latency.append((detected - frame_started) * 1000.0)
|
||||
tracking_latency.append((tracked - detected) * 1000.0)
|
||||
detections_by_label.update(str(item["label"]) for item in detections)
|
||||
tracks_by_label.update(track.label for track in tracks)
|
||||
for track in tracks:
|
||||
unique_tracks.add(track.track_id)
|
||||
duplicate_pairs += _duplicate_pairs(tracks)
|
||||
rejections.update(diagnostics["rejected"])
|
||||
|
||||
document = {
|
||||
"schema_version": "missioncore.rectified-yolox-frame/v1",
|
||||
"frame_index": processed,
|
||||
"sequence": processed + 1,
|
||||
"detections": detections,
|
||||
"tracks": [_track_document(track) for track in tracks],
|
||||
"processing_ms": round(frame_ms, 6),
|
||||
"tile_inference_ms": round(
|
||||
sum(float(tile["inference_ms"]) for tile in diagnostics["tiles"]),
|
||||
6,
|
||||
),
|
||||
}
|
||||
stream.write(
|
||||
json.dumps(
|
||||
document,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
if processed % arguments.preview_stride == 0:
|
||||
_draw_overlay(image, detections).save(
|
||||
previews / f"frame-{processed:06d}.jpg",
|
||||
format="JPEG",
|
||||
quality=92,
|
||||
optimize=True,
|
||||
)
|
||||
processed += 1
|
||||
if processed % 100 == 0:
|
||||
stream.flush()
|
||||
print(
|
||||
f"PHASE=rectified-yolox FRAMES={processed} "
|
||||
f"LAST_MS={frame_ms:.3f}",
|
||||
flush=True,
|
||||
)
|
||||
finally:
|
||||
capture.release()
|
||||
wall_seconds = time.perf_counter() - run_started
|
||||
expected = arguments.limit or source_count
|
||||
steady = latency[1:] if len(latency) > 1 else latency
|
||||
effective_fps = processed / wall_seconds
|
||||
steady_latency = _latency(steady)
|
||||
checks = {
|
||||
"complete_frame_accounting": processed == expected,
|
||||
"zero_failures": failures == 0,
|
||||
"minimum_effective_fps_9_5": effective_fps >= 9.5,
|
||||
"maximum_steady_p95_ms_100": steady_latency["p95"] <= 100.0,
|
||||
"same_class_duplicate_pairs_zero": duplicate_pairs == 0,
|
||||
}
|
||||
accepted = all(checks.values())
|
||||
report: dict[str, Any] = {
|
||||
"schema_version": "missioncore.rectified-yolox-qualification/v1",
|
||||
"state": "accepted" if accepted else "rejected",
|
||||
"source": {
|
||||
"job_id": job["job_id"],
|
||||
"session_id": job["input"]["session_id"],
|
||||
"source_id": job["input"]["source_id"],
|
||||
"frame_count": source_count,
|
||||
"calibration_sha256": detector_profile["source"]["calibration_sha256"],
|
||||
},
|
||||
"pipeline": {
|
||||
"id": "k1-kb4-core3-yolox-bytetrack/v1",
|
||||
"rectification": "five-perspective-gnomonic/v1",
|
||||
"active_tiles": sorted(active_tiles),
|
||||
"tile_size": arguments.tile_size,
|
||||
"detector": detector_profile["model"],
|
||||
"tracker": detector_profile["tracking"],
|
||||
"peripheral_views": "up/down remain on the existing 2 Hz semantic cadence",
|
||||
},
|
||||
"runtime": {
|
||||
"hostname": platform.node(),
|
||||
"python": platform.python_version(),
|
||||
"opencv": cv2.__version__,
|
||||
"rectification_map_build_ms": round(maps_ms, 6),
|
||||
"wall_seconds": round(wall_seconds, 6),
|
||||
"effective_fps": round(effective_fps, 6),
|
||||
},
|
||||
"metrics": {
|
||||
"frames_processed": processed,
|
||||
"failures": failures,
|
||||
"latency_ms": _latency(latency),
|
||||
"steady_latency_ms": steady_latency,
|
||||
"decode_latency_ms": _latency(decode_latency),
|
||||
"detector_latency_ms": _latency(detector_latency),
|
||||
"tracking_latency_ms": _latency(tracking_latency),
|
||||
"detections": int(sum(detections_by_label.values())),
|
||||
"detections_by_label": dict(sorted(detections_by_label.items())),
|
||||
"unique_confirmed_tracks": len(unique_tracks),
|
||||
"track_observations": int(sum(tracks_by_label.values())),
|
||||
"track_observations_by_label": dict(sorted(tracks_by_label.items())),
|
||||
"rejections": dict(sorted(rejections.items())),
|
||||
"same_class_duplicate_pairs_iou_ge_0_8": duplicate_pairs,
|
||||
"tracker_tracks_created": tracker.created,
|
||||
"tracker_tracks_retired": tracker.retired,
|
||||
},
|
||||
"acceptance": {"accepted": accepted, "checks": checks},
|
||||
"valid_fov": valid_fov,
|
||||
"limitations": [
|
||||
"RAVNOVES00 has no independent exhaustive object ground truth.",
|
||||
"This gate qualifies runtime and visual evidence, not safety accuracy.",
|
||||
"LiDAR association, temporal world state and segmentation are existing downstream stages and are not recomputed by this detector-only gate.",
|
||||
],
|
||||
"artifacts": {
|
||||
"frames": "frames.jsonl",
|
||||
"previews": "previews/",
|
||||
},
|
||||
}
|
||||
(output / "qualification.json").write_text(
|
||||
json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"accepted": accepted,
|
||||
"frames_processed": processed,
|
||||
"effective_fps": round(effective_fps, 6),
|
||||
"steady_p95_ms": steady_latency["p95"],
|
||||
"detections": report["metrics"]["detections"],
|
||||
"unique_confirmed_tracks": len(unique_tracks),
|
||||
},
|
||||
sort_keys=True,
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
raise SystemExit(0 if accepted else 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,292 @@
|
||||
"""Freeze one Mission Core LAB annotation session as an E48 review submission."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
from k1link.compute.e46_detector_truth_island import (
|
||||
E46_REFERENCES_NAME,
|
||||
E46DetectorTruthIslandError,
|
||||
read_e46_detector_truth_island,
|
||||
)
|
||||
from k1link.compute.e48_detector_truth_seal import (
|
||||
E48_REVIEW_SCHEMA,
|
||||
E48DetectorTruthSealError,
|
||||
validate_e48_detector_review_submission,
|
||||
)
|
||||
|
||||
E46_LAB_REVIEW_SCHEMA: Final = "missioncore.e46-lab-review-submission/v1"
|
||||
E46_LAB_REVIEW_MANIFEST: Final = "manifest.json"
|
||||
E46_LAB_REVIEW_DOCUMENT: Final = "review-submission.json"
|
||||
_SESSION_SCHEMA: Final = "missioncore.l34-annotation-session/v3"
|
||||
_RESULT_ID = re.compile(r"^e46-lab-review-submission-[a-f0-9]{64}$")
|
||||
_REVIEWER_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{1,63}$")
|
||||
_BLINDNESS: Final = {
|
||||
"candidate_identity_seen": False,
|
||||
"model_prelabels_seen": False,
|
||||
"model_predictions_seen": False,
|
||||
"model_scores_seen": False,
|
||||
}
|
||||
_AUTHORITY: Final = {
|
||||
"ground_truth": False,
|
||||
"independent_truth": False,
|
||||
"metric_grade_reference": False,
|
||||
"candidate_accepted": False,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
|
||||
class E46LabReviewSubmissionError(ValueError):
|
||||
"""Raised when an E46 LAB session cannot become an E48 review input."""
|
||||
|
||||
|
||||
def build_e46_lab_review_submission(
|
||||
*,
|
||||
truth_island_root: Path,
|
||||
annotation_session_path: Path,
|
||||
reviewer_id: str,
|
||||
output_root: Path,
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
truth = read_e46_detector_truth_island(truth_island_root)
|
||||
except (E46DetectorTruthIslandError, OSError) as exc:
|
||||
raise E46LabReviewSubmissionError("E46 truth island invalid") from exc
|
||||
reviewer = reviewer_id.strip()
|
||||
if _REVIEWER_ID.fullmatch(reviewer) is None:
|
||||
raise E46LabReviewSubmissionError("reviewer identity invalid")
|
||||
session_path = annotation_session_path.resolve(strict=True)
|
||||
if not session_path.is_file() or session_path.is_symlink():
|
||||
raise E46LabReviewSubmissionError("annotation session unavailable")
|
||||
session = _read_json(session_path)
|
||||
references = tuple(_read_jsonl(truth.result_root / E46_REFERENCES_NAME))
|
||||
_validate_session(session, truth.result_id, references)
|
||||
frames = {int(item["truth_island_sequence"]): item for item in session["frames"]}
|
||||
images: list[dict[str, Any]] = []
|
||||
for reference in references:
|
||||
sequence = int(reference["truth_island_sequence"])
|
||||
frame = frames[sequence]
|
||||
objects: list[dict[str, Any]] = []
|
||||
for item in frame["objects"]:
|
||||
category = item.get("category")
|
||||
if category == "unmapped" or item.get("proposed_label") is not None:
|
||||
raise E46LabReviewSubmissionError("E46 review class invalid")
|
||||
objects.append(
|
||||
{
|
||||
"object_id": item["object_id"],
|
||||
"category": category,
|
||||
"box_xyxy": item["box_xyxy"],
|
||||
"occluded": item["occluded"],
|
||||
"truncated": item["truncated"],
|
||||
"notes": None,
|
||||
}
|
||||
)
|
||||
images.append(
|
||||
{
|
||||
"truth_island_sequence": sequence,
|
||||
"image_id": reference["image_id"],
|
||||
"frame_index": reference["frame_index"],
|
||||
"session_seconds": reference["session_seconds"],
|
||||
"role": reference["role"],
|
||||
"group_id": reference["group_id"],
|
||||
"source_path": reference["source_path"],
|
||||
"source_sha256": reference["sha256"],
|
||||
"review_state": "reviewed",
|
||||
"hard_negative": frame["hard_negative"],
|
||||
"objects": objects,
|
||||
"notes": None,
|
||||
}
|
||||
)
|
||||
review = {
|
||||
"schema_version": E48_REVIEW_SCHEMA,
|
||||
"truth_island_id": truth.result_id,
|
||||
"state": "completed-independent-no-model-assistance",
|
||||
"reviewer_id": reviewer,
|
||||
"review_round": 1,
|
||||
"blindness": _BLINDNESS,
|
||||
"images": images,
|
||||
"acceptance": {
|
||||
"all_images_reviewed": True,
|
||||
"independent": True,
|
||||
"submitted_at_utc": session["updated_at_utc"],
|
||||
},
|
||||
}
|
||||
review_sha256 = hashlib.sha256(_canonical_json(review)).hexdigest()
|
||||
identity = {
|
||||
"schema_version": E46_LAB_REVIEW_SCHEMA,
|
||||
"truth_island_id": truth.result_id,
|
||||
"annotation_session_id": session["session_id"],
|
||||
"annotation_session_sha256": _sha256(session_path),
|
||||
"reviewer_id": reviewer,
|
||||
"review_sha256": review_sha256,
|
||||
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
"blindness": _BLINDNESS,
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
result_id = f"e46-lab-review-submission-{identity_sha256}"
|
||||
destination = output_root.expanduser().absolute() / result_id
|
||||
if destination.exists():
|
||||
return read_e46_lab_review_submission(destination, truth_island_root=truth.result_root)
|
||||
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700)
|
||||
try:
|
||||
_write_json(staging / E46_LAB_REVIEW_DOCUMENT, review)
|
||||
validate_e48_detector_review_submission(
|
||||
truth_island_root=truth.result_root,
|
||||
review_path=staging / E46_LAB_REVIEW_DOCUMENT,
|
||||
)
|
||||
_write_json(
|
||||
staging / E46_LAB_REVIEW_MANIFEST,
|
||||
{
|
||||
"schema_version": E46_LAB_REVIEW_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": session["updated_at_utc"],
|
||||
"state": "completed-e48-review-input-not-truth",
|
||||
"ground_truth": False,
|
||||
"artifacts": [
|
||||
{
|
||||
"path": E46_LAB_REVIEW_DOCUMENT,
|
||||
"role": "e48-review-submission",
|
||||
"byte_length": (staging / E46_LAB_REVIEW_DOCUMENT).stat().st_size,
|
||||
"sha256": _sha256(staging / E46_LAB_REVIEW_DOCUMENT),
|
||||
}
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
},
|
||||
)
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
return read_e46_lab_review_submission(destination, truth_island_root=truth.result_root)
|
||||
|
||||
|
||||
def read_e46_lab_review_submission(
|
||||
root: Path,
|
||||
*,
|
||||
truth_island_root: Path,
|
||||
) -> dict[str, Any]:
|
||||
resolved = root.resolve(strict=True)
|
||||
manifest = _read_json(resolved / E46_LAB_REVIEW_MANIFEST)
|
||||
identity = manifest.get("identity")
|
||||
if not isinstance(identity, dict):
|
||||
raise E46LabReviewSubmissionError("review identity invalid")
|
||||
digest = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
result_id = f"e46-lab-review-submission-{digest}"
|
||||
artifact = resolved / E46_LAB_REVIEW_DOCUMENT
|
||||
if (
|
||||
manifest.get("schema_version") != E46_LAB_REVIEW_SCHEMA
|
||||
or manifest.get("identity_sha256") != digest
|
||||
or manifest.get("result_id") != result_id
|
||||
or resolved.name != result_id
|
||||
or _RESULT_ID.fullmatch(result_id) is None
|
||||
or manifest.get("state") != "completed-e48-review-input-not-truth"
|
||||
or manifest.get("ground_truth") is not False
|
||||
or manifest.get("authority") != _AUTHORITY
|
||||
or not artifact.is_file()
|
||||
or artifact.is_symlink()
|
||||
or hashlib.sha256(_canonical_json(_read_json(artifact))).hexdigest()
|
||||
!= identity.get("review_sha256")
|
||||
):
|
||||
raise E46LabReviewSubmissionError("review submission changed")
|
||||
artifacts = manifest.get("artifacts")
|
||||
artifact_row = artifacts[0] if isinstance(artifacts, list) and len(artifacts) == 1 else None
|
||||
if (
|
||||
not isinstance(artifact_row, dict)
|
||||
or artifact_row.get("path") != E46_LAB_REVIEW_DOCUMENT
|
||||
or artifact_row.get("role") != "e48-review-submission"
|
||||
or artifact_row.get("byte_length") != artifact.stat().st_size
|
||||
or artifact_row.get("sha256") != _sha256(artifact)
|
||||
):
|
||||
raise E46LabReviewSubmissionError("review artifact changed")
|
||||
try:
|
||||
review = validate_e48_detector_review_submission(
|
||||
truth_island_root=truth_island_root,
|
||||
review_path=artifact,
|
||||
)
|
||||
except (E48DetectorTruthSealError, OSError) as exc:
|
||||
raise E46LabReviewSubmissionError("E48 review submission invalid") from exc
|
||||
return {
|
||||
"result_id": result_id,
|
||||
"result_root": resolved,
|
||||
"manifest": manifest,
|
||||
"review": review,
|
||||
}
|
||||
|
||||
|
||||
def _validate_session(
|
||||
session: dict[str, Any],
|
||||
truth_island_id: str,
|
||||
references: tuple[dict[str, Any], ...],
|
||||
) -> None:
|
||||
frames = session.get("frames")
|
||||
if (
|
||||
session.get("schema_version") != _SESSION_SCHEMA
|
||||
or session.get("result_id") != truth_island_id
|
||||
or session.get("truth_island_id") != truth_island_id
|
||||
or session.get("contract_id") != "e46-detector-blind-review/v1"
|
||||
or session.get("state") != "saved"
|
||||
or session.get("blindness") != _BLINDNESS
|
||||
or session.get("assistance")
|
||||
!= {"mode": "prediction-free-manual", "independent_truth_eligible": False}
|
||||
or not isinstance(frames, list)
|
||||
or len(frames) != len(references)
|
||||
):
|
||||
raise E46LabReviewSubmissionError("annotation session incomplete")
|
||||
by_sequence = {int(item["truth_island_sequence"]): item for item in frames}
|
||||
if len(by_sequence) != len(frames):
|
||||
raise E46LabReviewSubmissionError("annotation frame duplicated")
|
||||
for reference in references:
|
||||
frame = by_sequence.get(int(reference["truth_island_sequence"]))
|
||||
if (
|
||||
frame is None
|
||||
or frame.get("reviewed") is not True
|
||||
or frame.get("image_id") != reference["image_id"]
|
||||
or frame.get("frame_index") != reference["frame_index"]
|
||||
or frame.get("source_sha256") != reference["sha256"]
|
||||
or frame.get("hard_negative") != (len(frame.get("objects", [])) == 0)
|
||||
):
|
||||
raise E46LabReviewSubmissionError("annotation source identity changed")
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode()
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(value, dict):
|
||||
raise E46LabReviewSubmissionError("expected JSON object")
|
||||
return value
|
||||
|
||||
|
||||
def _read_jsonl(path: Path) -> list[dict[str, Any]]:
|
||||
rows = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line]
|
||||
if any(not isinstance(row, dict) for row in rows):
|
||||
raise E46LabReviewSubmissionError("expected JSONL objects")
|
||||
return rows
|
||||
|
||||
|
||||
def _write_json(path: Path, value: object) -> None:
|
||||
path.write_bytes(_canonical_json(value) + b"\n")
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
@@ -0,0 +1,766 @@
|
||||
"""Freeze an AI-audited engineering preannotation for the 32 E46 frames.
|
||||
|
||||
E46A is deliberately derived from the candidate-visible L3.4F engineering
|
||||
reference. It is useful as an editable starting point and visual evidence,
|
||||
but it is neither an independent review nor ground truth.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import uuid
|
||||
from collections import Counter
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
from k1link.compute.e46_detector_truth_island import (
|
||||
E46_MANIFEST_NAME,
|
||||
E46_REFERENCES_NAME,
|
||||
read_e46_detector_truth_island,
|
||||
)
|
||||
from k1link.compute.e47_detector_candidate_freeze import (
|
||||
E47_MANIFEST_NAME,
|
||||
E47_PREDICTIONS_NAME,
|
||||
read_e47_detector_candidate_freeze,
|
||||
)
|
||||
from k1link.compute.l34f_adjudicated_reference import (
|
||||
L34F_MANIFEST_NAME,
|
||||
read_l34f_adjudicated_reference,
|
||||
)
|
||||
|
||||
E46A_RESULT_SCHEMA: Final = "missioncore.e46a-ai-engineering-preannotation/v1"
|
||||
E46A_REPORT_SCHEMA: Final = "missioncore.e46a-ai-engineering-preannotation-report/v1"
|
||||
E46A_CASE_SCHEMA: Final = "missioncore.e46a-ai-engineering-preannotation-case/v1"
|
||||
E46A_MANIFEST_NAME: Final = "manifest.json"
|
||||
E46A_REPORT_NAME: Final = "ai-engineering-preannotation-report.json"
|
||||
E46A_CASES_NAME: Final = "ai-engineering-preannotations.jsonl"
|
||||
|
||||
_RESULT_ID = re.compile(r"^e46a-ai-engineering-preannotation-[a-f0-9]{64}$")
|
||||
_AUTHORITY: Final = {
|
||||
"ground_truth": False,
|
||||
"independent_truth": False,
|
||||
"metric_grade_reference": False,
|
||||
"candidate_accepted": False,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
_CUSTOM_CLASS_BY_PROPOSED_LABEL: Final = {
|
||||
"Детская коляска": "stroller",
|
||||
"Ноутбук": "laptop",
|
||||
}
|
||||
_VEHICLE_CATEGORIES: Final = frozenset({"car", "heavy_vehicle"})
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class E46AVisualAuditProfile:
|
||||
"""Source-scoped object QA applied to the candidate-visible seed."""
|
||||
|
||||
profile_id: str
|
||||
geometry_candidate_id: str
|
||||
candidate_nms_iou: float
|
||||
maximum_match_cost: float
|
||||
expected_source_object_count: int
|
||||
expected_final_object_count: int
|
||||
expected_geometry_snapped_count: int
|
||||
delete_object_ids: tuple[str, ...]
|
||||
category_overrides: tuple[tuple[str, str], ...]
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"profile_id": self.profile_id,
|
||||
"geometry_candidate_id": self.geometry_candidate_id,
|
||||
"candidate_nms_iou": self.candidate_nms_iou,
|
||||
"maximum_match_cost": self.maximum_match_cost,
|
||||
"expected_source_object_count": self.expected_source_object_count,
|
||||
"expected_final_object_count": self.expected_final_object_count,
|
||||
"expected_geometry_snapped_count": (
|
||||
self.expected_geometry_snapped_count
|
||||
),
|
||||
"delete_object_ids": list(self.delete_object_ids),
|
||||
"category_overrides": dict(self.category_overrides),
|
||||
}
|
||||
|
||||
|
||||
RAVNOVES00_E46A_VISUAL_AUDIT_V2: Final = E46AVisualAuditProfile(
|
||||
profile_id="ravnoves00-right-e46a-object-qa/v2",
|
||||
geometry_candidate_id="maskrcnn-kb4-valid-fov-fill",
|
||||
candidate_nms_iou=0.3,
|
||||
maximum_match_cost=1.3,
|
||||
expected_source_object_count=260,
|
||||
expected_final_object_count=245,
|
||||
expected_geometry_snapped_count=217,
|
||||
delete_object_ids=(
|
||||
"self-03-06",
|
||||
"self-04-09",
|
||||
"self-09-05",
|
||||
"self-29-08",
|
||||
"self-29-09",
|
||||
"self-29-10",
|
||||
"self-30-08",
|
||||
"self-30-09",
|
||||
"self-30-10",
|
||||
"self-31-08",
|
||||
"self-31-09",
|
||||
"self-31-10",
|
||||
"self-32-08",
|
||||
"self-32-09",
|
||||
"self-32-10",
|
||||
),
|
||||
category_overrides=(("self-20-06", "car"),),
|
||||
)
|
||||
|
||||
|
||||
class E46AAiEngineeringPreannotationError(ValueError):
|
||||
"""Raised when E46A input binding or immutable output is invalid."""
|
||||
|
||||
|
||||
def build_e46a_ai_engineering_preannotation(
|
||||
*,
|
||||
e46_root: Path,
|
||||
l34f_root: Path,
|
||||
output_root: Path,
|
||||
geometry_candidate_root: Path | None = None,
|
||||
audit_profile: E46AVisualAuditProfile = RAVNOVES00_E46A_VISUAL_AUDIT_V2,
|
||||
) -> dict[str, Any]:
|
||||
"""Build a path-free, hash-bound 32-frame engineering preannotation."""
|
||||
|
||||
e46 = read_e46_detector_truth_island(e46_root)
|
||||
l34f = read_l34f_adjudicated_reference(l34f_root)
|
||||
e46_references = tuple(_read_jsonl(e46.result_root / E46_REFERENCES_NAME))
|
||||
if len(e46_references) != 32 or len(l34f["cases"]) != 32:
|
||||
raise E46AAiEngineeringPreannotationError("E46A requires exactly 32 frames")
|
||||
e46_by_sequence = {
|
||||
int(row["truth_island_sequence"]): row for row in e46_references
|
||||
}
|
||||
cases: list[dict[str, Any]] = []
|
||||
relabeled_count = 0
|
||||
for source in sorted(
|
||||
l34f["cases"], key=lambda row: int(row["truth_island_sequence"])
|
||||
):
|
||||
sequence = int(source["truth_island_sequence"])
|
||||
e46_reference = e46_by_sequence.get(sequence)
|
||||
if e46_reference is None or any(
|
||||
source.get(key) != e46_reference.get(target)
|
||||
for key, target in (
|
||||
("image_id", "image_id"),
|
||||
("frame_index", "frame_index"),
|
||||
("group_id", "group_id"),
|
||||
("source_image_sha256", "sha256"),
|
||||
)
|
||||
):
|
||||
raise E46AAiEngineeringPreannotationError(
|
||||
f"L3.4F frame {sequence} is not bound to E46"
|
||||
)
|
||||
objects: list[dict[str, Any]] = []
|
||||
for raw in source.get("references", []):
|
||||
if not isinstance(raw, dict):
|
||||
raise E46AAiEngineeringPreannotationError("E46A object is invalid")
|
||||
item = copy.deepcopy(raw)
|
||||
category = item.get("category")
|
||||
if category == "unmapped":
|
||||
category = _CUSTOM_CLASS_BY_PROPOSED_LABEL.get(
|
||||
str(item.get("proposed_label"))
|
||||
)
|
||||
if category is None:
|
||||
raise E46AAiEngineeringPreannotationError(
|
||||
"E46A contains an unresolved custom class"
|
||||
)
|
||||
relabeled_count += 1
|
||||
if not isinstance(category, str) or not category:
|
||||
raise E46AAiEngineeringPreannotationError("E46A class is invalid")
|
||||
item["category"] = category
|
||||
item["origin"] = "ai_engineering_preannotation"
|
||||
item["source_origin"] = raw.get("origin")
|
||||
objects.append(item)
|
||||
cases.append(
|
||||
{
|
||||
"schema_version": E46A_CASE_SCHEMA,
|
||||
"truth_island_sequence": sequence,
|
||||
"image_id": int(source["image_id"]),
|
||||
"frame_index": int(source["frame_index"]),
|
||||
"group_id": str(source["group_id"]),
|
||||
"session_seconds": float(source["session_seconds"]),
|
||||
"source_image_sha256": str(source["source_image_sha256"]),
|
||||
"objects": objects,
|
||||
"object_count": len(objects),
|
||||
"hard_negative": len(objects) == 0,
|
||||
"visual_audit_state": "derived-reference-not-object-audited",
|
||||
}
|
||||
)
|
||||
|
||||
geometry_audit: dict[str, Any] | None = None
|
||||
geometry_candidate: dict[str, Any] | None = None
|
||||
if geometry_candidate_root is not None:
|
||||
geometry_candidate = read_e47_detector_candidate_freeze(
|
||||
geometry_candidate_root
|
||||
)
|
||||
truth_island = geometry_candidate["manifest"]["identity"].get(
|
||||
"truth_island"
|
||||
)
|
||||
if (
|
||||
not isinstance(truth_island, dict)
|
||||
or truth_island.get("result_id") != e46.result_id
|
||||
):
|
||||
raise E46AAiEngineeringPreannotationError(
|
||||
"E46A geometry candidate is not bound to E46"
|
||||
)
|
||||
prediction_rows = tuple(
|
||||
_read_jsonl(
|
||||
geometry_candidate["result_root"] / E47_PREDICTIONS_NAME
|
||||
)
|
||||
)
|
||||
geometry_audit = _apply_visual_audit(
|
||||
cases=cases,
|
||||
prediction_rows=prediction_rows,
|
||||
profile=audit_profile,
|
||||
)
|
||||
|
||||
class_counts = Counter(
|
||||
str(item["category"])
|
||||
for case in cases
|
||||
for item in case["objects"]
|
||||
)
|
||||
source_object_count = (
|
||||
int(geometry_audit["source_object_count"])
|
||||
if geometry_audit is not None
|
||||
else sum(len(case["objects"]) for case in cases)
|
||||
)
|
||||
metrics = {
|
||||
"frame_count": len(cases),
|
||||
"reviewed_frame_count": len(cases) if geometry_audit is not None else 0,
|
||||
"source_object_count": source_object_count,
|
||||
"object_count": sum(len(case["objects"]) for case in cases),
|
||||
"custom_class_relabel_count": relabeled_count,
|
||||
"hard_negative_frame_count": sum(bool(case["hard_negative"]) for case in cases),
|
||||
"class_counts": dict(sorted(class_counts.items())),
|
||||
"independent_review_submission_count": 0,
|
||||
}
|
||||
if geometry_audit is not None:
|
||||
metrics.update(
|
||||
{
|
||||
"deleted_false_box_count": geometry_audit[
|
||||
"deleted_false_box_count"
|
||||
],
|
||||
"geometry_snapped_object_count": geometry_audit[
|
||||
"geometry_snapped_object_count"
|
||||
],
|
||||
"source_geometry_retained_object_count": geometry_audit[
|
||||
"source_geometry_retained_object_count"
|
||||
],
|
||||
"category_corrected_object_count": geometry_audit[
|
||||
"category_corrected_object_count"
|
||||
],
|
||||
}
|
||||
)
|
||||
visual_review_complete = geometry_audit is not None
|
||||
report_basis = {
|
||||
"schema_version": E46A_REPORT_SCHEMA,
|
||||
"status": "completed-ai-engineering-preannotation-not-independent-not-truth",
|
||||
"metrics": metrics,
|
||||
"assistance": {
|
||||
"candidate_identity_seen": True,
|
||||
"model_predictions_seen": True,
|
||||
"model_scores_seen": geometry_audit is not None,
|
||||
"derived_reference_seen": True,
|
||||
"independent_truth_eligible": False,
|
||||
},
|
||||
"object_level_audit": copy.deepcopy(geometry_audit),
|
||||
"taxonomy": {
|
||||
"classes": sorted(class_counts),
|
||||
"custom_classes": ["laptop", "stroller"],
|
||||
"unresolved_class_count": 0,
|
||||
},
|
||||
"decision": {
|
||||
"preannotation_available": True,
|
||||
"visual_review_complete": visual_review_complete,
|
||||
"independent_review_count_affected": False,
|
||||
"e48_truth_seal_open": False,
|
||||
"l35_acceptance_open": False,
|
||||
"next_action": (
|
||||
"use E46A only for assisted correction/approval; keep Reviewer A and "
|
||||
"Reviewer B prediction-free and independent"
|
||||
),
|
||||
},
|
||||
"limitations": [
|
||||
(
|
||||
"the labels derive from a candidate-visible engineering reference; "
|
||||
"the frozen Mask R-CNN candidate is used only to refine geometry"
|
||||
if geometry_audit is not None
|
||||
else "the derived reference has not completed object-level visual QA"
|
||||
),
|
||||
(
|
||||
"E46A is an assisted preannotation and cannot occupy either "
|
||||
"independent E46 reviewer slot"
|
||||
),
|
||||
(
|
||||
"no AP, recall, miss, false-positive, detector acceptance, live, "
|
||||
"LiDAR-range, command, navigation, or safety claim is made"
|
||||
),
|
||||
(
|
||||
"the evidence is limited to recorded sensor.camera.right frames "
|
||||
"from one RAVNOVES00 route"
|
||||
),
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
"ground_truth": False,
|
||||
}
|
||||
identity = {
|
||||
"schema_version": E46A_RESULT_SCHEMA,
|
||||
"e46_source": {
|
||||
"result_id": e46.result_id,
|
||||
"manifest_sha256": _sha256(e46.result_root / E46_MANIFEST_NAME),
|
||||
},
|
||||
"l34f_engineering_reference": {
|
||||
"result_id": l34f["result_id"],
|
||||
"manifest_sha256": _sha256(l34f["result_root"] / L34F_MANIFEST_NAME),
|
||||
},
|
||||
"audit_profile": (
|
||||
audit_profile.to_dict()
|
||||
if geometry_audit is not None
|
||||
else "derived-reference-no-object-level-audit/v1"
|
||||
),
|
||||
"custom_class_mapping": _CUSTOM_CLASS_BY_PROPOSED_LABEL,
|
||||
"report_sha256": hashlib.sha256(_canonical_json(report_basis)).hexdigest(),
|
||||
"cases_sha256": hashlib.sha256(_canonical_json(cases)).hexdigest(),
|
||||
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
if geometry_candidate is not None:
|
||||
identity["e47_geometry_candidate"] = {
|
||||
"result_id": geometry_candidate["result_id"],
|
||||
"manifest_sha256": _sha256(
|
||||
geometry_candidate["result_root"] / E47_MANIFEST_NAME
|
||||
),
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
result_id = f"e46a-ai-engineering-preannotation-{identity_sha256}"
|
||||
destination = output_root.expanduser().absolute() / result_id
|
||||
if destination.exists():
|
||||
return read_e46a_ai_engineering_preannotation(destination)
|
||||
created_at_utc = _utc_now()
|
||||
report = {
|
||||
**report_basis,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"created_at_utc": created_at_utc,
|
||||
"source_session_id": "RAVNOVES00",
|
||||
"camera_source_id": "sensor.camera.right",
|
||||
}
|
||||
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
_write_json(staging / E46A_REPORT_NAME, report)
|
||||
_write_jsonl(staging / E46A_CASES_NAME, cases)
|
||||
artifacts = [
|
||||
_artifact(staging / E46A_REPORT_NAME, "preannotation-report"),
|
||||
_artifact(staging / E46A_CASES_NAME, "preannotation-cases"),
|
||||
]
|
||||
_write_json(
|
||||
staging / E46A_MANIFEST_NAME,
|
||||
{
|
||||
"schema_version": E46A_RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": created_at_utc,
|
||||
"acceptance_state": "ai-engineering-preannotation-not-truth",
|
||||
"ground_truth": False,
|
||||
"artifacts": artifacts,
|
||||
"authority": _AUTHORITY,
|
||||
},
|
||||
)
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
return read_e46a_ai_engineering_preannotation(destination)
|
||||
|
||||
|
||||
def read_e46a_ai_engineering_preannotation(root: Path) -> dict[str, Any]:
|
||||
resolved = root.resolve(strict=True)
|
||||
manifest = _read_json(resolved / E46A_MANIFEST_NAME)
|
||||
identity = manifest.get("identity")
|
||||
if not isinstance(identity, dict):
|
||||
raise E46AAiEngineeringPreannotationError("E46A identity is invalid")
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
if (
|
||||
manifest.get("schema_version") != E46A_RESULT_SCHEMA
|
||||
or manifest.get("identity_sha256") != identity_sha256
|
||||
or manifest.get("result_id")
|
||||
!= f"e46a-ai-engineering-preannotation-{identity_sha256}"
|
||||
or resolved.name != manifest.get("result_id")
|
||||
or _RESULT_ID.fullmatch(resolved.name) is None
|
||||
or manifest.get("acceptance_state")
|
||||
!= "ai-engineering-preannotation-not-truth"
|
||||
or manifest.get("authority") != _AUTHORITY
|
||||
or manifest.get("ground_truth") is not False
|
||||
):
|
||||
raise E46AAiEngineeringPreannotationError("E46A identity is invalid")
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(artifacts, list) or len(artifacts) != 2:
|
||||
raise E46AAiEngineeringPreannotationError("E46A artifacts are invalid")
|
||||
by_role = {item.get("role"): item for item in artifacts if isinstance(item, dict)}
|
||||
report = _read_json(_validated_artifact(resolved, by_role.get("preannotation-report")))
|
||||
cases = tuple(_read_jsonl(_validated_artifact(resolved, by_role.get("preannotation-cases"))))
|
||||
if (
|
||||
report.get("schema_version") != E46A_REPORT_SCHEMA
|
||||
or report.get("result_id") != resolved.name
|
||||
or report.get("status")
|
||||
!= "completed-ai-engineering-preannotation-not-independent-not-truth"
|
||||
or report.get("authority") != _AUTHORITY
|
||||
or report.get("ground_truth") is not False
|
||||
or len(cases) != 32
|
||||
or any(case.get("schema_version") != E46A_CASE_SCHEMA for case in cases)
|
||||
or any(
|
||||
item.get("category") == "unmapped"
|
||||
for case in cases
|
||||
for item in case.get("objects", [])
|
||||
if isinstance(item, dict)
|
||||
)
|
||||
or hashlib.sha256(_canonical_json(cases)).hexdigest()
|
||||
!= identity.get("cases_sha256")
|
||||
):
|
||||
raise E46AAiEngineeringPreannotationError("E46A result changed")
|
||||
return {
|
||||
"result_id": resolved.name,
|
||||
"result_root": resolved,
|
||||
"manifest": manifest,
|
||||
"report": report,
|
||||
"cases": cases,
|
||||
}
|
||||
|
||||
|
||||
def _apply_visual_audit(
|
||||
*,
|
||||
cases: list[dict[str, Any]],
|
||||
prediction_rows: tuple[dict[str, Any], ...],
|
||||
profile: E46AVisualAuditProfile,
|
||||
) -> dict[str, Any]:
|
||||
source_object_ids = {
|
||||
str(item.get("object_id"))
|
||||
for case in cases
|
||||
for item in case.get("objects", [])
|
||||
if isinstance(item, dict)
|
||||
}
|
||||
source_object_count = sum(len(case.get("objects", [])) for case in cases)
|
||||
delete_object_ids = set(profile.delete_object_ids)
|
||||
category_overrides = dict(profile.category_overrides)
|
||||
if (
|
||||
source_object_count != profile.expected_source_object_count
|
||||
or len(source_object_ids) != source_object_count
|
||||
or not delete_object_ids <= source_object_ids
|
||||
or not set(category_overrides) <= source_object_ids - delete_object_ids
|
||||
or not 0.0 < profile.candidate_nms_iou < 1.0
|
||||
or profile.maximum_match_cost <= 0.0
|
||||
):
|
||||
raise E46AAiEngineeringPreannotationError(
|
||||
"E46A visual-audit profile is not bound to the source objects"
|
||||
)
|
||||
|
||||
rows = {
|
||||
int(row["truth_island_sequence"]): row
|
||||
for row in prediction_rows
|
||||
if row.get("candidate_id") == profile.geometry_candidate_id
|
||||
}
|
||||
if set(rows) != set(range(1, 33)):
|
||||
raise E46AAiEngineeringPreannotationError(
|
||||
"E46A geometry candidate coverage is incomplete"
|
||||
)
|
||||
|
||||
snapped_count = 0
|
||||
retained_count = 0
|
||||
category_corrections: list[dict[str, str]] = []
|
||||
for case in cases:
|
||||
sequence = int(case["truth_island_sequence"])
|
||||
row = rows[sequence]
|
||||
if row.get("source_image_sha256") != case.get("source_image_sha256"):
|
||||
raise E46AAiEngineeringPreannotationError(
|
||||
f"E46A geometry frame {sequence} changed"
|
||||
)
|
||||
raw_objects = case.get("objects")
|
||||
raw_predictions = row.get("predictions")
|
||||
if not isinstance(raw_objects, list) or not isinstance(raw_predictions, list):
|
||||
raise E46AAiEngineeringPreannotationError(
|
||||
"E46A visual-audit payload is invalid"
|
||||
)
|
||||
objects = [
|
||||
item
|
||||
for item in raw_objects
|
||||
if str(item.get("object_id")) not in delete_object_ids
|
||||
]
|
||||
for item in objects:
|
||||
object_id = str(item["object_id"])
|
||||
override = category_overrides.get(object_id)
|
||||
if override is not None and override != item.get("category"):
|
||||
category_corrections.append(
|
||||
{
|
||||
"object_id": object_id,
|
||||
"before": str(item.get("category")),
|
||||
"after": override,
|
||||
}
|
||||
)
|
||||
item["source_category"] = item.get("category")
|
||||
item["category"] = override
|
||||
|
||||
predictions = _candidate_nms(
|
||||
tuple(_validated_prediction(item) for item in raw_predictions),
|
||||
threshold=profile.candidate_nms_iou,
|
||||
)
|
||||
pairs: list[tuple[float, int, int]] = []
|
||||
for object_index, item in enumerate(objects):
|
||||
category = str(item["category"])
|
||||
if category in {"stroller", "laptop"}:
|
||||
continue
|
||||
source_box = _box(item.get("box_xyxy"))
|
||||
source_center = _center(source_box)
|
||||
source_diagonal = max(
|
||||
12.0,
|
||||
math.hypot(
|
||||
source_box[2] - source_box[0],
|
||||
source_box[3] - source_box[1],
|
||||
),
|
||||
)
|
||||
for prediction_index, prediction in enumerate(predictions):
|
||||
if _category_group(category) != _category_group(
|
||||
str(prediction["category"])
|
||||
):
|
||||
continue
|
||||
candidate_box = _box(prediction["box_xyxy"])
|
||||
candidate_center = _center(candidate_box)
|
||||
normalized_distance = math.hypot(
|
||||
source_center[0] - candidate_center[0],
|
||||
source_center[1] - candidate_center[1],
|
||||
) / source_diagonal
|
||||
overlap = _iou(source_box, candidate_box)
|
||||
if normalized_distance > 1.3 and overlap < 0.05:
|
||||
continue
|
||||
area_ratio = abs(
|
||||
math.log(
|
||||
max(_area(candidate_box), 1.0)
|
||||
/ max(_area(source_box), 1.0)
|
||||
)
|
||||
)
|
||||
cost = (
|
||||
normalized_distance
|
||||
+ 0.12 * area_ratio
|
||||
- 0.35 * overlap
|
||||
- 0.03 * float(prediction["score"])
|
||||
)
|
||||
pairs.append((cost, object_index, prediction_index))
|
||||
|
||||
used_objects: set[int] = set()
|
||||
used_predictions: set[int] = set()
|
||||
for cost, object_index, prediction_index in sorted(pairs):
|
||||
if (
|
||||
cost > profile.maximum_match_cost
|
||||
or object_index in used_objects
|
||||
or prediction_index in used_predictions
|
||||
):
|
||||
continue
|
||||
used_objects.add(object_index)
|
||||
used_predictions.add(prediction_index)
|
||||
item = objects[object_index]
|
||||
prediction = predictions[prediction_index]
|
||||
source_box = _box(item["box_xyxy"])
|
||||
item["source_box_xyxy"] = list(source_box)
|
||||
item["box_xyxy"] = [
|
||||
round(value, 3) for value in _box(prediction["box_xyxy"])
|
||||
]
|
||||
item["geometry_origin"] = "maskrcnn_valid_fov_visual_snap"
|
||||
item["geometry_candidate_id"] = profile.geometry_candidate_id
|
||||
item["geometry_candidate_score"] = round(
|
||||
float(prediction["score"]), 9
|
||||
)
|
||||
snapped_count += 1
|
||||
|
||||
for object_index, item in enumerate(objects):
|
||||
if object_index not in used_objects:
|
||||
item["geometry_origin"] = "l34f_engineering_reference_retained"
|
||||
retained_count += 1
|
||||
case["objects"] = objects
|
||||
case["object_count"] = len(objects)
|
||||
case["hard_negative"] = len(objects) == 0
|
||||
case["visual_audit_state"] = "ai-object-level-audited-v2"
|
||||
|
||||
final_object_count = sum(len(case["objects"]) for case in cases)
|
||||
if (
|
||||
final_object_count != profile.expected_final_object_count
|
||||
or snapped_count != profile.expected_geometry_snapped_count
|
||||
or snapped_count + retained_count != final_object_count
|
||||
):
|
||||
raise E46AAiEngineeringPreannotationError(
|
||||
"E46A visual-audit result does not match the frozen profile"
|
||||
)
|
||||
return {
|
||||
"profile_id": profile.profile_id,
|
||||
"geometry_candidate_id": profile.geometry_candidate_id,
|
||||
"source_object_count": source_object_count,
|
||||
"deleted_false_box_count": len(delete_object_ids),
|
||||
"deleted_false_box_ids": sorted(delete_object_ids),
|
||||
"geometry_snapped_object_count": snapped_count,
|
||||
"source_geometry_retained_object_count": retained_count,
|
||||
"category_corrected_object_count": len(category_corrections),
|
||||
"category_corrections": category_corrections,
|
||||
"reviewed_frame_count": len(cases),
|
||||
"ground_truth": False,
|
||||
}
|
||||
|
||||
|
||||
def _validated_prediction(raw: object) -> dict[str, Any]:
|
||||
if not isinstance(raw, dict):
|
||||
raise E46AAiEngineeringPreannotationError(
|
||||
"E46A geometry candidate row is invalid"
|
||||
)
|
||||
category = raw.get("category")
|
||||
score = raw.get("score")
|
||||
box = _box(raw.get("box_xyxy"))
|
||||
if (
|
||||
not isinstance(category, str)
|
||||
or not category
|
||||
or not isinstance(score, (int, float))
|
||||
or isinstance(score, bool)
|
||||
or not math.isfinite(float(score))
|
||||
or not 0.0 <= float(score) <= 1.0
|
||||
):
|
||||
raise E46AAiEngineeringPreannotationError(
|
||||
"E46A geometry candidate row is invalid"
|
||||
)
|
||||
return {"category": category, "score": float(score), "box_xyxy": box}
|
||||
|
||||
|
||||
def _candidate_nms(
|
||||
predictions: tuple[dict[str, Any], ...], *, threshold: float
|
||||
) -> tuple[dict[str, Any], ...]:
|
||||
kept: list[dict[str, Any]] = []
|
||||
for prediction in sorted(
|
||||
predictions, key=lambda item: float(item["score"]), reverse=True
|
||||
):
|
||||
if all(
|
||||
_category_group(str(prediction["category"]))
|
||||
!= _category_group(str(other["category"]))
|
||||
or _iou(
|
||||
_box(prediction["box_xyxy"]),
|
||||
_box(other["box_xyxy"]),
|
||||
)
|
||||
< threshold
|
||||
for other in kept
|
||||
):
|
||||
kept.append(prediction)
|
||||
return tuple(kept)
|
||||
|
||||
|
||||
def _category_group(category: str) -> str:
|
||||
return "vehicle" if category in _VEHICLE_CATEGORIES else category
|
||||
|
||||
|
||||
def _box(raw: object) -> tuple[float, float, float, float]:
|
||||
if (
|
||||
not isinstance(raw, (list, tuple))
|
||||
or len(raw) != 4
|
||||
or any(
|
||||
not isinstance(value, (int, float))
|
||||
or isinstance(value, bool)
|
||||
or not math.isfinite(float(value))
|
||||
for value in raw
|
||||
)
|
||||
):
|
||||
raise E46AAiEngineeringPreannotationError("E46A box is invalid")
|
||||
box = tuple(float(value) for value in raw)
|
||||
if (
|
||||
box[0] < 0.0
|
||||
or box[1] < 0.0
|
||||
or box[2] > 800.0
|
||||
or box[3] > 600.0
|
||||
or box[2] <= box[0]
|
||||
or box[3] <= box[1]
|
||||
):
|
||||
raise E46AAiEngineeringPreannotationError("E46A box is invalid")
|
||||
return box
|
||||
|
||||
|
||||
def _area(box: tuple[float, float, float, float]) -> float:
|
||||
return (box[2] - box[0]) * (box[3] - box[1])
|
||||
|
||||
|
||||
def _center(box: tuple[float, float, float, float]) -> tuple[float, float]:
|
||||
return ((box[0] + box[2]) / 2.0, (box[1] + box[3]) / 2.0)
|
||||
|
||||
|
||||
def _iou(
|
||||
first: tuple[float, float, float, float],
|
||||
second: tuple[float, float, float, float],
|
||||
) -> float:
|
||||
width = max(0.0, min(first[2], second[2]) - max(first[0], second[0]))
|
||||
height = max(0.0, min(first[3], second[3]) - max(first[1], second[1]))
|
||||
intersection = width * height
|
||||
if intersection == 0.0:
|
||||
return 0.0
|
||||
return intersection / (_area(first) + _area(second) - intersection)
|
||||
|
||||
|
||||
def _artifact(path: Path, role: str) -> dict[str, Any]:
|
||||
return {
|
||||
"role": role,
|
||||
"path": path.name,
|
||||
"byte_length": path.stat().st_size,
|
||||
"sha256": _sha256(path),
|
||||
}
|
||||
|
||||
|
||||
def _validated_artifact(root: Path, raw: object) -> Path:
|
||||
if not isinstance(raw, dict) or not isinstance(raw.get("path"), str):
|
||||
raise E46AAiEngineeringPreannotationError("E46A artifact is invalid")
|
||||
path = (root / str(raw["path"])).resolve(strict=True)
|
||||
if (
|
||||
path.parent != root
|
||||
or path.is_symlink()
|
||||
or path.stat().st_size != raw.get("byte_length")
|
||||
or _sha256(path) != raw.get("sha256")
|
||||
):
|
||||
raise E46AAiEngineeringPreannotationError("E46A artifact changed")
|
||||
return path
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value, ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(value, dict):
|
||||
raise E46AAiEngineeringPreannotationError("E46A JSON object expected")
|
||||
return value
|
||||
|
||||
|
||||
def _read_jsonl(path: Path) -> list[dict[str, Any]]:
|
||||
values = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line]
|
||||
if any(not isinstance(value, dict) for value in values):
|
||||
raise E46AAiEngineeringPreannotationError("E46A JSONL object expected")
|
||||
return values
|
||||
|
||||
|
||||
def _write_json(path: Path, value: object) -> None:
|
||||
path.write_bytes(_canonical_json(value) + b"\n")
|
||||
|
||||
|
||||
def _write_jsonl(path: Path, values: list[dict[str, Any]]) -> None:
|
||||
path.write_bytes(b"".join(_canonical_json(value) + b"\n" for value in values))
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def _utc_now() -> str:
|
||||
return datetime.now(UTC).isoformat().replace("+00:00", "Z")
|
||||
@@ -0,0 +1,526 @@
|
||||
"""Freeze source-scoped temporal tracks and conservative motion state for E46A."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import uuid
|
||||
from collections import Counter, defaultdict
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
from k1link.compute.e46a_ai_engineering_preannotation import (
|
||||
E46A_MANIFEST_NAME,
|
||||
read_e46a_ai_engineering_preannotation,
|
||||
)
|
||||
|
||||
E46B_RESULT_SCHEMA: Final = "missioncore.e46b-temporal-motion/v1"
|
||||
E46B_REPORT_SCHEMA: Final = "missioncore.e46b-temporal-motion-report/v1"
|
||||
E46B_CASE_SCHEMA: Final = "missioncore.e46b-temporal-motion-case/v1"
|
||||
E46B_MANIFEST_NAME: Final = "manifest.json"
|
||||
E46B_REPORT_NAME: Final = "temporal-motion-report.json"
|
||||
E46B_CASES_NAME: Final = "temporal-motion-cases.jsonl"
|
||||
|
||||
_RESULT_ID = re.compile(r"^e46b-temporal-motion-[a-f0-9]{64}$")
|
||||
_TEMPORAL_GROUPS: Final = (
|
||||
"clip-stroller-person",
|
||||
"clip-close-car",
|
||||
"clip-vehicle-occlusion",
|
||||
"clip-near-structure",
|
||||
)
|
||||
_EXPECTED_COUNTS: Final = {
|
||||
"clip-stroller-person": {"person": 2, "stroller": 1, "car": 6},
|
||||
"clip-close-car": {"car": 9, "heavy_vehicle": 1},
|
||||
"clip-vehicle-occlusion": {"car": 6, "heavy_vehicle": 1},
|
||||
"clip-near-structure": {"car": 7, "laptop": 1},
|
||||
}
|
||||
_AUTHORITY: Final = {
|
||||
"ground_truth": False,
|
||||
"independent_truth": False,
|
||||
"metric_grade_reference": False,
|
||||
"candidate_accepted": False,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
|
||||
class E46BTemporalMotionError(ValueError):
|
||||
"""Raised when an E46B input binding or immutable output is invalid."""
|
||||
|
||||
|
||||
def build_e46b_temporal_motion(
|
||||
*,
|
||||
e46a_root: Path,
|
||||
e26_root: Path,
|
||||
output_root: Path,
|
||||
) -> dict[str, Any]:
|
||||
"""Build 16 source-scoped temporal frames with stable IDs and motion evidence."""
|
||||
|
||||
e46a = read_e46a_ai_engineering_preannotation(e46a_root)
|
||||
source_cases = [
|
||||
copy.deepcopy(case)
|
||||
for case in e46a["cases"]
|
||||
if case.get("group_id") in _TEMPORAL_GROUPS
|
||||
]
|
||||
source_cases.sort(key=lambda case: int(case["truth_island_sequence"]))
|
||||
_validate_source_cases(source_cases)
|
||||
target_frames = {int(case["frame_index"]) for case in source_cases}
|
||||
e26 = _read_e26_motion_source(e26_root, target_frames)
|
||||
|
||||
track_observations: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
||||
for group_id in _TEMPORAL_GROUPS:
|
||||
group_cases = [case for case in source_cases if case["group_id"] == group_id]
|
||||
for case in group_cases:
|
||||
by_category: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
||||
for item in case["objects"]:
|
||||
by_category[str(item["category"])].append(item)
|
||||
for category, objects in by_category.items():
|
||||
objects.sort(key=lambda item: _center(item["box_xyxy"])[0])
|
||||
for rank, item in enumerate(objects, start=1):
|
||||
track_id = f"{group_id}/{category}-{rank:02d}"
|
||||
item["track_id"] = track_id
|
||||
item["track_index"] = rank
|
||||
track_observations[track_id].append(
|
||||
{
|
||||
"sequence": int(case["truth_island_sequence"]),
|
||||
"frame_index": int(case["frame_index"]),
|
||||
"center_xy": list(_center(item["box_xyxy"])),
|
||||
"object": item,
|
||||
}
|
||||
)
|
||||
|
||||
matched_total = 0
|
||||
for case in source_cases:
|
||||
motion_objects = e26["frames"][int(case["frame_index"])]
|
||||
associations = _associate(case["objects"], motion_objects)
|
||||
for item in case["objects"]:
|
||||
match = associations.get(str(item["object_id"]))
|
||||
if match is None:
|
||||
item["motion_observation"] = None
|
||||
continue
|
||||
matched_total += 1
|
||||
item["motion_observation"] = {
|
||||
"source_track_id": match.get("source_track_id"),
|
||||
"track_id": match.get("track_id"),
|
||||
"motion_state": match.get("motion_state"),
|
||||
"motion_confidence": match.get("motion_confidence"),
|
||||
"motion_status": match.get("motion_status"),
|
||||
"camera_motion_state": match.get("camera_motion_state"),
|
||||
"lidar_motion_state": match.get("lidar_motion_state"),
|
||||
"bbox_xyxy": copy.deepcopy(match.get("bbox_xyxy")),
|
||||
}
|
||||
|
||||
track_summaries: dict[str, dict[str, Any]] = {}
|
||||
for track_id, observations in track_observations.items():
|
||||
decisive = [
|
||||
item["object"]["motion_observation"]
|
||||
for item in observations
|
||||
if isinstance(item["object"].get("motion_observation"), dict)
|
||||
and item["object"]["motion_observation"].get("motion_state")
|
||||
in {"static", "dynamic"}
|
||||
]
|
||||
states = {str(item["motion_state"]) for item in decisive}
|
||||
state = next(iter(states)) if len(states) == 1 else "unknown"
|
||||
confidence = (
|
||||
round(sum(float(item["motion_confidence"]) for item in decisive) / len(decisive), 6)
|
||||
if state != "unknown" and decisive
|
||||
else 0.0
|
||||
)
|
||||
track_summaries[track_id] = {
|
||||
"track_id": track_id,
|
||||
"category": str(observations[0]["object"]["category"]),
|
||||
"motion_state": state,
|
||||
"motion_confidence": confidence,
|
||||
"motion_evidence_observation_count": len(decisive),
|
||||
"observation_count": len(observations),
|
||||
"source_track_ids": sorted(
|
||||
{
|
||||
int(item["source_track_id"])
|
||||
for item in decisive
|
||||
if isinstance(item.get("source_track_id"), int)
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
cases: list[dict[str, Any]] = []
|
||||
for source in source_cases:
|
||||
sequence = int(source["truth_island_sequence"])
|
||||
objects: list[dict[str, Any]] = []
|
||||
for raw in source["objects"]:
|
||||
item = copy.deepcopy(raw)
|
||||
summary = track_summaries[str(item["track_id"])]
|
||||
history = [
|
||||
observation["center_xy"]
|
||||
for observation in track_observations[str(item["track_id"])]
|
||||
if int(observation["sequence"]) <= sequence
|
||||
]
|
||||
item.update(
|
||||
{
|
||||
"motion_state": summary["motion_state"],
|
||||
"motion_confidence": summary["motion_confidence"],
|
||||
"motion_evidence_observation_count": summary[
|
||||
"motion_evidence_observation_count"
|
||||
],
|
||||
"trail_centers_xy": history,
|
||||
}
|
||||
)
|
||||
objects.append(item)
|
||||
frame_counts = Counter(str(item["motion_state"]) for item in objects)
|
||||
cases.append(
|
||||
{
|
||||
"schema_version": E46B_CASE_SCHEMA,
|
||||
"truth_island_sequence": sequence,
|
||||
"image_id": int(source["image_id"]),
|
||||
"frame_index": int(source["frame_index"]),
|
||||
"group_id": str(source["group_id"]),
|
||||
"session_seconds": float(source["session_seconds"]),
|
||||
"source_image_sha256": str(source["source_image_sha256"]),
|
||||
"objects": objects,
|
||||
"object_count": len(objects),
|
||||
"track_count": len(objects),
|
||||
"motion_counts": {
|
||||
key: int(frame_counts.get(key, 0))
|
||||
for key in ("dynamic", "static", "unknown")
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
track_counts = Counter(
|
||||
str(summary["motion_state"]) for summary in track_summaries.values()
|
||||
)
|
||||
metrics = {
|
||||
"frame_count": len(cases),
|
||||
"temporal_group_count": len(_TEMPORAL_GROUPS),
|
||||
"object_observation_count": sum(len(case["objects"]) for case in cases),
|
||||
"track_count": len(track_summaries),
|
||||
"matched_motion_observation_count": matched_total,
|
||||
"unmatched_motion_observation_count": 136 - matched_total,
|
||||
"dynamic_track_count": int(track_counts.get("dynamic", 0)),
|
||||
"static_track_count": int(track_counts.get("static", 0)),
|
||||
"unknown_track_count": int(track_counts.get("unknown", 0)),
|
||||
"visually_reviewed_frame_count": 16,
|
||||
}
|
||||
if metrics["object_observation_count"] != 136 or metrics["track_count"] != 34:
|
||||
raise E46BTemporalMotionError("E46B source-scoped accounting changed")
|
||||
|
||||
report_basis = {
|
||||
"schema_version": E46B_REPORT_SCHEMA,
|
||||
"status": "completed-source-scoped-temporal-motion-engineering-evidence",
|
||||
"metrics": metrics,
|
||||
"tracks": [track_summaries[key] for key in sorted(track_summaries)],
|
||||
"decision": {
|
||||
"stable_ids_available": True,
|
||||
"motion_state_available": True,
|
||||
"metric_velocity_available": False,
|
||||
"next_action": (
|
||||
"use the 34 tracks as the recorded RIGHT-camera temporal object "
|
||||
"layer; keep unknown where E26 has no decisive evidence"
|
||||
),
|
||||
},
|
||||
"limitations": [
|
||||
(
|
||||
"track IDs are source-scoped to four fixed four-frame clips and "
|
||||
"are not route-global identities"
|
||||
),
|
||||
(
|
||||
"motion state is inherited from accepted E26 KB4 ego-motion/LiDAR "
|
||||
"engineering evidence; unmatched or conflicting evidence remains unknown"
|
||||
),
|
||||
(
|
||||
"camera-only motion has no metric velocity and no AP, live, command, "
|
||||
"navigation, or safety claim is made"
|
||||
),
|
||||
(
|
||||
"E46A is candidate-visible assisted engineering material and is not "
|
||||
"independent ground truth"
|
||||
),
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
"ground_truth": False,
|
||||
}
|
||||
identity = {
|
||||
"schema_version": E46B_RESULT_SCHEMA,
|
||||
"e46a_source": {
|
||||
"result_id": e46a["result_id"],
|
||||
"manifest_sha256": _sha256(e46a["result_root"] / E46A_MANIFEST_NAME),
|
||||
},
|
||||
"e46_source": copy.deepcopy(e46a["manifest"]["identity"]["e46_source"]),
|
||||
"e26_motion_source": e26["identity"],
|
||||
"association_profile": {
|
||||
"profile_id": "e46b-source-scoped-category-x-rank-plus-e26-bbox/v1",
|
||||
"track_scope": "four-frame-group",
|
||||
"motion_conflict_state": "unknown",
|
||||
"maximum_match_cost": 1.4,
|
||||
},
|
||||
"report_sha256": hashlib.sha256(_canonical_json(report_basis)).hexdigest(),
|
||||
"cases_sha256": hashlib.sha256(_canonical_json(cases)).hexdigest(),
|
||||
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
result_id = f"e46b-temporal-motion-{identity_sha256}"
|
||||
destination = output_root.expanduser().absolute() / result_id
|
||||
if destination.exists():
|
||||
return read_e46b_temporal_motion(destination)
|
||||
created_at_utc = datetime.now(UTC).isoformat().replace("+00:00", "Z")
|
||||
report = {
|
||||
**report_basis,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"created_at_utc": created_at_utc,
|
||||
"source_session_id": "RAVNOVES00",
|
||||
"camera_source_id": "sensor.camera.right",
|
||||
}
|
||||
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
_write_json(staging / E46B_REPORT_NAME, report)
|
||||
_write_jsonl(staging / E46B_CASES_NAME, cases)
|
||||
_write_json(
|
||||
staging / E46B_MANIFEST_NAME,
|
||||
{
|
||||
"schema_version": E46B_RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": created_at_utc,
|
||||
"acceptance_state": "source-scoped-temporal-motion-engineering-evidence",
|
||||
"ground_truth": False,
|
||||
"artifacts": [
|
||||
_artifact(staging / E46B_REPORT_NAME, "temporal-motion-report"),
|
||||
_artifact(staging / E46B_CASES_NAME, "temporal-motion-cases"),
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
},
|
||||
)
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
return read_e46b_temporal_motion(destination)
|
||||
|
||||
|
||||
def read_e46b_temporal_motion(root: Path) -> dict[str, Any]:
|
||||
resolved = root.resolve(strict=True)
|
||||
manifest = _read_json(resolved / E46B_MANIFEST_NAME)
|
||||
identity = manifest.get("identity")
|
||||
if not isinstance(identity, dict):
|
||||
raise E46BTemporalMotionError("E46B identity is invalid")
|
||||
digest = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
if (
|
||||
manifest.get("schema_version") != E46B_RESULT_SCHEMA
|
||||
or manifest.get("identity_sha256") != digest
|
||||
or manifest.get("result_id") != f"e46b-temporal-motion-{digest}"
|
||||
or resolved.name != manifest.get("result_id")
|
||||
or _RESULT_ID.fullmatch(resolved.name) is None
|
||||
or manifest.get("authority") != _AUTHORITY
|
||||
or manifest.get("ground_truth") is not False
|
||||
):
|
||||
raise E46BTemporalMotionError("E46B identity is invalid")
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(artifacts, list) or len(artifacts) != 2:
|
||||
raise E46BTemporalMotionError("E46B artifacts are invalid")
|
||||
by_role = {item.get("role"): item for item in artifacts if isinstance(item, dict)}
|
||||
report = _read_json(_validated_artifact(resolved, by_role.get("temporal-motion-report")))
|
||||
cases = tuple(_read_jsonl(_validated_artifact(resolved, by_role.get("temporal-motion-cases"))))
|
||||
if (
|
||||
report.get("schema_version") != E46B_REPORT_SCHEMA
|
||||
or report.get("result_id") != resolved.name
|
||||
or report.get("authority") != _AUTHORITY
|
||||
or report.get("ground_truth") is not False
|
||||
or len(cases) != 16
|
||||
or any(case.get("schema_version") != E46B_CASE_SCHEMA for case in cases)
|
||||
or hashlib.sha256(_canonical_json(cases)).hexdigest() != identity.get("cases_sha256")
|
||||
):
|
||||
raise E46BTemporalMotionError("E46B result changed")
|
||||
return {
|
||||
"result_id": resolved.name,
|
||||
"result_root": resolved,
|
||||
"manifest": manifest,
|
||||
"report": report,
|
||||
"cases": cases,
|
||||
}
|
||||
|
||||
|
||||
def _validate_source_cases(cases: list[dict[str, Any]]) -> None:
|
||||
if len(cases) != 16:
|
||||
raise E46BTemporalMotionError("E46B requires exactly 16 temporal frames")
|
||||
for group_id in _TEMPORAL_GROUPS:
|
||||
selected = [case for case in cases if case.get("group_id") == group_id]
|
||||
if len(selected) != 4:
|
||||
raise E46BTemporalMotionError(f"E46B group {group_id} changed")
|
||||
for case in selected:
|
||||
counts = Counter(str(item.get("category")) for item in case.get("objects", []))
|
||||
if dict(counts) != _EXPECTED_COUNTS[group_id]:
|
||||
raise E46BTemporalMotionError(f"E46B object accounting changed in {group_id}")
|
||||
|
||||
|
||||
def _read_e26_motion_source(root: Path, target_frames: set[int]) -> dict[str, Any]:
|
||||
resolved = root.resolve(strict=True)
|
||||
result = _read_json(resolved / "result.json")
|
||||
identity = result.get("identity")
|
||||
if (
|
||||
result.get("schema_version") != "missioncore.e10-integrated-perception-result/v1"
|
||||
or not isinstance(identity, dict)
|
||||
or identity.get("source_id") != "sensor.camera.right"
|
||||
or identity.get("configuration", {}).get("pipeline")
|
||||
!= "kb4-multiview-static-hypothesis-lidar-fusion/v1"
|
||||
or result.get("acceptance_state") != "accepted"
|
||||
):
|
||||
raise E46BTemporalMotionError("E26 motion source contract is invalid")
|
||||
artifact = next(
|
||||
(
|
||||
item
|
||||
for item in result.get("artifacts", [])
|
||||
if isinstance(item, dict) and item.get("path") == "fusion-frames.jsonl"
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not isinstance(artifact, dict):
|
||||
raise E46BTemporalMotionError("E26 fusion artifact is missing")
|
||||
path = _validated_artifact(resolved, artifact)
|
||||
frames: dict[int, list[dict[str, Any]]] = {}
|
||||
for row in _read_jsonl(path):
|
||||
frame_index = row.get("source_frame_index")
|
||||
if frame_index in target_frames:
|
||||
objects = row.get("objects")
|
||||
if not isinstance(objects, list):
|
||||
raise E46BTemporalMotionError("E26 fusion objects are invalid")
|
||||
frames[int(frame_index)] = objects
|
||||
if set(frames) != target_frames:
|
||||
raise E46BTemporalMotionError("E26 temporal coverage is incomplete")
|
||||
return {
|
||||
"frames": frames,
|
||||
"identity": {
|
||||
"result_id": result.get("result_id"),
|
||||
"result_sha256": _sha256(resolved / "result.json"),
|
||||
"fusion_frames_sha256": artifact.get("sha256"),
|
||||
"pipeline": identity["configuration"]["pipeline"],
|
||||
"profile_sha256": identity["configuration"].get("profile_sha256"),
|
||||
"implementation_sha256": identity["configuration"].get("implementation_sha256"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _associate(
|
||||
source: list[dict[str, Any]],
|
||||
candidates: list[dict[str, Any]],
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
pairs: list[tuple[float, str, int]] = []
|
||||
for item in source:
|
||||
group = (
|
||||
"vehicle"
|
||||
if item.get("category") in {"car", "heavy_vehicle"}
|
||||
else item.get("category")
|
||||
)
|
||||
for index, candidate in enumerate(candidates):
|
||||
if candidate.get("association_group") != group:
|
||||
continue
|
||||
box = candidate.get("bbox_xyxy")
|
||||
if not isinstance(box, list) or len(box) != 4:
|
||||
continue
|
||||
source_box = item["box_xyxy"]
|
||||
sx, sy = _center(source_box)
|
||||
cx, cy = _center(box)
|
||||
distance = math.hypot(sx - cx, sy - cy) / 1000.0
|
||||
area_ratio = abs(math.log(max(_area(source_box), 1.0) / max(_area(box), 1.0)))
|
||||
cost = distance + 0.25 * area_ratio + (1.0 - _iou(source_box, box))
|
||||
if cost <= 1.4:
|
||||
pairs.append((cost, str(item["object_id"]), index))
|
||||
output: dict[str, dict[str, Any]] = {}
|
||||
used: set[int] = set()
|
||||
for _, object_id, index in sorted(pairs):
|
||||
if object_id in output or index in used:
|
||||
continue
|
||||
output[object_id] = candidates[index]
|
||||
used.add(index)
|
||||
return output
|
||||
|
||||
|
||||
def _center(box: list[float]) -> tuple[float, float]:
|
||||
return ((float(box[0]) + float(box[2])) / 2.0, (float(box[1]) + float(box[3])) / 2.0)
|
||||
|
||||
|
||||
def _area(box: list[float]) -> float:
|
||||
return max(0.0, float(box[2]) - float(box[0])) * max(0.0, float(box[3]) - float(box[1]))
|
||||
|
||||
|
||||
def _iou(left: list[float], right: list[float]) -> float:
|
||||
x1, y1 = max(left[0], right[0]), max(left[1], right[1])
|
||||
x2, y2 = min(left[2], right[2]), min(left[3], right[3])
|
||||
intersection = max(0.0, x2 - x1) * max(0.0, y2 - y1)
|
||||
union = _area(left) + _area(right) - intersection
|
||||
return intersection / union if union > 0.0 else 0.0
|
||||
|
||||
|
||||
def _artifact(path: Path, role: str) -> dict[str, object]:
|
||||
return {
|
||||
"role": role,
|
||||
"path": path.name,
|
||||
"size_bytes": path.stat().st_size,
|
||||
"sha256": _sha256(path),
|
||||
}
|
||||
|
||||
|
||||
def _validated_artifact(root: Path, raw: object) -> Path:
|
||||
if not isinstance(raw, dict) or not isinstance(raw.get("path"), str):
|
||||
raise E46BTemporalMotionError("artifact metadata is invalid")
|
||||
path = (root / raw["path"]).resolve(strict=True)
|
||||
if path.parent != root or path.is_symlink() or not path.is_file():
|
||||
raise E46BTemporalMotionError("artifact path is invalid")
|
||||
expected_size = raw.get("size_bytes", raw.get("byte_length"))
|
||||
if path.stat().st_size != expected_size or _sha256(path) != raw.get("sha256"):
|
||||
raise E46BTemporalMotionError("artifact changed")
|
||||
return path
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode()
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(value, dict):
|
||||
raise E46BTemporalMotionError("JSON document is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _read_jsonl(path: Path):
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
for line in handle:
|
||||
if line.strip():
|
||||
value = json.loads(line)
|
||||
if not isinstance(value, dict):
|
||||
raise E46BTemporalMotionError("JSONL row is invalid")
|
||||
yield value
|
||||
|
||||
|
||||
def _write_json(path: Path, value: object) -> None:
|
||||
path.write_bytes(_canonical_json(value) + b"\n")
|
||||
|
||||
|
||||
def _write_jsonl(path: Path, values: list[dict[str, Any]]) -> None:
|
||||
with path.open("wb") as handle:
|
||||
for value in values:
|
||||
handle.write(_canonical_json(value) + b"\n")
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for block in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
@@ -0,0 +1,571 @@
|
||||
"""Freeze the full recorded RIGHT route-track and world-state qualification."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import uuid
|
||||
from collections import Counter
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
from k1link.compute.e46a_ai_engineering_preannotation import (
|
||||
E46A_MANIFEST_NAME,
|
||||
read_e46a_ai_engineering_preannotation,
|
||||
)
|
||||
|
||||
E46C_RESULT_SCHEMA: Final = "missioncore.e46c-full-replay-world-tracks/v1"
|
||||
E46C_REPORT_SCHEMA: Final = "missioncore.e46c-full-replay-world-tracks-report/v1"
|
||||
E46C_CASE_SCHEMA: Final = "missioncore.e46c-full-replay-world-track-case/v1"
|
||||
E46C_MANIFEST_NAME: Final = "manifest.json"
|
||||
E46C_REPORT_NAME: Final = "full-replay-world-track-report.json"
|
||||
E46C_CASES_NAME: Final = "full-replay-world-track-cases.jsonl"
|
||||
|
||||
_RESULT_ID = re.compile(r"^e46c-full-replay-world-tracks-[a-f0-9]{64}$")
|
||||
_AUTHORITY: Final = {
|
||||
"ground_truth": False,
|
||||
"independent_truth": False,
|
||||
"metric_grade_reference": False,
|
||||
"candidate_accepted": False,
|
||||
"free_space_authority": False,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
|
||||
class E46CFullReplayWorldTracksError(ValueError):
|
||||
"""Raised when an E46C source or immutable result is invalid."""
|
||||
|
||||
|
||||
def build_e46c_full_replay_world_tracks(
|
||||
*, e46a_root: Path, e26_root: Path, output_root: Path
|
||||
) -> dict[str, Any]:
|
||||
e46a = read_e46a_ai_engineering_preannotation(e46a_root)
|
||||
source_cases = sorted(
|
||||
(copy.deepcopy(case) for case in e46a["cases"]),
|
||||
key=lambda case: int(case["truth_island_sequence"]),
|
||||
)
|
||||
if len(source_cases) != 32:
|
||||
raise E46CFullReplayWorldTracksError("E46C requires all 32 E46A frames")
|
||||
target_frames = {int(case["frame_index"]) for case in source_cases}
|
||||
e26 = _read_e26(e26_root, target_frames)
|
||||
|
||||
cases: list[dict[str, Any]] = []
|
||||
matched_objects = 0
|
||||
sample_world_frames = 0
|
||||
for source in source_cases:
|
||||
frame_index = int(source["frame_index"])
|
||||
fusion = e26["sample_fusion"][frame_index]
|
||||
world = e26["sample_world"][frame_index]
|
||||
associations = _associate(source["objects"], fusion["objects"])
|
||||
objects: list[dict[str, Any]] = []
|
||||
for raw in source["objects"]:
|
||||
item = copy.deepcopy(raw)
|
||||
match = associations.get(str(item["object_id"]))
|
||||
if match is None:
|
||||
item.update(
|
||||
{
|
||||
"route_track_id": None,
|
||||
"world_track_id": None,
|
||||
"motion_state": "unknown",
|
||||
"motion_confidence": 0.0,
|
||||
"world_binding_state": "unmatched",
|
||||
}
|
||||
)
|
||||
else:
|
||||
matched_objects += 1
|
||||
source_track_id = match.get("source_track_id")
|
||||
world_track_id = match.get("track_id")
|
||||
world_bound = (
|
||||
isinstance(source_track_id, int)
|
||||
and isinstance(world_track_id, int)
|
||||
and world_track_id != source_track_id
|
||||
)
|
||||
item.update(
|
||||
{
|
||||
"route_track_id": source_track_id,
|
||||
"world_track_id": world_track_id if world_bound else None,
|
||||
"motion_state": match.get("motion_state", "unknown"),
|
||||
"motion_confidence": float(
|
||||
match.get("motion_confidence") or 0.0
|
||||
),
|
||||
"world_binding_state": (
|
||||
"world-track-bound" if world_bound else "camera-track-only"
|
||||
),
|
||||
}
|
||||
)
|
||||
objects.append(item)
|
||||
world_objects = [_world_projection(item) for item in world["objects"]]
|
||||
if world_objects:
|
||||
sample_world_frames += 1
|
||||
cases.append(
|
||||
{
|
||||
"schema_version": E46C_CASE_SCHEMA,
|
||||
"truth_island_sequence": int(source["truth_island_sequence"]),
|
||||
"image_id": int(source["image_id"]),
|
||||
"frame_index": frame_index,
|
||||
"group_id": str(source["group_id"]),
|
||||
"session_seconds": float(source["session_seconds"]),
|
||||
"source_image_sha256": str(source["source_image_sha256"]),
|
||||
"fusion_state": str(fusion["fusion_state"]),
|
||||
"objects": objects,
|
||||
"object_count": len(objects),
|
||||
"matched_route_object_count": sum(
|
||||
item["route_track_id"] is not None for item in objects
|
||||
),
|
||||
"world_objects": world_objects,
|
||||
"world_object_count": len(world_objects),
|
||||
}
|
||||
)
|
||||
|
||||
route = e26["route_metrics"]
|
||||
metrics = {
|
||||
**route,
|
||||
"visual_sample_frame_count": 32,
|
||||
"object_audited_sample_frame_count": 32,
|
||||
"sample_object_count": sum(len(case["objects"]) for case in cases),
|
||||
"sample_matched_route_object_count": matched_objects,
|
||||
"sample_unmatched_object_count": (
|
||||
sum(len(case["objects"]) for case in cases) - matched_objects
|
||||
),
|
||||
"sample_world_frame_count": sample_world_frames,
|
||||
}
|
||||
report_basis = {
|
||||
"schema_version": E46C_REPORT_SCHEMA,
|
||||
"status": "completed-full-recorded-right-route-world-track-qualification",
|
||||
"metrics": metrics,
|
||||
"acceptance": {
|
||||
"e26_diagnostic_accepted": True,
|
||||
"benchmark_passed_events": e26["benchmark_passed_events"],
|
||||
"benchmark_total_events": e26["benchmark_total_events"],
|
||||
"route_accounting_complete": True,
|
||||
"visual_sample_available": True,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
"decision": {
|
||||
"route_track_layer_available": True,
|
||||
"world_occupied_layer_available": True,
|
||||
"unknown_remains_occupied": True,
|
||||
"free_space_available": False,
|
||||
"next_action": (
|
||||
"use E46C as the recorded full-route diagnostic object/world layer; "
|
||||
"qualify identity continuity and world-binding exceptions before any "
|
||||
"live or planner-facing promotion"
|
||||
),
|
||||
},
|
||||
"limitations": [
|
||||
(
|
||||
"route-track IDs are detector-tracker identities with a 2.5 second idle "
|
||||
"bound, not permanent physical identities"
|
||||
),
|
||||
(
|
||||
"world-state is available only where pose/LiDAR support passes the E26 "
|
||||
"sync and evidence gates; missing evidence remains unknown occupied"
|
||||
),
|
||||
(
|
||||
"the 32 exact E46A frames are a visual audit sample; they do not make all "
|
||||
"4489 frames independently human reviewed"
|
||||
),
|
||||
(
|
||||
"E26 is accepted diagnostic engineering evidence, not independent truth, "
|
||||
"free space, commands, navigation, or safety authority"
|
||||
),
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
"ground_truth": False,
|
||||
}
|
||||
identity = {
|
||||
"schema_version": E46C_RESULT_SCHEMA,
|
||||
"e46a_visual_sample": {
|
||||
"result_id": e46a["result_id"],
|
||||
"manifest_sha256": _sha256(e46a["result_root"] / E46A_MANIFEST_NAME),
|
||||
},
|
||||
"e46_source": copy.deepcopy(e46a["manifest"]["identity"]["e46_source"]),
|
||||
"e26_full_replay": e26["identity"],
|
||||
"projection_profile": {
|
||||
"profile_id": "e46c-full-route-plus-e46a-visual-sample/v1",
|
||||
"camera_object_binding": "same-frame-group-bbox-one-to-one/v1",
|
||||
"world_binding": "e26-source-track-to-world-track/v1",
|
||||
"unknown_policy": "occupied-no-free-space-claim",
|
||||
},
|
||||
"report_sha256": hashlib.sha256(_canonical_json(report_basis)).hexdigest(),
|
||||
"cases_sha256": hashlib.sha256(_canonical_json(cases)).hexdigest(),
|
||||
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
digest = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
result_id = f"e46c-full-replay-world-tracks-{digest}"
|
||||
destination = output_root.expanduser().absolute() / result_id
|
||||
if destination.exists():
|
||||
return read_e46c_full_replay_world_tracks(destination)
|
||||
created_at_utc = datetime.now(UTC).isoformat().replace("+00:00", "Z")
|
||||
report = {
|
||||
**report_basis,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": digest,
|
||||
"created_at_utc": created_at_utc,
|
||||
"source_session_id": "RAVNOVES00",
|
||||
"camera_source_id": "sensor.camera.right",
|
||||
}
|
||||
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
_write_json(staging / E46C_REPORT_NAME, report)
|
||||
_write_jsonl(staging / E46C_CASES_NAME, cases)
|
||||
_write_json(
|
||||
staging / E46C_MANIFEST_NAME,
|
||||
{
|
||||
"schema_version": E46C_RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": digest,
|
||||
"identity": identity,
|
||||
"created_at_utc": created_at_utc,
|
||||
"acceptance_state": "full-recorded-route-diagnostic-world-tracks",
|
||||
"ground_truth": False,
|
||||
"artifacts": [
|
||||
_artifact(staging / E46C_REPORT_NAME, "world-track-report"),
|
||||
_artifact(staging / E46C_CASES_NAME, "world-track-cases"),
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
},
|
||||
)
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
return read_e46c_full_replay_world_tracks(destination)
|
||||
|
||||
|
||||
def read_e46c_full_replay_world_tracks(root: Path) -> dict[str, Any]:
|
||||
resolved = root.resolve(strict=True)
|
||||
manifest = _read_json(resolved / E46C_MANIFEST_NAME)
|
||||
identity = manifest.get("identity")
|
||||
if not isinstance(identity, dict):
|
||||
raise E46CFullReplayWorldTracksError("E46C identity is invalid")
|
||||
digest = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
if (
|
||||
manifest.get("schema_version") != E46C_RESULT_SCHEMA
|
||||
or manifest.get("identity_sha256") != digest
|
||||
or manifest.get("result_id") != f"e46c-full-replay-world-tracks-{digest}"
|
||||
or manifest.get("result_id") != resolved.name
|
||||
or _RESULT_ID.fullmatch(resolved.name) is None
|
||||
or manifest.get("authority") != _AUTHORITY
|
||||
or manifest.get("ground_truth") is not False
|
||||
):
|
||||
raise E46CFullReplayWorldTracksError("E46C identity is invalid")
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(artifacts, list) or len(artifacts) != 2:
|
||||
raise E46CFullReplayWorldTracksError("E46C artifacts are invalid")
|
||||
by_role = {item.get("role"): item for item in artifacts if isinstance(item, dict)}
|
||||
report = _read_json(_validated_artifact(resolved, by_role.get("world-track-report")))
|
||||
cases = tuple(
|
||||
_read_jsonl(_validated_artifact(resolved, by_role.get("world-track-cases")))
|
||||
)
|
||||
if (
|
||||
report.get("schema_version") != E46C_REPORT_SCHEMA
|
||||
or report.get("result_id") != resolved.name
|
||||
or report.get("authority") != _AUTHORITY
|
||||
or report.get("ground_truth") is not False
|
||||
or len(cases) != 32
|
||||
or any(case.get("schema_version") != E46C_CASE_SCHEMA for case in cases)
|
||||
or hashlib.sha256(_canonical_json(cases)).hexdigest()
|
||||
!= identity.get("cases_sha256")
|
||||
):
|
||||
raise E46CFullReplayWorldTracksError("E46C result changed")
|
||||
return {
|
||||
"result_id": resolved.name,
|
||||
"result_root": resolved,
|
||||
"manifest": manifest,
|
||||
"report": report,
|
||||
"cases": cases,
|
||||
}
|
||||
|
||||
|
||||
def _read_e26(root: Path, target_frames: set[int]) -> dict[str, Any]:
|
||||
resolved = root.resolve(strict=True)
|
||||
result_path = resolved / "result.json"
|
||||
result = _read_json(result_path)
|
||||
identity = result.get("identity")
|
||||
if (
|
||||
result.get("schema_version") != "missioncore.e10-integrated-perception-result/v1"
|
||||
or result.get("acceptance_state") != "accepted"
|
||||
or not isinstance(identity, dict)
|
||||
or identity.get("source_id") != "sensor.camera.right"
|
||||
or identity.get("selection", {}).get("frame_count") != 4489
|
||||
or identity.get("configuration", {}).get("pipeline")
|
||||
!= "kb4-multiview-static-hypothesis-lidar-fusion/v1"
|
||||
):
|
||||
raise E46CFullReplayWorldTracksError("E26 full replay contract is invalid")
|
||||
artifacts = {
|
||||
item.get("path"): item
|
||||
for item in result.get("artifacts", [])
|
||||
if isinstance(item, dict)
|
||||
}
|
||||
fusion_path = _validated_artifact(resolved, artifacts.get("fusion-frames.jsonl"))
|
||||
world_path = _validated_artifact(resolved, artifacts.get("world-state.jsonl"))
|
||||
report_path = _validated_artifact(resolved, artifacts.get("run-report.json"))
|
||||
run_report = _read_json(report_path)
|
||||
acceptance = run_report.get("acceptance")
|
||||
benchmark = run_report.get("metrics", {}).get("benchmark")
|
||||
if (
|
||||
not isinstance(acceptance, dict)
|
||||
or acceptance.get("accepted") is not True
|
||||
or not isinstance(benchmark, dict)
|
||||
or benchmark.get("passed") is not True
|
||||
):
|
||||
raise E46CFullReplayWorldTracksError("E26 diagnostic acceptance is invalid")
|
||||
|
||||
sample_fusion: dict[int, dict[str, Any]] = {}
|
||||
source_tracks: set[int] = set()
|
||||
world_tracks: set[int] = set()
|
||||
source_world_bound: set[int] = set()
|
||||
fusion_states: Counter[str] = Counter()
|
||||
motion_states: Counter[str] = Counter()
|
||||
labels: Counter[str] = Counter()
|
||||
fusion_observations = 0
|
||||
fusion_frames = 0
|
||||
for row in _read_jsonl(fusion_path):
|
||||
frame_index = int(row["source_frame_index"])
|
||||
fusion_frames += 1
|
||||
fusion_states[str(row["fusion_state"])] += 1
|
||||
raw_objects = row.get("objects")
|
||||
if not isinstance(raw_objects, list):
|
||||
raise E46CFullReplayWorldTracksError("E26 fusion objects are invalid")
|
||||
for item in raw_objects:
|
||||
fusion_observations += 1
|
||||
source_id, track_id = item.get("source_track_id"), item.get("track_id")
|
||||
if isinstance(source_id, int):
|
||||
source_tracks.add(source_id)
|
||||
if (
|
||||
isinstance(source_id, int)
|
||||
and isinstance(track_id, int)
|
||||
and track_id != source_id
|
||||
):
|
||||
source_world_bound.add(source_id)
|
||||
world_tracks.add(track_id)
|
||||
motion_states[str(item.get("motion_state", "unknown"))] += 1
|
||||
labels[str(item.get("label", "unknown"))] += 1
|
||||
if frame_index in target_frames:
|
||||
sample_fusion[frame_index] = row
|
||||
|
||||
sample_world: dict[int, dict[str, Any]] = {}
|
||||
world_frames_with_objects = 0
|
||||
world_observations = 0
|
||||
world_current = 0
|
||||
world_held = 0
|
||||
world_motion: Counter[str] = Counter()
|
||||
occupancy_cells = 0
|
||||
world_frames = 0
|
||||
unique_world_state_tracks: set[int] = set()
|
||||
for row in _read_jsonl(world_path):
|
||||
frame_index = int(row["source_frame_index"])
|
||||
world_frames += 1
|
||||
raw_objects = row.get("objects")
|
||||
if not isinstance(raw_objects, list):
|
||||
raise E46CFullReplayWorldTracksError("E26 world objects are invalid")
|
||||
if raw_objects:
|
||||
world_frames_with_objects += 1
|
||||
for item in raw_objects:
|
||||
world_observations += 1
|
||||
if isinstance(item.get("track_id"), int):
|
||||
unique_world_state_tracks.add(int(item["track_id"]))
|
||||
if item.get("occupancy_evidence_current") is True:
|
||||
world_current += 1
|
||||
else:
|
||||
world_held += 1
|
||||
occupancy_cells += int(item.get("occupancy_cell_count") or 0)
|
||||
world_motion[str(item.get("motion_state", "unknown"))] += 1
|
||||
if frame_index in target_frames:
|
||||
sample_world[frame_index] = row
|
||||
if (
|
||||
fusion_frames != 4489
|
||||
or world_frames != 4489
|
||||
or set(sample_fusion) != target_frames
|
||||
or set(sample_world) != target_frames
|
||||
):
|
||||
raise E46CFullReplayWorldTracksError("E26 full replay accounting changed")
|
||||
selection = identity["selection"]
|
||||
route_metrics = {
|
||||
"route_frame_count": fusion_frames,
|
||||
"route_span_seconds": round(
|
||||
float(selection["timeline_end_seconds"])
|
||||
- float(selection["timeline_start_seconds"]),
|
||||
6,
|
||||
),
|
||||
"fusion_observation_count": fusion_observations,
|
||||
"source_track_count": len(source_tracks),
|
||||
"world_track_candidate_count": len(world_tracks),
|
||||
"world_track_count": len(unique_world_state_tracks),
|
||||
"source_track_world_bound_count": len(source_world_bound),
|
||||
"world_frame_count": world_frames_with_objects,
|
||||
"world_observation_count": world_observations,
|
||||
"world_current_observation_count": world_current,
|
||||
"world_held_observation_count": world_held,
|
||||
"occupancy_cell_observation_count": occupancy_cells,
|
||||
"fusion_state_counts": dict(sorted(fusion_states.items())),
|
||||
"motion_observation_counts": dict(sorted(motion_states.items())),
|
||||
"world_motion_observation_counts": dict(sorted(world_motion.items())),
|
||||
"class_observation_counts": dict(sorted(labels.items())),
|
||||
}
|
||||
return {
|
||||
"sample_fusion": sample_fusion,
|
||||
"sample_world": sample_world,
|
||||
"route_metrics": route_metrics,
|
||||
"benchmark_passed_events": int(benchmark["passed_events"]),
|
||||
"benchmark_total_events": int(benchmark["total_events"]),
|
||||
"identity": {
|
||||
"result_id": result["result_id"],
|
||||
"result_sha256": _sha256(result_path),
|
||||
"fusion_frames_sha256": artifacts["fusion-frames.jsonl"]["sha256"],
|
||||
"world_state_sha256": artifacts["world-state.jsonl"]["sha256"],
|
||||
"run_report_sha256": artifacts["run-report.json"]["sha256"],
|
||||
"input_sha256": identity["input_sha256"],
|
||||
"timeline_sha256": selection["timeline_sha256"],
|
||||
"pipeline": identity["configuration"]["pipeline"],
|
||||
"profile_sha256": identity["configuration"]["profile_sha256"],
|
||||
"implementation_sha256": identity["configuration"][
|
||||
"implementation_sha256"
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _world_projection(item: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"world_track_id": item.get("track_id"),
|
||||
"route_track_id": item.get("source_track_id"),
|
||||
"source_track_aliases": copy.deepcopy(item.get("source_track_aliases", [])),
|
||||
"category": item.get("detector_label", item.get("class", "unknown")),
|
||||
"motion_state": item.get("motion_state", "unknown"),
|
||||
"motion_confidence": float(item.get("motion_confidence") or 0.0),
|
||||
"position_map_m": copy.deepcopy(item.get("position_map_m")),
|
||||
"size_m": copy.deepcopy(item.get("size_m")),
|
||||
"occupancy_footprint_map_xy": copy.deepcopy(
|
||||
item.get("occupancy_footprint_map_xy", [])
|
||||
),
|
||||
"occupancy_evidence_current": bool(item.get("occupancy_evidence_current")),
|
||||
"occupancy_observation_age_ms": item.get("occupancy_observation_age_ms"),
|
||||
"occupancy_cell_count": int(item.get("occupancy_cell_count") or 0),
|
||||
"temporal_status": item.get("temporal_status"),
|
||||
}
|
||||
|
||||
|
||||
def _associate(
|
||||
source: list[dict[str, Any]], candidates: list[dict[str, Any]]
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
pairs: list[tuple[float, str, int]] = []
|
||||
for item in source:
|
||||
category = item.get("category")
|
||||
group = "vehicle" if category in {"car", "heavy_vehicle"} else category
|
||||
for index, candidate in enumerate(candidates):
|
||||
if candidate.get("association_group") != group:
|
||||
continue
|
||||
box = candidate.get("bbox_xyxy")
|
||||
if not isinstance(box, list) or len(box) != 4:
|
||||
continue
|
||||
source_box = item["box_xyxy"]
|
||||
sx, sy = _center(source_box)
|
||||
cx, cy = _center(box)
|
||||
distance = math.hypot(sx - cx, sy - cy) / 1000.0
|
||||
area_ratio = abs(
|
||||
math.log(max(_area(source_box), 1.0) / max(_area(box), 1.0))
|
||||
)
|
||||
cost = distance + 0.25 * area_ratio + (1.0 - _iou(source_box, box))
|
||||
if cost <= 1.4:
|
||||
pairs.append((cost, str(item["object_id"]), index))
|
||||
output: dict[str, dict[str, Any]] = {}
|
||||
used: set[int] = set()
|
||||
for _, object_id, index in sorted(pairs):
|
||||
if object_id not in output and index not in used:
|
||||
output[object_id] = candidates[index]
|
||||
used.add(index)
|
||||
return output
|
||||
|
||||
|
||||
def _center(box: list[float]) -> tuple[float, float]:
|
||||
return ((float(box[0]) + float(box[2])) / 2, (float(box[1]) + float(box[3])) / 2)
|
||||
|
||||
|
||||
def _area(box: list[float]) -> float:
|
||||
return max(0.0, box[2] - box[0]) * max(0.0, box[3] - box[1])
|
||||
|
||||
|
||||
def _iou(left: list[float], right: list[float]) -> float:
|
||||
x1, y1 = max(left[0], right[0]), max(left[1], right[1])
|
||||
x2, y2 = min(left[2], right[2]), min(left[3], right[3])
|
||||
intersection = max(0.0, x2 - x1) * max(0.0, y2 - y1)
|
||||
union = _area(left) + _area(right) - intersection
|
||||
return intersection / union if union > 0 else 0.0
|
||||
|
||||
|
||||
def _artifact(path: Path, role: str) -> dict[str, object]:
|
||||
return {
|
||||
"role": role,
|
||||
"path": path.name,
|
||||
"size_bytes": path.stat().st_size,
|
||||
"sha256": _sha256(path),
|
||||
}
|
||||
|
||||
|
||||
def _validated_artifact(root: Path, raw: object) -> Path:
|
||||
if not isinstance(raw, dict) or not isinstance(raw.get("path"), str):
|
||||
raise E46CFullReplayWorldTracksError("artifact metadata is invalid")
|
||||
path = (root / raw["path"]).resolve(strict=True)
|
||||
if path.parent != root or path.is_symlink() or not path.is_file():
|
||||
raise E46CFullReplayWorldTracksError("artifact path is invalid")
|
||||
expected_size = raw.get("size_bytes", raw.get("byte_length"))
|
||||
if path.stat().st_size != expected_size or _sha256(path) != raw.get("sha256"):
|
||||
raise E46CFullReplayWorldTracksError("artifact changed")
|
||||
return path
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode()
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(value, dict):
|
||||
raise E46CFullReplayWorldTracksError("JSON document is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _read_jsonl(path: Path):
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
for line in handle:
|
||||
if line.strip():
|
||||
value = json.loads(line)
|
||||
if not isinstance(value, dict):
|
||||
raise E46CFullReplayWorldTracksError("JSONL row is invalid")
|
||||
yield value
|
||||
|
||||
|
||||
def _write_json(path: Path, value: object) -> None:
|
||||
path.write_bytes(_canonical_json(value) + b"\n")
|
||||
|
||||
|
||||
def _write_jsonl(path: Path, values: list[dict[str, Any]]) -> None:
|
||||
with path.open("wb") as handle:
|
||||
for value in values:
|
||||
handle.write(_canonical_json(value) + b"\n")
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for block in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
@@ -0,0 +1,993 @@
|
||||
"""Freeze a deterministic temporal-failure audit of the full E46C replay."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import uuid
|
||||
from collections import Counter, defaultdict
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
from k1link.compute.e46c_full_replay_world_tracks import (
|
||||
E46C_MANIFEST_NAME,
|
||||
read_e46c_full_replay_world_tracks,
|
||||
)
|
||||
|
||||
E46D_RESULT_SCHEMA: Final = "missioncore.e46d-temporal-failure-audit/v1"
|
||||
E46D_REPORT_SCHEMA: Final = "missioncore.e46d-temporal-failure-audit-report/v1"
|
||||
E46D_SIGNAL_SCHEMA: Final = "missioncore.e46d-temporal-failure-signal/v1"
|
||||
E46D_CLIP_SCHEMA: Final = "missioncore.e46d-temporal-review-clip/v1"
|
||||
E46D_MANIFEST_NAME: Final = "manifest.json"
|
||||
E46D_REPORT_NAME: Final = "temporal-failure-audit-report.json"
|
||||
E46D_SIGNALS_NAME: Final = "temporal-failure-signals.jsonl"
|
||||
E46D_CLIPS_NAME: Final = "temporal-review-clips.jsonl"
|
||||
|
||||
_RESULT_ID = re.compile(r"^e46d-temporal-failure-audit-[a-f0-9]{64}$")
|
||||
_E26_RESULT_ID = re.compile(r"^e10-integrated-perception-[a-f0-9]{64}$")
|
||||
_AUTHORITY: Final = {
|
||||
"ground_truth": False,
|
||||
"independent_truth": False,
|
||||
"metric_grade_reference": False,
|
||||
"candidate_accepted": False,
|
||||
"free_space_authority": False,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
_PROFILE: Final = {
|
||||
"profile_id": "e46d-full-replay-temporal-failure-audit/v1",
|
||||
"layer_blackout_min_frames": 3,
|
||||
"detector_hold_min_frames": 3,
|
||||
"route_gap_min_frames": 3,
|
||||
"route_gap_max_frames": 25,
|
||||
"id_rebirth_min_iou": 0.5,
|
||||
"bbox_jump_max_iou": 0.1,
|
||||
"bbox_jump_min_area_px2": 300.0,
|
||||
"flap_window_seconds": 1.0,
|
||||
"flap_min_transitions": 3,
|
||||
"short_track_max_observations": 3,
|
||||
"short_track_burst_min_count": 4,
|
||||
"review_clip_lead_seconds": 2.0,
|
||||
"review_clip_tail_seconds": 2.0,
|
||||
"review_clip_separation_seconds": 1.5,
|
||||
"review_clip_limit": 48,
|
||||
}
|
||||
_PRIORITY = {"critical": 3, "high": 2, "medium": 1}
|
||||
|
||||
|
||||
class E46DTemporalFailureAuditError(ValueError):
|
||||
"""Raised when an E46D source or immutable result is invalid."""
|
||||
|
||||
|
||||
def build_e46d_temporal_failure_audit(
|
||||
*, e46c_root: Path, e26_results_root: Path, output_root: Path
|
||||
) -> dict[str, Any]:
|
||||
"""Audit all E46C temporal rows and freeze prioritized recorded-video clips."""
|
||||
|
||||
e46c = read_e46c_full_replay_world_tracks(e46c_root)
|
||||
e26_binding = e46c["manifest"]["identity"].get("e26_full_replay")
|
||||
if not isinstance(e26_binding, dict):
|
||||
raise E46DTemporalFailureAuditError("E46C E26 binding is invalid")
|
||||
e26_result_id = e26_binding.get("result_id")
|
||||
if not isinstance(e26_result_id, str) or _E26_RESULT_ID.fullmatch(e26_result_id) is None:
|
||||
raise E46DTemporalFailureAuditError("E46C E26 identity is invalid")
|
||||
e26_root = (e26_results_root.expanduser().absolute() / e26_result_id).resolve(strict=True)
|
||||
results_root = e26_results_root.expanduser().absolute().resolve(strict=True)
|
||||
if e26_root.parent != results_root or e26_root.is_symlink():
|
||||
raise E46DTemporalFailureAuditError("E26 source root is invalid")
|
||||
frames, source_identity = _read_e26_frames(e26_root, e26_binding)
|
||||
signals, clips, metrics = analyze_temporal_frames(frames)
|
||||
|
||||
method = {
|
||||
"schema_version": "missioncore.laboratory-method/v1",
|
||||
"completeness": "complete",
|
||||
"execution_class": "deterministic",
|
||||
"pipeline_id": "e46d-full-replay-temporal-failure-audit/v1",
|
||||
"components": [
|
||||
{
|
||||
"kind": "source",
|
||||
"name": e46c["result_id"],
|
||||
"version": "E46C full recorded RIGHT route/world tracks",
|
||||
"role": "admitted full-route diagnostic result and video binding",
|
||||
"identity_sha256": _sha256(e46c["result_root"] / E46C_MANIFEST_NAME),
|
||||
},
|
||||
{
|
||||
"kind": "source",
|
||||
"name": e26_result_id,
|
||||
"version": "accepted E26 fusion frames",
|
||||
"role": "4489-frame temporal object, track, world and motion evidence",
|
||||
"identity_sha256": source_identity["fusion_frames_sha256"],
|
||||
},
|
||||
{
|
||||
"kind": "algorithm",
|
||||
"name": "deterministic temporal exception scanner",
|
||||
"version": _PROFILE["profile_id"],
|
||||
"role": "detect, classify, rank and clip observable temporal discontinuities",
|
||||
"identity_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
},
|
||||
],
|
||||
}
|
||||
report_basis = {
|
||||
"schema_version": E46D_REPORT_SCHEMA,
|
||||
"status": "completed-full-recorded-right-temporal-failure-audit",
|
||||
"metrics": metrics,
|
||||
"acceptance": {
|
||||
"full_route_accounted": metrics["route_frame_count"] == 4489,
|
||||
"temporal_continuity_passed": metrics["temporal_continuity_passed"],
|
||||
"independent_truth_available": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
"decision": {
|
||||
"temporal_regression_confirmed": not metrics["temporal_continuity_passed"],
|
||||
"detector_gap_visible": metrics["detector_hold_episode_count"] > 0,
|
||||
"route_fragmentation_visible": metrics["short_route_track_count"] > 0,
|
||||
"world_binding_instability_visible": metrics["world_binding_flap_episode_count"] > 0,
|
||||
"next_action": (
|
||||
"use the frozen clips to separate detector gaps from route-tracker and "
|
||||
"world-binding failures, then rerun the same recorded RIGHT source as an A/B replay"
|
||||
),
|
||||
},
|
||||
"method": method,
|
||||
"limitations": [
|
||||
(
|
||||
"signals prove discontinuities in the published diagnostic layer; without "
|
||||
"independent frame truth they do not by themselves prove that a visible "
|
||||
"physical object was missed or that a short track was a false positive"
|
||||
),
|
||||
(
|
||||
"route-ID rebirth uses same-class image-space overlap and is a high-priority "
|
||||
"candidate for visual review, not a permanent physical-identity verdict"
|
||||
),
|
||||
(
|
||||
"bbox jumps are measured in the distorted 800x600 RIGHT image plane; the "
|
||||
"audit does not claim metric velocity or calibrated image-plane motion"
|
||||
),
|
||||
(
|
||||
"recorded replay only: no LEFT camera, live hardware, commands, free-space, "
|
||||
"navigation or safety authority is introduced"
|
||||
),
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
"ground_truth": False,
|
||||
}
|
||||
producer_sha256 = _sha256(Path(__file__).resolve(strict=True))
|
||||
identity = {
|
||||
"schema_version": E46D_RESULT_SCHEMA,
|
||||
"source_session_id": "RAVNOVES00",
|
||||
"camera_source_id": "sensor.camera.right",
|
||||
"e46c_source": {
|
||||
"result_id": e46c["result_id"],
|
||||
"manifest_sha256": _sha256(e46c["result_root"] / E46C_MANIFEST_NAME),
|
||||
},
|
||||
"e26_source": source_identity,
|
||||
"analysis_profile": copy.deepcopy(_PROFILE),
|
||||
"method": method,
|
||||
"report_sha256": hashlib.sha256(_canonical_json(report_basis)).hexdigest(),
|
||||
"signals_sha256": hashlib.sha256(_canonical_json(signals)).hexdigest(),
|
||||
"clips_sha256": hashlib.sha256(_canonical_json(clips)).hexdigest(),
|
||||
"producer_sha256": producer_sha256,
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
result_id = f"e46d-temporal-failure-audit-{identity_sha256}"
|
||||
destination = output_root.expanduser().absolute() / result_id
|
||||
if destination.exists():
|
||||
return read_e46d_temporal_failure_audit(destination)
|
||||
created_at_utc = datetime.now(UTC).isoformat().replace("+00:00", "Z")
|
||||
report = {
|
||||
**report_basis,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"created_at_utc": created_at_utc,
|
||||
"source_session_id": "RAVNOVES00",
|
||||
"camera_source_id": "sensor.camera.right",
|
||||
}
|
||||
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
_write_json(staging / E46D_REPORT_NAME, report)
|
||||
_write_jsonl(staging / E46D_SIGNALS_NAME, signals)
|
||||
_write_jsonl(staging / E46D_CLIPS_NAME, clips)
|
||||
_write_json(
|
||||
staging / E46D_MANIFEST_NAME,
|
||||
{
|
||||
"schema_version": E46D_RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": created_at_utc,
|
||||
"acceptance_state": "recorded-temporal-regression-diagnostic",
|
||||
"ground_truth": False,
|
||||
"artifacts": [
|
||||
_artifact(staging / E46D_REPORT_NAME, "temporal-failure-report"),
|
||||
_artifact(staging / E46D_SIGNALS_NAME, "temporal-failure-signals"),
|
||||
_artifact(staging / E46D_CLIPS_NAME, "temporal-review-clips"),
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
},
|
||||
)
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
return read_e46d_temporal_failure_audit(destination)
|
||||
|
||||
|
||||
def read_e46d_temporal_failure_audit(root: Path) -> dict[str, Any]:
|
||||
resolved = root.resolve(strict=True)
|
||||
manifest = _read_json(resolved / E46D_MANIFEST_NAME)
|
||||
identity = manifest.get("identity")
|
||||
if not isinstance(identity, dict):
|
||||
raise E46DTemporalFailureAuditError("E46D identity is invalid")
|
||||
digest = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
if (
|
||||
manifest.get("schema_version") != E46D_RESULT_SCHEMA
|
||||
or manifest.get("identity_sha256") != digest
|
||||
or manifest.get("result_id") != f"e46d-temporal-failure-audit-{digest}"
|
||||
or resolved.name != manifest.get("result_id")
|
||||
or _RESULT_ID.fullmatch(resolved.name) is None
|
||||
or manifest.get("authority") != _AUTHORITY
|
||||
or manifest.get("ground_truth") is not False
|
||||
):
|
||||
raise E46DTemporalFailureAuditError("E46D identity is invalid")
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(artifacts, list) or len(artifacts) != 3:
|
||||
raise E46DTemporalFailureAuditError("E46D artifacts are invalid")
|
||||
by_role = {item.get("role"): item for item in artifacts if isinstance(item, dict)}
|
||||
report = _read_json(_validated_artifact(resolved, by_role.get("temporal-failure-report")))
|
||||
signals = tuple(
|
||||
_read_jsonl(_validated_artifact(resolved, by_role.get("temporal-failure-signals")))
|
||||
)
|
||||
clips = tuple(_read_jsonl(_validated_artifact(resolved, by_role.get("temporal-review-clips"))))
|
||||
if (
|
||||
report.get("schema_version") != E46D_REPORT_SCHEMA
|
||||
or report.get("result_id") != resolved.name
|
||||
or report.get("authority") != _AUTHORITY
|
||||
or report.get("ground_truth") is not False
|
||||
or any(item.get("schema_version") != E46D_SIGNAL_SCHEMA for item in signals)
|
||||
or any(item.get("schema_version") != E46D_CLIP_SCHEMA for item in clips)
|
||||
or hashlib.sha256(_canonical_json(signals)).hexdigest() != identity.get("signals_sha256")
|
||||
or hashlib.sha256(_canonical_json(clips)).hexdigest() != identity.get("clips_sha256")
|
||||
):
|
||||
raise E46DTemporalFailureAuditError("E46D result changed")
|
||||
metrics = report.get("metrics")
|
||||
if (
|
||||
not isinstance(metrics, dict)
|
||||
or metrics.get("failure_signal_count") != len(signals)
|
||||
or metrics.get("review_clip_count") != len(clips)
|
||||
):
|
||||
raise E46DTemporalFailureAuditError("E46D accounting changed")
|
||||
return {
|
||||
"result_id": resolved.name,
|
||||
"result_root": resolved,
|
||||
"manifest": manifest,
|
||||
"report": report,
|
||||
"signals": signals,
|
||||
"clips": clips,
|
||||
}
|
||||
|
||||
|
||||
def analyze_temporal_frames(
|
||||
frames: list[dict[str, Any]],
|
||||
) -> tuple[list[dict[str, Any]], list[dict[str, Any]], dict[str, Any]]:
|
||||
"""Return deterministic signals, ranked clip windows and full-route metrics."""
|
||||
|
||||
_validate_frames(frames)
|
||||
times = [float(frame["session_seconds"]) for frame in frames]
|
||||
timeline_start, timeline_end = times[0], times[-1]
|
||||
signals: list[dict[str, Any]] = []
|
||||
by_track: dict[int, list[tuple[int, dict[str, Any]]]] = defaultdict(list)
|
||||
for position, frame in enumerate(frames):
|
||||
for item in frame["objects"]:
|
||||
by_track[int(item["route_track_id"])].append((position, item))
|
||||
|
||||
counts = [len(frame["objects"]) for frame in frames]
|
||||
position = 0
|
||||
while position < len(frames):
|
||||
if counts[position] != 0:
|
||||
position += 1
|
||||
continue
|
||||
start = position
|
||||
while position + 1 < len(frames) and counts[position + 1] == 0:
|
||||
position += 1
|
||||
end = position
|
||||
length = end - start + 1
|
||||
if (
|
||||
start > 0
|
||||
and end + 1 < len(frames)
|
||||
and length >= int(_PROFILE["layer_blackout_min_frames"])
|
||||
):
|
||||
before, after = counts[start - 1], counts[end + 1]
|
||||
if before > 0 and after > 0:
|
||||
duration = times[end + 1] - times[start]
|
||||
tracks = sorted(
|
||||
{
|
||||
int(item["route_track_id"])
|
||||
for item in frames[start - 1]["objects"] + frames[end + 1]["objects"]
|
||||
}
|
||||
)
|
||||
signals.append(
|
||||
_signal(
|
||||
"layer-blackout",
|
||||
"critical" if duration >= 0.5 else "high",
|
||||
frames,
|
||||
start,
|
||||
end,
|
||||
route_track_ids=tracks,
|
||||
evidence={
|
||||
"duration_seconds": round(duration, 6),
|
||||
"zero_frame_count": length,
|
||||
"before_object_count": before,
|
||||
"minimum_object_count": 0,
|
||||
"after_object_count": after,
|
||||
},
|
||||
)
|
||||
)
|
||||
position += 1
|
||||
|
||||
detector_hold_observations = 0
|
||||
route_gap_count = 0
|
||||
for route_track_id, sequence in by_track.items():
|
||||
cursor = 0
|
||||
while cursor < len(sequence):
|
||||
position, item = sequence[cursor]
|
||||
if bool(item["camera_evidence_current"]):
|
||||
cursor += 1
|
||||
continue
|
||||
start_cursor = cursor
|
||||
while (
|
||||
cursor + 1 < len(sequence)
|
||||
and sequence[cursor + 1][0] == sequence[cursor][0] + 1
|
||||
and not bool(sequence[cursor + 1][1]["camera_evidence_current"])
|
||||
):
|
||||
cursor += 1
|
||||
end_cursor = cursor
|
||||
length = end_cursor - start_cursor + 1
|
||||
detector_hold_observations += length
|
||||
first_position = sequence[start_cursor][0]
|
||||
last_position = sequence[end_cursor][0]
|
||||
current_before = (
|
||||
start_cursor > 0
|
||||
and sequence[start_cursor - 1][0] == first_position - 1
|
||||
and bool(sequence[start_cursor - 1][1]["camera_evidence_current"])
|
||||
)
|
||||
current_after = (
|
||||
end_cursor + 1 < len(sequence)
|
||||
and sequence[end_cursor + 1][0] == last_position + 1
|
||||
and bool(sequence[end_cursor + 1][1]["camera_evidence_current"])
|
||||
)
|
||||
if length >= int(_PROFILE["detector_hold_min_frames"]):
|
||||
duration_end = (
|
||||
times[last_position + 1]
|
||||
if last_position + 1 < len(times)
|
||||
else times[last_position]
|
||||
)
|
||||
duration = duration_end - times[first_position]
|
||||
signals.append(
|
||||
_signal(
|
||||
"camera-evidence-hold",
|
||||
"high" if duration >= 0.5 else "medium",
|
||||
frames,
|
||||
first_position,
|
||||
last_position,
|
||||
route_track_ids=[route_track_id],
|
||||
evidence={
|
||||
"duration_seconds": round(duration, 6),
|
||||
"held_frame_count": length,
|
||||
"camera_evidence_current": False,
|
||||
"route_identity_retained": True,
|
||||
"current_evidence_before": current_before,
|
||||
"current_evidence_after": current_after,
|
||||
},
|
||||
)
|
||||
)
|
||||
cursor += 1
|
||||
|
||||
for (previous_position, previous), (next_position, following) in zip(
|
||||
sequence, sequence[1:], strict=False
|
||||
):
|
||||
missing = next_position - previous_position - 1
|
||||
if not (
|
||||
int(_PROFILE["route_gap_min_frames"])
|
||||
<= missing
|
||||
<= int(_PROFILE["route_gap_max_frames"])
|
||||
):
|
||||
continue
|
||||
route_gap_count += 1
|
||||
signals.append(
|
||||
_signal(
|
||||
"route-layer-gap",
|
||||
"high" if missing >= 5 else "medium",
|
||||
frames,
|
||||
previous_position + 1,
|
||||
next_position - 1,
|
||||
route_track_ids=[route_track_id],
|
||||
evidence={
|
||||
"missing_frame_count": missing,
|
||||
"duration_seconds": round(
|
||||
times[next_position] - times[previous_position], 6
|
||||
),
|
||||
"same_route_identity_returned": True,
|
||||
"boundary_iou": round(
|
||||
_iou(previous["bbox_xyxy"], following["bbox_xyxy"]), 6
|
||||
),
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
seen_rebirths: set[tuple[int, int]] = set()
|
||||
for position, (previous, following) in enumerate(zip(frames, frames[1:], strict=False)):
|
||||
previous_ids = {int(item["route_track_id"]) for item in previous["objects"]}
|
||||
following_ids = {int(item["route_track_id"]) for item in following["objects"]}
|
||||
gone = [
|
||||
item
|
||||
for item in previous["objects"]
|
||||
if int(item["route_track_id"]) not in following_ids
|
||||
and item["camera_evidence_current"] is True
|
||||
]
|
||||
born = [
|
||||
item
|
||||
for item in following["objects"]
|
||||
if int(item["route_track_id"]) not in previous_ids
|
||||
and item["camera_evidence_current"] is True
|
||||
]
|
||||
candidates = sorted(
|
||||
(
|
||||
(_iou(left["bbox_xyxy"], right["bbox_xyxy"]), left, right)
|
||||
for left in gone
|
||||
for right in born
|
||||
if left["category"] == right["category"]
|
||||
and _iou(left["bbox_xyxy"], right["bbox_xyxy"])
|
||||
>= float(_PROFILE["id_rebirth_min_iou"])
|
||||
),
|
||||
key=lambda value: value[0],
|
||||
reverse=True,
|
||||
)
|
||||
used_old: set[int] = set()
|
||||
used_new: set[int] = set()
|
||||
for overlap, left, right in candidates:
|
||||
old_id, new_id = int(left["route_track_id"]), int(right["route_track_id"])
|
||||
if old_id in used_old or new_id in used_new or (old_id, new_id) in seen_rebirths:
|
||||
continue
|
||||
used_old.add(old_id)
|
||||
used_new.add(new_id)
|
||||
seen_rebirths.add((old_id, new_id))
|
||||
signals.append(
|
||||
_signal(
|
||||
"route-id-rebirth-candidate",
|
||||
"critical",
|
||||
frames,
|
||||
position,
|
||||
position + 1,
|
||||
route_track_ids=[old_id, new_id],
|
||||
evidence={
|
||||
"category": left["category"],
|
||||
"bbox_iou": round(overlap, 6),
|
||||
"old_route_track_id": old_id,
|
||||
"new_route_track_id": new_id,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
for route_track_id, sequence in by_track.items():
|
||||
for (previous_position, previous), (next_position, following) in zip(
|
||||
sequence, sequence[1:], strict=False
|
||||
):
|
||||
if (
|
||||
next_position != previous_position + 1
|
||||
or previous["camera_evidence_current"] is not True
|
||||
or following["camera_evidence_current"] is not True
|
||||
):
|
||||
continue
|
||||
area = min(_box_area(previous["bbox_xyxy"]), _box_area(following["bbox_xyxy"]))
|
||||
overlap = _iou(previous["bbox_xyxy"], following["bbox_xyxy"])
|
||||
if area < float(_PROFILE["bbox_jump_min_area_px2"]) or overlap >= float(
|
||||
_PROFILE["bbox_jump_max_iou"]
|
||||
):
|
||||
continue
|
||||
signals.append(
|
||||
_signal(
|
||||
"bbox-jump",
|
||||
"high",
|
||||
frames,
|
||||
previous_position,
|
||||
next_position,
|
||||
route_track_ids=[route_track_id],
|
||||
evidence={
|
||||
"bbox_iou": round(overlap, 6),
|
||||
"minimum_box_area_px2": round(area, 3),
|
||||
"both_camera_evidence_current": True,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
for route_track_id, sequence in by_track.items():
|
||||
signals.extend(
|
||||
_flap_signals(frames, route_track_id, sequence, "motion_state", "motion-state-flap")
|
||||
)
|
||||
signals.extend(
|
||||
_flap_signals(frames, route_track_id, sequence, "world_track_id", "world-binding-flap")
|
||||
)
|
||||
|
||||
short_tracks = [
|
||||
(route_track_id, sequence)
|
||||
for route_track_id, sequence in by_track.items()
|
||||
if len(sequence) <= int(_PROFILE["short_track_max_observations"])
|
||||
]
|
||||
short_starts = sorted(
|
||||
(times[sequence[0][0]], route_track_id, sequence[0][0])
|
||||
for route_track_id, sequence in short_tracks
|
||||
)
|
||||
cursor = 0
|
||||
while cursor < len(short_starts):
|
||||
end = cursor
|
||||
while end < len(short_starts) and short_starts[end][0] - short_starts[cursor][0] <= 1.0:
|
||||
end += 1
|
||||
window = short_starts[cursor:end]
|
||||
if len(window) >= int(_PROFILE["short_track_burst_min_count"]):
|
||||
track_ids = [item[1] for item in window]
|
||||
signals.append(
|
||||
_signal(
|
||||
"short-track-burst",
|
||||
"high" if len(window) >= 6 else "medium",
|
||||
frames,
|
||||
window[0][2],
|
||||
window[-1][2],
|
||||
route_track_ids=track_ids,
|
||||
evidence={
|
||||
"short_track_count": len(window),
|
||||
"maximum_observations_per_track": int(
|
||||
_PROFILE["short_track_max_observations"]
|
||||
),
|
||||
"window_seconds": round(window[-1][0] - window[0][0], 6),
|
||||
},
|
||||
)
|
||||
)
|
||||
cursor = end
|
||||
else:
|
||||
cursor += 1
|
||||
|
||||
signals.sort(
|
||||
key=lambda item: (float(item["start_seconds"]), str(item["kind"]), str(item["signal_id"]))
|
||||
)
|
||||
clips = _review_clips(signals, timeline_start, timeline_end)
|
||||
signal_counts = Counter(str(item["kind"]) for item in signals)
|
||||
priority_counts = Counter(str(item["priority"]) for item in signals)
|
||||
object_observations = sum(counts)
|
||||
camera_held = sum(
|
||||
item["camera_evidence_current"] is False for frame in frames for item in frame["objects"]
|
||||
)
|
||||
metrics = {
|
||||
"route_frame_count": len(frames),
|
||||
"route_span_seconds": round(timeline_end - timeline_start, 6),
|
||||
"object_observation_count": object_observations,
|
||||
"route_track_count": len(by_track),
|
||||
"zero_object_frame_count": sum(value == 0 for value in counts),
|
||||
"zero_object_frame_fraction": round(sum(value == 0 for value in counts) / len(frames), 9),
|
||||
"camera_held_observation_count": camera_held,
|
||||
"camera_held_observation_fraction": round(camera_held / object_observations, 9),
|
||||
"detector_hold_episode_count": int(signal_counts["camera-evidence-hold"]),
|
||||
"layer_blackout_episode_count": int(signal_counts["layer-blackout"]),
|
||||
"route_layer_gap_episode_count": route_gap_count,
|
||||
"route_id_rebirth_candidate_count": int(signal_counts["route-id-rebirth-candidate"]),
|
||||
"bbox_jump_episode_count": int(signal_counts["bbox-jump"]),
|
||||
"motion_state_flap_episode_count": int(signal_counts["motion-state-flap"]),
|
||||
"world_binding_flap_episode_count": int(signal_counts["world-binding-flap"]),
|
||||
"short_route_track_count": len(short_tracks),
|
||||
"short_route_track_fraction": round(len(short_tracks) / len(by_track), 9),
|
||||
"short_track_burst_episode_count": int(signal_counts["short-track-burst"]),
|
||||
"failure_signal_count": len(signals),
|
||||
"review_clip_count": len(clips),
|
||||
"signal_counts": dict(sorted(signal_counts.items())),
|
||||
"priority_counts": {
|
||||
key: int(priority_counts.get(key, 0)) for key in ("critical", "high", "medium")
|
||||
},
|
||||
"temporal_continuity_passed": (
|
||||
signal_counts["layer-blackout"] == 0
|
||||
and signal_counts["route-id-rebirth-candidate"] == 0
|
||||
and signal_counts["bbox-jump"] == 0
|
||||
),
|
||||
}
|
||||
return signals, clips, metrics
|
||||
|
||||
|
||||
def _flap_signals(
|
||||
frames: list[dict[str, Any]],
|
||||
route_track_id: int,
|
||||
sequence: list[tuple[int, dict[str, Any]]],
|
||||
field: str,
|
||||
kind: str,
|
||||
) -> list[dict[str, Any]]:
|
||||
transitions: list[tuple[int, object, object]] = []
|
||||
for (previous_position, previous), (next_position, following) in zip(
|
||||
sequence, sequence[1:], strict=False
|
||||
):
|
||||
if next_position == previous_position + 1 and previous[field] != following[field]:
|
||||
transitions.append((next_position, previous[field], following[field]))
|
||||
output: list[dict[str, Any]] = []
|
||||
cursor = 0
|
||||
while cursor < len(transitions):
|
||||
end = cursor
|
||||
start_seconds = float(frames[transitions[cursor][0]]["session_seconds"])
|
||||
while end < len(transitions) and float(
|
||||
frames[transitions[end][0]]["session_seconds"]
|
||||
) - start_seconds <= float(_PROFILE["flap_window_seconds"]):
|
||||
end += 1
|
||||
window = transitions[cursor:end]
|
||||
if len(window) >= int(_PROFILE["flap_min_transitions"]):
|
||||
values = {value for _, before, after in window for value in (before, after)}
|
||||
output.append(
|
||||
_signal(
|
||||
kind,
|
||||
"high" if len(window) >= 4 else "medium",
|
||||
frames,
|
||||
window[0][0] - 1,
|
||||
window[-1][0],
|
||||
route_track_ids=[route_track_id],
|
||||
world_track_ids=(
|
||||
sorted(
|
||||
int(value)
|
||||
for value in values
|
||||
if isinstance(value, int) and not isinstance(value, bool)
|
||||
)
|
||||
if field == "world_track_id"
|
||||
else []
|
||||
),
|
||||
evidence={
|
||||
"transition_count": len(window),
|
||||
"window_seconds": round(
|
||||
float(frames[window[-1][0]]["session_seconds"])
|
||||
- float(frames[window[0][0]]["session_seconds"]),
|
||||
6,
|
||||
),
|
||||
"state_count": len(values),
|
||||
},
|
||||
)
|
||||
)
|
||||
cursor = end
|
||||
else:
|
||||
cursor += 1
|
||||
return output
|
||||
|
||||
|
||||
def _signal(
|
||||
kind: str,
|
||||
priority: str,
|
||||
frames: list[dict[str, Any]],
|
||||
start_position: int,
|
||||
end_position: int,
|
||||
*,
|
||||
route_track_ids: list[int],
|
||||
evidence: dict[str, object],
|
||||
world_track_ids: list[int] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
start_position = max(0, start_position)
|
||||
end_position = min(len(frames) - 1, end_position)
|
||||
basis = {
|
||||
"kind": kind,
|
||||
"priority": priority,
|
||||
"start_frame": int(frames[start_position]["frame_index"]),
|
||||
"end_frame": int(frames[end_position]["frame_index"]),
|
||||
"route_track_ids": sorted(set(route_track_ids)),
|
||||
"world_track_ids": sorted(set(world_track_ids or [])),
|
||||
"evidence": evidence,
|
||||
}
|
||||
signal_id = "e46d-signal-" + hashlib.sha256(_canonical_json(basis)).hexdigest()[:20]
|
||||
return {
|
||||
"schema_version": E46D_SIGNAL_SCHEMA,
|
||||
"signal_id": signal_id,
|
||||
**basis,
|
||||
"start_seconds": float(frames[start_position]["session_seconds"]),
|
||||
"end_seconds": float(frames[end_position]["session_seconds"]),
|
||||
}
|
||||
|
||||
|
||||
def _review_clips(
|
||||
signals: list[dict[str, Any]], timeline_start: float, timeline_end: float
|
||||
) -> list[dict[str, Any]]:
|
||||
ranked = sorted(
|
||||
signals,
|
||||
key=lambda item: (
|
||||
-_signal_score(item),
|
||||
float(item["start_seconds"]),
|
||||
str(item["signal_id"]),
|
||||
),
|
||||
)
|
||||
selected: list[dict[str, Any]] = []
|
||||
selected_ids: set[str] = set()
|
||||
|
||||
def admit(signal: dict[str, Any]) -> bool:
|
||||
center = (float(signal["start_seconds"]) + float(signal["end_seconds"])) / 2
|
||||
if any(
|
||||
abs(center - (float(item["start_seconds"]) + float(item["end_seconds"])) / 2)
|
||||
< float(_PROFILE["review_clip_separation_seconds"])
|
||||
for item in selected
|
||||
):
|
||||
return False
|
||||
selected.append(signal)
|
||||
selected_ids.add(str(signal["signal_id"]))
|
||||
return True
|
||||
|
||||
for kind in sorted({str(item["kind"]) for item in signals}):
|
||||
admitted = 0
|
||||
for signal in ranked:
|
||||
if (
|
||||
signal["kind"] == kind
|
||||
and str(signal["signal_id"]) not in selected_ids
|
||||
and admit(signal)
|
||||
):
|
||||
admitted += 1
|
||||
if admitted == 2:
|
||||
break
|
||||
for signal in ranked:
|
||||
if len(selected) >= int(_PROFILE["review_clip_limit"]):
|
||||
break
|
||||
if str(signal["signal_id"]) not in selected_ids:
|
||||
admit(signal)
|
||||
selected.sort(key=lambda item: (-_signal_score(item), float(item["start_seconds"])))
|
||||
clips: list[dict[str, Any]] = []
|
||||
for rank, signal in enumerate(selected, start=1):
|
||||
clips.append(
|
||||
{
|
||||
"schema_version": E46D_CLIP_SCHEMA,
|
||||
"clip_id": (
|
||||
f"e46d-clip-{rank:02d}-{str(signal['signal_id']).removeprefix('e46d-signal-')}"
|
||||
),
|
||||
"rank": rank,
|
||||
"priority": signal["priority"],
|
||||
"kind": signal["kind"],
|
||||
"signal_id": signal["signal_id"],
|
||||
"start_seconds": max(
|
||||
timeline_start,
|
||||
float(signal["start_seconds"]) - float(_PROFILE["review_clip_lead_seconds"]),
|
||||
),
|
||||
"event_start_seconds": float(signal["start_seconds"]),
|
||||
"event_end_seconds": float(signal["end_seconds"]),
|
||||
"end_seconds": min(
|
||||
timeline_end,
|
||||
float(signal["end_seconds"]) + float(_PROFILE["review_clip_tail_seconds"]),
|
||||
),
|
||||
"start_frame": signal["start_frame"],
|
||||
"end_frame": signal["end_frame"],
|
||||
"route_track_ids": copy.deepcopy(signal["route_track_ids"]),
|
||||
"world_track_ids": copy.deepcopy(signal["world_track_ids"]),
|
||||
"evidence": copy.deepcopy(signal["evidence"]),
|
||||
}
|
||||
)
|
||||
return clips
|
||||
|
||||
|
||||
def _signal_score(signal: dict[str, Any]) -> float:
|
||||
base = {
|
||||
"layer-blackout": 100.0,
|
||||
"route-id-rebirth-candidate": 95.0,
|
||||
"bbox-jump": 90.0,
|
||||
"route-layer-gap": 82.0,
|
||||
"camera-evidence-hold": 75.0,
|
||||
"world-binding-flap": 68.0,
|
||||
"motion-state-flap": 62.0,
|
||||
"short-track-burst": 55.0,
|
||||
}.get(str(signal["kind"]), 40.0)
|
||||
evidence = signal.get("evidence")
|
||||
duration = float(evidence.get("duration_seconds", 0.0)) if isinstance(evidence, dict) else 0.0
|
||||
count = 0.0
|
||||
if isinstance(evidence, dict):
|
||||
for key in (
|
||||
"zero_frame_count",
|
||||
"missing_frame_count",
|
||||
"transition_count",
|
||||
"short_track_count",
|
||||
):
|
||||
value = evidence.get(key)
|
||||
if isinstance(value, int | float) and not isinstance(value, bool):
|
||||
count = max(count, float(value))
|
||||
return base + _PRIORITY[str(signal["priority"])] * 10 + min(duration * 5, 20) + min(count, 20)
|
||||
|
||||
|
||||
def _read_e26_frames(
|
||||
root: Path, expected_binding: dict[str, Any]
|
||||
) -> tuple[list[dict[str, Any]], dict[str, str]]:
|
||||
result_path = root / "result.json"
|
||||
expected_result_sha = expected_binding.get("result_sha256")
|
||||
expected_fusion_sha = expected_binding.get("fusion_frames_sha256")
|
||||
if _sha256(result_path) != expected_result_sha:
|
||||
raise E46DTemporalFailureAuditError("E26 result identity changed")
|
||||
result = _read_json(result_path)
|
||||
identity = result.get("identity")
|
||||
artifacts = result.get("artifacts")
|
||||
if (
|
||||
result.get("schema_version") != "missioncore.e10-integrated-perception-result/v1"
|
||||
or result.get("acceptance_state") != "accepted"
|
||||
or not isinstance(identity, dict)
|
||||
or identity.get("source_id") != "sensor.camera.right"
|
||||
or not isinstance(artifacts, list)
|
||||
):
|
||||
raise E46DTemporalFailureAuditError("E26 result contract is invalid")
|
||||
selection = identity.get("selection")
|
||||
if not isinstance(selection, dict) or selection.get("frame_count") != 4489:
|
||||
raise E46DTemporalFailureAuditError("E26 timeline contract is invalid")
|
||||
fusion_artifact = next(
|
||||
(
|
||||
item
|
||||
for item in artifacts
|
||||
if isinstance(item, dict) and item.get("path") == "fusion-frames.jsonl"
|
||||
),
|
||||
None,
|
||||
)
|
||||
fusion_path = _validated_artifact(root, fusion_artifact)
|
||||
if _sha256(fusion_path) != expected_fusion_sha:
|
||||
raise E46DTemporalFailureAuditError("E26 fusion identity changed")
|
||||
frames: list[dict[str, Any]] = []
|
||||
for expected_index, row in enumerate(_read_jsonl(fusion_path)):
|
||||
if row.get("source_frame_index") != expected_index:
|
||||
raise E46DTemporalFailureAuditError("E26 frame sequence changed")
|
||||
frames.append(
|
||||
{
|
||||
"frame_index": expected_index,
|
||||
"session_seconds": row.get("session_seconds"),
|
||||
"objects": [
|
||||
_temporal_object(item)
|
||||
for item in row.get("objects", [])
|
||||
if isinstance(item, dict)
|
||||
],
|
||||
}
|
||||
)
|
||||
_validate_frames(frames, expected_count=4489)
|
||||
if frames[0]["session_seconds"] != selection.get("timeline_start_seconds") or frames[-1][
|
||||
"session_seconds"
|
||||
] != selection.get("timeline_end_seconds"):
|
||||
raise E46DTemporalFailureAuditError("E26 timeline changed")
|
||||
return frames, {
|
||||
"result_id": str(result.get("result_id", root.name)),
|
||||
"result_sha256": str(expected_result_sha),
|
||||
"fusion_frames_sha256": str(expected_fusion_sha),
|
||||
}
|
||||
|
||||
|
||||
def _temporal_object(item: dict[str, Any]) -> dict[str, Any]:
|
||||
box = item.get("bbox_xyxy")
|
||||
source_track_id = item.get("source_track_id")
|
||||
if (
|
||||
not isinstance(box, list)
|
||||
or len(box) != 4
|
||||
or not all(_finite(value) for value in box)
|
||||
or not isinstance(source_track_id, int)
|
||||
or isinstance(source_track_id, bool)
|
||||
):
|
||||
raise E46DTemporalFailureAuditError("E26 temporal object is invalid")
|
||||
world_track_id = item.get("track_id")
|
||||
if (
|
||||
not isinstance(world_track_id, int)
|
||||
or isinstance(world_track_id, bool)
|
||||
or world_track_id == source_track_id
|
||||
):
|
||||
world_track_id = None
|
||||
return {
|
||||
"bbox_xyxy": [float(value) for value in box],
|
||||
"category": str(item.get("label", "unknown")),
|
||||
"route_track_id": source_track_id,
|
||||
"world_track_id": world_track_id,
|
||||
"motion_state": str(item.get("motion_state", "unknown")),
|
||||
"camera_evidence_current": item.get("camera_evidence_current") is True,
|
||||
}
|
||||
|
||||
|
||||
def _validate_frames(frames: list[dict[str, Any]], expected_count: int | None = None) -> None:
|
||||
if expected_count is not None and len(frames) != expected_count:
|
||||
raise E46DTemporalFailureAuditError("E46D frame count is invalid")
|
||||
if len(frames) < 2:
|
||||
raise E46DTemporalFailureAuditError("E46D requires a temporal sequence")
|
||||
previous_seconds = -math.inf
|
||||
for expected_index, frame in enumerate(frames):
|
||||
seconds = frame.get("session_seconds")
|
||||
if (
|
||||
frame.get("frame_index") != expected_index
|
||||
or not _finite(seconds)
|
||||
or float(seconds) <= previous_seconds
|
||||
or not isinstance(frame.get("objects"), list)
|
||||
):
|
||||
raise E46DTemporalFailureAuditError("E46D temporal sequence is invalid")
|
||||
previous_seconds = float(seconds)
|
||||
for item in frame["objects"]:
|
||||
if not isinstance(item, dict):
|
||||
raise E46DTemporalFailureAuditError("E46D temporal object is invalid")
|
||||
|
||||
|
||||
def _iou(left: list[float], right: list[float]) -> float:
|
||||
x1, y1 = max(left[0], right[0]), max(left[1], right[1])
|
||||
x2, y2 = min(left[2], right[2]), min(left[3], right[3])
|
||||
intersection = max(0.0, x2 - x1) * max(0.0, y2 - y1)
|
||||
union = _box_area(left) + _box_area(right) - intersection
|
||||
return intersection / union if union > 0 else 0.0
|
||||
|
||||
|
||||
def _box_area(box: list[float]) -> float:
|
||||
return max(0.0, box[2] - box[0]) * max(0.0, box[3] - box[1])
|
||||
|
||||
|
||||
def _finite(value: object) -> bool:
|
||||
return (
|
||||
isinstance(value, int | float)
|
||||
and not isinstance(value, bool)
|
||||
and math.isfinite(float(value))
|
||||
)
|
||||
|
||||
|
||||
def _validated_artifact(root: Path, raw: object) -> Path:
|
||||
if not isinstance(raw, dict):
|
||||
raise E46DTemporalFailureAuditError("artifact metadata is invalid")
|
||||
relative = raw.get("path")
|
||||
expected_sha = raw.get("sha256")
|
||||
expected_size = raw.get("byte_length", raw.get("size_bytes"))
|
||||
if (
|
||||
not isinstance(relative, str)
|
||||
or not isinstance(expected_sha, str)
|
||||
or not isinstance(expected_size, int)
|
||||
):
|
||||
raise E46DTemporalFailureAuditError("artifact metadata is invalid")
|
||||
path = (root / relative).resolve(strict=True)
|
||||
if (
|
||||
path.parent != root
|
||||
or path.is_symlink()
|
||||
or not path.is_file()
|
||||
or path.stat().st_size != expected_size
|
||||
or _sha256(path) != expected_sha
|
||||
):
|
||||
raise E46DTemporalFailureAuditError("artifact changed")
|
||||
return path
|
||||
|
||||
|
||||
def _artifact(path: Path, role: str) -> dict[str, object]:
|
||||
return {
|
||||
"path": path.name,
|
||||
"role": role,
|
||||
"media_type": "application/x-ndjson" if path.suffix == ".jsonl" else "application/json",
|
||||
"byte_length": path.stat().st_size,
|
||||
"sha256": _sha256(path),
|
||||
}
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(value, dict):
|
||||
raise E46DTemporalFailureAuditError("JSON document is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _read_jsonl(path: Path):
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
for line in handle:
|
||||
if line.strip():
|
||||
value = json.loads(line)
|
||||
if not isinstance(value, dict):
|
||||
raise E46DTemporalFailureAuditError("JSONL row is invalid")
|
||||
yield value
|
||||
|
||||
|
||||
def _write_json(path: Path, value: object) -> None:
|
||||
path.write_bytes(_canonical_json(value) + b"\n")
|
||||
|
||||
|
||||
def _write_jsonl(path: Path, rows: list[dict[str, Any]]) -> None:
|
||||
with path.open("wb") as handle:
|
||||
for row in rows:
|
||||
handle.write(_canonical_json(row) + b"\n")
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for block in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
@@ -0,0 +1,772 @@
|
||||
"""Admit one stock NVIDIA detector/tracker replay as immutable E46E evidence.
|
||||
|
||||
The module deliberately contains no association, hold, stitch, NMS, or tracking
|
||||
logic. It only validates and projects DeepStream detector/NvDCF KITTI output
|
||||
onto the exact recorded RIGHT-camera timeline.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import uuid
|
||||
from collections import Counter, defaultdict
|
||||
from collections.abc import Iterable
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
E46E_PROFILE_SCHEMA: Final = "missioncore.e46e-ready-stack-profile/v1"
|
||||
E46E_RUNTIME_SCHEMA: Final = "missioncore.e46e-deepstream-runtime/v1"
|
||||
E46E_RESULT_SCHEMA: Final = "missioncore.e46e-ready-stack-result/v1"
|
||||
E46E_REPORT_SCHEMA: Final = "missioncore.e46e-ready-stack-report/v1"
|
||||
E46E_FRAME_SCHEMA: Final = "missioncore.e46e-ready-stack-frame/v1"
|
||||
E46E_PACKAGE_SCHEMA: Final = "missioncore.e46e-worker-package/v1"
|
||||
E46E_MANIFEST_NAME: Final = "manifest.json"
|
||||
E46E_REPORT_NAME: Final = "ready-stack-report.json"
|
||||
E46E_FRAMES_NAME: Final = "tracked-frames.jsonl"
|
||||
E46E_OVERLAY_NAME: Final = "overlay.mp4"
|
||||
E46E_RUNTIME_NAME: Final = "runtime.json"
|
||||
E46E_LOG_NAME: Final = "deepstream.log"
|
||||
|
||||
_RESULT_ID = re.compile(r"^e46e-ready-stack-[a-f0-9]{64}$")
|
||||
_KITTI_NAME = re.compile(r"^\d{2}_\d{3}_(\d{6})\.txt$")
|
||||
_AUTHORITY: Final = {
|
||||
"ground_truth": False,
|
||||
"independent_truth": False,
|
||||
"metric_grade_reference": False,
|
||||
"candidate_accepted": False,
|
||||
"free_space_authority": False,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
|
||||
class E46EReadyStackError(ValueError):
|
||||
"""Raised when source, raw NVIDIA output, or immutable result is invalid."""
|
||||
|
||||
|
||||
def build_e46e_ready_stack(
|
||||
*, source_job_root: Path, raw_root: Path, profile_path: Path, output_root: Path
|
||||
) -> dict[str, Any]:
|
||||
"""Validate raw DeepStream output and freeze a content-addressed result."""
|
||||
|
||||
profile_source = profile_path.resolve(strict=True)
|
||||
profile = _read_json(profile_source)
|
||||
_validate_profile(profile)
|
||||
source = _read_source(source_job_root.resolve(strict=True), profile)
|
||||
raw = raw_root.resolve(strict=True)
|
||||
if raw.is_symlink():
|
||||
raise E46EReadyStackError("E46E raw root must not be a symlink")
|
||||
runtime = _read_json(raw / E46E_RUNTIME_NAME)
|
||||
_validate_runtime(runtime, profile)
|
||||
overlay = _regular_file(raw / E46E_OVERLAY_NAME)
|
||||
log = _regular_file(raw / E46E_LOG_NAME, allow_empty=True)
|
||||
if runtime["overlay_sha256"] != _sha256(overlay):
|
||||
raise E46EReadyStackError("E46E runtime overlay identity changed")
|
||||
detector_files = _indexed_kitti_files(raw / "detections", source["frame_count"])
|
||||
tracker_files = _indexed_kitti_files(raw / "tracks", source["frame_count"])
|
||||
|
||||
frames: list[dict[str, Any]] = []
|
||||
for frame_index, source_row in enumerate(source["index"]):
|
||||
detections = _parse_detector_file(detector_files[frame_index])
|
||||
objects = _parse_tracker_file(tracker_files[frame_index])
|
||||
frames.append(
|
||||
{
|
||||
"schema_version": E46E_FRAME_SCHEMA,
|
||||
"frame_index": frame_index,
|
||||
"sequence": int(source_row["sequence"]),
|
||||
"session_seconds": source["timeline_start_seconds"]
|
||||
+ (
|
||||
int(source_row["session_monotonic_ns"])
|
||||
- source["first_session_monotonic_ns"]
|
||||
)
|
||||
/ 1_000_000_000.0,
|
||||
"source_image_sha256": str(source_row["sha256"]),
|
||||
"detection_count": len(detections),
|
||||
"tracked_object_count": len(objects),
|
||||
"detections": detections,
|
||||
"objects": objects,
|
||||
}
|
||||
)
|
||||
metrics = analyze_e46e_frames(frames)
|
||||
method = {
|
||||
"schema_version": "missioncore.laboratory-method/v1",
|
||||
"completeness": "complete",
|
||||
"execution_class": "hybrid",
|
||||
"pipeline_id": str(profile["profile_id"]),
|
||||
"components": [
|
||||
{
|
||||
"kind": "source",
|
||||
"name": str(profile["source"]["job_id"]),
|
||||
"version": "immutable recorded RIGHT replay",
|
||||
"role": "exact recorded camera evidence",
|
||||
"identity_sha256": source["job_sha256"],
|
||||
},
|
||||
{
|
||||
"kind": "model",
|
||||
"name": str(profile["detector"]["name"]),
|
||||
"version": str(profile["detector"]["version"]),
|
||||
"role": "framewise traffic-object detection",
|
||||
"identity_sha256": str(profile["detector"]["model_sha256"]),
|
||||
},
|
||||
{
|
||||
"kind": "tool",
|
||||
"name": str(profile["parser"]["name"]),
|
||||
"version": str(profile["parser"]["commit"]),
|
||||
"role": "official RT-DETR output decoding",
|
||||
"identity_sha256": str(runtime["parser_library_sha256"]),
|
||||
},
|
||||
{
|
||||
"kind": "algorithm",
|
||||
"name": str(profile["tracker"]["name"]),
|
||||
"version": str(profile["tracker"]["configuration"]),
|
||||
"role": "route-local temporal association",
|
||||
"identity_sha256": str(runtime["tracker_config_sha256"]),
|
||||
},
|
||||
{
|
||||
"kind": "runtime",
|
||||
"name": "NVIDIA DeepStream",
|
||||
"version": str(profile["runtime"]["deepstream_version"]),
|
||||
"role": "GPU inference and media pipeline",
|
||||
"identity_sha256": str(runtime["container_image_digest"]),
|
||||
},
|
||||
],
|
||||
}
|
||||
report_basis = {
|
||||
"schema_version": E46E_REPORT_SCHEMA,
|
||||
"status": "completed-stock-nvidia-recorded-right-replay",
|
||||
"metrics": metrics,
|
||||
"acceptance": {
|
||||
"full_route_accounted": metrics["frame_count"]
|
||||
== int(profile["source"]["segment_count"]),
|
||||
"stock_detector_tracker_executed": True,
|
||||
"visual_overlay_available": True,
|
||||
"independent_truth_available": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
"decision": {
|
||||
"ready_stack_baseline_available": True,
|
||||
"custom_temporal_logic_used": False,
|
||||
"next_action": (
|
||||
"review the full overlay and compare the same objective failure metrics "
|
||||
"against the frozen YOLOX custom-temporal baseline before promotion"
|
||||
),
|
||||
},
|
||||
"method": method,
|
||||
"limitations": [
|
||||
(
|
||||
"TrafficCamNet Transformer Lite has four traffic classes; detections outside "
|
||||
"bicycle, car, person, and road_sign are not claimed"
|
||||
),
|
||||
(
|
||||
"NvDCF IDs are route-local tracker identities, not permanent physical identities"
|
||||
),
|
||||
(
|
||||
f"{metrics['track_box_clipped_count']} stock NvDCF observations crossed the "
|
||||
"800x600 source boundary; the Mission Core evidence adapter clips only their "
|
||||
"display geometry and preserves every raw LTRB coordinate, ID, class, and score"
|
||||
),
|
||||
(
|
||||
"this is recorded RIGHT-camera evidence only; no LEFT camera, live hardware, "
|
||||
"free-space, command, navigation, or safety authority is introduced"
|
||||
),
|
||||
(
|
||||
"without independent frame truth, counts describe output continuity and cannot "
|
||||
"alone establish precision or recall"
|
||||
),
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
"ground_truth": False,
|
||||
}
|
||||
raw_identity = {
|
||||
"runtime_schema": runtime["schema_version"],
|
||||
"worker_host": runtime["worker_host"],
|
||||
"gpu_name": runtime["gpu_name"],
|
||||
"container_image": runtime["container_image"],
|
||||
"container_image_digest": runtime["container_image_digest"],
|
||||
"model_sha256": runtime["model_sha256"],
|
||||
"model_engine_sha256": runtime["model_engine_sha256"],
|
||||
"deepstream_config_sha256": runtime["deepstream_config_sha256"],
|
||||
"detector_config_sha256": runtime["detector_config_sha256"],
|
||||
"parser_library_sha256": runtime["parser_library_sha256"],
|
||||
"tracker_config_sha256": runtime["tracker_config_sha256"],
|
||||
"input_stream_sha256": runtime["input_stream_sha256"],
|
||||
"overlay_sha256": _sha256(overlay),
|
||||
"deepstream_log_sha256": _sha256(log),
|
||||
}
|
||||
identity = {
|
||||
"schema_version": E46E_RESULT_SCHEMA,
|
||||
"source": source["identity"],
|
||||
"profile_sha256": _sha256(profile_source),
|
||||
"profile": copy.deepcopy(profile),
|
||||
"raw_execution": raw_identity,
|
||||
"report_sha256": hashlib.sha256(_canonical_json(report_basis)).hexdigest(),
|
||||
"frames_sha256": hashlib.sha256(_canonical_json(frames)).hexdigest(),
|
||||
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
result_id = f"e46e-ready-stack-{identity_sha256}"
|
||||
destination = output_root.expanduser().absolute() / result_id
|
||||
if destination.exists():
|
||||
return read_e46e_ready_stack(destination)
|
||||
|
||||
created_at_utc = datetime.now(UTC).isoformat(timespec="milliseconds").replace(
|
||||
"+00:00", "Z"
|
||||
)
|
||||
report = {
|
||||
**report_basis,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"created_at_utc": created_at_utc,
|
||||
"source_session_id": str(profile["source"]["session_id"]),
|
||||
"camera_source_id": str(profile["source"]["camera_source_id"]),
|
||||
}
|
||||
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
_write_json(staging / E46E_REPORT_NAME, report)
|
||||
_write_jsonl(staging / E46E_FRAMES_NAME, frames)
|
||||
shutil.copyfile(overlay, staging / E46E_OVERLAY_NAME)
|
||||
shutil.copyfile(raw / E46E_RUNTIME_NAME, staging / E46E_RUNTIME_NAME)
|
||||
shutil.copyfile(log, staging / E46E_LOG_NAME)
|
||||
artifacts = [
|
||||
_artifact(staging / E46E_REPORT_NAME, "ready-stack-report"),
|
||||
_artifact(staging / E46E_FRAMES_NAME, "tracked-frames"),
|
||||
_artifact(staging / E46E_OVERLAY_NAME, "visual-overlay-video"),
|
||||
_artifact(staging / E46E_RUNTIME_NAME, "runtime-record"),
|
||||
_artifact(staging / E46E_LOG_NAME, "deepstream-log"),
|
||||
]
|
||||
_write_json(
|
||||
staging / E46E_MANIFEST_NAME,
|
||||
{
|
||||
"schema_version": E46E_RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": created_at_utc,
|
||||
"acceptance_state": "stock-ready-stack-recorded-diagnostic",
|
||||
"ground_truth": False,
|
||||
"artifacts": artifacts,
|
||||
"authority": _AUTHORITY,
|
||||
},
|
||||
)
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
return read_e46e_ready_stack(destination)
|
||||
|
||||
|
||||
def read_e46e_ready_stack(root: Path) -> dict[str, Any]:
|
||||
"""Read and fully validate an immutable E46E result."""
|
||||
|
||||
resolved = root.resolve(strict=True)
|
||||
if resolved.is_symlink():
|
||||
raise E46EReadyStackError("E46E result root must not be a symlink")
|
||||
manifest = _read_json(resolved / E46E_MANIFEST_NAME)
|
||||
identity = manifest.get("identity")
|
||||
digest = (
|
||||
hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
if isinstance(identity, dict)
|
||||
else ""
|
||||
)
|
||||
if (
|
||||
manifest.get("schema_version") != E46E_RESULT_SCHEMA
|
||||
or manifest.get("result_id") != f"e46e-ready-stack-{digest}"
|
||||
or manifest.get("identity_sha256") != digest
|
||||
or resolved.name != manifest.get("result_id")
|
||||
or _RESULT_ID.fullmatch(resolved.name) is None
|
||||
or manifest.get("authority") != _AUTHORITY
|
||||
or manifest.get("ground_truth") is not False
|
||||
):
|
||||
raise E46EReadyStackError("E46E result identity is invalid")
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(artifacts, list) or len(artifacts) != 5:
|
||||
raise E46EReadyStackError("E46E artifact inventory is invalid")
|
||||
by_role = {row.get("role"): row for row in artifacts if isinstance(row, dict)}
|
||||
paths = {
|
||||
role: _validated_artifact(resolved, by_role.get(role))
|
||||
for role in (
|
||||
"ready-stack-report",
|
||||
"tracked-frames",
|
||||
"visual-overlay-video",
|
||||
"runtime-record",
|
||||
"deepstream-log",
|
||||
)
|
||||
}
|
||||
report = _read_json(paths["ready-stack-report"])
|
||||
frames = tuple(_read_jsonl(paths["tracked-frames"]))
|
||||
runtime = _read_json(paths["runtime-record"])
|
||||
if (
|
||||
report.get("schema_version") != E46E_REPORT_SCHEMA
|
||||
or report.get("result_id") != resolved.name
|
||||
or report.get("authority") != _AUTHORITY
|
||||
or report.get("ground_truth") is not False
|
||||
or runtime.get("schema_version") != E46E_RUNTIME_SCHEMA
|
||||
or any(row.get("schema_version") != E46E_FRAME_SCHEMA for row in frames)
|
||||
or hashlib.sha256(_canonical_json(frames)).hexdigest()
|
||||
!= identity.get("frames_sha256")
|
||||
or report.get("metrics", {}).get("frame_count") != len(frames)
|
||||
):
|
||||
raise E46EReadyStackError("E46E result changed")
|
||||
return {
|
||||
"result_id": resolved.name,
|
||||
"result_root": resolved,
|
||||
"manifest": manifest,
|
||||
"report": report,
|
||||
"frames": frames,
|
||||
"runtime": runtime,
|
||||
"overlay_path": paths["visual-overlay-video"],
|
||||
}
|
||||
|
||||
|
||||
def analyze_e46e_frames(frames: Iterable[dict[str, Any]]) -> dict[str, Any]:
|
||||
"""Calculate objective continuity metrics without modifying tracker output."""
|
||||
|
||||
rows = list(frames)
|
||||
if not rows:
|
||||
raise E46EReadyStackError("E46E requires at least one frame")
|
||||
detection_observations = 0
|
||||
track_observations = 0
|
||||
detection_box_clips = 0
|
||||
track_box_clips = 0
|
||||
zero_detection_frames = 0
|
||||
zero_track_frames = 0
|
||||
recovered_frames = 0
|
||||
detector_classes: Counter[str] = Counter()
|
||||
tracker_classes: Counter[str] = Counter()
|
||||
observations: dict[int, list[tuple[int, str]]] = defaultdict(list)
|
||||
track_counts: list[int] = []
|
||||
for expected, frame in enumerate(rows):
|
||||
if frame.get("frame_index") != expected:
|
||||
raise E46EReadyStackError("E46E frame sequence is not contiguous")
|
||||
detections = frame.get("detections")
|
||||
objects = frame.get("objects")
|
||||
if not isinstance(detections, list) or not isinstance(objects, list):
|
||||
raise E46EReadyStackError("E46E frame payload is invalid")
|
||||
detection_observations += len(detections)
|
||||
track_observations += len(objects)
|
||||
detection_box_clips += sum(
|
||||
item.get("source_plane_clipped") is True for item in detections
|
||||
)
|
||||
track_box_clips += sum(
|
||||
item.get("source_plane_clipped") is True for item in objects
|
||||
)
|
||||
zero_detection_frames += not detections
|
||||
zero_track_frames += not objects
|
||||
recovered_frames += not detections and bool(objects)
|
||||
track_counts.append(len(objects))
|
||||
detector_classes.update(str(item["class_name"]) for item in detections)
|
||||
tracker_classes.update(str(item["class_name"]) for item in objects)
|
||||
for item in objects:
|
||||
observations[int(item["source_track_id"])].append(
|
||||
(expected, str(item["class_name"]))
|
||||
)
|
||||
|
||||
route_gap_events = 0
|
||||
short_tracks = 0
|
||||
class_switches = 0
|
||||
for track_rows in observations.values():
|
||||
ordered = sorted(track_rows)
|
||||
short_tracks += len(ordered) <= 3
|
||||
route_gap_events += sum(
|
||||
next_frame - frame > 1
|
||||
for (frame, _), (next_frame, _) in zip(
|
||||
ordered, ordered[1:], strict=False
|
||||
)
|
||||
)
|
||||
class_switches += sum(
|
||||
class_name != next_class
|
||||
for (_, class_name), (_, next_class) in zip(
|
||||
ordered, ordered[1:], strict=False
|
||||
)
|
||||
)
|
||||
|
||||
blackouts = 0
|
||||
start: int | None = None
|
||||
for index, count in enumerate(track_counts + [1]):
|
||||
if count == 0 and start is None:
|
||||
start = index
|
||||
elif count > 0 and start is not None:
|
||||
if start > 0 and index < len(track_counts) and index - start >= 3:
|
||||
blackouts += 1
|
||||
start = None
|
||||
duration = float(rows[-1]["session_seconds"]) - float(rows[0]["session_seconds"])
|
||||
return {
|
||||
"frame_count": len(rows),
|
||||
"route_duration_seconds": round(max(0.0, duration), 6),
|
||||
"detection_observation_count": detection_observations,
|
||||
"track_observation_count": track_observations,
|
||||
"detection_box_clipped_count": detection_box_clips,
|
||||
"track_box_clipped_count": track_box_clips,
|
||||
"unique_track_count": len(observations),
|
||||
"mean_tracked_objects_per_frame": round(track_observations / len(rows), 6),
|
||||
"zero_detection_frame_count": zero_detection_frames,
|
||||
"zero_track_frame_count": zero_track_frames,
|
||||
"tracker_recovered_frame_count": recovered_frames,
|
||||
"full_layer_blackout_event_count": blackouts,
|
||||
"route_id_gap_event_count": route_gap_events,
|
||||
"short_track_count": short_tracks,
|
||||
"short_track_fraction": round(short_tracks / max(1, len(observations)), 6),
|
||||
"track_class_switch_count": class_switches,
|
||||
"detector_class_observations": dict(sorted(detector_classes.items())),
|
||||
"tracker_class_observations": dict(sorted(tracker_classes.items())),
|
||||
}
|
||||
|
||||
|
||||
def _read_source(root: Path, profile: dict[str, Any]) -> dict[str, Any]:
|
||||
job_path = _regular_file(root / "job.json")
|
||||
job = _read_json(job_path)
|
||||
expected = profile["source"]
|
||||
input_value = job.get("input")
|
||||
if not isinstance(input_value, dict):
|
||||
raise E46EReadyStackError("E46E source job input is invalid")
|
||||
camera = root / "input" / "camera" / str(expected["camera_source_id"]) / "epoch-1"
|
||||
summary_path = _regular_file(camera / "summary.json")
|
||||
index_path = _regular_file(camera / "index.jsonl")
|
||||
summary = _read_json(summary_path)
|
||||
index = list(_read_jsonl(index_path))
|
||||
frame_count = int(expected["segment_count"])
|
||||
timeline = input_value.get("timeline")
|
||||
if (
|
||||
job.get("schema_version") != "missioncore.compute-job/v1"
|
||||
or job.get("job_id") != expected["job_id"]
|
||||
or input_value.get("session_id") != expected["session_id"]
|
||||
or input_value.get("source_id") != expected["camera_source_id"]
|
||||
or input_value.get("segment_count") != frame_count
|
||||
or input_value.get("archive_index_sha256") != expected["archive_index_sha256"]
|
||||
or input_value.get("archive_summary_sha256") != expected["archive_summary_sha256"]
|
||||
or summary.get("stream_sha256") != expected["stream_sha256"]
|
||||
or summary.get("segment_count") != frame_count
|
||||
or _sha256(index_path) != expected["archive_index_sha256"]
|
||||
or _sha256(summary_path) != expected["archive_summary_sha256"]
|
||||
or not isinstance(timeline, dict)
|
||||
or not isinstance(timeline.get("start_seconds"), (int, float))
|
||||
or len(index) != frame_count
|
||||
):
|
||||
raise E46EReadyStackError("E46E exact recorded source binding changed")
|
||||
for position, row in enumerate(index, start=1):
|
||||
if (
|
||||
row.get("schema_version") != "missioncore.camera-recording-index/v1"
|
||||
or row.get("kind") != "media"
|
||||
or row.get("sequence") != position
|
||||
or not isinstance(row.get("session_monotonic_ns"), int)
|
||||
or not _is_sha256(row.get("sha256"))
|
||||
):
|
||||
raise E46EReadyStackError("E46E source index is invalid")
|
||||
return {
|
||||
"frame_count": frame_count,
|
||||
"index": index,
|
||||
"timeline_start_seconds": float(timeline["start_seconds"]),
|
||||
"first_session_monotonic_ns": int(index[0]["session_monotonic_ns"]),
|
||||
"job_sha256": _sha256(job_path),
|
||||
"identity": {
|
||||
"job_id": expected["job_id"],
|
||||
"job_sha256": _sha256(job_path),
|
||||
"session_id": expected["session_id"],
|
||||
"camera_source_id": expected["camera_source_id"],
|
||||
"stream_sha256": expected["stream_sha256"],
|
||||
"archive_index_sha256": expected["archive_index_sha256"],
|
||||
"archive_summary_sha256": expected["archive_summary_sha256"],
|
||||
"frame_count": frame_count,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _validate_profile(profile: dict[str, Any]) -> None:
|
||||
try:
|
||||
source = profile["source"]
|
||||
runtime = profile["runtime"]
|
||||
detector = profile["detector"]
|
||||
parser = profile["parser"]
|
||||
tracker = profile["tracker"]
|
||||
output = profile["output"]
|
||||
except KeyError as exc:
|
||||
raise E46EReadyStackError("E46E profile is incomplete") from exc
|
||||
if (
|
||||
profile.get("schema_version") != E46E_PROFILE_SCHEMA
|
||||
or profile.get("authority") != {
|
||||
"ground_truth": False,
|
||||
"candidate_accepted": False,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
or not _is_sha256(source.get("stream_sha256"))
|
||||
or not _is_sha256(source.get("archive_index_sha256"))
|
||||
or not _is_sha256(source.get("archive_summary_sha256"))
|
||||
or not str(runtime.get("container_image", "")).startswith(
|
||||
"nvcr.io/nvidia/deepstream:9.1-samples-multiarch@sha256:"
|
||||
)
|
||||
or not _is_sha256(detector.get("model_sha256"))
|
||||
or detector.get("custom_postprocessing") is not False
|
||||
or parser.get("repository") != "https://github.com/NVIDIA/DeepStream.git"
|
||||
or parser.get("symbol") != "NvDsInferParseCustomDDETRTAO"
|
||||
or not _is_sha256(parser.get("library_sha256"))
|
||||
or parser.get("custom_mission_core_logic") is not False
|
||||
or tracker.get("custom_association") is not False
|
||||
or tracker.get("custom_hold_or_stitch") is not False
|
||||
or output.get("frame_width") != 800
|
||||
or output.get("frame_height") != 600
|
||||
):
|
||||
raise E46EReadyStackError("E46E profile contract is invalid")
|
||||
|
||||
|
||||
def _validate_runtime(runtime: dict[str, Any], profile: dict[str, Any]) -> None:
|
||||
expected_image = str(profile["runtime"]["container_image"])
|
||||
expected_digest = expected_image.rsplit("@sha256:", 1)[1]
|
||||
required_sha = (
|
||||
"container_image_digest",
|
||||
"model_sha256",
|
||||
"model_engine_sha256",
|
||||
"deepstream_config_sha256",
|
||||
"detector_config_sha256",
|
||||
"parser_library_sha256",
|
||||
"tracker_config_sha256",
|
||||
"input_stream_sha256",
|
||||
"overlay_sha256",
|
||||
)
|
||||
if (
|
||||
runtime.get("schema_version") != E46E_RUNTIME_SCHEMA
|
||||
or runtime.get("status") != "completed"
|
||||
or runtime.get("container_image") != expected_image
|
||||
or runtime.get("container_image_digest") != expected_digest
|
||||
or runtime.get("model_sha256") != profile["detector"]["model_sha256"]
|
||||
or runtime.get("parser_library_sha256") != profile["parser"]["library_sha256"]
|
||||
or runtime.get("input_stream_sha256") != profile["source"]["stream_sha256"]
|
||||
or not isinstance(runtime.get("worker_host"), str)
|
||||
or not runtime.get("worker_host")
|
||||
or not isinstance(runtime.get("gpu_name"), str)
|
||||
or not runtime.get("gpu_name")
|
||||
or any(not _is_sha256(runtime.get(name)) for name in required_sha)
|
||||
):
|
||||
raise E46EReadyStackError("E46E runtime identity is invalid")
|
||||
|
||||
|
||||
def _indexed_kitti_files(root: Path, frame_count: int) -> dict[int, Path]:
|
||||
resolved = root.resolve(strict=True)
|
||||
if not resolved.is_dir() or resolved.is_symlink():
|
||||
raise E46EReadyStackError("E46E KITTI directory is invalid")
|
||||
indexed: dict[int, Path] = {}
|
||||
for path in resolved.iterdir():
|
||||
if not path.is_file() or path.is_symlink():
|
||||
raise E46EReadyStackError("E46E KITTI member is invalid")
|
||||
match = _KITTI_NAME.fullmatch(path.name)
|
||||
if match is None:
|
||||
raise E46EReadyStackError("E46E KITTI filename is invalid")
|
||||
frame_index = int(match.group(1))
|
||||
if frame_index in indexed or frame_index >= frame_count:
|
||||
raise E46EReadyStackError("E46E KITTI frame inventory is invalid")
|
||||
indexed[frame_index] = path
|
||||
if set(indexed) != set(range(frame_count)):
|
||||
raise E46EReadyStackError("E46E KITTI frame coverage is incomplete")
|
||||
return indexed
|
||||
|
||||
|
||||
def _parse_detector_file(path: Path) -> list[dict[str, Any]]:
|
||||
output: list[dict[str, Any]] = []
|
||||
for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
|
||||
tokens = line.split()
|
||||
if len(tokens) != 16:
|
||||
raise E46EReadyStackError(f"invalid detector KITTI row {path.name}:{line_number}")
|
||||
box, source_box, clipped = _parse_box(tokens[4:8], path, line_number)
|
||||
confidence = _finite_float(tokens[15], path, line_number)
|
||||
output.append(
|
||||
{
|
||||
"class_name": tokens[0],
|
||||
"bbox": box,
|
||||
"source_bbox_ltrb": source_box,
|
||||
"source_plane_clipped": clipped,
|
||||
"confidence": confidence,
|
||||
"provenance": "nvidia-trafficcamnet-rtdetr",
|
||||
}
|
||||
)
|
||||
return output
|
||||
|
||||
|
||||
def _parse_tracker_file(path: Path) -> list[dict[str, Any]]:
|
||||
output: list[dict[str, Any]] = []
|
||||
seen: dict[int, dict[str, Any]] = {}
|
||||
for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
|
||||
tokens = line.split()
|
||||
if len(tokens) < 17:
|
||||
raise E46EReadyStackError(f"invalid tracker KITTI row {path.name}:{line_number}")
|
||||
try:
|
||||
source_track_id = int(tokens[1])
|
||||
except ValueError as exc:
|
||||
raise E46EReadyStackError(
|
||||
f"invalid tracker id {path.name}:{line_number}"
|
||||
) from exc
|
||||
if source_track_id < 0:
|
||||
raise E46EReadyStackError("negative NvDCF track id")
|
||||
box, source_box, clipped = _parse_box(tokens[5:9], path, line_number)
|
||||
item = {
|
||||
"object_id": f"nvdcf-{source_track_id}",
|
||||
"source_track_id": source_track_id,
|
||||
"class_name": tokens[0],
|
||||
"bbox": box,
|
||||
"source_bbox_ltrb": source_box,
|
||||
"source_plane_clipped": clipped,
|
||||
"confidence": _finite_float(tokens[16], path, line_number),
|
||||
"provenance": "nvidia-nvdcf-stock-output",
|
||||
}
|
||||
previous = seen.get(source_track_id)
|
||||
if previous is not None and previous != item:
|
||||
raise E46EReadyStackError(
|
||||
f"conflicting NvDCF track rows {path.name}:{source_track_id}"
|
||||
)
|
||||
if previous is None:
|
||||
seen[source_track_id] = item
|
||||
output.append(item)
|
||||
return output
|
||||
|
||||
|
||||
def _parse_box(
|
||||
tokens: list[str], path: Path, line_number: int
|
||||
) -> tuple[list[float], list[float], bool]:
|
||||
left, top, right, bottom = (
|
||||
_finite_float(token, path, line_number) for token in tokens
|
||||
)
|
||||
if right <= left or bottom <= top:
|
||||
raise E46EReadyStackError(f"invalid bounding box {path.name}:{line_number}")
|
||||
projected_left = max(0.0, min(800.0, left))
|
||||
projected_top = max(0.0, min(600.0, top))
|
||||
projected_right = max(0.0, min(800.0, right))
|
||||
projected_bottom = max(0.0, min(600.0, bottom))
|
||||
if projected_right <= projected_left or projected_bottom <= projected_top:
|
||||
raise E46EReadyStackError(f"bounding box outside source plane {path.name}:{line_number}")
|
||||
source = [left, top, right, bottom]
|
||||
projected = [
|
||||
projected_left,
|
||||
projected_top,
|
||||
projected_right - projected_left,
|
||||
projected_bottom - projected_top,
|
||||
]
|
||||
return projected, source, source != [
|
||||
projected_left,
|
||||
projected_top,
|
||||
projected_right,
|
||||
projected_bottom,
|
||||
]
|
||||
|
||||
|
||||
def _finite_float(token: str, path: Path, line_number: int) -> float:
|
||||
try:
|
||||
value = float(token)
|
||||
except ValueError as exc:
|
||||
raise E46EReadyStackError(f"invalid float {path.name}:{line_number}") from exc
|
||||
if not math.isfinite(value):
|
||||
raise E46EReadyStackError(f"non-finite float {path.name}:{line_number}")
|
||||
return value
|
||||
|
||||
|
||||
def _validated_artifact(root: Path, row: object) -> Path:
|
||||
if not isinstance(row, dict):
|
||||
raise E46EReadyStackError("E46E artifact descriptor is invalid")
|
||||
relative = row.get("path")
|
||||
if (
|
||||
not isinstance(relative, str)
|
||||
or Path(relative).is_absolute()
|
||||
or ".." in Path(relative).parts
|
||||
):
|
||||
raise E46EReadyStackError("E46E artifact path is invalid")
|
||||
path = root / relative
|
||||
if (
|
||||
not path.is_file()
|
||||
or path.is_symlink()
|
||||
or row.get("byte_length") != path.stat().st_size
|
||||
or row.get("sha256") != _sha256(path)
|
||||
):
|
||||
raise E46EReadyStackError("E46E artifact changed")
|
||||
return path
|
||||
|
||||
|
||||
def _artifact(path: Path, role: str) -> dict[str, Any]:
|
||||
return {
|
||||
"role": role,
|
||||
"path": path.name,
|
||||
"byte_length": path.stat().st_size,
|
||||
"sha256": _sha256(path),
|
||||
}
|
||||
|
||||
|
||||
def _regular_file(path: Path, *, allow_empty: bool = False) -> Path:
|
||||
resolved = path.resolve(strict=True)
|
||||
if (
|
||||
not resolved.is_file()
|
||||
or resolved.is_symlink()
|
||||
or (not allow_empty and resolved.stat().st_size == 0)
|
||||
):
|
||||
raise E46EReadyStackError(f"E46E file is invalid: {path.name}")
|
||||
return resolved
|
||||
|
||||
|
||||
def _is_sha256(value: object) -> bool:
|
||||
return isinstance(value, str) and re.fullmatch(r"[a-f0-9]{64}", value) is not None
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False).encode()
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
raise E46EReadyStackError(f"invalid JSON: {path.name}") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise E46EReadyStackError(f"JSON object expected: {path.name}")
|
||||
return value
|
||||
|
||||
|
||||
def _read_jsonl(path: Path) -> Iterable[dict[str, Any]]:
|
||||
with path.open("r", encoding="utf-8-sig") as stream:
|
||||
for line_number, line in enumerate(stream, 1):
|
||||
try:
|
||||
row = json.loads(line)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise E46EReadyStackError(
|
||||
f"invalid JSONL: {path.name}:{line_number}"
|
||||
) from exc
|
||||
if not isinstance(row, dict):
|
||||
raise E46EReadyStackError(f"JSON object expected: {path.name}:{line_number}")
|
||||
yield row
|
||||
|
||||
|
||||
def _write_json(path: Path, value: object) -> None:
|
||||
with path.open("x", encoding="utf-8") as stream:
|
||||
json.dump(value, stream, ensure_ascii=False, indent=2, allow_nan=False)
|
||||
stream.write("\n")
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
|
||||
|
||||
def _write_jsonl(path: Path, rows: Iterable[dict[str, Any]]) -> None:
|
||||
with path.open("x", encoding="utf-8") as stream:
|
||||
for row in rows:
|
||||
stream.write(
|
||||
json.dumps(
|
||||
row,
|
||||
ensure_ascii=False,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
)
|
||||
)
|
||||
stream.write("\n")
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
@@ -0,0 +1,433 @@
|
||||
"""Freeze an NVIDIA DashCamNet versus E46E detector-only bake-off as E46F.
|
||||
|
||||
The replay, DeepStream image, FP16 precision, NvDCF configuration, source
|
||||
plane, and evidence adapter are held constant. Only the stock NVIDIA detector
|
||||
and its stock provider post-processing change. Mission Core performs no NMS,
|
||||
association, hold, stitch, or semantic correction in this module.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
from k1link.compute.e46e_ready_stack import (
|
||||
E46EReadyStackError,
|
||||
_artifact,
|
||||
_canonical_json,
|
||||
_indexed_kitti_files,
|
||||
_parse_detector_file,
|
||||
_parse_tracker_file,
|
||||
_read_json,
|
||||
_read_jsonl,
|
||||
_read_source,
|
||||
_regular_file,
|
||||
_sha256,
|
||||
_validated_artifact,
|
||||
_write_json,
|
||||
_write_jsonl,
|
||||
analyze_e46e_frames,
|
||||
)
|
||||
|
||||
E46F_PROFILE_SCHEMA: Final = "missioncore.e46f-dashcam-bakeoff-profile/v1"
|
||||
E46F_RUNTIME_SCHEMA: Final = "missioncore.e46f-dashcam-deepstream-runtime/v1"
|
||||
E46F_RESULT_SCHEMA: Final = "missioncore.e46f-dashcam-bakeoff-result/v1"
|
||||
E46F_REPORT_SCHEMA: Final = "missioncore.e46f-dashcam-bakeoff-report/v1"
|
||||
E46F_FRAME_SCHEMA: Final = "missioncore.e46f-dashcam-bakeoff-frame/v1"
|
||||
E46F_PACKAGE_SCHEMA: Final = "missioncore.e46f-worker-package/v1"
|
||||
E46F_MANIFEST_NAME: Final = "manifest.json"
|
||||
E46F_REPORT_NAME: Final = "dashcam-bakeoff-report.json"
|
||||
E46F_FRAMES_NAME: Final = "tracked-frames.jsonl"
|
||||
E46F_OVERLAY_NAME: Final = "overlay.mp4"
|
||||
E46F_RUNTIME_NAME: Final = "runtime.json"
|
||||
E46F_LOG_NAME: Final = "deepstream.log"
|
||||
|
||||
_RESULT_ID = re.compile(r"^e46f-dashcam-bakeoff-[a-f0-9]{64}$")
|
||||
_AUTHORITY: Final = {
|
||||
"ground_truth": False,
|
||||
"independent_truth": False,
|
||||
"metric_grade_reference": False,
|
||||
"candidate_accepted": False,
|
||||
"free_space_authority": False,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
|
||||
class E46FDashCamBakeoffError(ValueError):
|
||||
"""Raised when the E46F source, execution, or result is invalid."""
|
||||
|
||||
|
||||
def build_e46f_dashcam_bakeoff(
|
||||
*, source_job_root: Path, raw_root: Path, profile_path: Path, output_root: Path
|
||||
) -> dict[str, Any]:
|
||||
"""Validate one stock DashCamNet/NvDCF replay and freeze immutable evidence."""
|
||||
|
||||
try:
|
||||
return _build(
|
||||
source_job_root=source_job_root,
|
||||
raw_root=raw_root,
|
||||
profile_path=profile_path,
|
||||
output_root=output_root,
|
||||
)
|
||||
except E46EReadyStackError as exc:
|
||||
raise E46FDashCamBakeoffError(str(exc).replace("E46E", "E46F")) from exc
|
||||
|
||||
|
||||
def _build(
|
||||
*, source_job_root: Path, raw_root: Path, profile_path: Path, output_root: Path
|
||||
) -> dict[str, Any]:
|
||||
profile_source = profile_path.resolve(strict=True)
|
||||
profile = _read_json(profile_source)
|
||||
_validate_profile(profile)
|
||||
source = _read_source(source_job_root.resolve(strict=True), profile)
|
||||
raw = raw_root.resolve(strict=True)
|
||||
if raw.is_symlink():
|
||||
raise E46FDashCamBakeoffError("E46F raw root must not be a symlink")
|
||||
runtime = _read_json(raw / E46F_RUNTIME_NAME)
|
||||
_validate_runtime(runtime, profile)
|
||||
overlay = _regular_file(raw / E46F_OVERLAY_NAME)
|
||||
log = _regular_file(raw / E46F_LOG_NAME, allow_empty=True)
|
||||
if runtime["overlay_sha256"] != _sha256(overlay):
|
||||
raise E46FDashCamBakeoffError("E46F runtime overlay identity changed")
|
||||
detector_files = _indexed_kitti_files(raw / "detections", source["frame_count"])
|
||||
tracker_files = _indexed_kitti_files(raw / "tracks", source["frame_count"])
|
||||
|
||||
frames: list[dict[str, Any]] = []
|
||||
for frame_index, source_row in enumerate(source["index"]):
|
||||
detections = _parse_detector_file(detector_files[frame_index])
|
||||
for detection in detections:
|
||||
detection["provenance"] = "nvidia-dashcamnet-detectnet-v2"
|
||||
objects = _parse_tracker_file(tracker_files[frame_index])
|
||||
frames.append(
|
||||
{
|
||||
"schema_version": E46F_FRAME_SCHEMA,
|
||||
"frame_index": frame_index,
|
||||
"sequence": int(source_row["sequence"]),
|
||||
"session_seconds": source["timeline_start_seconds"]
|
||||
+ (int(source_row["session_monotonic_ns"]) - source["first_session_monotonic_ns"])
|
||||
/ 1_000_000_000.0,
|
||||
"source_image_sha256": str(source_row["sha256"]),
|
||||
"detection_count": len(detections),
|
||||
"tracked_object_count": len(objects),
|
||||
"detections": detections,
|
||||
"objects": objects,
|
||||
}
|
||||
)
|
||||
metrics = analyze_e46e_frames(frames)
|
||||
method = {
|
||||
"schema_version": "missioncore.laboratory-method/v1",
|
||||
"completeness": "complete",
|
||||
"execution_class": "hybrid",
|
||||
"pipeline_id": str(profile["profile_id"]),
|
||||
"components": [
|
||||
{
|
||||
"kind": "source",
|
||||
"name": str(profile["source"]["job_id"]),
|
||||
"version": "immutable recorded RIGHT replay",
|
||||
"role": "exact E46E/E46F controlled camera evidence",
|
||||
"identity_sha256": source["job_sha256"],
|
||||
},
|
||||
{
|
||||
"kind": "model",
|
||||
"name": str(profile["detector"]["name"]),
|
||||
"version": str(profile["detector"]["version"]),
|
||||
"role": "moving-camera traffic-object detection",
|
||||
"identity_sha256": str(profile["detector"]["model_sha256"]),
|
||||
},
|
||||
{
|
||||
"kind": "tool",
|
||||
"name": str(profile["postprocessor"]["name"]),
|
||||
"version": str(profile["postprocessor"]["reference_commit"]),
|
||||
"role": "stock DetectNet_v2 decode and NMS",
|
||||
"identity_sha256": str(runtime["detector_config_sha256"]),
|
||||
},
|
||||
{
|
||||
"kind": "algorithm",
|
||||
"name": str(profile["tracker"]["name"]),
|
||||
"version": str(profile["tracker"]["configuration"]),
|
||||
"role": "route-local temporal association",
|
||||
"identity_sha256": str(runtime["tracker_config_sha256"]),
|
||||
},
|
||||
{
|
||||
"kind": "runtime",
|
||||
"name": "NVIDIA DeepStream",
|
||||
"version": str(profile["runtime"]["deepstream_version"]),
|
||||
"role": "GPU inference and media pipeline",
|
||||
"identity_sha256": str(runtime["container_image_digest"]),
|
||||
},
|
||||
],
|
||||
}
|
||||
report_basis = {
|
||||
"schema_version": E46F_REPORT_SCHEMA,
|
||||
"status": "completed-stock-nvidia-detector-only-bakeoff",
|
||||
"comparison_contract": copy.deepcopy(profile["comparison_contract"]),
|
||||
"metrics": metrics,
|
||||
"acceptance": {
|
||||
"full_route_accounted": metrics["frame_count"]
|
||||
== int(profile["source"]["segment_count"]),
|
||||
"stock_detector_tracker_executed": True,
|
||||
"controlled_detector_only_change": True,
|
||||
"visual_overlay_available": True,
|
||||
"independent_truth_available": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
"decision": {
|
||||
"dashcam_detector_bakeoff_available": True,
|
||||
"custom_temporal_logic_used": False,
|
||||
"next_action": (
|
||||
"perform full-video semantic review against E46E and retain the detector "
|
||||
"only if moving-camera false positives improve without temporal regression"
|
||||
),
|
||||
},
|
||||
"method": method,
|
||||
"limitations": [
|
||||
(
|
||||
"DashCamNet is evaluated by NVIDIA primarily for car detection; person, "
|
||||
"bicycle, and road_sign quality is not claimed by this LAB"
|
||||
),
|
||||
(
|
||||
"the four-class detector cannot represent stroller, facade, vegetation, "
|
||||
"free-space, or dynamic/static motion state"
|
||||
),
|
||||
"NvDCF IDs are route-local tracker identities, not permanent physical identities",
|
||||
(
|
||||
f"{metrics['track_box_clipped_count']} stock NvDCF observations crossed the "
|
||||
"800x600 source boundary; only display geometry is clipped while raw LTRB, "
|
||||
"ID, class, and score remain preserved"
|
||||
),
|
||||
(
|
||||
"this is recorded RIGHT-camera evidence only; no LEFT camera, live hardware, "
|
||||
"LiDAR fusion, free-space, command, navigation, or safety authority is introduced"
|
||||
),
|
||||
(
|
||||
"without independent full-route truth, output counts and continuity cannot "
|
||||
"establish absolute precision or recall"
|
||||
),
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
"ground_truth": False,
|
||||
}
|
||||
raw_identity = {
|
||||
"runtime_schema": runtime["schema_version"],
|
||||
"worker_host": runtime["worker_host"],
|
||||
"gpu_name": runtime["gpu_name"],
|
||||
"container_image": runtime["container_image"],
|
||||
"container_image_digest": runtime["container_image_digest"],
|
||||
"model_sha256": runtime["model_sha256"],
|
||||
"model_engine_sha256": runtime["model_engine_sha256"],
|
||||
"deepstream_config_sha256": runtime["deepstream_config_sha256"],
|
||||
"detector_config_sha256": runtime["detector_config_sha256"],
|
||||
"tracker_config_sha256": runtime["tracker_config_sha256"],
|
||||
"input_stream_sha256": runtime["input_stream_sha256"],
|
||||
"overlay_sha256": _sha256(overlay),
|
||||
"deepstream_log_sha256": _sha256(log),
|
||||
}
|
||||
identity = {
|
||||
"schema_version": E46F_RESULT_SCHEMA,
|
||||
"source": source["identity"],
|
||||
"profile_sha256": _sha256(profile_source),
|
||||
"profile": copy.deepcopy(profile),
|
||||
"raw_execution": raw_identity,
|
||||
"report_sha256": hashlib.sha256(_canonical_json(report_basis)).hexdigest(),
|
||||
"frames_sha256": hashlib.sha256(_canonical_json(frames)).hexdigest(),
|
||||
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
result_id = f"e46f-dashcam-bakeoff-{identity_sha256}"
|
||||
destination = output_root.expanduser().absolute() / result_id
|
||||
if destination.exists():
|
||||
return read_e46f_dashcam_bakeoff(destination)
|
||||
|
||||
created_at_utc = datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
||||
report = {
|
||||
**report_basis,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"created_at_utc": created_at_utc,
|
||||
"source_session_id": str(profile["source"]["session_id"]),
|
||||
"camera_source_id": str(profile["source"]["camera_source_id"]),
|
||||
}
|
||||
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
_write_json(staging / E46F_REPORT_NAME, report)
|
||||
_write_jsonl(staging / E46F_FRAMES_NAME, frames)
|
||||
shutil.copyfile(overlay, staging / E46F_OVERLAY_NAME)
|
||||
shutil.copyfile(raw / E46F_RUNTIME_NAME, staging / E46F_RUNTIME_NAME)
|
||||
shutil.copyfile(log, staging / E46F_LOG_NAME)
|
||||
artifacts = [
|
||||
_artifact(staging / E46F_REPORT_NAME, "dashcam-bakeoff-report"),
|
||||
_artifact(staging / E46F_FRAMES_NAME, "tracked-frames"),
|
||||
_artifact(staging / E46F_OVERLAY_NAME, "visual-overlay-video"),
|
||||
_artifact(staging / E46F_RUNTIME_NAME, "runtime-record"),
|
||||
_artifact(staging / E46F_LOG_NAME, "deepstream-log"),
|
||||
]
|
||||
_write_json(
|
||||
staging / E46F_MANIFEST_NAME,
|
||||
{
|
||||
"schema_version": E46F_RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": created_at_utc,
|
||||
"acceptance_state": "stock-detector-only-recorded-diagnostic",
|
||||
"ground_truth": False,
|
||||
"artifacts": artifacts,
|
||||
"authority": _AUTHORITY,
|
||||
},
|
||||
)
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
return read_e46f_dashcam_bakeoff(destination)
|
||||
|
||||
|
||||
def read_e46f_dashcam_bakeoff(root: Path) -> dict[str, Any]:
|
||||
"""Read and fully validate an immutable E46F result."""
|
||||
|
||||
try:
|
||||
resolved = root.resolve(strict=True)
|
||||
if resolved.is_symlink():
|
||||
raise E46FDashCamBakeoffError("E46F result root must not be a symlink")
|
||||
manifest = _read_json(resolved / E46F_MANIFEST_NAME)
|
||||
identity = manifest.get("identity")
|
||||
digest = (
|
||||
hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
if isinstance(identity, dict)
|
||||
else ""
|
||||
)
|
||||
if (
|
||||
manifest.get("schema_version") != E46F_RESULT_SCHEMA
|
||||
or manifest.get("result_id") != f"e46f-dashcam-bakeoff-{digest}"
|
||||
or manifest.get("identity_sha256") != digest
|
||||
or resolved.name != manifest.get("result_id")
|
||||
or _RESULT_ID.fullmatch(resolved.name) is None
|
||||
or manifest.get("authority") != _AUTHORITY
|
||||
or manifest.get("ground_truth") is not False
|
||||
):
|
||||
raise E46FDashCamBakeoffError("E46F result identity is invalid")
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(artifacts, list) or len(artifacts) != 5:
|
||||
raise E46FDashCamBakeoffError("E46F artifact inventory is invalid")
|
||||
by_role = {row.get("role"): row for row in artifacts if isinstance(row, dict)}
|
||||
paths = {
|
||||
role: _validated_artifact(resolved, by_role.get(role))
|
||||
for role in (
|
||||
"dashcam-bakeoff-report",
|
||||
"tracked-frames",
|
||||
"visual-overlay-video",
|
||||
"runtime-record",
|
||||
"deepstream-log",
|
||||
)
|
||||
}
|
||||
report = _read_json(paths["dashcam-bakeoff-report"])
|
||||
frames = tuple(_read_jsonl(paths["tracked-frames"]))
|
||||
runtime = _read_json(paths["runtime-record"])
|
||||
if (
|
||||
report.get("schema_version") != E46F_REPORT_SCHEMA
|
||||
or report.get("result_id") != resolved.name
|
||||
or report.get("authority") != _AUTHORITY
|
||||
or report.get("ground_truth") is not False
|
||||
or runtime.get("schema_version") != E46F_RUNTIME_SCHEMA
|
||||
or any(row.get("schema_version") != E46F_FRAME_SCHEMA for row in frames)
|
||||
or hashlib.sha256(_canonical_json(frames)).hexdigest() != identity.get("frames_sha256")
|
||||
or report.get("metrics", {}).get("frame_count") != len(frames)
|
||||
):
|
||||
raise E46FDashCamBakeoffError("E46F result changed")
|
||||
return {
|
||||
"result_id": resolved.name,
|
||||
"result_root": resolved,
|
||||
"manifest": manifest,
|
||||
"report": report,
|
||||
"frames": frames,
|
||||
"runtime": runtime,
|
||||
"overlay_path": paths["visual-overlay-video"],
|
||||
}
|
||||
except E46EReadyStackError as exc:
|
||||
raise E46FDashCamBakeoffError(str(exc).replace("E46E", "E46F")) from exc
|
||||
|
||||
|
||||
def _validate_profile(profile: dict[str, Any]) -> None:
|
||||
try:
|
||||
comparison = profile["comparison_contract"]
|
||||
source = profile["source"]
|
||||
runtime = profile["runtime"]
|
||||
detector = profile["detector"]
|
||||
postprocessor = profile["postprocessor"]
|
||||
tracker = profile["tracker"]
|
||||
output = profile["output"]
|
||||
except KeyError as exc:
|
||||
raise E46FDashCamBakeoffError("E46F profile is incomplete") from exc
|
||||
sha_pattern = re.compile(r"^[a-f0-9]{64}$")
|
||||
if (
|
||||
profile.get("schema_version") != E46F_PROFILE_SCHEMA
|
||||
or comparison.get("controlled_change") != "detector-only"
|
||||
or not re.fullmatch(
|
||||
r"e46e-ready-stack-[a-f0-9]{64}", str(comparison.get("baseline_result_id", ""))
|
||||
)
|
||||
or profile.get("authority")
|
||||
!= {
|
||||
"ground_truth": False,
|
||||
"candidate_accepted": False,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
or any(
|
||||
sha_pattern.fullmatch(str(source.get(key, ""))) is None
|
||||
for key in ("stream_sha256", "archive_index_sha256", "archive_summary_sha256")
|
||||
)
|
||||
or not str(runtime.get("container_image", "")).startswith(
|
||||
"nvcr.io/nvidia/deepstream:9.1-samples-multiarch@sha256:"
|
||||
)
|
||||
or sha_pattern.fullmatch(str(detector.get("model_sha256", ""))) is None
|
||||
or detector.get("custom_postprocessing") is not False
|
||||
or postprocessor.get("cluster_mode") != "NMS"
|
||||
or postprocessor.get("custom_mission_core_logic") is not False
|
||||
or sha_pattern.fullmatch(str(postprocessor.get("reference_config_sha256", ""))) is None
|
||||
or tracker.get("custom_association") is not False
|
||||
or tracker.get("custom_hold_or_stitch") is not False
|
||||
or output.get("frame_width") != 800
|
||||
or output.get("frame_height") != 600
|
||||
):
|
||||
raise E46FDashCamBakeoffError("E46F profile contract is invalid")
|
||||
|
||||
|
||||
def _validate_runtime(runtime: dict[str, Any], profile: dict[str, Any]) -> None:
|
||||
expected_image = str(profile["runtime"]["container_image"])
|
||||
expected_digest = expected_image.rsplit("@sha256:", 1)[1]
|
||||
sha_pattern = re.compile(r"^[a-f0-9]{64}$")
|
||||
required_sha = (
|
||||
"container_image_digest",
|
||||
"model_sha256",
|
||||
"model_engine_sha256",
|
||||
"deepstream_config_sha256",
|
||||
"detector_config_sha256",
|
||||
"tracker_config_sha256",
|
||||
"input_stream_sha256",
|
||||
"overlay_sha256",
|
||||
)
|
||||
if (
|
||||
runtime.get("schema_version") != E46F_RUNTIME_SCHEMA
|
||||
or runtime.get("status") != "completed"
|
||||
or runtime.get("container_image") != expected_image
|
||||
or runtime.get("container_image_digest") != expected_digest
|
||||
or runtime.get("model_sha256") != profile["detector"]["model_sha256"]
|
||||
or runtime.get("input_stream_sha256") != profile["source"]["stream_sha256"]
|
||||
or not isinstance(runtime.get("worker_host"), str)
|
||||
or not runtime.get("worker_host")
|
||||
or not isinstance(runtime.get("gpu_name"), str)
|
||||
or not runtime.get("gpu_name")
|
||||
or any(sha_pattern.fullmatch(str(runtime.get(name, ""))) is None for name in required_sha)
|
||||
):
|
||||
raise E46FDashCamBakeoffError("E46F runtime identity is invalid")
|
||||
@@ -0,0 +1,688 @@
|
||||
"""Freeze the calibrated E46G ready-detector bake-off.
|
||||
|
||||
E46G changes only the camera geometry presented to the two already-qualified
|
||||
NVIDIA detector providers. Factory KB4 calibration is consumed by NVIDIA
|
||||
``nvdewarper`` and every detector run uses stock DeepStream decoding, NMS and
|
||||
NvDCF. This module is an evidence adapter: it validates and publishes output,
|
||||
but contains no detector, suppression, association, hold or stitch logic.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import uuid
|
||||
from collections.abc import Iterable
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
from k1link.compute.e46e_ready_stack import (
|
||||
E46EReadyStackError,
|
||||
_artifact,
|
||||
_canonical_json,
|
||||
_finite_float,
|
||||
_indexed_kitti_files,
|
||||
_read_json,
|
||||
_read_jsonl,
|
||||
_read_source,
|
||||
_regular_file,
|
||||
_sha256,
|
||||
_validated_artifact,
|
||||
_write_json,
|
||||
_write_jsonl,
|
||||
analyze_e46e_frames,
|
||||
)
|
||||
|
||||
E46G_PROFILE_SCHEMA: Final = "missioncore.e46g-rectified-detector-bakeoff-profile/v1"
|
||||
E46G_RUNTIME_SCHEMA: Final = "missioncore.e46g-rectified-detector-runtime/v1"
|
||||
E46G_RESULT_SCHEMA: Final = "missioncore.e46g-rectified-detector-bakeoff-result/v1"
|
||||
E46G_REPORT_SCHEMA: Final = "missioncore.e46g-rectified-detector-bakeoff-report/v1"
|
||||
E46G_FRAME_SCHEMA: Final = "missioncore.e46g-rectified-detector-bakeoff-frame/v1"
|
||||
E46G_PACKAGE_SCHEMA: Final = "missioncore.e46g-worker-package/v1"
|
||||
E46G_MANIFEST_NAME: Final = "manifest.json"
|
||||
E46G_REPORT_NAME: Final = "rectified-detector-bakeoff-report.json"
|
||||
E46G_RUNTIME_NAME: Final = "runtime.json"
|
||||
E46G_LOG_NAME: Final = "worker.log"
|
||||
|
||||
_RESULT_ID = re.compile(r"^e46g-rectified-detector-bakeoff-[a-f0-9]{64}$")
|
||||
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
|
||||
_CANDIDATES: Final = ("trafficcamnet", "dashcamnet")
|
||||
_VIEWS: Final = ("left", "front", "right")
|
||||
_AUTHORITY: Final = {
|
||||
"ground_truth": False,
|
||||
"independent_truth": False,
|
||||
"metric_grade_reference": False,
|
||||
"candidate_accepted": False,
|
||||
"free_space_authority": False,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
|
||||
class E46GRectifiedDetectorBakeoffError(ValueError):
|
||||
"""Raised when the E46G source, execution, or result is invalid."""
|
||||
|
||||
|
||||
def build_e46g_rectified_detector_bakeoff(
|
||||
*, source_job_root: Path, raw_root: Path, profile_path: Path, output_root: Path
|
||||
) -> dict[str, Any]:
|
||||
"""Validate one calibrated stock-detector A/B and freeze immutable evidence."""
|
||||
|
||||
try:
|
||||
return _build_e46g_rectified_detector_bakeoff(
|
||||
source_job_root=source_job_root,
|
||||
raw_root=raw_root,
|
||||
profile_path=profile_path,
|
||||
output_root=output_root,
|
||||
)
|
||||
except E46EReadyStackError as exc:
|
||||
raise E46GRectifiedDetectorBakeoffError(str(exc).replace("E46E", "E46G")) from exc
|
||||
|
||||
|
||||
def _build_e46g_rectified_detector_bakeoff(
|
||||
*, source_job_root: Path, raw_root: Path, profile_path: Path, output_root: Path
|
||||
) -> dict[str, Any]:
|
||||
profile_source = profile_path.resolve(strict=True)
|
||||
profile = _read_json(profile_source)
|
||||
_validate_profile(profile)
|
||||
source = _read_source(source_job_root.resolve(strict=True), profile)
|
||||
raw = raw_root.resolve(strict=True)
|
||||
if raw.is_symlink():
|
||||
raise E46GRectifiedDetectorBakeoffError("E46G raw root must not be a symlink")
|
||||
runtime = _read_json(raw / E46G_RUNTIME_NAME)
|
||||
_validate_runtime(runtime, profile, raw)
|
||||
worker_log = _regular_file(raw / E46G_LOG_NAME, allow_empty=True)
|
||||
|
||||
first_source_frame = int(profile["selection"]["first_source_frame_index"])
|
||||
sample_frame_count = int(profile["selection"]["frame_count"])
|
||||
frames_by_run: dict[str, tuple[dict[str, Any], ...]] = {}
|
||||
metrics_by_candidate: dict[str, Any] = {}
|
||||
for candidate in _CANDIDATES:
|
||||
per_view: dict[str, Any] = {}
|
||||
for view in _VIEWS:
|
||||
run_root = raw / "runs" / candidate / view
|
||||
detector_files = _indexed_kitti_files(run_root / "detections", sample_frame_count)
|
||||
tracker_files = _indexed_kitti_files(run_root / "tracks", sample_frame_count)
|
||||
rows: list[dict[str, Any]] = []
|
||||
for sample_frame_index in range(sample_frame_count):
|
||||
source_frame_index = first_source_frame + sample_frame_index
|
||||
source_row = source["index"][source_frame_index]
|
||||
detections = _parse_detector_file(
|
||||
detector_files[sample_frame_index], profile, candidate
|
||||
)
|
||||
objects = _parse_tracker_file(
|
||||
tracker_files[sample_frame_index], profile, candidate, view
|
||||
)
|
||||
rows.append(
|
||||
{
|
||||
"schema_version": E46G_FRAME_SCHEMA,
|
||||
"candidate": candidate,
|
||||
"view": view,
|
||||
"frame_index": sample_frame_index,
|
||||
"source_frame_index": source_frame_index,
|
||||
"sequence": int(source_row["sequence"]),
|
||||
"session_seconds": source["timeline_start_seconds"]
|
||||
+ (
|
||||
int(source_row["session_monotonic_ns"])
|
||||
- source["first_session_monotonic_ns"]
|
||||
)
|
||||
/ 1_000_000_000.0,
|
||||
"source_image_sha256": str(source_row["sha256"]),
|
||||
"detection_count": len(detections),
|
||||
"tracked_object_count": len(objects),
|
||||
"detections": detections,
|
||||
"objects": objects,
|
||||
}
|
||||
)
|
||||
key = f"{candidate}-{view}"
|
||||
frames_by_run[key] = tuple(rows)
|
||||
per_view[view] = _metrics(rows, profile)
|
||||
metrics_by_candidate[candidate] = {
|
||||
"source_frame_count": sample_frame_count,
|
||||
"view_frame_count": sample_frame_count * len(_VIEWS),
|
||||
"views": per_view,
|
||||
"detection_observation_count": sum(
|
||||
item["detection_observation_count"] for item in per_view.values()
|
||||
),
|
||||
"track_observation_count": sum(
|
||||
item["track_observation_count"] for item in per_view.values()
|
||||
),
|
||||
"unique_track_count": sum(item["unique_track_count"] for item in per_view.values()),
|
||||
"large_track_observation_count": sum(
|
||||
item["large_track_observation_count"] for item in per_view.values()
|
||||
),
|
||||
"large_track_fraction": round(
|
||||
sum(item["large_track_observation_count"] for item in per_view.values())
|
||||
/ max(
|
||||
1,
|
||||
sum(item["track_observation_count"] for item in per_view.values()),
|
||||
),
|
||||
6,
|
||||
),
|
||||
}
|
||||
|
||||
comparison_paths = {
|
||||
candidate: _regular_file(raw / "comparison" / f"{candidate}.mp4")
|
||||
for candidate in _CANDIDATES
|
||||
}
|
||||
method = _method(profile, runtime, source)
|
||||
report_basis = {
|
||||
"schema_version": E46G_REPORT_SCHEMA,
|
||||
"status": "completed-awaiting-visual-semantic-adjudication",
|
||||
"comparison_contract": copy.deepcopy(profile["comparison_contract"]),
|
||||
"selection": copy.deepcopy(profile["selection"]),
|
||||
"rectification": copy.deepcopy(profile["rectification"]),
|
||||
"metrics": metrics_by_candidate,
|
||||
"acceptance": {
|
||||
"exact_recorded_right_source_bound": True,
|
||||
"factory_calibration_bound": True,
|
||||
"official_nvidia_dewarper_executed": True,
|
||||
"stock_detector_tracker_executed": True,
|
||||
"same_views_and_frames_for_both_candidates": True,
|
||||
"visual_comparison_videos_available": True,
|
||||
"independent_truth_available": False,
|
||||
"candidate_accepted": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
"decision": {
|
||||
"automatic_winner_selected": False,
|
||||
"custom_detector_or_tracker_logic_used": False,
|
||||
"next_action": (
|
||||
"review both synchronized left/front/right videos for semantic false "
|
||||
"positives, missed task objects and edge geometry; then run the selected "
|
||||
"stock detector with NvDCF over the complete rectified route"
|
||||
),
|
||||
},
|
||||
"method": method,
|
||||
"limitations": [
|
||||
(
|
||||
"E46G is a controlled 60-second detector-selection gate, not the complete "
|
||||
"route continuity result"
|
||||
),
|
||||
(
|
||||
"left/front/right are three calibrated projections from one physical RIGHT "
|
||||
"camera, not three cameras"
|
||||
),
|
||||
(
|
||||
"DeepStream 9.1 decoding omits the terminal source frame at EOS; E46G admits "
|
||||
"the timestamp-aligned 0..4487 prefix and the tested 1000..1599 gate is "
|
||||
"unaffected"
|
||||
),
|
||||
("NvDCF identities are view-local; E46G does not invent cross-view identity stitching"),
|
||||
(
|
||||
"the two ready detectors expose only their provider taxonomies and do not "
|
||||
"establish free-space or dynamic/static state"
|
||||
),
|
||||
(
|
||||
"without independent exhaustive truth, numerical counts cannot select a "
|
||||
"winner without visual semantic review"
|
||||
),
|
||||
(
|
||||
"recorded RIGHT evidence introduces no LEFT camera, live hardware, command, "
|
||||
"navigation or safety authority"
|
||||
),
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
"ground_truth": False,
|
||||
}
|
||||
raw_identity = {
|
||||
"runtime_schema": runtime["schema_version"],
|
||||
"worker_host": runtime["worker_host"],
|
||||
"gpu_name": runtime["gpu_name"],
|
||||
"container_image": runtime["container_image"],
|
||||
"container_image_digest": runtime["container_image_digest"],
|
||||
"source_stream_sha256": runtime["source_stream_sha256"],
|
||||
"geometry": copy.deepcopy(runtime["geometry"]),
|
||||
"candidates": copy.deepcopy(runtime["candidates"]),
|
||||
"comparison": copy.deepcopy(runtime["comparison"]),
|
||||
"worker_log_sha256": _sha256(worker_log),
|
||||
}
|
||||
frames_identity = {
|
||||
key: hashlib.sha256(_canonical_json(rows)).hexdigest()
|
||||
for key, rows in sorted(frames_by_run.items())
|
||||
}
|
||||
identity = {
|
||||
"schema_version": E46G_RESULT_SCHEMA,
|
||||
"source": source["identity"],
|
||||
"profile_sha256": _sha256(profile_source),
|
||||
"profile": copy.deepcopy(profile),
|
||||
"raw_execution": raw_identity,
|
||||
"report_sha256": hashlib.sha256(_canonical_json(report_basis)).hexdigest(),
|
||||
"frames_sha256": frames_identity,
|
||||
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
result_id = f"e46g-rectified-detector-bakeoff-{identity_sha256}"
|
||||
destination = output_root.expanduser().absolute() / result_id
|
||||
if destination.exists():
|
||||
return read_e46g_rectified_detector_bakeoff(destination)
|
||||
|
||||
created_at_utc = datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
||||
report = {
|
||||
**report_basis,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"created_at_utc": created_at_utc,
|
||||
"source_session_id": str(profile["source"]["session_id"]),
|
||||
"camera_source_id": str(profile["source"]["camera_source_id"]),
|
||||
}
|
||||
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
_write_json(staging / E46G_REPORT_NAME, report)
|
||||
for key, rows in frames_by_run.items():
|
||||
_write_jsonl(staging / f"{key}.jsonl", rows)
|
||||
for candidate, source_path in comparison_paths.items():
|
||||
shutil.copyfile(source_path, staging / f"{candidate}.mp4")
|
||||
shutil.copyfile(raw / E46G_RUNTIME_NAME, staging / E46G_RUNTIME_NAME)
|
||||
shutil.copyfile(worker_log, staging / E46G_LOG_NAME)
|
||||
artifacts = [
|
||||
_artifact(staging / E46G_REPORT_NAME, "rectified-detector-bakeoff-report"),
|
||||
*[
|
||||
_artifact(staging / f"{key}.jsonl", f"tracked-frames-{key}")
|
||||
for key in sorted(frames_by_run)
|
||||
],
|
||||
*[
|
||||
_artifact(staging / f"{candidate}.mp4", f"comparison-video-{candidate}")
|
||||
for candidate in _CANDIDATES
|
||||
],
|
||||
_artifact(staging / E46G_RUNTIME_NAME, "runtime-record"),
|
||||
_artifact(staging / E46G_LOG_NAME, "worker-log"),
|
||||
]
|
||||
_write_json(
|
||||
staging / E46G_MANIFEST_NAME,
|
||||
{
|
||||
"schema_version": E46G_RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": created_at_utc,
|
||||
"acceptance_state": "rectified-detector-bakeoff-awaiting-visual-review",
|
||||
"ground_truth": False,
|
||||
"artifacts": artifacts,
|
||||
"authority": _AUTHORITY,
|
||||
},
|
||||
)
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
return read_e46g_rectified_detector_bakeoff(destination)
|
||||
|
||||
|
||||
def read_e46g_rectified_detector_bakeoff(root: Path) -> dict[str, Any]:
|
||||
"""Read and fully validate an immutable E46G result."""
|
||||
|
||||
try:
|
||||
return _read_e46g_rectified_detector_bakeoff(root)
|
||||
except E46EReadyStackError as exc:
|
||||
raise E46GRectifiedDetectorBakeoffError(str(exc).replace("E46E", "E46G")) from exc
|
||||
|
||||
|
||||
def _read_e46g_rectified_detector_bakeoff(root: Path) -> dict[str, Any]:
|
||||
resolved = root.resolve(strict=True)
|
||||
if resolved.is_symlink():
|
||||
raise E46GRectifiedDetectorBakeoffError("E46G result root must not be a symlink")
|
||||
manifest = _read_json(resolved / E46G_MANIFEST_NAME)
|
||||
identity = manifest.get("identity")
|
||||
digest = (
|
||||
hashlib.sha256(_canonical_json(identity)).hexdigest() if isinstance(identity, dict) else ""
|
||||
)
|
||||
if (
|
||||
manifest.get("schema_version") != E46G_RESULT_SCHEMA
|
||||
or manifest.get("result_id") != f"e46g-rectified-detector-bakeoff-{digest}"
|
||||
or manifest.get("identity_sha256") != digest
|
||||
or resolved.name != manifest.get("result_id")
|
||||
or _RESULT_ID.fullmatch(resolved.name) is None
|
||||
or manifest.get("authority") != _AUTHORITY
|
||||
or manifest.get("ground_truth") is not False
|
||||
):
|
||||
raise E46GRectifiedDetectorBakeoffError("E46G result identity is invalid")
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(artifacts, list) or len(artifacts) != 11:
|
||||
raise E46GRectifiedDetectorBakeoffError("E46G artifact inventory is invalid")
|
||||
by_role = {row.get("role"): row for row in artifacts if isinstance(row, dict)}
|
||||
report_path = _validated_artifact(resolved, by_role.get("rectified-detector-bakeoff-report"))
|
||||
runtime_path = _validated_artifact(resolved, by_role.get("runtime-record"))
|
||||
_validated_artifact(resolved, by_role.get("worker-log"))
|
||||
comparison_paths = {
|
||||
candidate: _validated_artifact(resolved, by_role.get(f"comparison-video-{candidate}"))
|
||||
for candidate in _CANDIDATES
|
||||
}
|
||||
frames: dict[str, tuple[dict[str, Any], ...]] = {}
|
||||
for candidate in _CANDIDATES:
|
||||
for view in _VIEWS:
|
||||
key = f"{candidate}-{view}"
|
||||
path = _validated_artifact(resolved, by_role.get(f"tracked-frames-{key}"))
|
||||
rows = tuple(_read_jsonl(path))
|
||||
if hashlib.sha256(_canonical_json(rows)).hexdigest() != identity.get(
|
||||
"frames_sha256", {}
|
||||
).get(key) or any(row.get("schema_version") != E46G_FRAME_SCHEMA for row in rows):
|
||||
raise E46GRectifiedDetectorBakeoffError("E46G frames changed")
|
||||
frames[key] = rows
|
||||
report = _read_json(report_path)
|
||||
runtime = _read_json(runtime_path)
|
||||
if (
|
||||
report.get("schema_version") != E46G_REPORT_SCHEMA
|
||||
or report.get("result_id") != resolved.name
|
||||
or report.get("authority") != _AUTHORITY
|
||||
or report.get("ground_truth") is not False
|
||||
or runtime.get("schema_version") != E46G_RUNTIME_SCHEMA
|
||||
):
|
||||
raise E46GRectifiedDetectorBakeoffError("E46G result changed")
|
||||
return {
|
||||
"result_id": resolved.name,
|
||||
"result_root": resolved,
|
||||
"manifest": manifest,
|
||||
"report": report,
|
||||
"frames": frames,
|
||||
"runtime": runtime,
|
||||
"comparison_paths": comparison_paths,
|
||||
}
|
||||
|
||||
|
||||
def _validate_profile(profile: dict[str, Any]) -> None:
|
||||
source = profile.get("source")
|
||||
runtime = profile.get("runtime")
|
||||
calibration = profile.get("calibration")
|
||||
rectification = profile.get("rectification")
|
||||
selection = profile.get("selection")
|
||||
candidates = profile.get("candidates")
|
||||
comparison = profile.get("comparison_contract")
|
||||
authority = profile.get("authority")
|
||||
image = runtime.get("container_image") if isinstance(runtime, dict) else None
|
||||
if (
|
||||
profile.get("schema_version") != E46G_PROFILE_SCHEMA
|
||||
or not isinstance(source, dict)
|
||||
or source.get("camera_source_id") != "sensor.camera.right"
|
||||
or source.get("segment_count") != 4489
|
||||
or not isinstance(image, str)
|
||||
or "@sha256:" not in image
|
||||
or not isinstance(calibration, dict)
|
||||
or calibration.get("slot") != "camera_1"
|
||||
or calibration.get("model") != "KB4"
|
||||
or calibration.get("calibration_sha256")
|
||||
!= "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9"
|
||||
or not isinstance(rectification, dict)
|
||||
or rectification.get("provider") != "NVIDIA Gst-nvdewarper"
|
||||
or rectification.get("projection_type") != 4
|
||||
or tuple(rectification.get("view_order", ())) != _VIEWS
|
||||
or rectification.get("expected_full_frame_count") != 4488
|
||||
or rectification.get("retained_source_frame_index_range") != [0, 4487]
|
||||
or rectification.get("excluded_source_tail_frame_count") != 1
|
||||
or not isinstance(selection, dict)
|
||||
or not isinstance(selection.get("first_source_frame_index"), int)
|
||||
or not isinstance(selection.get("frame_count"), int)
|
||||
or selection["first_source_frame_index"] < 0
|
||||
or selection["frame_count"] < 1
|
||||
or selection["first_source_frame_index"] + selection["frame_count"]
|
||||
> rectification["expected_full_frame_count"]
|
||||
or not isinstance(candidates, dict)
|
||||
or set(candidates) != set(_CANDIDATES)
|
||||
or any(candidates[name].get("custom_postprocessing") is not False for name in candidates)
|
||||
or not isinstance(comparison, dict)
|
||||
or comparison.get("controlled_change") != "detector-provider-only"
|
||||
or authority
|
||||
!= {
|
||||
"ground_truth": False,
|
||||
"candidate_accepted": False,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
):
|
||||
raise E46GRectifiedDetectorBakeoffError("E46G profile is invalid")
|
||||
|
||||
|
||||
def _validate_runtime(runtime: dict[str, Any], profile: dict[str, Any], raw: Path) -> None:
|
||||
image = str(profile["runtime"]["container_image"])
|
||||
digest = image.rsplit("@sha256:", 1)[-1]
|
||||
geometry = runtime.get("geometry")
|
||||
candidates = runtime.get("candidates")
|
||||
comparison = runtime.get("comparison")
|
||||
if (
|
||||
runtime.get("schema_version") != E46G_RUNTIME_SCHEMA
|
||||
or runtime.get("status") != "completed"
|
||||
or runtime.get("container_image") != image
|
||||
or runtime.get("container_image_digest") != digest
|
||||
or runtime.get("source_stream_sha256") != profile["source"]["stream_sha256"]
|
||||
or runtime.get("first_source_frame_index")
|
||||
!= profile["selection"]["first_source_frame_index"]
|
||||
or runtime.get("sample_frame_count") != profile["selection"]["frame_count"]
|
||||
or not runtime.get("worker_host")
|
||||
or not runtime.get("gpu_name")
|
||||
or not isinstance(geometry, dict)
|
||||
or set(geometry) != set(_VIEWS)
|
||||
or not isinstance(candidates, dict)
|
||||
or set(candidates) != set(_CANDIDATES)
|
||||
or not isinstance(comparison, dict)
|
||||
or set(comparison) != set(_CANDIDATES)
|
||||
):
|
||||
raise E46GRectifiedDetectorBakeoffError("E46G runtime identity is invalid")
|
||||
for view in _VIEWS:
|
||||
row = geometry[view]
|
||||
_runtime_artifact(raw, row, "full_rectified_video", f"geometry/{view}.mp4")
|
||||
_runtime_artifact(raw, row, "sample_video", f"samples/{view}.mp4")
|
||||
if (
|
||||
row.get("dewarper_config_sha256")
|
||||
!= profile["rectification"]["views"][view]["config_sha256"]
|
||||
or row.get("full_frame_count") != profile["rectification"]["expected_full_frame_count"]
|
||||
or row.get("retained_source_frame_index_range")
|
||||
!= profile["rectification"]["retained_source_frame_index_range"]
|
||||
or row.get("excluded_source_tail_frame_count")
|
||||
!= profile["rectification"]["excluded_source_tail_frame_count"]
|
||||
or row.get("sample_frame_count") != profile["selection"]["frame_count"]
|
||||
):
|
||||
raise E46GRectifiedDetectorBakeoffError("E46G dewarper config changed")
|
||||
for candidate in _CANDIDATES:
|
||||
candidate_row = candidates[candidate]
|
||||
if candidate_row.get("model_sha256") != profile["candidates"][candidate][
|
||||
"model_sha256"
|
||||
] or set(candidate_row.get("runs", {})) != set(_VIEWS):
|
||||
raise E46GRectifiedDetectorBakeoffError("E46G candidate identity changed")
|
||||
for view in _VIEWS:
|
||||
row = candidate_row["runs"][view]
|
||||
prefix = f"runs/{candidate}/{view}"
|
||||
_runtime_artifact(raw, row, "overlay", f"{prefix}/overlay.mp4")
|
||||
_runtime_artifact(raw, row, "deepstream_log", f"{prefix}/deepstream.log")
|
||||
if row.get("frame_count") != profile["selection"]["frame_count"]:
|
||||
raise E46GRectifiedDetectorBakeoffError("E46G run coverage changed")
|
||||
_runtime_artifact(
|
||||
raw,
|
||||
comparison[candidate],
|
||||
"video",
|
||||
f"comparison/{candidate}.mp4",
|
||||
)
|
||||
|
||||
|
||||
def _runtime_artifact(raw: Path, row: object, key: str, expected_relative: str) -> None:
|
||||
if not isinstance(row, dict):
|
||||
raise E46GRectifiedDetectorBakeoffError("E46G runtime artifact is invalid")
|
||||
relative = row.get(f"{key}_path")
|
||||
digest = row.get(f"{key}_sha256")
|
||||
path = raw / str(relative)
|
||||
if (
|
||||
relative != expected_relative
|
||||
or not isinstance(digest, str)
|
||||
or _SHA256.fullmatch(digest) is None
|
||||
or not path.is_file()
|
||||
or path.is_symlink()
|
||||
or _sha256(path) != digest
|
||||
):
|
||||
raise E46GRectifiedDetectorBakeoffError("E46G runtime artifact changed")
|
||||
|
||||
|
||||
def _parse_detector_file(
|
||||
path: Path, profile: dict[str, Any], candidate: str
|
||||
) -> list[dict[str, Any]]:
|
||||
output: list[dict[str, Any]] = []
|
||||
width, height = profile["rectification"]["output_resolution"]
|
||||
for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
|
||||
tokens = line.split()
|
||||
if len(tokens) != 16:
|
||||
raise E46GRectifiedDetectorBakeoffError(
|
||||
f"invalid detector KITTI row {path.name}:{line_number}"
|
||||
)
|
||||
box, source_box, clipped = _parse_box(tokens[4:8], path, line_number, width, height)
|
||||
output.append(
|
||||
{
|
||||
"class_name": tokens[0],
|
||||
"bbox": box,
|
||||
"source_bbox_ltrb": source_box,
|
||||
"source_plane_clipped": clipped,
|
||||
"confidence": _finite_float(tokens[15], path, line_number),
|
||||
"provenance": f"nvidia-{candidate}-stock-deepstream",
|
||||
}
|
||||
)
|
||||
return output
|
||||
|
||||
|
||||
def _parse_tracker_file(
|
||||
path: Path, profile: dict[str, Any], candidate: str, view: str
|
||||
) -> list[dict[str, Any]]:
|
||||
output: list[dict[str, Any]] = []
|
||||
seen: set[int] = set()
|
||||
width, height = profile["rectification"]["output_resolution"]
|
||||
for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
|
||||
tokens = line.split()
|
||||
if len(tokens) < 17:
|
||||
raise E46GRectifiedDetectorBakeoffError(
|
||||
f"invalid tracker KITTI row {path.name}:{line_number}"
|
||||
)
|
||||
try:
|
||||
track_id = int(tokens[1])
|
||||
except ValueError as exc:
|
||||
raise E46GRectifiedDetectorBakeoffError(
|
||||
f"invalid tracker id {path.name}:{line_number}"
|
||||
) from exc
|
||||
if track_id < 0 or track_id in seen:
|
||||
raise E46GRectifiedDetectorBakeoffError("invalid NvDCF track identity")
|
||||
seen.add(track_id)
|
||||
box, source_box, clipped = _parse_box(tokens[5:9], path, line_number, width, height)
|
||||
output.append(
|
||||
{
|
||||
"object_id": f"{candidate}-{view}-nvdcf-{track_id}",
|
||||
"source_track_id": track_id,
|
||||
"class_name": tokens[0],
|
||||
"bbox": box,
|
||||
"source_bbox_ltrb": source_box,
|
||||
"source_plane_clipped": clipped,
|
||||
"confidence": _finite_float(tokens[16], path, line_number),
|
||||
"provenance": "nvidia-nvdcf-stock-view-local-output",
|
||||
}
|
||||
)
|
||||
return output
|
||||
|
||||
|
||||
def _parse_box(
|
||||
tokens: list[str], path: Path, line_number: int, width: int, height: int
|
||||
) -> tuple[list[float], list[float], bool]:
|
||||
left, top, right, bottom = (_finite_float(token, path, line_number) for token in tokens)
|
||||
if right <= left or bottom <= top:
|
||||
raise E46GRectifiedDetectorBakeoffError(f"invalid bounding box {path.name}:{line_number}")
|
||||
projected = [
|
||||
max(0.0, min(float(width), left)),
|
||||
max(0.0, min(float(height), top)),
|
||||
max(0.0, min(float(width), right)),
|
||||
max(0.0, min(float(height), bottom)),
|
||||
]
|
||||
if projected[2] <= projected[0] or projected[3] <= projected[1]:
|
||||
raise E46GRectifiedDetectorBakeoffError(
|
||||
f"bounding box outside rectified plane {path.name}:{line_number}"
|
||||
)
|
||||
source = [left, top, right, bottom]
|
||||
return (
|
||||
[projected[0], projected[1], projected[2] - projected[0], projected[3] - projected[1]],
|
||||
source,
|
||||
source != projected,
|
||||
)
|
||||
|
||||
|
||||
def _metrics(rows: Iterable[dict[str, Any]], profile: dict[str, Any]) -> dict[str, Any]:
|
||||
values = list(rows)
|
||||
base = analyze_e46e_frames(values)
|
||||
width, height = profile["rectification"]["output_resolution"]
|
||||
plane_area = float(width * height)
|
||||
large = sum(
|
||||
float(item["bbox"][2]) * float(item["bbox"][3]) / plane_area >= 0.2
|
||||
for row in values
|
||||
for item in row["objects"]
|
||||
)
|
||||
return {
|
||||
**base,
|
||||
"large_track_observation_count": large,
|
||||
"large_track_fraction": round(large / max(1, base["track_observation_count"]), 6),
|
||||
}
|
||||
|
||||
|
||||
def _method(
|
||||
profile: dict[str, Any], runtime: dict[str, Any], source: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
components: list[dict[str, Any]] = [
|
||||
{
|
||||
"kind": "source",
|
||||
"name": profile["source"]["job_id"],
|
||||
"version": "immutable recorded RIGHT replay",
|
||||
"role": "single physical camera source",
|
||||
"identity_sha256": source["job_sha256"],
|
||||
},
|
||||
{
|
||||
"kind": "tool",
|
||||
"name": "XGRIDS K1 factory camera_1 KB4",
|
||||
"version": profile["calibration"]["model"],
|
||||
"role": "fisheye source geometry",
|
||||
"identity_sha256": profile["calibration"]["calibration_sha256"],
|
||||
},
|
||||
{
|
||||
"kind": "tool",
|
||||
"name": "NVIDIA Gst-nvdewarper",
|
||||
"version": profile["runtime"]["deepstream_version"],
|
||||
"role": "calibrated fisheye-to-perspective adapter",
|
||||
"identity_sha256": hashlib.sha256(
|
||||
_canonical_json(profile["rectification"])
|
||||
).hexdigest(),
|
||||
},
|
||||
]
|
||||
for candidate in _CANDIDATES:
|
||||
row = profile["candidates"][candidate]
|
||||
components.append(
|
||||
{
|
||||
"kind": "model",
|
||||
"name": row["name"],
|
||||
"version": row["version"],
|
||||
"role": "controlled ready detector candidate",
|
||||
"identity_sha256": row["model_sha256"],
|
||||
}
|
||||
)
|
||||
components.extend(
|
||||
[
|
||||
{
|
||||
"kind": "algorithm",
|
||||
"name": "NVIDIA NvDCF",
|
||||
"version": profile["tracker"]["configuration"],
|
||||
"role": "view-local temporal association",
|
||||
"identity_sha256": runtime["candidates"]["trafficcamnet"]["runs"]["front"][
|
||||
"tracker_config_sha256"
|
||||
],
|
||||
},
|
||||
{
|
||||
"kind": "runtime",
|
||||
"name": "NVIDIA DeepStream",
|
||||
"version": profile["runtime"]["deepstream_version"],
|
||||
"role": "GPU media, inference and tracking runtime",
|
||||
"identity_sha256": runtime["container_image_digest"],
|
||||
},
|
||||
]
|
||||
)
|
||||
return {
|
||||
"schema_version": "missioncore.laboratory-method/v1",
|
||||
"completeness": "complete",
|
||||
"execution_class": "hybrid",
|
||||
"pipeline_id": profile["profile_id"],
|
||||
"components": components,
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
"""Freeze the selected stock NVIDIA provider over the retained FRONT route.
|
||||
|
||||
E46H is deliberately narrow: one immutable recorded RIGHT source, factory KB4,
|
||||
the official NVIDIA dewarper FRONT projection, TrafficCamNet and stock NvDCF.
|
||||
Mission Core validates and publishes the evidence but implements none of the
|
||||
detector, suppression, association, hold or stitch logic.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
from k1link.compute.e46e_ready_stack import (
|
||||
E46EReadyStackError,
|
||||
_artifact,
|
||||
_canonical_json,
|
||||
_indexed_kitti_files,
|
||||
_read_json,
|
||||
_read_jsonl,
|
||||
_read_source,
|
||||
_regular_file,
|
||||
_sha256,
|
||||
_validated_artifact,
|
||||
_write_json,
|
||||
_write_jsonl,
|
||||
)
|
||||
from k1link.compute.e46g_rectified_detector_bakeoff import (
|
||||
E46GRectifiedDetectorBakeoffError,
|
||||
_metrics,
|
||||
_parse_detector_file,
|
||||
_parse_tracker_file,
|
||||
)
|
||||
|
||||
E46H_PROFILE_SCHEMA: Final = "missioncore.e46h-full-rectified-front-replay-profile/v1"
|
||||
E46H_RUNTIME_SCHEMA: Final = "missioncore.e46h-full-rectified-front-runtime/v1"
|
||||
E46H_RESULT_SCHEMA: Final = "missioncore.e46h-full-rectified-front-replay-result/v1"
|
||||
E46H_REPORT_SCHEMA: Final = "missioncore.e46h-full-rectified-front-replay-report/v1"
|
||||
E46H_FRAME_SCHEMA: Final = "missioncore.e46h-full-rectified-front-replay-frame/v1"
|
||||
E46H_PACKAGE_SCHEMA: Final = "missioncore.e46h-worker-package/v1"
|
||||
E46H_MANIFEST_NAME: Final = "manifest.json"
|
||||
E46H_REPORT_NAME: Final = "full-rectified-front-report.json"
|
||||
E46H_FRAMES_NAME: Final = "tracked-frames.jsonl"
|
||||
E46H_OVERLAY_NAME: Final = "overlay.mp4"
|
||||
E46H_RUNTIME_NAME: Final = "runtime.json"
|
||||
E46H_LOG_NAME: Final = "worker.log"
|
||||
|
||||
_RESULT_ID = re.compile(r"^e46h-full-rectified-front-replay-[a-f0-9]{64}$")
|
||||
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
|
||||
_AUTHORITY: Final = {
|
||||
"ground_truth": False,
|
||||
"independent_truth": False,
|
||||
"metric_grade_reference": False,
|
||||
"candidate_accepted": False,
|
||||
"free_space_authority": False,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
|
||||
class E46HFullRectifiedFrontReplayError(ValueError):
|
||||
"""Raised when the E46H source, execution, or result is invalid."""
|
||||
|
||||
|
||||
def build_e46h_full_rectified_front_replay(
|
||||
*, source_job_root: Path, raw_root: Path, profile_path: Path, output_root: Path
|
||||
) -> dict[str, Any]:
|
||||
"""Validate the full retained FRONT replay and freeze immutable evidence."""
|
||||
|
||||
try:
|
||||
return _build_e46h_full_rectified_front_replay(
|
||||
source_job_root=source_job_root,
|
||||
raw_root=raw_root,
|
||||
profile_path=profile_path,
|
||||
output_root=output_root,
|
||||
)
|
||||
except (E46EReadyStackError, E46GRectifiedDetectorBakeoffError) as exc:
|
||||
raise E46HFullRectifiedFrontReplayError(str(exc).replace("E46E", "E46H")) from exc
|
||||
|
||||
|
||||
def _build_e46h_full_rectified_front_replay(
|
||||
*, source_job_root: Path, raw_root: Path, profile_path: Path, output_root: Path
|
||||
) -> dict[str, Any]:
|
||||
profile_source = profile_path.resolve(strict=True)
|
||||
profile = _read_json(profile_source)
|
||||
_validate_profile(profile)
|
||||
source = _read_source(source_job_root.resolve(strict=True), profile)
|
||||
raw = raw_root.resolve(strict=True)
|
||||
if raw.is_symlink():
|
||||
raise E46HFullRectifiedFrontReplayError("E46H raw root must not be a symlink")
|
||||
runtime = _read_json(raw / E46H_RUNTIME_NAME)
|
||||
_validate_runtime(runtime, profile, raw)
|
||||
overlay = _regular_file(raw / "run" / E46H_OVERLAY_NAME)
|
||||
worker_log = _regular_file(raw / E46H_LOG_NAME, allow_empty=True)
|
||||
|
||||
frame_count = int(profile["selection"]["frame_count"])
|
||||
detector_files = _indexed_kitti_files(raw / "run" / "detections", frame_count)
|
||||
tracker_files = _indexed_kitti_files(raw / "run" / "tracks", frame_count)
|
||||
frames: list[dict[str, Any]] = []
|
||||
for frame_index in range(frame_count):
|
||||
source_row = source["index"][frame_index]
|
||||
detections = _parse_detector_file(detector_files[frame_index], profile, "trafficcamnet")
|
||||
objects = _parse_tracker_file(
|
||||
tracker_files[frame_index], profile, "trafficcamnet", "front"
|
||||
)
|
||||
frames.append(
|
||||
{
|
||||
"schema_version": E46H_FRAME_SCHEMA,
|
||||
"frame_index": frame_index,
|
||||
"source_frame_index": frame_index,
|
||||
"sequence": int(source_row["sequence"]),
|
||||
"session_seconds": source["timeline_start_seconds"]
|
||||
+ (
|
||||
int(source_row["session_monotonic_ns"])
|
||||
- source["first_session_monotonic_ns"]
|
||||
)
|
||||
/ 1_000_000_000.0,
|
||||
"source_image_sha256": str(source_row["sha256"]),
|
||||
"detection_count": len(detections),
|
||||
"tracked_object_count": len(objects),
|
||||
"detections": detections,
|
||||
"objects": objects,
|
||||
}
|
||||
)
|
||||
metrics = _metrics(frames, profile)
|
||||
method = _method(profile, runtime, source)
|
||||
report_basis = {
|
||||
"schema_version": E46H_REPORT_SCHEMA,
|
||||
"status": "completed-awaiting-full-route-visual-review",
|
||||
"baseline_result_id": profile["baseline_result_id"],
|
||||
"selection": copy.deepcopy(profile["selection"]),
|
||||
"rectification": copy.deepcopy(profile["rectification"]),
|
||||
"metrics": metrics,
|
||||
"acceptance": {
|
||||
"exact_recorded_right_source_bound": True,
|
||||
"factory_calibration_bound": True,
|
||||
"official_nvidia_dewarper_executed": True,
|
||||
"selected_stock_detector_tracker_executed": True,
|
||||
"retained_route_accounted": metrics["frame_count"] == frame_count,
|
||||
"terminal_source_frame_excluded": True,
|
||||
"full_visual_review_completed": False,
|
||||
"independent_truth_available": False,
|
||||
"candidate_accepted": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
"decision": {
|
||||
"selected_provider": "front-trafficcamnet-stock-nvdcf",
|
||||
"custom_detector_or_tracker_logic_used": False,
|
||||
"provider_promoted": False,
|
||||
"next_action": (
|
||||
"review the complete seekable FRONT overlay for continuity, duplicates, stale "
|
||||
"tracks, semantic false positives and long object-layer blackouts"
|
||||
),
|
||||
},
|
||||
"method": method,
|
||||
"limitations": [
|
||||
(
|
||||
"E46H covers the timestamp-aligned 0..4487 prefix; DeepStream 9.1 decoding "
|
||||
"reproducibly omits the terminal source frame 4488 at EOS"
|
||||
),
|
||||
(
|
||||
"TrafficCamNet and NvDCF remain ready providers; E46H contains no custom "
|
||||
"detector, NMS, association, hold or stitch logic"
|
||||
),
|
||||
(
|
||||
"FRONT is one calibrated projection of the physical RIGHT camera; no LEFT "
|
||||
"camera or cross-view identity is introduced"
|
||||
),
|
||||
(
|
||||
"without independent exhaustive truth, continuity counts cannot establish "
|
||||
"precision, recall or physical identity correctness"
|
||||
),
|
||||
(
|
||||
"class, temporal identity, LiDAR range and dynamic/static state remain separate "
|
||||
"evidence layers; E46H introduces no command, navigation or safety authority"
|
||||
),
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
"ground_truth": False,
|
||||
}
|
||||
raw_identity = {
|
||||
"runtime_schema": runtime["schema_version"],
|
||||
"worker_host": runtime["worker_host"],
|
||||
"gpu_name": runtime["gpu_name"],
|
||||
"container_image": runtime["container_image"],
|
||||
"container_image_digest": runtime["container_image_digest"],
|
||||
"source_stream_sha256": runtime["source_stream_sha256"],
|
||||
"geometry": copy.deepcopy(runtime["geometry"]),
|
||||
"run": copy.deepcopy(runtime["run"]),
|
||||
"worker_log_sha256": _sha256(worker_log),
|
||||
}
|
||||
identity = {
|
||||
"schema_version": E46H_RESULT_SCHEMA,
|
||||
"source": source["identity"],
|
||||
"profile_sha256": _sha256(profile_source),
|
||||
"profile": copy.deepcopy(profile),
|
||||
"raw_execution": raw_identity,
|
||||
"report_sha256": hashlib.sha256(_canonical_json(report_basis)).hexdigest(),
|
||||
"frames_sha256": hashlib.sha256(_canonical_json(frames)).hexdigest(),
|
||||
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
result_id = f"e46h-full-rectified-front-replay-{identity_sha256}"
|
||||
destination = output_root.expanduser().absolute() / result_id
|
||||
if destination.exists():
|
||||
return read_e46h_full_rectified_front_replay(destination)
|
||||
|
||||
created_at = datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
||||
report = {
|
||||
**report_basis,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"created_at_utc": created_at,
|
||||
"source_session_id": profile["source"]["session_id"],
|
||||
"camera_source_id": profile["source"]["camera_source_id"],
|
||||
}
|
||||
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
_write_json(staging / E46H_REPORT_NAME, report)
|
||||
_write_jsonl(staging / E46H_FRAMES_NAME, frames)
|
||||
shutil.copyfile(overlay, staging / E46H_OVERLAY_NAME)
|
||||
shutil.copyfile(raw / E46H_RUNTIME_NAME, staging / E46H_RUNTIME_NAME)
|
||||
shutil.copyfile(worker_log, staging / E46H_LOG_NAME)
|
||||
artifacts = [
|
||||
_artifact(staging / E46H_REPORT_NAME, "full-rectified-front-report"),
|
||||
_artifact(staging / E46H_FRAMES_NAME, "tracked-frames"),
|
||||
_artifact(staging / E46H_OVERLAY_NAME, "visual-overlay-video"),
|
||||
_artifact(staging / E46H_RUNTIME_NAME, "runtime-record"),
|
||||
_artifact(staging / E46H_LOG_NAME, "worker-log"),
|
||||
]
|
||||
_write_json(
|
||||
staging / E46H_MANIFEST_NAME,
|
||||
{
|
||||
"schema_version": E46H_RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": created_at,
|
||||
"acceptance_state": "full-rectified-front-awaiting-visual-review",
|
||||
"ground_truth": False,
|
||||
"artifacts": artifacts,
|
||||
"authority": _AUTHORITY,
|
||||
},
|
||||
)
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
return read_e46h_full_rectified_front_replay(destination)
|
||||
|
||||
|
||||
def read_e46h_full_rectified_front_replay(root: Path) -> dict[str, Any]:
|
||||
"""Read and fully validate one immutable E46H result."""
|
||||
|
||||
try:
|
||||
return _read_e46h_full_rectified_front_replay(root)
|
||||
except (E46EReadyStackError, E46GRectifiedDetectorBakeoffError) as exc:
|
||||
raise E46HFullRectifiedFrontReplayError(str(exc).replace("E46E", "E46H")) from exc
|
||||
|
||||
|
||||
def _read_e46h_full_rectified_front_replay(root: Path) -> dict[str, Any]:
|
||||
resolved = root.resolve(strict=True)
|
||||
if resolved.is_symlink():
|
||||
raise E46HFullRectifiedFrontReplayError("E46H result root must not be a symlink")
|
||||
manifest = _read_json(resolved / E46H_MANIFEST_NAME)
|
||||
identity = manifest.get("identity")
|
||||
digest = (
|
||||
hashlib.sha256(_canonical_json(identity)).hexdigest() if isinstance(identity, dict) else ""
|
||||
)
|
||||
if (
|
||||
manifest.get("schema_version") != E46H_RESULT_SCHEMA
|
||||
or manifest.get("result_id") != f"e46h-full-rectified-front-replay-{digest}"
|
||||
or manifest.get("identity_sha256") != digest
|
||||
or resolved.name != manifest.get("result_id")
|
||||
or _RESULT_ID.fullmatch(resolved.name) is None
|
||||
or manifest.get("authority") != _AUTHORITY
|
||||
or manifest.get("ground_truth") is not False
|
||||
):
|
||||
raise E46HFullRectifiedFrontReplayError("E46H result identity is invalid")
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(artifacts, list) or len(artifacts) != 5:
|
||||
raise E46HFullRectifiedFrontReplayError("E46H artifact inventory is invalid")
|
||||
by_role = {row.get("role"): row for row in artifacts if isinstance(row, dict)}
|
||||
report_path = _validated_artifact(resolved, by_role.get("full-rectified-front-report"))
|
||||
frames_path = _validated_artifact(resolved, by_role.get("tracked-frames"))
|
||||
overlay_path = _validated_artifact(resolved, by_role.get("visual-overlay-video"))
|
||||
runtime_path = _validated_artifact(resolved, by_role.get("runtime-record"))
|
||||
_validated_artifact(resolved, by_role.get("worker-log"))
|
||||
report = _read_json(report_path)
|
||||
frames = tuple(_read_jsonl(frames_path))
|
||||
runtime = _read_json(runtime_path)
|
||||
if (
|
||||
report.get("schema_version") != E46H_REPORT_SCHEMA
|
||||
or report.get("result_id") != resolved.name
|
||||
or report.get("authority") != _AUTHORITY
|
||||
or report.get("ground_truth") is not False
|
||||
or runtime.get("schema_version") != E46H_RUNTIME_SCHEMA
|
||||
or any(row.get("schema_version") != E46H_FRAME_SCHEMA for row in frames)
|
||||
or hashlib.sha256(_canonical_json(frames)).hexdigest() != identity.get("frames_sha256")
|
||||
or report.get("metrics", {}).get("frame_count") != len(frames)
|
||||
):
|
||||
raise E46HFullRectifiedFrontReplayError("E46H result changed")
|
||||
return {
|
||||
"result_id": resolved.name,
|
||||
"result_root": resolved,
|
||||
"manifest": manifest,
|
||||
"report": report,
|
||||
"frames": frames,
|
||||
"runtime": runtime,
|
||||
"overlay_path": overlay_path,
|
||||
}
|
||||
|
||||
|
||||
def _validate_profile(profile: dict[str, Any]) -> None:
|
||||
source = profile.get("source")
|
||||
selection = profile.get("selection")
|
||||
calibration = profile.get("calibration")
|
||||
rectification = profile.get("rectification")
|
||||
runtime = profile.get("runtime")
|
||||
detector = profile.get("detector")
|
||||
parser = profile.get("parser")
|
||||
tracker = profile.get("tracker")
|
||||
authority = profile.get("authority")
|
||||
image = runtime.get("container_image") if isinstance(runtime, dict) else None
|
||||
if (
|
||||
profile.get("schema_version") != E46H_PROFILE_SCHEMA
|
||||
or not isinstance(profile.get("baseline_result_id"), str)
|
||||
or not profile["baseline_result_id"].startswith("e46g-rectified-detector-bakeoff-")
|
||||
or not isinstance(source, dict)
|
||||
or source.get("camera_source_id") != "sensor.camera.right"
|
||||
or source.get("segment_count") != 4489
|
||||
or not isinstance(selection, dict)
|
||||
or selection.get("first_source_frame_index") != 0
|
||||
or selection.get("last_source_frame_index") != 4487
|
||||
or selection.get("frame_count") != 4488
|
||||
or selection.get("excluded_source_tail_frame_count") != 1
|
||||
or not isinstance(calibration, dict)
|
||||
or calibration.get("slot") != "camera_1"
|
||||
or calibration.get("model") != "KB4"
|
||||
or calibration.get("calibration_sha256")
|
||||
!= "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9"
|
||||
or not isinstance(rectification, dict)
|
||||
or rectification.get("provider") != "NVIDIA Gst-nvdewarper"
|
||||
or rectification.get("projection_type") != 4
|
||||
or rectification.get("view") != "front"
|
||||
or rectification.get("output_resolution") != [960, 544]
|
||||
or not isinstance(image, str)
|
||||
or "@sha256:" not in image
|
||||
or not isinstance(detector, dict)
|
||||
or detector.get("name") != "NVIDIA TrafficCamNet Transformer Lite"
|
||||
or detector.get("custom_postprocessing") is not False
|
||||
or not isinstance(parser, dict)
|
||||
or parser.get("symbol") != "NvDsInferParseCustomDDETRTAO"
|
||||
or parser.get("custom_mission_core_logic") is not False
|
||||
or not isinstance(tracker, dict)
|
||||
or tracker.get("custom_association") is not False
|
||||
or tracker.get("custom_hold_or_stitch") is not False
|
||||
or authority
|
||||
!= {
|
||||
"ground_truth": False,
|
||||
"candidate_accepted": False,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
):
|
||||
raise E46HFullRectifiedFrontReplayError("E46H profile is invalid")
|
||||
|
||||
|
||||
def _validate_runtime(runtime: dict[str, Any], profile: dict[str, Any], raw: Path) -> None:
|
||||
image = profile["runtime"]["container_image"]
|
||||
geometry = runtime.get("geometry")
|
||||
run = runtime.get("run")
|
||||
if (
|
||||
runtime.get("schema_version") != E46H_RUNTIME_SCHEMA
|
||||
or runtime.get("status") != "completed"
|
||||
or runtime.get("container_image") != image
|
||||
or runtime.get("container_image_digest") != image.rsplit("@sha256:", 1)[-1]
|
||||
or runtime.get("source_stream_sha256") != profile["source"]["stream_sha256"]
|
||||
or runtime.get("frame_count") != profile["selection"]["frame_count"]
|
||||
or runtime.get("retained_source_frame_index_range") != [0, 4487]
|
||||
or not runtime.get("worker_host")
|
||||
or not runtime.get("gpu_name")
|
||||
or not isinstance(geometry, dict)
|
||||
or not isinstance(run, dict)
|
||||
):
|
||||
raise E46HFullRectifiedFrontReplayError("E46H runtime identity is invalid")
|
||||
_runtime_artifact(raw, geometry, "video", "geometry/front.mp4")
|
||||
_runtime_artifact(raw, geometry, "log", "geometry/front.log")
|
||||
_runtime_artifact(raw, run, "overlay", "run/overlay.mp4")
|
||||
_runtime_artifact(raw, run, "deepstream_log", "run/deepstream.log")
|
||||
required = (
|
||||
geometry.get("config_sha256") == profile["rectification"]["config_sha256"],
|
||||
geometry.get("frame_count") == profile["selection"]["frame_count"],
|
||||
run.get("frame_count") == profile["selection"]["frame_count"],
|
||||
run.get("model_sha256") == profile["detector"]["model_sha256"],
|
||||
run.get("parser_library_sha256") == profile["parser"]["library_sha256"],
|
||||
run.get("deepstream_app_config_sha256")
|
||||
== profile["detector"]["deepstream_app_config_sha256"],
|
||||
run.get("detector_config_sha256")
|
||||
== profile["detector"]["detector_config_sha256"],
|
||||
_SHA256.fullmatch(str(run.get("tracker_config_sha256"))) is not None,
|
||||
_SHA256.fullmatch(str(run.get("model_engine_sha256"))) is not None,
|
||||
)
|
||||
if not all(required):
|
||||
raise E46HFullRectifiedFrontReplayError("E46H runtime component identity changed")
|
||||
|
||||
|
||||
def _runtime_artifact(raw: Path, row: dict[str, Any], key: str, expected: str) -> None:
|
||||
relative = row.get(f"{key}_path")
|
||||
digest = row.get(f"{key}_sha256")
|
||||
path = raw / str(relative)
|
||||
if (
|
||||
relative != expected
|
||||
or not isinstance(digest, str)
|
||||
or _SHA256.fullmatch(digest) is None
|
||||
or not path.is_file()
|
||||
or path.is_symlink()
|
||||
or _sha256(path) != digest
|
||||
):
|
||||
raise E46HFullRectifiedFrontReplayError("E46H runtime artifact changed")
|
||||
|
||||
|
||||
def _method(
|
||||
profile: dict[str, Any], runtime: dict[str, Any], source: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"schema_version": "missioncore.laboratory-method/v1",
|
||||
"completeness": "complete",
|
||||
"execution_class": "hybrid",
|
||||
"pipeline_id": profile["profile_id"],
|
||||
"components": [
|
||||
{
|
||||
"kind": "source",
|
||||
"name": profile["source"]["job_id"],
|
||||
"version": "immutable recorded RIGHT replay",
|
||||
"role": "single physical camera source",
|
||||
"identity_sha256": source["job_sha256"],
|
||||
},
|
||||
{
|
||||
"kind": "tool",
|
||||
"name": "XGRIDS K1 factory camera_1 KB4",
|
||||
"version": profile["calibration"]["model"],
|
||||
"role": "fisheye source geometry",
|
||||
"identity_sha256": profile["calibration"]["calibration_sha256"],
|
||||
},
|
||||
{
|
||||
"kind": "tool",
|
||||
"name": "NVIDIA Gst-nvdewarper",
|
||||
"version": profile["runtime"]["deepstream_version"],
|
||||
"role": "FRONT fisheye-to-perspective adapter",
|
||||
"identity_sha256": profile["rectification"]["config_sha256"],
|
||||
},
|
||||
{
|
||||
"kind": "model",
|
||||
"name": profile["detector"]["name"],
|
||||
"version": profile["detector"]["version"],
|
||||
"role": "selected ready detector provider",
|
||||
"identity_sha256": profile["detector"]["model_sha256"],
|
||||
},
|
||||
{
|
||||
"kind": "algorithm",
|
||||
"name": profile["tracker"]["name"],
|
||||
"version": profile["tracker"]["configuration"],
|
||||
"role": "FRONT route-local temporal association",
|
||||
"identity_sha256": runtime["run"]["tracker_config_sha256"],
|
||||
},
|
||||
{
|
||||
"kind": "runtime",
|
||||
"name": "NVIDIA DeepStream",
|
||||
"version": profile["runtime"]["deepstream_version"],
|
||||
"role": "GPU media, inference and tracking runtime",
|
||||
"identity_sha256": runtime["container_image_digest"],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,594 @@
|
||||
"""Freeze the ready NVIDIA Grounding DINO provider over the full FRONT replay.
|
||||
|
||||
E46I keeps the E46H camera adapter and changes only the detector provider. The
|
||||
result is diagnostic: detections and visual evidence are published, while
|
||||
tracking, physical identity, motion state, LiDAR range and command authority
|
||||
remain explicitly outside this experiment.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import uuid
|
||||
from collections import Counter
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from statistics import fmean
|
||||
from typing import Any, Final
|
||||
|
||||
from k1link.compute.e46e_ready_stack import (
|
||||
E46EReadyStackError,
|
||||
_artifact,
|
||||
_canonical_json,
|
||||
_read_json,
|
||||
_read_jsonl,
|
||||
_sha256,
|
||||
_validated_artifact,
|
||||
_write_json,
|
||||
_write_jsonl,
|
||||
)
|
||||
|
||||
E46I_PROFILE_SCHEMA: Final = "missioncore.e46i-grounding-dino-full-replay-profile/v1"
|
||||
E46I_RUNTIME_SCHEMA: Final = "missioncore.e46i-grounding-dino-full-runtime/v1"
|
||||
E46I_RESULT_SCHEMA: Final = "missioncore.e46i-grounding-dino-full-replay-result/v1"
|
||||
E46I_REPORT_SCHEMA: Final = "missioncore.e46i-grounding-dino-full-replay-report/v1"
|
||||
E46I_FRAME_SCHEMA: Final = "missioncore.e46i-grounding-dino-full-replay-frame/v1"
|
||||
E46I_MANIFEST_NAME: Final = "manifest.json"
|
||||
E46I_REPORT_NAME: Final = "grounding-dino-full-report.json"
|
||||
E46I_FRAMES_NAME: Final = "detection-frames.jsonl"
|
||||
E46I_OVERLAY_NAME: Final = "grounding-dino-full-overlay.mp4"
|
||||
E46I_LABELS_NAME: Final = "e46i-full-labels.tar"
|
||||
E46I_RUNTIME_NAME: Final = "runtime.json"
|
||||
E46I_SHADOW_SHEET_NAME: Final = "shadow-gate-contact-sheet.png"
|
||||
E46I_ROUTE_SHEET_NAME: Final = "full-route-10s-contact-sheet.png"
|
||||
E46I_TARGETED_SHEET_NAME: Final = "targeted-windows-contact-sheet.png"
|
||||
|
||||
_RESULT_ID = re.compile(r"^e46i-grounding-dino-full-replay-[a-f0-9]{64}$")
|
||||
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
|
||||
_AUTHORITY: Final = {
|
||||
"ground_truth": False,
|
||||
"independent_truth": False,
|
||||
"candidate_accepted": False,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
_REVIEW_WINDOWS: Final = [
|
||||
{
|
||||
"id": "legacy-false-wall",
|
||||
"label": "6.0–10.9 с · прежний false car на стене",
|
||||
"start_seconds": 6.0,
|
||||
"end_seconds": 10.9,
|
||||
"verdict": "legacy-background-false-positive-suppressed",
|
||||
},
|
||||
{
|
||||
"id": "legacy-false-shrub",
|
||||
"label": "178.6–180.3 с · прежний false car на кусте",
|
||||
"start_seconds": 178.6,
|
||||
"end_seconds": 180.3,
|
||||
"verdict": "legacy-background-false-positive-suppressed",
|
||||
},
|
||||
{
|
||||
"id": "legacy-false-ground",
|
||||
"label": "250.7–265.6 с · прежний false car на полотне",
|
||||
"start_seconds": 250.7,
|
||||
"end_seconds": 265.6,
|
||||
"verdict": "legacy-background-false-positive-suppressed",
|
||||
},
|
||||
{
|
||||
"id": "legacy-false-road",
|
||||
"label": "392.2–400.6 с · прежний false car на дороге",
|
||||
"start_seconds": 392.2,
|
||||
"end_seconds": 400.6,
|
||||
"verdict": "legacy-background-false-positive-suppressed",
|
||||
},
|
||||
{
|
||||
"id": "empty-scene",
|
||||
"label": "419.4–426.9 с · пустая сцена",
|
||||
"start_seconds": 419.4,
|
||||
"end_seconds": 426.9,
|
||||
"verdict": "empty-scene-mostly-preserved",
|
||||
},
|
||||
{
|
||||
"id": "legacy-false-terrace",
|
||||
"label": "440.8–448.4 с · прежний false car на террасе",
|
||||
"start_seconds": 440.8,
|
||||
"end_seconds": 448.4,
|
||||
"verdict": "legacy-background-false-positive-suppressed",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class E46IGroundingDinoFullReplayError(ValueError):
|
||||
"""Raised when E46I evidence or identity is invalid."""
|
||||
|
||||
|
||||
def build_e46i_grounding_dino_full_replay(
|
||||
*, raw_root: Path, profile_path: Path, output_root: Path
|
||||
) -> dict[str, Any]:
|
||||
"""Validate raw NVIDIA output and freeze one immutable E46I result."""
|
||||
|
||||
try:
|
||||
return _build_e46i_grounding_dino_full_replay(
|
||||
raw_root=raw_root, profile_path=profile_path, output_root=output_root
|
||||
)
|
||||
except E46EReadyStackError as exc:
|
||||
raise E46IGroundingDinoFullReplayError(str(exc).replace("E46E", "E46I")) from exc
|
||||
|
||||
|
||||
def _build_e46i_grounding_dino_full_replay(
|
||||
*, raw_root: Path, profile_path: Path, output_root: Path
|
||||
) -> dict[str, Any]:
|
||||
raw = raw_root.resolve(strict=True)
|
||||
if raw.is_symlink():
|
||||
raise E46IGroundingDinoFullReplayError("E46I raw root must not be a symlink")
|
||||
profile_source = profile_path.resolve(strict=True)
|
||||
profile = _read_json(profile_source)
|
||||
_validate_profile(profile)
|
||||
runtime_source = raw / E46I_RUNTIME_NAME
|
||||
runtime = _read_json(runtime_source)
|
||||
_validate_runtime(runtime, profile, raw)
|
||||
|
||||
labels_root = raw / "labels"
|
||||
if not labels_root.is_dir() or labels_root.is_symlink():
|
||||
raise E46IGroundingDinoFullReplayError("E46I labels root is invalid")
|
||||
frame_count = int(profile["source"]["video_frame_count"])
|
||||
frames = _read_detection_frames(labels_root, profile, frame_count)
|
||||
metrics = _metrics(frames, runtime)
|
||||
method = _method(profile)
|
||||
|
||||
report_basis = {
|
||||
"schema_version": E46I_REPORT_SCHEMA,
|
||||
"status": "semantic-regression-suppressed-awaiting-temporal-layer",
|
||||
"baseline_result_id": profile["baseline_result_id"],
|
||||
"source": copy.deepcopy(profile["source"]),
|
||||
"provider": copy.deepcopy(profile["provider"]),
|
||||
"inference": copy.deepcopy(profile["inference"]),
|
||||
"metrics": metrics,
|
||||
"shadow_gate": copy.deepcopy(profile["visual_shadow_gate"]),
|
||||
"visual_review": {
|
||||
"status": "full-playback-and-targeted-window-review-completed",
|
||||
"complete_video_playback_completed": True,
|
||||
"reviewed_video_range_seconds": [0.0, 448.8],
|
||||
"playback_rate": 4.0,
|
||||
"review_windows": copy.deepcopy(_REVIEW_WINDOWS),
|
||||
"verdict": "material-semantic-progress-not-yet-complete-perception",
|
||||
"finding": (
|
||||
"All five large E46H background false-car cases are absent in the new "
|
||||
"provider output; real vehicles and people remain visible across the route."
|
||||
),
|
||||
"known_error": (
|
||||
"The fixed four-caption ontology misses one partially cropped person and "
|
||||
"labels a stroller as bicycle in the anchor review."
|
||||
),
|
||||
},
|
||||
"acceptance": {
|
||||
"exact_recorded_right_source_bound": True,
|
||||
"same_calibrated_front_adapter_as_baseline": True,
|
||||
"official_nvidia_model_executed": True,
|
||||
"fixed_threshold_full_route_executed": True,
|
||||
"retained_route_accounted": metrics["frame_count"] == frame_count,
|
||||
"legacy_large_false_background_gate_passed": True,
|
||||
"full_overlay_published": True,
|
||||
"offline_reproducibility_completed": False,
|
||||
"temporal_identity_available": False,
|
||||
"motion_state_available": False,
|
||||
"candidate_accepted": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
"decision": {
|
||||
"selected_provider": "nvidia-grounding-dino-swin-tiny-commercial-v1.0",
|
||||
"provider_semantic_progress": True,
|
||||
"provider_promoted": False,
|
||||
"custom_detector_or_postprocessing_used": False,
|
||||
"next_action": (
|
||||
"freeze this detector output, vendor the tokenizer for offline replay, then "
|
||||
"attach a ready temporal tracker before evaluating dynamic/static state"
|
||||
),
|
||||
},
|
||||
"method": method,
|
||||
"limitations": [
|
||||
(
|
||||
"E46I has no independent exhaustive truth, so observation counts are not "
|
||||
"precision or recall."
|
||||
),
|
||||
(
|
||||
"Grounding DINO output is frame-local and contains no stable object identity "
|
||||
"or motion state."
|
||||
),
|
||||
"The fixed caption set contains car, person, bicycle and road sign only.",
|
||||
(
|
||||
"TAO downloaded bert-base-uncased tokenizer data at startup; offline replay "
|
||||
"is not sealed yet."
|
||||
),
|
||||
(
|
||||
"LiDAR range, dynamic/static state, free space, commands, navigation and "
|
||||
"safety remain unaccepted."
|
||||
),
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
"ground_truth": False,
|
||||
}
|
||||
identity = {
|
||||
"schema_version": E46I_RESULT_SCHEMA,
|
||||
"profile_sha256": _sha256(profile_source),
|
||||
"profile": copy.deepcopy(profile),
|
||||
"runtime_sha256": _sha256(runtime_source),
|
||||
"runtime": copy.deepcopy(runtime),
|
||||
"report_sha256": hashlib.sha256(_canonical_json(report_basis)).hexdigest(),
|
||||
"frames_sha256": hashlib.sha256(_canonical_json(frames)).hexdigest(),
|
||||
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
result_id = f"e46i-grounding-dino-full-replay-{identity_sha256}"
|
||||
destination = output_root.expanduser().absolute() / result_id
|
||||
if destination.exists():
|
||||
return read_e46i_grounding_dino_full_replay(destination)
|
||||
|
||||
created_at = datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
||||
report = {
|
||||
**report_basis,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"created_at_utc": created_at,
|
||||
}
|
||||
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
_write_json(staging / E46I_REPORT_NAME, report)
|
||||
_write_jsonl(staging / E46I_FRAMES_NAME, frames)
|
||||
for name in (
|
||||
E46I_OVERLAY_NAME,
|
||||
E46I_LABELS_NAME,
|
||||
E46I_RUNTIME_NAME,
|
||||
E46I_SHADOW_SHEET_NAME,
|
||||
E46I_ROUTE_SHEET_NAME,
|
||||
E46I_TARGETED_SHEET_NAME,
|
||||
):
|
||||
shutil.copyfile(raw / name, staging / name)
|
||||
artifacts = [
|
||||
_artifact(staging / E46I_REPORT_NAME, "grounding-dino-full-report"),
|
||||
_artifact(staging / E46I_FRAMES_NAME, "detection-frames"),
|
||||
_artifact(staging / E46I_OVERLAY_NAME, "visual-overlay-video"),
|
||||
_artifact(staging / E46I_LABELS_NAME, "raw-labels-archive"),
|
||||
_artifact(staging / E46I_RUNTIME_NAME, "runtime-record"),
|
||||
_artifact(staging / E46I_SHADOW_SHEET_NAME, "shadow-gate-contact-sheet"),
|
||||
_artifact(staging / E46I_ROUTE_SHEET_NAME, "full-route-contact-sheet"),
|
||||
_artifact(staging / E46I_TARGETED_SHEET_NAME, "targeted-windows-contact-sheet"),
|
||||
]
|
||||
_write_json(
|
||||
staging / E46I_MANIFEST_NAME,
|
||||
{
|
||||
"schema_version": E46I_RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": created_at,
|
||||
"acceptance_state": "semantic-progress-awaiting-temporal-layer",
|
||||
"ground_truth": False,
|
||||
"artifacts": artifacts,
|
||||
"authority": _AUTHORITY,
|
||||
},
|
||||
)
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
return read_e46i_grounding_dino_full_replay(destination)
|
||||
|
||||
|
||||
def read_e46i_grounding_dino_full_replay(root: Path) -> dict[str, Any]:
|
||||
"""Read and fully validate one immutable E46I result."""
|
||||
|
||||
try:
|
||||
return _read_e46i_grounding_dino_full_replay(root)
|
||||
except E46EReadyStackError as exc:
|
||||
raise E46IGroundingDinoFullReplayError(str(exc).replace("E46E", "E46I")) from exc
|
||||
|
||||
|
||||
def _read_e46i_grounding_dino_full_replay(root: Path) -> dict[str, Any]:
|
||||
resolved = root.resolve(strict=True)
|
||||
if resolved.is_symlink():
|
||||
raise E46IGroundingDinoFullReplayError("E46I result root must not be a symlink")
|
||||
manifest = _read_json(resolved / E46I_MANIFEST_NAME)
|
||||
identity = manifest.get("identity")
|
||||
digest = (
|
||||
hashlib.sha256(_canonical_json(identity)).hexdigest() if isinstance(identity, dict) else ""
|
||||
)
|
||||
if (
|
||||
manifest.get("schema_version") != E46I_RESULT_SCHEMA
|
||||
or manifest.get("result_id") != f"e46i-grounding-dino-full-replay-{digest}"
|
||||
or manifest.get("identity_sha256") != digest
|
||||
or resolved.name != manifest.get("result_id")
|
||||
or _RESULT_ID.fullmatch(resolved.name) is None
|
||||
or manifest.get("authority") != _AUTHORITY
|
||||
or manifest.get("ground_truth") is not False
|
||||
):
|
||||
raise E46IGroundingDinoFullReplayError("E46I result identity is invalid")
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(artifacts, list) or len(artifacts) != 8:
|
||||
raise E46IGroundingDinoFullReplayError("E46I artifact inventory is invalid")
|
||||
by_role = {row.get("role"): row for row in artifacts if isinstance(row, dict)}
|
||||
report_path = _validated_artifact(resolved, by_role.get("grounding-dino-full-report"))
|
||||
frames_path = _validated_artifact(resolved, by_role.get("detection-frames"))
|
||||
overlay_path = _validated_artifact(resolved, by_role.get("visual-overlay-video"))
|
||||
labels_path = _validated_artifact(resolved, by_role.get("raw-labels-archive"))
|
||||
runtime_path = _validated_artifact(resolved, by_role.get("runtime-record"))
|
||||
shadow_sheet_path = _validated_artifact(
|
||||
resolved, by_role.get("shadow-gate-contact-sheet")
|
||||
)
|
||||
route_sheet_path = _validated_artifact(
|
||||
resolved, by_role.get("full-route-contact-sheet")
|
||||
)
|
||||
targeted_sheet_path = _validated_artifact(
|
||||
resolved, by_role.get("targeted-windows-contact-sheet")
|
||||
)
|
||||
report = _read_json(report_path)
|
||||
frames = tuple(_read_jsonl(frames_path))
|
||||
runtime = _read_json(runtime_path)
|
||||
if (
|
||||
report.get("schema_version") != E46I_REPORT_SCHEMA
|
||||
or report.get("result_id") != resolved.name
|
||||
or report.get("authority") != _AUTHORITY
|
||||
or report.get("ground_truth") is not False
|
||||
or runtime.get("schema_version") != E46I_RUNTIME_SCHEMA
|
||||
or any(row.get("schema_version") != E46I_FRAME_SCHEMA for row in frames)
|
||||
or hashlib.sha256(_canonical_json(frames)).hexdigest() != identity.get("frames_sha256")
|
||||
or report.get("metrics", {}).get("frame_count") != len(frames)
|
||||
):
|
||||
raise E46IGroundingDinoFullReplayError("E46I result changed")
|
||||
return {
|
||||
"result_id": resolved.name,
|
||||
"result_root": resolved,
|
||||
"manifest": manifest,
|
||||
"report": report,
|
||||
"frames": frames,
|
||||
"runtime": runtime,
|
||||
"overlay_path": overlay_path,
|
||||
"labels_path": labels_path,
|
||||
"shadow_sheet_path": shadow_sheet_path,
|
||||
"route_sheet_path": route_sheet_path,
|
||||
"targeted_sheet_path": targeted_sheet_path,
|
||||
}
|
||||
|
||||
|
||||
def _validate_profile(profile: dict[str, Any]) -> None:
|
||||
source = profile.get("source")
|
||||
provider = profile.get("provider")
|
||||
inference = profile.get("inference")
|
||||
shadow = profile.get("visual_shadow_gate")
|
||||
if (
|
||||
profile.get("schema_version") != E46I_PROFILE_SCHEMA
|
||||
or not str(profile.get("baseline_result_id", "")).startswith(
|
||||
"e46h-full-rectified-front-replay-"
|
||||
)
|
||||
or not isinstance(source, dict)
|
||||
or source.get("camera_source_id") != "sensor.camera.right"
|
||||
or source.get("view") != "front"
|
||||
or source.get("projection_resolution") != [960, 544]
|
||||
or source.get("video_frame_count") != 4488
|
||||
or source.get("video_frame_rate") != 10.0
|
||||
or source.get("video_duration_seconds") != 448.8
|
||||
or _SHA256.fullmatch(str(source.get("video_sha256"))) is None
|
||||
or not isinstance(provider, dict)
|
||||
or provider.get("name") != "NVIDIA TAO Grounding DINO Swin-Tiny Commercial"
|
||||
or provider.get("custom_detector_or_postprocessing") is not False
|
||||
or _SHA256.fullmatch(str(provider.get("model_sha256"))) is None
|
||||
or _SHA256.fullmatch(str(provider.get("engine_sha256"))) is None
|
||||
or not isinstance(inference, dict)
|
||||
or inference.get("captions") != ["car", "person", "bicycle", "road sign"]
|
||||
or inference.get("confidence_threshold") != 0.5
|
||||
or inference.get("processed_frame_count") != 4488
|
||||
or inference.get("input_resolution") != [960, 544]
|
||||
or not isinstance(shadow, dict)
|
||||
or shadow.get("threshold_changed_after_review") is not False
|
||||
or shadow.get("legacy_large_false_background_cases_suppressed") != 5
|
||||
or profile.get("authority") != _AUTHORITY
|
||||
):
|
||||
raise E46IGroundingDinoFullReplayError("E46I profile is invalid")
|
||||
|
||||
|
||||
def _validate_runtime(runtime: dict[str, Any], profile: dict[str, Any], raw: Path) -> None:
|
||||
overlay = runtime.get("overlay")
|
||||
labels = runtime.get("labels_archive")
|
||||
if (
|
||||
runtime.get("schema_version") != E46I_RUNTIME_SCHEMA
|
||||
or runtime.get("run_status") != "SUCCESS"
|
||||
or runtime.get("processed_frame_count") != 4488
|
||||
or runtime.get("annotated_frame_count") != 4488
|
||||
or runtime.get("label_file_count") != 4488
|
||||
or runtime.get("source_video_sha256") != profile["source"]["video_sha256"]
|
||||
or runtime.get("model_onnx_sha256") != profile["provider"]["model_sha256"]
|
||||
or runtime.get("tensorrt_engine_sha256") != profile["provider"]["engine_sha256"]
|
||||
or runtime.get("spec_sha256") != profile["inference"]["spec_sha256"]
|
||||
or not isinstance(overlay, dict)
|
||||
or overlay.get("file") != E46I_OVERLAY_NAME
|
||||
or overlay.get("frame_count") != 4488
|
||||
or overlay.get("duration_seconds") != 448.8
|
||||
or not isinstance(labels, dict)
|
||||
or labels.get("file") != E46I_LABELS_NAME
|
||||
):
|
||||
raise E46IGroundingDinoFullReplayError("E46I runtime identity is invalid")
|
||||
for row in (overlay, labels):
|
||||
path = raw / str(row["file"])
|
||||
if (
|
||||
not path.is_file()
|
||||
or path.is_symlink()
|
||||
or path.stat().st_size != row.get("byte_length")
|
||||
or _sha256(path) != row.get("sha256")
|
||||
):
|
||||
raise E46IGroundingDinoFullReplayError("E46I runtime artifact changed")
|
||||
for name in (
|
||||
E46I_SHADOW_SHEET_NAME,
|
||||
E46I_ROUTE_SHEET_NAME,
|
||||
E46I_TARGETED_SHEET_NAME,
|
||||
):
|
||||
path = raw / name
|
||||
if not path.is_file() or path.is_symlink() or path.stat().st_size <= 0:
|
||||
raise E46IGroundingDinoFullReplayError("E46I visual artifact is missing")
|
||||
|
||||
|
||||
def _read_detection_frames(
|
||||
labels_root: Path, profile: dict[str, Any], frame_count: int
|
||||
) -> list[dict[str, Any]]:
|
||||
allowed = set(profile["inference"]["captions"])
|
||||
threshold = float(profile["inference"]["confidence_threshold"])
|
||||
frames: list[dict[str, Any]] = []
|
||||
for frame_number in range(1, frame_count + 1):
|
||||
path = labels_root / f"frame_{frame_number:06d}.txt"
|
||||
if not path.is_file() or path.is_symlink():
|
||||
raise E46IGroundingDinoFullReplayError("E46I label sequence is incomplete")
|
||||
detections: list[dict[str, Any]] = []
|
||||
for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
|
||||
if not line.strip():
|
||||
continue
|
||||
tokens = line.split()
|
||||
if len(tokens) < 16:
|
||||
raise E46IGroundingDinoFullReplayError("E46I label row is invalid")
|
||||
label = " ".join(tokens[:-15])
|
||||
try:
|
||||
numeric = [float(value) for value in tokens[-15:]]
|
||||
except ValueError as exc:
|
||||
raise E46IGroundingDinoFullReplayError(
|
||||
f"E46I label row {path.name}:{line_number} is invalid"
|
||||
) from exc
|
||||
x1, y1, x2, y2 = numeric[3:7]
|
||||
confidence = numeric[-1]
|
||||
if (
|
||||
label not in allowed
|
||||
or not threshold <= confidence <= 1.0
|
||||
or not 0.0 <= x1 < x2 <= 960.0
|
||||
or not 0.0 <= y1 < y2 <= 544.0
|
||||
):
|
||||
raise E46IGroundingDinoFullReplayError("E46I detection contract is invalid")
|
||||
area_fraction = ((x2 - x1) * (y2 - y1)) / (960.0 * 544.0)
|
||||
detections.append(
|
||||
{
|
||||
"class_name": label,
|
||||
"confidence": round(confidence, 6),
|
||||
"bbox_xyxy": [
|
||||
round(x1, 3),
|
||||
round(y1, 3),
|
||||
round(x2, 3),
|
||||
round(y2, 3),
|
||||
],
|
||||
"area_fraction": round(area_fraction, 9),
|
||||
}
|
||||
)
|
||||
frames.append(
|
||||
{
|
||||
"schema_version": E46I_FRAME_SCHEMA,
|
||||
"frame_index": frame_number - 1,
|
||||
"session_seconds": round((frame_number - 1) / 10.0, 1),
|
||||
"label_sha256": _sha256(path),
|
||||
"detection_count": len(detections),
|
||||
"detections": detections,
|
||||
}
|
||||
)
|
||||
extras = [
|
||||
path
|
||||
for path in labels_root.glob("frame_*.txt")
|
||||
if path.name > f"frame_{frame_count:06d}.txt"
|
||||
]
|
||||
if extras:
|
||||
raise E46IGroundingDinoFullReplayError("E46I label sequence has extra frames")
|
||||
return frames
|
||||
|
||||
|
||||
def _metrics(frames: list[dict[str, Any]], runtime: dict[str, Any]) -> dict[str, Any]:
|
||||
detections = [detection for frame in frames for detection in frame["detections"]]
|
||||
counts = [int(frame["detection_count"]) for frame in frames]
|
||||
confidences = [float(detection["confidence"]) for detection in detections]
|
||||
areas = [float(detection["area_fraction"]) for detection in detections]
|
||||
classes = Counter(str(detection["class_name"]) for detection in detections)
|
||||
zero_runs: list[int] = []
|
||||
run = 0
|
||||
for count in counts:
|
||||
if count == 0:
|
||||
run += 1
|
||||
elif run:
|
||||
zero_runs.append(run)
|
||||
run = 0
|
||||
if run:
|
||||
zero_runs.append(run)
|
||||
return {
|
||||
"frame_count": len(frames),
|
||||
"route_duration_seconds": 448.8,
|
||||
"detection_observation_count": len(detections),
|
||||
"class_observation_counts": dict(sorted(classes.items())),
|
||||
"mean_detections_per_frame": round(fmean(counts), 6),
|
||||
"max_detections_per_frame": max(counts),
|
||||
"zero_detection_frame_count": sum(count == 0 for count in counts),
|
||||
"zero_detection_frame_fraction": round(
|
||||
sum(count == 0 for count in counts) / len(frames), 9
|
||||
),
|
||||
"zero_detection_run_count": len(zero_runs),
|
||||
"longest_zero_detection_run_frames": max(zero_runs, default=0),
|
||||
"confidence_mean": round(fmean(confidences), 6),
|
||||
"confidence_min": min(confidences),
|
||||
"confidence_max": max(confidences),
|
||||
"large_box_observation_count": sum(area >= 0.25 for area in areas),
|
||||
"large_box_observation_fraction": round(
|
||||
sum(area >= 0.25 for area in areas) / len(areas), 9
|
||||
),
|
||||
"largest_box_area_fraction": round(max(areas), 9),
|
||||
"worker_elapsed_seconds": float(runtime["elapsed_seconds"]),
|
||||
"worker_mean_frames_per_second": float(runtime["mean_frames_per_second"]),
|
||||
}
|
||||
|
||||
|
||||
def _method(profile: dict[str, Any]) -> dict[str, Any]:
|
||||
source = profile["source"]
|
||||
provider = profile["provider"]
|
||||
inference = profile["inference"]
|
||||
return {
|
||||
"schema_version": "missioncore.laboratory-method/v1",
|
||||
"completeness": "complete",
|
||||
"execution_class": "hybrid",
|
||||
"pipeline_id": profile["profile_id"],
|
||||
"components": [
|
||||
{
|
||||
"kind": "source",
|
||||
"name": source["session_id"],
|
||||
"version": "immutable recorded RIGHT FRONT replay",
|
||||
"role": "single physical camera source",
|
||||
"identity_sha256": source["video_sha256"],
|
||||
},
|
||||
{
|
||||
"kind": "tool",
|
||||
"name": "NVIDIA Gst-nvdewarper",
|
||||
"version": "DeepStream 9.1",
|
||||
"role": "existing E46H calibrated FRONT adapter",
|
||||
"identity_sha256": (
|
||||
"f861e31278550bbe3fc82f41df4381c8a7aaf113a98a10761d98fa085c6a56b4"
|
||||
),
|
||||
},
|
||||
{
|
||||
"kind": "model",
|
||||
"name": provider["name"],
|
||||
"version": provider["version"],
|
||||
"role": "ready open-vocabulary detector provider",
|
||||
"identity_sha256": provider["model_sha256"],
|
||||
},
|
||||
{
|
||||
"kind": "runtime",
|
||||
"name": provider["deployment_toolkit"],
|
||||
"version": "TensorRT FP16",
|
||||
"role": "GPU inference runtime",
|
||||
"identity_sha256": provider["engine_sha256"],
|
||||
},
|
||||
{
|
||||
"kind": "algorithm",
|
||||
"name": "fixed open-vocabulary caption contract",
|
||||
"version": ", ".join(inference["captions"]),
|
||||
"role": "source-independent semantic query set",
|
||||
"identity_sha256": inference["spec_sha256"],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,625 @@
|
||||
"""Freeze the one-pass YOLOX-S full-raw-fisheye realtime qualification.
|
||||
|
||||
E46J deliberately evaluates the production-shaped detector path: one physical
|
||||
K1 RIGHT frame produces one inference request. It does not dewarp, crop, tile,
|
||||
track, hold or stitch detections. The result proves replay capacity and keeps
|
||||
visual quality findings separate from ground-truth claims.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import uuid
|
||||
from collections import Counter
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from statistics import fmean
|
||||
from typing import Any, Final
|
||||
|
||||
from k1link.compute.e46e_ready_stack import (
|
||||
E46EReadyStackError,
|
||||
_artifact,
|
||||
_canonical_json,
|
||||
_read_json,
|
||||
_read_jsonl,
|
||||
_sha256,
|
||||
_validated_artifact,
|
||||
_write_json,
|
||||
)
|
||||
|
||||
E46J_PROFILE_SCHEMA: Final = "missioncore.e46j-raw-fisheye-realtime-profile/v1"
|
||||
E46J_RUNTIME_SCHEMA: Final = "missioncore.e46j-raw-fisheye-realtime-runtime/v1"
|
||||
E46J_FRAME_SCHEMA: Final = "missioncore.e46j-raw-fisheye-realtime-frame/v1"
|
||||
E46J_RESULT_SCHEMA: Final = "missioncore.e46j-raw-fisheye-realtime-result/v1"
|
||||
E46J_REPORT_SCHEMA: Final = "missioncore.e46j-raw-fisheye-realtime-report/v1"
|
||||
|
||||
E46J_MANIFEST_NAME: Final = "manifest.json"
|
||||
E46J_REPORT_NAME: Final = "raw-fisheye-realtime-report.json"
|
||||
E46J_FRAMES_NAME: Final = "frames.jsonl"
|
||||
E46J_RUNTIME_NAME: Final = "runtime.json"
|
||||
E46J_OVERLAY_NAME: Final = "raw-fisheye-yolox-overlay.mp4"
|
||||
E46J_ROUTE_SHEET_NAME: Final = "full-route-10s-contact-sheet.png"
|
||||
E46J_TARGETED_SHEET_NAME: Final = "targeted-windows-contact-sheet.png"
|
||||
E46J_SHADOW_SHEET_NAME: Final = "operator-shadow-exception-contact-sheet.png"
|
||||
|
||||
_RESULT_ID = re.compile(r"^e46j-raw-fisheye-realtime-[a-f0-9]{64}$")
|
||||
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
|
||||
_CLASS_BY_ID: Final = {
|
||||
0: "person",
|
||||
1: "bicycle",
|
||||
2: "car",
|
||||
3: "motorcycle",
|
||||
5: "bus",
|
||||
7: "truck",
|
||||
}
|
||||
_AUTHORITY: Final = {
|
||||
"ground_truth": False,
|
||||
"provider_promoted": False,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
_REVIEW_WINDOWS: Final = [
|
||||
{
|
||||
"id": "legacy-false-wall",
|
||||
"label": "6.0–10.9 с · прежний false car на стене",
|
||||
"start_seconds": 6.0,
|
||||
"end_seconds": 10.9,
|
||||
"verdict": "legacy-background-false-positive-suppressed",
|
||||
},
|
||||
{
|
||||
"id": "legacy-false-shrub",
|
||||
"label": "178.6–180.3 с · прежний false car на кусте",
|
||||
"start_seconds": 178.6,
|
||||
"end_seconds": 180.3,
|
||||
"verdict": "legacy-background-false-positive-suppressed",
|
||||
},
|
||||
{
|
||||
"id": "legacy-false-ground",
|
||||
"label": "250.7–265.6 с · прежний false car на полотне",
|
||||
"start_seconds": 250.7,
|
||||
"end_seconds": 265.6,
|
||||
"verdict": "legacy-background-false-positive-suppressed",
|
||||
},
|
||||
{
|
||||
"id": "legacy-false-road",
|
||||
"label": "392.2–400.6 с · прежний false car на дороге",
|
||||
"start_seconds": 392.2,
|
||||
"end_seconds": 400.6,
|
||||
"verdict": "legacy-background-false-positive-suppressed",
|
||||
},
|
||||
{
|
||||
"id": "operator-shadow",
|
||||
"label": "419.4–426.9 с · тень оператора",
|
||||
"start_seconds": 419.4,
|
||||
"end_seconds": 426.9,
|
||||
"verdict": "operator-shadow-person-false-positive-observed",
|
||||
},
|
||||
{
|
||||
"id": "legacy-false-terrace",
|
||||
"label": "440.8–448.4 с · прежний false car на террасе",
|
||||
"start_seconds": 440.8,
|
||||
"end_seconds": 448.4,
|
||||
"verdict": "legacy-background-false-positive-suppressed",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class E46JRawFisheyeRealtimeError(ValueError):
|
||||
"""Raised when E46J evidence or immutable identity is invalid."""
|
||||
|
||||
|
||||
def build_e46j_raw_fisheye_realtime(
|
||||
*, raw_root: Path, profile_path: Path, output_root: Path
|
||||
) -> dict[str, Any]:
|
||||
"""Validate worker evidence and freeze one immutable E46J result."""
|
||||
|
||||
try:
|
||||
return _build_e46j_raw_fisheye_realtime(
|
||||
raw_root=raw_root,
|
||||
profile_path=profile_path,
|
||||
output_root=output_root,
|
||||
)
|
||||
except E46EReadyStackError as exc:
|
||||
raise E46JRawFisheyeRealtimeError(str(exc).replace("E46E", "E46J")) from exc
|
||||
|
||||
|
||||
def _build_e46j_raw_fisheye_realtime(
|
||||
*, raw_root: Path, profile_path: Path, output_root: Path
|
||||
) -> dict[str, Any]:
|
||||
raw = raw_root.resolve(strict=True)
|
||||
if raw.is_symlink():
|
||||
raise E46JRawFisheyeRealtimeError("E46J raw root must not be a symlink")
|
||||
profile_source = profile_path.resolve(strict=True)
|
||||
profile = _read_json(profile_source)
|
||||
_validate_profile(profile)
|
||||
runtime_source = raw / E46J_RUNTIME_NAME
|
||||
runtime = _read_json(runtime_source)
|
||||
_validate_runtime(runtime, profile, profile_source, raw)
|
||||
frames_source = raw / E46J_FRAMES_NAME
|
||||
frames = _validated_frames(frames_source, profile, runtime)
|
||||
metrics = _metrics(frames, runtime)
|
||||
method = _method(profile)
|
||||
|
||||
report_basis = {
|
||||
"schema_version": E46J_REPORT_SCHEMA,
|
||||
"status": "realtime-capacity-passed-awaiting-temporal-layer",
|
||||
"source": copy.deepcopy(profile["source"]),
|
||||
"detector": copy.deepcopy(profile["detector"]),
|
||||
"preprocessing": copy.deepcopy(profile["preprocessing"]),
|
||||
"detection": copy.deepcopy(profile["detection"]),
|
||||
"metrics": metrics,
|
||||
"visual_review": {
|
||||
"status": "route-contact-sheet-and-targeted-window-review-completed",
|
||||
"full_route_contact_sheet_review_completed": True,
|
||||
"targeted_window_review_completed": True,
|
||||
"reviewed_video_range_seconds": [0.0, 448.723],
|
||||
"review_windows": copy.deepcopy(_REVIEW_WINDOWS),
|
||||
"verdict": "realtime-detector-progress-with-known-shadow-exception",
|
||||
"finding": (
|
||||
"Full raw KB4 fisheye coverage is retained. Vehicles remain visible on "
|
||||
"the route and at the circular image edge; the five previously reviewed "
|
||||
"large background car failures are absent in the targeted samples."
|
||||
),
|
||||
"known_error": (
|
||||
"The operator shadow is classified as person in 35 of the 75 frames "
|
||||
"inside the 419.4–426.9 second review window."
|
||||
),
|
||||
},
|
||||
"acceptance": {
|
||||
"exact_recorded_right_source_bound": True,
|
||||
"full_raw_fisheye_retained": True,
|
||||
"single_inference_per_source_frame": True,
|
||||
"all_source_frames_processed": metrics["frame_count"] == 4489,
|
||||
"zero_failed_frames": metrics["failed_frame_count"] == 0,
|
||||
"ten_hz_capacity_gate_passed": metrics["core_capacity_fps"] >= 10.0,
|
||||
"latency_gate_passed": (
|
||||
metrics["core_path_p95_ms"] <= 80.0
|
||||
and metrics["inference_request_p95_ms"] <= 60.0
|
||||
),
|
||||
"legacy_large_false_background_gate_passed": True,
|
||||
"full_overlay_published": True,
|
||||
"temporal_identity_available": False,
|
||||
"motion_state_available": False,
|
||||
"candidate_accepted": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
"decision": {
|
||||
"selected_provider": "megvii-yolox-s-0.1.1rc0",
|
||||
"realtime_capacity_passed": True,
|
||||
"ready_for_temporal_bakeoff": True,
|
||||
"provider_promoted": False,
|
||||
"custom_detector_logic_used": False,
|
||||
"route_specific_filtering_used": False,
|
||||
"next_action": (
|
||||
"Keep the single raw-fisheye detector pass unchanged and attach a ready "
|
||||
"temporal tracker. Evaluate stable IDs, drop/recovery and dynamic/static "
|
||||
"state on the same full video before any live-hardware claim."
|
||||
),
|
||||
},
|
||||
"method": method,
|
||||
"limitations": [
|
||||
(
|
||||
"E46J has no independent exhaustive truth; detection observation counts "
|
||||
"are not precision or recall."
|
||||
),
|
||||
(
|
||||
"The detector is frame-local and does not provide persistent object IDs, "
|
||||
"track continuity or dynamic/static state."
|
||||
),
|
||||
"A known person false positive occurs on the operator shadow.",
|
||||
(
|
||||
"LiDAR range, free space, commands, navigation and safety remain outside "
|
||||
"this result."
|
||||
),
|
||||
(
|
||||
"The accepted capacity assumes the camera adapter and Triton share a "
|
||||
"local GPU host path; routing full FP32 tensors through an external Windows "
|
||||
"bridge is not an accepted realtime topology."
|
||||
),
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
"ground_truth": False,
|
||||
}
|
||||
identity = {
|
||||
"schema_version": E46J_RESULT_SCHEMA,
|
||||
"profile_sha256": _sha256(profile_source),
|
||||
"profile": copy.deepcopy(profile),
|
||||
"runtime_sha256": _sha256(runtime_source),
|
||||
"runtime": copy.deepcopy(runtime),
|
||||
"report_sha256": hashlib.sha256(_canonical_json(report_basis)).hexdigest(),
|
||||
"frames_sha256": hashlib.sha256(_canonical_json(frames)).hexdigest(),
|
||||
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
result_id = f"e46j-raw-fisheye-realtime-{identity_sha256}"
|
||||
destination = output_root.expanduser().absolute() / result_id
|
||||
if destination.exists():
|
||||
return read_e46j_raw_fisheye_realtime(destination)
|
||||
|
||||
created_at = datetime.now(UTC).isoformat(timespec="milliseconds").replace(
|
||||
"+00:00", "Z"
|
||||
)
|
||||
report = {
|
||||
**report_basis,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"created_at_utc": created_at,
|
||||
}
|
||||
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
_write_json(staging / E46J_REPORT_NAME, report)
|
||||
for name in (
|
||||
E46J_FRAMES_NAME,
|
||||
E46J_RUNTIME_NAME,
|
||||
E46J_OVERLAY_NAME,
|
||||
E46J_ROUTE_SHEET_NAME,
|
||||
E46J_TARGETED_SHEET_NAME,
|
||||
E46J_SHADOW_SHEET_NAME,
|
||||
):
|
||||
shutil.copyfile(raw / name, staging / name)
|
||||
artifacts = [
|
||||
_artifact(staging / E46J_REPORT_NAME, "raw-fisheye-realtime-report"),
|
||||
_artifact(staging / E46J_FRAMES_NAME, "detection-frames"),
|
||||
_artifact(staging / E46J_RUNTIME_NAME, "runtime-record"),
|
||||
_artifact(staging / E46J_OVERLAY_NAME, "visual-overlay-video"),
|
||||
_artifact(staging / E46J_ROUTE_SHEET_NAME, "full-route-contact-sheet"),
|
||||
_artifact(staging / E46J_TARGETED_SHEET_NAME, "targeted-windows-contact-sheet"),
|
||||
_artifact(staging / E46J_SHADOW_SHEET_NAME, "operator-shadow-contact-sheet"),
|
||||
]
|
||||
_write_json(
|
||||
staging / E46J_MANIFEST_NAME,
|
||||
{
|
||||
"schema_version": E46J_RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": created_at,
|
||||
"acceptance_state": "realtime-capacity-passed-awaiting-temporal-layer",
|
||||
"ground_truth": False,
|
||||
"artifacts": artifacts,
|
||||
"authority": _AUTHORITY,
|
||||
},
|
||||
)
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
return read_e46j_raw_fisheye_realtime(destination)
|
||||
|
||||
|
||||
def read_e46j_raw_fisheye_realtime(root: Path) -> dict[str, Any]:
|
||||
"""Read and fully validate one immutable E46J result."""
|
||||
|
||||
try:
|
||||
return _read_e46j_raw_fisheye_realtime(root)
|
||||
except E46EReadyStackError as exc:
|
||||
raise E46JRawFisheyeRealtimeError(str(exc).replace("E46E", "E46J")) from exc
|
||||
|
||||
|
||||
def _read_e46j_raw_fisheye_realtime(root: Path) -> dict[str, Any]:
|
||||
resolved = root.resolve(strict=True)
|
||||
if resolved.is_symlink():
|
||||
raise E46JRawFisheyeRealtimeError("E46J result root must not be a symlink")
|
||||
manifest = _read_json(resolved / E46J_MANIFEST_NAME)
|
||||
identity = manifest.get("identity")
|
||||
digest = (
|
||||
hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
if isinstance(identity, dict)
|
||||
else ""
|
||||
)
|
||||
if (
|
||||
manifest.get("schema_version") != E46J_RESULT_SCHEMA
|
||||
or manifest.get("result_id") != f"e46j-raw-fisheye-realtime-{digest}"
|
||||
or manifest.get("identity_sha256") != digest
|
||||
or resolved.name != manifest.get("result_id")
|
||||
or _RESULT_ID.fullmatch(resolved.name) is None
|
||||
or manifest.get("authority") != _AUTHORITY
|
||||
or manifest.get("ground_truth") is not False
|
||||
):
|
||||
raise E46JRawFisheyeRealtimeError("E46J result identity is invalid")
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(artifacts, list) or len(artifacts) != 7:
|
||||
raise E46JRawFisheyeRealtimeError("E46J artifact inventory is invalid")
|
||||
by_role = {row.get("role"): row for row in artifacts if isinstance(row, dict)}
|
||||
paths = {
|
||||
role: _validated_artifact(resolved, by_role.get(role))
|
||||
for role in (
|
||||
"raw-fisheye-realtime-report",
|
||||
"detection-frames",
|
||||
"runtime-record",
|
||||
"visual-overlay-video",
|
||||
"full-route-contact-sheet",
|
||||
"targeted-windows-contact-sheet",
|
||||
"operator-shadow-contact-sheet",
|
||||
)
|
||||
}
|
||||
report = _read_json(paths["raw-fisheye-realtime-report"])
|
||||
runtime = _read_json(paths["runtime-record"])
|
||||
frames = tuple(_read_jsonl(paths["detection-frames"]))
|
||||
if (
|
||||
report.get("schema_version") != E46J_REPORT_SCHEMA
|
||||
or report.get("result_id") != resolved.name
|
||||
or report.get("authority") != _AUTHORITY
|
||||
or report.get("ground_truth") is not False
|
||||
or runtime.get("schema_version") != E46J_RUNTIME_SCHEMA
|
||||
or len(frames) != 4489
|
||||
or any(row.get("schema_version") != E46J_FRAME_SCHEMA for row in frames)
|
||||
or hashlib.sha256(_canonical_json(frames)).hexdigest()
|
||||
!= identity.get("frames_sha256")
|
||||
or report.get("metrics", {}).get("frame_count") != len(frames)
|
||||
):
|
||||
raise E46JRawFisheyeRealtimeError("E46J result changed")
|
||||
return {
|
||||
"result_id": resolved.name,
|
||||
"result_root": resolved,
|
||||
"manifest": manifest,
|
||||
"report": report,
|
||||
"frames": frames,
|
||||
"runtime": runtime,
|
||||
"overlay_path": paths["visual-overlay-video"],
|
||||
"route_sheet_path": paths["full-route-contact-sheet"],
|
||||
"targeted_sheet_path": paths["targeted-windows-contact-sheet"],
|
||||
"shadow_sheet_path": paths["operator-shadow-contact-sheet"],
|
||||
}
|
||||
|
||||
|
||||
def _validate_profile(profile: dict[str, Any]) -> None:
|
||||
source = profile.get("source")
|
||||
detector = profile.get("detector")
|
||||
detection = profile.get("detection")
|
||||
acceptance = profile.get("acceptance")
|
||||
if (
|
||||
profile.get("schema_version") != E46J_PROFILE_SCHEMA
|
||||
or profile.get("profile_id") != "e46j-k1-right-raw-kb4-yolox-s-one-pass/v1"
|
||||
or not isinstance(source, dict)
|
||||
or source.get("camera_source_id") != "sensor.camera.right"
|
||||
or source.get("stream_sha256")
|
||||
!= "cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8"
|
||||
or source.get("frame_count") != 4489
|
||||
or source.get("average_rate") != "4489000/448723"
|
||||
or source.get("resolution") != [800, 600]
|
||||
or source.get("calibration_model") != "KB4"
|
||||
or not isinstance(detector, dict)
|
||||
or detector.get("architecture") != "YOLOX-S"
|
||||
or detector.get("license") != "Apache-2.0"
|
||||
or detector.get("single_inference_per_source_frame") is not True
|
||||
or _SHA256.fullmatch(str(detector.get("model_sha256"))) is None
|
||||
or _SHA256.fullmatch(str(detector.get("config_sha256"))) is None
|
||||
or not isinstance(detection, dict)
|
||||
or detection.get("minimum_score") != 0.5
|
||||
or detection.get("nms_iou_threshold") != 0.45
|
||||
or detection.get("target_class_ids") != [0, 1, 2, 3, 5, 7]
|
||||
or detection.get("custom_detector_logic") is not False
|
||||
or detection.get("route_specific_filtering") is not False
|
||||
or not isinstance(acceptance, dict)
|
||||
or acceptance.get("minimum_core_capacity_fps") != 10.0
|
||||
or acceptance.get("require_full_raw_fov") is not True
|
||||
or profile.get("authority") != _AUTHORITY
|
||||
):
|
||||
raise E46JRawFisheyeRealtimeError("E46J profile is invalid")
|
||||
|
||||
|
||||
def _validate_runtime(
|
||||
runtime: dict[str, Any],
|
||||
profile: dict[str, Any],
|
||||
profile_path: Path,
|
||||
raw: Path,
|
||||
) -> None:
|
||||
source = runtime.get("source")
|
||||
model = runtime.get("model")
|
||||
metrics = runtime.get("metrics")
|
||||
acceptance = runtime.get("acceptance")
|
||||
artifacts = runtime.get("artifacts")
|
||||
if (
|
||||
runtime.get("schema_version") != E46J_RUNTIME_SCHEMA
|
||||
or runtime.get("status") != "completed"
|
||||
or runtime.get("profile_sha256") != _sha256(profile_path)
|
||||
or not isinstance(source, dict)
|
||||
or source.get("video_sha256") != profile["source"]["stream_sha256"]
|
||||
or source.get("decoded_frame_count") != 4489
|
||||
or source.get("resolution") != [800, 600]
|
||||
or "no crop/dewarp/tile" not in str(source.get("preprocessing"))
|
||||
or not isinstance(model, dict)
|
||||
or model.get("id") != profile["detector"]["id"]
|
||||
or model.get("model_sha256") != profile["detector"]["model_sha256"]
|
||||
or model.get("config_sha256") != profile["detector"]["config_sha256"]
|
||||
or model.get("inference_requests") != 4489
|
||||
or not isinstance(metrics, dict)
|
||||
or metrics.get("processed_frame_count") != 4489
|
||||
or metrics.get("failed_frame_count") != 0
|
||||
or float(metrics.get("core_capacity_fps", 0.0)) < 10.0
|
||||
or not isinstance(acceptance, dict)
|
||||
or acceptance.get("passed") is not True
|
||||
or not all(acceptance.get("checks", {}).values())
|
||||
or runtime.get("authority") != _AUTHORITY
|
||||
or not isinstance(artifacts, dict)
|
||||
):
|
||||
raise E46JRawFisheyeRealtimeError("E46J runtime identity is invalid")
|
||||
expected = {
|
||||
"frames": E46J_FRAMES_NAME,
|
||||
"overlay": E46J_OVERLAY_NAME,
|
||||
}
|
||||
for role, name in expected.items():
|
||||
row = artifacts.get(role)
|
||||
path = raw / name
|
||||
if (
|
||||
not isinstance(row, dict)
|
||||
or row.get("file") != name
|
||||
or not path.is_file()
|
||||
or path.is_symlink()
|
||||
or path.stat().st_size != row.get("byte_length")
|
||||
or _sha256(path) != row.get("sha256")
|
||||
):
|
||||
raise E46JRawFisheyeRealtimeError("E46J runtime artifact changed")
|
||||
for name in (
|
||||
E46J_ROUTE_SHEET_NAME,
|
||||
E46J_TARGETED_SHEET_NAME,
|
||||
E46J_SHADOW_SHEET_NAME,
|
||||
):
|
||||
path = raw / name
|
||||
if not path.is_file() or path.is_symlink() or path.stat().st_size <= 0:
|
||||
raise E46JRawFisheyeRealtimeError("E46J visual artifact is missing")
|
||||
|
||||
|
||||
def _validated_frames(
|
||||
path: Path, profile: dict[str, Any], runtime: dict[str, Any]
|
||||
) -> list[dict[str, Any]]:
|
||||
if not path.is_file() or path.is_symlink():
|
||||
raise E46JRawFisheyeRealtimeError("E46J frame evidence is missing")
|
||||
rows = list(_read_jsonl(path))
|
||||
if len(rows) != 4489:
|
||||
raise E46JRawFisheyeRealtimeError("E46J frame sequence is incomplete")
|
||||
artifact = runtime["artifacts"]["frames"]
|
||||
if path.stat().st_size != artifact["byte_length"] or _sha256(path) != artifact["sha256"]:
|
||||
raise E46JRawFisheyeRealtimeError("E46J frame evidence changed")
|
||||
frame_rate = float(profile["source"]["frame_rate"])
|
||||
class_counts: Counter[str] = Counter()
|
||||
for index, row in enumerate(rows):
|
||||
detections = row.get("detections")
|
||||
if (
|
||||
row.get("schema_version") != E46J_FRAME_SCHEMA
|
||||
or row.get("frame_index") != index
|
||||
or abs(float(row.get("session_seconds", -1.0)) - index / frame_rate) > 1e-5
|
||||
or not isinstance(detections, list)
|
||||
or not isinstance(row.get("latency_ms"), dict)
|
||||
):
|
||||
raise E46JRawFisheyeRealtimeError("E46J frame contract is invalid")
|
||||
for detection in detections:
|
||||
if not isinstance(detection, dict):
|
||||
raise E46JRawFisheyeRealtimeError("E46J detection is invalid")
|
||||
class_id = detection.get("class_id")
|
||||
box = detection.get("bbox_xyxy")
|
||||
score = detection.get("score")
|
||||
if (
|
||||
not isinstance(class_id, int)
|
||||
or detection.get("label") != _CLASS_BY_ID.get(class_id)
|
||||
or not isinstance(score, (int, float))
|
||||
or not 0.5 <= float(score) <= 1.0
|
||||
or not isinstance(box, list)
|
||||
or len(box) != 4
|
||||
or not 0.0 <= float(box[0]) < float(box[2]) <= 800.0
|
||||
or not 0.0 <= float(box[1]) < float(box[3]) <= 600.0
|
||||
):
|
||||
raise E46JRawFisheyeRealtimeError("E46J detection contract is invalid")
|
||||
class_counts[str(detection["label"])] += 1
|
||||
if dict(sorted(class_counts.items())) != runtime["metrics"]["class_observation_counts"]:
|
||||
raise E46JRawFisheyeRealtimeError("E46J class accounting changed")
|
||||
return rows
|
||||
|
||||
|
||||
def _metrics(frames: list[dict[str, Any]], runtime: dict[str, Any]) -> dict[str, Any]:
|
||||
detections = [item for frame in frames for item in frame["detections"]]
|
||||
counts = [len(frame["detections"]) for frame in frames]
|
||||
confidences = [float(item["score"]) for item in detections]
|
||||
zero_runs: list[int] = []
|
||||
run = 0
|
||||
for count in counts:
|
||||
if count == 0:
|
||||
run += 1
|
||||
elif run:
|
||||
zero_runs.append(run)
|
||||
run = 0
|
||||
if run:
|
||||
zero_runs.append(run)
|
||||
shadow_frames = [
|
||||
frame
|
||||
for frame in frames
|
||||
if 419.4 <= float(frame["session_seconds"]) <= 426.9
|
||||
]
|
||||
shadow_person_frames = sum(
|
||||
any(item["label"] == "person" for item in frame["detections"])
|
||||
for frame in shadow_frames
|
||||
)
|
||||
runtime_metrics = runtime["metrics"]
|
||||
latency = runtime_metrics["latency_ms"]
|
||||
return {
|
||||
"frame_count": len(frames),
|
||||
"route_duration_seconds": 448.723,
|
||||
"failed_frame_count": int(runtime_metrics["failed_frame_count"]),
|
||||
"detection_observation_count": len(detections),
|
||||
"class_observation_counts": copy.deepcopy(
|
||||
runtime_metrics["class_observation_counts"]
|
||||
),
|
||||
"mean_detections_per_frame": round(fmean(counts), 6),
|
||||
"max_detections_per_frame": max(counts),
|
||||
"zero_detection_frame_count": sum(count == 0 for count in counts),
|
||||
"zero_detection_frame_fraction": round(
|
||||
sum(count == 0 for count in counts) / len(frames), 9
|
||||
),
|
||||
"zero_detection_run_count": len(zero_runs),
|
||||
"longest_zero_detection_run_frames": max(zero_runs, default=0),
|
||||
"confidence_mean": round(fmean(confidences), 6),
|
||||
"confidence_min": min(confidences),
|
||||
"confidence_max": max(confidences),
|
||||
"core_capacity_fps": float(runtime_metrics["core_capacity_fps"]),
|
||||
"core_path_mean_ms": float(latency["core_path_ms"]["mean"]),
|
||||
"core_path_p95_ms": float(latency["core_path_ms"]["p95"]),
|
||||
"inference_request_mean_ms": float(
|
||||
latency["inference_request_ms"]["mean"]
|
||||
),
|
||||
"inference_request_p95_ms": float(latency["inference_request_ms"]["p95"]),
|
||||
"gpu_utilization_mean_percent": float(
|
||||
runtime_metrics["gpu"]["gpu_utilization_percent"]["mean"]
|
||||
),
|
||||
"operator_shadow_window_frame_count": len(shadow_frames),
|
||||
"operator_shadow_person_frame_count": shadow_person_frames,
|
||||
}
|
||||
|
||||
|
||||
def _method(profile: dict[str, Any]) -> dict[str, Any]:
|
||||
source = profile["source"]
|
||||
detector = profile["detector"]
|
||||
detection = profile["detection"]
|
||||
return {
|
||||
"schema_version": "missioncore.laboratory-method/v1",
|
||||
"completeness": "complete",
|
||||
"execution_class": "hybrid",
|
||||
"pipeline_id": profile["profile_id"],
|
||||
"components": [
|
||||
{
|
||||
"kind": "source",
|
||||
"name": source["session_id"],
|
||||
"version": "immutable recorded K1 RIGHT raw KB4",
|
||||
"role": "single physical camera source; full 800×600 raster",
|
||||
"identity_sha256": source["stream_sha256"],
|
||||
},
|
||||
{
|
||||
"kind": "tool",
|
||||
"name": "valid-FOV mask plus top-left letterbox",
|
||||
"version": "raw KB4 adapter v1",
|
||||
"role": "fill invalid pixels without crop, dewarp or virtual views",
|
||||
"identity_sha256": source["calibration_sha256"],
|
||||
},
|
||||
{
|
||||
"kind": "model",
|
||||
"name": detector["architecture"],
|
||||
"version": detector["source"],
|
||||
"role": "ready COCO-80 detector provider",
|
||||
"identity_sha256": detector["model_sha256"],
|
||||
},
|
||||
{
|
||||
"kind": "runtime",
|
||||
"name": detector["runtime"],
|
||||
"version": "co-located GPU network path",
|
||||
"role": "one synchronous inference request per source frame",
|
||||
"identity_sha256": detector["config_sha256"],
|
||||
},
|
||||
{
|
||||
"kind": "algorithm",
|
||||
"name": "standard YOLOX decode and class-wise NMS",
|
||||
"version": (
|
||||
f"score {detection['minimum_score']} · IoU "
|
||||
f"{detection['nms_iou_threshold']}"
|
||||
),
|
||||
"role": "fixed source-independent detector output contract",
|
||||
"identity_sha256": detector["config_sha256"],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -117,7 +117,7 @@ def build_e49_detector_truth_evaluation(
|
||||
"prediction freeze belongs to another truth island"
|
||||
)
|
||||
|
||||
valid_fov = _read_valid_fov(
|
||||
valid_fov = read_valid_fov_mask(
|
||||
valid_fov_root,
|
||||
calibration_sha256=str(
|
||||
truth_island.manifest["identity"]["source"]["calibration_sha256"]
|
||||
@@ -640,12 +640,14 @@ def _class_count(predictions: list[dict[str, Any]]) -> dict[str, int]:
|
||||
return dict(result)
|
||||
|
||||
|
||||
def _read_valid_fov(
|
||||
def read_valid_fov_mask(
|
||||
root: Path,
|
||||
*,
|
||||
calibration_sha256: str,
|
||||
calibration_slot: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Read an exact calibration-bound valid-FOV mask for detector metrics."""
|
||||
|
||||
resolved = root.resolve(strict=True)
|
||||
manifest_path = resolved / E49_MANIFEST_NAME
|
||||
manifest = _read_json(manifest_path)
|
||||
|
||||
@@ -0,0 +1,435 @@
|
||||
"""Camera-bound visual evidence for the sealed RAVNOVES00 PointPillars run.
|
||||
|
||||
L3.2 does not execute the detector again. It binds the immutable L3.1 visual
|
||||
sample to exact right-camera frames, projects the same LiDAR sample and model
|
||||
hypotheses through the admitted K1 calibration, and seals a new review result.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import shutil
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
import numpy as np
|
||||
|
||||
from k1link.compute.e30_camera_evidence import (
|
||||
E30CameraEvidenceError,
|
||||
materialize_e30_camera_frames,
|
||||
open_e30_camera_evidence_source,
|
||||
)
|
||||
from k1link.compute.lidar_field_review import E10LidarFieldSource
|
||||
from k1link.compute.semantic_geometry_fusion import projection_profile_from_source
|
||||
from k1link.device_plugins.xgrids_k1.analyze.calibrated_projection import (
|
||||
Kb4ProjectionProfile,
|
||||
)
|
||||
|
||||
RESULT_SCHEMA: Final = "missioncore.l32-pointpillars-camera-review/v1"
|
||||
CATALOG_SCHEMA: Final = "missioncore.l32-pointpillars-camera-review-catalog/v1"
|
||||
FRAME_SCHEMA: Final = "missioncore.l32-pointpillars-camera-review-frame/v1"
|
||||
MAX_CAMERA_BINDING_DELTA_MS: Final = 100.0
|
||||
BOX_EDGES: Final = (
|
||||
(0, 1), (1, 2), (2, 3), (3, 0),
|
||||
(4, 5), (5, 6), (6, 7), (7, 4),
|
||||
(0, 4), (1, 5), (2, 6), (3, 7),
|
||||
)
|
||||
|
||||
|
||||
class L32PointPillarsCameraReviewError(RuntimeError):
|
||||
"""The L3.2 evidence sources or generated result violate the contract."""
|
||||
|
||||
|
||||
def build_l32_pointpillars_camera_review(
|
||||
*,
|
||||
l31_result_root: Path,
|
||||
e10_pack_root: Path,
|
||||
camera_job_root: Path,
|
||||
ffmpeg_path: Path,
|
||||
output_root: Path,
|
||||
) -> Path:
|
||||
"""Build one immutable camera-first review from sealed local sources."""
|
||||
|
||||
l31_root = l31_result_root.expanduser().resolve(strict=True)
|
||||
if l31_root.is_symlink() or not l31_root.is_dir():
|
||||
raise L32PointPillarsCameraReviewError("L3.1 result root is invalid")
|
||||
l31_manifest = _read_json(l31_root / "manifest.json")
|
||||
l31_catalog = _read_json(l31_root / "catalog.json")
|
||||
if (
|
||||
l31_manifest.get("schema_version")
|
||||
!= "missioncore.l31-pointpillars-ravnoves/v1"
|
||||
or l31_manifest.get("result_id") != l31_root.name
|
||||
or l31_catalog.get("schema_version")
|
||||
!= "missioncore.l31-pointpillars-ravnoves-catalog/v1"
|
||||
or l31_catalog.get("result_id") != l31_root.name
|
||||
):
|
||||
raise L32PointPillarsCameraReviewError("L3.1 source identity is invalid")
|
||||
|
||||
source = E10LidarFieldSource(e10_pack_root)
|
||||
try:
|
||||
if (
|
||||
source.identity.get("session_id")
|
||||
!= l31_manifest["identity"].get("source_session_id")
|
||||
or source.identity.get("source_id") != "sensor.camera.right"
|
||||
):
|
||||
raise L32PointPillarsCameraReviewError(
|
||||
"camera-aligned LiDAR pack does not match L3.1"
|
||||
)
|
||||
projection = projection_profile_from_source(source)
|
||||
try:
|
||||
camera = open_e30_camera_evidence_source(
|
||||
camera_job_root=camera_job_root,
|
||||
ffmpeg_path=ffmpeg_path,
|
||||
expected_session_id=str(source.identity["session_id"]),
|
||||
expected_source_id=str(source.identity["source_id"]),
|
||||
)
|
||||
except E30CameraEvidenceError as exc:
|
||||
raise L32PointPillarsCameraReviewError(
|
||||
"right-camera evidence source is invalid"
|
||||
) from exc
|
||||
|
||||
bindings = _select_bindings(l31_root, l31_catalog, source)
|
||||
if not bindings:
|
||||
raise L32PointPillarsCameraReviewError(
|
||||
"no L3.1 visual frames overlap the right camera"
|
||||
)
|
||||
identity = {
|
||||
"schema_version": RESULT_SCHEMA,
|
||||
"source_session_id": source.identity["session_id"],
|
||||
"source_l31_result_id": l31_root.name,
|
||||
"source_l31_manifest_sha256": _sha256(l31_root / "manifest.json"),
|
||||
"source_l31_catalog_sha256": _sha256(l31_root / "catalog.json"),
|
||||
"source_e10_pack_id": source.pack_id,
|
||||
"source_camera_job": camera.identity(),
|
||||
"projection": {
|
||||
"model": "kb4",
|
||||
"source_id": projection.source_id,
|
||||
"calibration_slot": projection.calibration_slot,
|
||||
"width": projection.width,
|
||||
"height": projection.height,
|
||||
},
|
||||
"camera_binding": {
|
||||
"clock": "session-monotonic",
|
||||
"selection": "nearest-camera-frame",
|
||||
"maximum_absolute_delta_ms": MAX_CAMERA_BINDING_DELTA_MS,
|
||||
},
|
||||
"review_frame_indices": [item["frame_index"] for item in bindings],
|
||||
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
"authority": {
|
||||
"shadow_only": True,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
"accuracy_accepted": False,
|
||||
},
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
result_id = f"l32-pointpillars-camera-review-{identity_sha256}"
|
||||
root = output_root.expanduser().resolve()
|
||||
root.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
destination = root / result_id
|
||||
if destination.exists():
|
||||
_validate_existing(destination, identity)
|
||||
return destination
|
||||
|
||||
staging = root / f".{result_id}.{os.getpid()}.incomplete"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
camera_artifacts = materialize_e30_camera_frames(
|
||||
source=camera,
|
||||
source_frame_indices=tuple(
|
||||
int(item["camera_source_frame_index"]) for item in bindings
|
||||
),
|
||||
destination_root=staging / "frames",
|
||||
width=projection.width,
|
||||
height=projection.height,
|
||||
)
|
||||
catalog_frames: list[dict[str, object]] = []
|
||||
score_values: list[float] = []
|
||||
visible_boxes = 0
|
||||
projected_points = 0
|
||||
for binding in bindings:
|
||||
old_payload = binding.pop("source_payload")
|
||||
frame_id = str(binding["frame_id"])
|
||||
points = np.asarray(
|
||||
old_payload["points"]["values"], dtype=np.float64
|
||||
).reshape((-1, 4))
|
||||
projected = _project_sensor_points(points[:, :3], projection)
|
||||
projected_box_items = [
|
||||
_project_box(box, projection)
|
||||
for box in old_payload["prediction_boxes"]
|
||||
]
|
||||
projected_box_items = [item for item in projected_box_items if item]
|
||||
visible_boxes += len(projected_box_items)
|
||||
projected_points += int(projected.shape[0])
|
||||
score_values.extend(
|
||||
float(box["score"]) for box in old_payload["prediction_boxes"]
|
||||
)
|
||||
camera_frame_index = int(binding["camera_source_frame_index"])
|
||||
frame_payload = {
|
||||
"schema_version": FRAME_SCHEMA,
|
||||
"frame_id": frame_id,
|
||||
"summary": binding,
|
||||
"camera": camera_artifacts[camera_frame_index],
|
||||
"points": old_payload["points"],
|
||||
"prediction_boxes": old_payload["prediction_boxes"],
|
||||
"camera_projection": {
|
||||
"point_layout": "flat-xy-depth-m",
|
||||
"point_count": int(projected.shape[0]),
|
||||
"point_values": projected.reshape(-1).tolist(),
|
||||
"boxes": projected_box_items,
|
||||
},
|
||||
"interpretation": {
|
||||
"camera_is_semantic_reference": True,
|
||||
"lidar_is_metric_overlay": True,
|
||||
"boxes_are_model_hypotheses": True,
|
||||
"ground_truth_available": False,
|
||||
"accuracy_claim_allowed": False,
|
||||
},
|
||||
}
|
||||
frame_path = staging / f"frame-{frame_id}.json"
|
||||
_write_json(frame_path, frame_payload)
|
||||
catalog_frames.append(
|
||||
{
|
||||
**binding,
|
||||
"detail_path": frame_path.name,
|
||||
"detail_sha256": _sha256(frame_path),
|
||||
"detail_byte_length": frame_path.stat().st_size,
|
||||
"camera_path": camera_artifacts[camera_frame_index]["path"],
|
||||
"camera_sha256": camera_artifacts[camera_frame_index]["sha256"],
|
||||
}
|
||||
)
|
||||
|
||||
catalog = {
|
||||
"schema_version": CATALOG_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"source_session_id": source.identity["session_id"],
|
||||
"frame_count": len(catalog_frames),
|
||||
"frames": catalog_frames,
|
||||
}
|
||||
catalog_path = staging / "catalog.json"
|
||||
_write_json(catalog_path, catalog)
|
||||
scores = np.asarray(score_values, dtype=np.float64)
|
||||
deltas = np.asarray(
|
||||
[abs(float(item["camera_delta_ms"])) for item in bindings],
|
||||
dtype=np.float64,
|
||||
)
|
||||
metrics = {
|
||||
**l31_manifest["metrics"],
|
||||
"review_frame_count": len(bindings),
|
||||
"review_prediction_count": len(score_values),
|
||||
"review_visible_projected_box_count": visible_boxes,
|
||||
"review_projected_point_count": projected_points,
|
||||
"score_below_0_25_fraction": float(np.mean(scores < 0.25)),
|
||||
"score_below_0_50_fraction": float(np.mean(scores < 0.50)),
|
||||
"camera_binding_absolute_delta_ms": {
|
||||
"maximum": float(np.max(deltas)),
|
||||
"p50": float(np.percentile(deltas, 50)),
|
||||
"p95": float(np.percentile(deltas, 95)),
|
||||
},
|
||||
}
|
||||
limitations = [
|
||||
"The camera is a semantic reference, not labeled 3D ground truth.",
|
||||
"PointPillars boxes remain cross-domain model hypotheses.",
|
||||
(
|
||||
"LiDAR overlay uses the admitted factory KB4 calibration and "
|
||||
"nearest camera frame within 100 ms."
|
||||
),
|
||||
"The review cannot establish precision, recall or safety fitness.",
|
||||
]
|
||||
manifest = {
|
||||
"schema_version": RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity": identity,
|
||||
"identity_sha256": identity_sha256,
|
||||
"created_at_utc": datetime.now(UTC).isoformat(timespec="milliseconds"),
|
||||
"status": "camera-bound-review-rejects-current-candidate",
|
||||
"metrics": metrics,
|
||||
"catalog": {
|
||||
"path": "catalog.json",
|
||||
"sha256": _sha256(catalog_path),
|
||||
"byte_length": catalog_path.stat().st_size,
|
||||
},
|
||||
"limitations": limitations,
|
||||
"authority": identity["authority"],
|
||||
}
|
||||
_write_json(staging / "manifest.json", manifest)
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
return destination
|
||||
finally:
|
||||
source.close()
|
||||
|
||||
|
||||
def _select_bindings(
|
||||
l31_root: Path,
|
||||
catalog: dict[str, Any],
|
||||
source: E10LidarFieldSource,
|
||||
) -> list[dict[str, Any]]:
|
||||
times = np.asarray(source.arrays["session_seconds"], dtype=np.float64)
|
||||
selected: list[dict[str, Any]] = []
|
||||
for descriptor in catalog.get("frames", []):
|
||||
if not isinstance(descriptor, dict):
|
||||
raise L32PointPillarsCameraReviewError("L3.1 catalog frame is invalid")
|
||||
target = float(descriptor["session_seconds"])
|
||||
row = int(np.argmin(np.abs(times - target)))
|
||||
delta_ms = float((times[row] - target) * 1000.0)
|
||||
if abs(delta_ms) > MAX_CAMERA_BINDING_DELTA_MS:
|
||||
continue
|
||||
frame_id = str(descriptor["frame_id"])
|
||||
payload_path = l31_root / str(descriptor["detail_path"])
|
||||
if (
|
||||
_sha256(payload_path) != descriptor.get("detail_sha256")
|
||||
or payload_path.stat().st_size != descriptor.get("detail_byte_length")
|
||||
):
|
||||
raise L32PointPillarsCameraReviewError("L3.1 visual frame changed")
|
||||
payload = _read_json(payload_path)
|
||||
selected.append(
|
||||
{
|
||||
"frame_id": frame_id,
|
||||
"frame_index": int(descriptor["frame_index"]),
|
||||
"session_seconds": target,
|
||||
"source_point_count": int(descriptor["source_point_count"]),
|
||||
"prediction_count": int(descriptor["prediction_count"]),
|
||||
"class_counts": descriptor["class_counts"],
|
||||
"inference_ms": float(descriptor["inference_ms"]),
|
||||
"camera_source_frame_index": int(
|
||||
source.arrays["source_frame_indices"][row]
|
||||
),
|
||||
"camera_session_seconds": float(times[row]),
|
||||
"camera_delta_ms": delta_ms,
|
||||
"source_payload": payload,
|
||||
}
|
||||
)
|
||||
return selected
|
||||
|
||||
|
||||
def _project_sensor_points(
|
||||
points_lidar: np.ndarray,
|
||||
profile: Kb4ProjectionProfile,
|
||||
) -> np.ndarray:
|
||||
pixels, depths, valid = _project_camera(points_lidar, profile)
|
||||
if not np.any(valid):
|
||||
return np.empty((0, 3), dtype=np.float64)
|
||||
return np.column_stack((pixels[valid], depths[valid]))
|
||||
|
||||
|
||||
def _project_box(
|
||||
box: dict[str, Any],
|
||||
profile: Kb4ProjectionProfile,
|
||||
) -> dict[str, object] | None:
|
||||
center = np.asarray([box["x_m"], box["y_m"], box["z_m"]], dtype=np.float64)
|
||||
length = float(box["length_m"])
|
||||
width = float(box["width_m"])
|
||||
height = float(box["height_m"])
|
||||
yaw = float(box["yaw_rad"])
|
||||
cosine = math.cos(yaw)
|
||||
sine = math.sin(yaw)
|
||||
corners: list[list[float]] = []
|
||||
for z_offset in (-height / 2.0, height / 2.0):
|
||||
for x_offset, y_offset in (
|
||||
(-length / 2.0, -width / 2.0),
|
||||
(length / 2.0, -width / 2.0),
|
||||
(length / 2.0, width / 2.0),
|
||||
(-length / 2.0, width / 2.0),
|
||||
):
|
||||
corners.append(
|
||||
[
|
||||
center[0] + x_offset * cosine - y_offset * sine,
|
||||
center[1] + x_offset * sine + y_offset * cosine,
|
||||
center[2] + z_offset,
|
||||
]
|
||||
)
|
||||
pixels, _, valid = _project_camera(np.asarray(corners), profile)
|
||||
segments: list[float] = []
|
||||
for start, end in BOX_EDGES:
|
||||
if valid[start] and valid[end]:
|
||||
segments.extend(
|
||||
[
|
||||
float(pixels[start, 0]),
|
||||
float(pixels[start, 1]),
|
||||
float(pixels[end, 0]),
|
||||
float(pixels[end, 1]),
|
||||
]
|
||||
)
|
||||
if not segments:
|
||||
return None
|
||||
return {
|
||||
"model_class": box["model_class"],
|
||||
"score": float(box["score"]),
|
||||
"segments_xyxy": segments,
|
||||
}
|
||||
|
||||
|
||||
def _project_camera(
|
||||
points_lidar: np.ndarray,
|
||||
profile: Kb4ProjectionProfile,
|
||||
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
points = np.asarray(points_lidar, dtype=np.float64)
|
||||
transform = profile.t_camera_from_lidar
|
||||
camera = points @ transform[:3, :3].T + transform[:3, 3]
|
||||
x, y, z = camera.T
|
||||
radial = np.hypot(x, y)
|
||||
theta = np.arctan2(radial, z)
|
||||
theta2 = theta * theta
|
||||
k1, k2, k3, k4 = profile.distortion_kb4
|
||||
distorted = theta * (
|
||||
1.0 + k1 * theta2 + k2 * theta2**2 + k3 * theta2**3 + k4 * theta2**4
|
||||
)
|
||||
scale = np.divide(
|
||||
distorted, radial, out=np.zeros_like(distorted), where=radial > 1e-12
|
||||
)
|
||||
fx, fy, cx, cy = profile.intrinsic_fx_fy_cx_cy
|
||||
pixels = np.column_stack((fx * x * scale + cx, fy * y * scale + cy))
|
||||
valid = (
|
||||
(z > 1e-6)
|
||||
& np.isfinite(pixels).all(axis=1)
|
||||
& (pixels[:, 0] >= 0.0)
|
||||
& (pixels[:, 0] < profile.width)
|
||||
& (pixels[:, 1] >= 0.0)
|
||||
& (pixels[:, 1] < profile.height)
|
||||
)
|
||||
return pixels, z, valid
|
||||
|
||||
|
||||
def _validate_existing(root: Path, identity: dict[str, object]) -> None:
|
||||
manifest = _read_json(root / "manifest.json")
|
||||
if (
|
||||
manifest.get("schema_version") != RESULT_SCHEMA
|
||||
or manifest.get("identity") != identity
|
||||
or manifest.get("result_id") != root.name
|
||||
):
|
||||
raise L32PointPillarsCameraReviewError("existing L3.2 result differs")
|
||||
|
||||
|
||||
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 L32PointPillarsCameraReviewError(f"invalid JSON: {path.name}") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise L32PointPillarsCameraReviewError(f"invalid object: {path.name}")
|
||||
return value
|
||||
|
||||
|
||||
def _write_json(path: Path, value: object) -> None:
|
||||
path.write_bytes(_canonical_json(value) + b"\n")
|
||||
|
||||
|
||||
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:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Fail-closed source admission for the RAVNOVES00 L3.3 review."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Final
|
||||
|
||||
RAVNOVES00_SESSION_ID: Final = "20260720T065719Z_viewer_live"
|
||||
RAVNOVES00_CAMERA_SOURCE_ID: Final = "sensor.camera.right"
|
||||
RAVNOVES00_ADMITTED_WORLD_STATE_RESULT_ID: Final = (
|
||||
"rectified-camera-world-state-04f0bb519af614ca16eae3d924ca930fb68869e0f5131916a56705e8b24d4bf7"
|
||||
)
|
||||
|
||||
|
||||
def is_admitted_world_state(
|
||||
result_id: object,
|
||||
identity: Mapping[str, object] | None = None,
|
||||
) -> bool:
|
||||
"""Return whether one world-state result is the sealed RAVNOVES00 parent."""
|
||||
|
||||
if result_id != RAVNOVES00_ADMITTED_WORLD_STATE_RESULT_ID:
|
||||
return False
|
||||
if identity is None:
|
||||
return True
|
||||
source = identity.get("source")
|
||||
return (
|
||||
isinstance(source, Mapping)
|
||||
and source.get("session_id") == RAVNOVES00_SESSION_ID
|
||||
and source.get("source_id") == RAVNOVES00_CAMERA_SOURCE_ID
|
||||
)
|
||||
|
||||
|
||||
def is_admitted_l33_identity(identity: Mapping[str, object]) -> bool:
|
||||
"""Return whether an L3.3 identity is bound to the admitted replay source."""
|
||||
|
||||
return (
|
||||
identity.get("source_session_id") == RAVNOVES00_SESSION_ID
|
||||
and identity.get("source_world_state_result_id")
|
||||
== RAVNOVES00_ADMITTED_WORLD_STATE_RESULT_ID
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,579 @@
|
||||
"""Freeze the first YOLOX candidate for RAVNOVES00_RIGHT_YOLOX_TRUTH_ISLAND_V1."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import uuid
|
||||
from collections import Counter
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
from .e46_detector_truth_island import (
|
||||
E46_CONTRACT_NAME,
|
||||
E46_MANIFEST_NAME,
|
||||
E46_REFERENCES_NAME,
|
||||
E46DetectorTruthIslandError,
|
||||
read_e46_detector_truth_island,
|
||||
)
|
||||
|
||||
L34_RESULT_SCHEMA: Final = "missioncore.l34-right-yolox-truth-island-freeze/v1"
|
||||
L34_REPORT_SCHEMA: Final = "missioncore.l34-right-yolox-truth-island-report/v1"
|
||||
L34_PREDICTION_SCHEMA: Final = (
|
||||
"missioncore.l34-right-yolox-truth-island-prediction/v1"
|
||||
)
|
||||
L34_PROFILE_SCHEMA: Final = "missioncore.l34-right-yolox-truth-island-profile/v1"
|
||||
L34_MANIFEST_NAME: Final = "manifest.json"
|
||||
L34_REPORT_NAME: Final = "benchmark-report.json"
|
||||
L34_PREDICTIONS_NAME: Final = "candidate-predictions.jsonl"
|
||||
|
||||
_L33_SCHEMA: Final = "missioncore.l33-camera-first-detector-review/v1"
|
||||
_DETECTOR_FRAME_SCHEMA: Final = "missioncore.rectified-yolox-frame/v1"
|
||||
_RESULT_ID: Final = re.compile(r"^l34-right-yolox-truth-island-freeze-[a-f0-9]{64}$")
|
||||
_SHA256: Final = re.compile(r"^[a-f0-9]{64}$")
|
||||
_CANDIDATE_ID: Final = "yolox-s-kb4-core3"
|
||||
_TARGET_CLASSES: Final = (
|
||||
"person",
|
||||
"bicycle",
|
||||
"motorcycle",
|
||||
"car",
|
||||
"heavy_vehicle",
|
||||
"static_obstacle",
|
||||
"animal",
|
||||
)
|
||||
_DETECTOR_LABELS: Final = frozenset(
|
||||
{"person", "bicycle", "motorcycle", "car", "truck", "bus"}
|
||||
)
|
||||
_AUTHORITY: Final = {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
|
||||
class L34RightYoloxTruthIslandError(RuntimeError):
|
||||
"""The benchmark preregistration or one of its immutable inputs is invalid."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class L34RightYoloxTruthIsland:
|
||||
result_id: str
|
||||
result_root: Path
|
||||
manifest: dict[str, Any]
|
||||
report: dict[str, Any]
|
||||
predictions: tuple[dict[str, Any], ...]
|
||||
|
||||
|
||||
def build_l34_right_yolox_truth_island_freeze(
|
||||
*,
|
||||
profile_path: Path,
|
||||
truth_island_root: Path,
|
||||
detector_qualification_root: Path,
|
||||
l33_result_root: Path,
|
||||
output_root: Path,
|
||||
) -> L34RightYoloxTruthIsland:
|
||||
"""Freeze right-camera YOLOX predictions without reading human labels."""
|
||||
|
||||
profile = _read_profile(profile_path)
|
||||
try:
|
||||
truth_island = read_e46_detector_truth_island(truth_island_root)
|
||||
except E46DetectorTruthIslandError as reason:
|
||||
raise L34RightYoloxTruthIslandError("E46 truth island is invalid") from reason
|
||||
|
||||
contract = _read_json(truth_island.result_root / E46_CONTRACT_NAME)
|
||||
annotation = _object(contract.get("annotation"), "E46 annotation")
|
||||
truth_source = _object(
|
||||
_object(truth_island.manifest.get("identity"), "E46 identity").get(
|
||||
"source"
|
||||
),
|
||||
"E46 source",
|
||||
)
|
||||
if (
|
||||
contract.get("truth_state") != "labels-unavailable"
|
||||
or annotation.get("classes") != list(_TARGET_CLASSES)
|
||||
or truth_source.get("source_id") != "sensor.camera.right"
|
||||
or truth_source.get("session_id") != "20260720T065719Z_viewer_live"
|
||||
):
|
||||
raise L34RightYoloxTruthIslandError(
|
||||
"truth island does not preserve the blind right-camera contract"
|
||||
)
|
||||
|
||||
qualification_root = _directory(detector_qualification_root)
|
||||
qualification_path = qualification_root / "qualification.json"
|
||||
frames_path = qualification_root / "frames.jsonl"
|
||||
qualification = _read_json(qualification_path)
|
||||
if (
|
||||
qualification.get("schema_version")
|
||||
!= "missioncore.rectified-yolox-qualification/v1"
|
||||
or _object(qualification.get("metrics"), "qualification metrics").get(
|
||||
"frames_processed"
|
||||
)
|
||||
!= 4489
|
||||
):
|
||||
raise L34RightYoloxTruthIslandError("YOLOX qualification is invalid")
|
||||
|
||||
l33_root = _directory(l33_result_root)
|
||||
l33_manifest_path = l33_root / L34_MANIFEST_NAME
|
||||
l33_manifest = _read_json(l33_manifest_path)
|
||||
l33_identity = _object(l33_manifest.get("identity"), "L3.3 identity")
|
||||
detector = _object(l33_identity.get("detector"), "L3.3 detector")
|
||||
semantic = _object(
|
||||
l33_identity.get("semantic_contract"),
|
||||
"L3.3 semantic contract",
|
||||
)
|
||||
if (
|
||||
l33_manifest.get("schema_version") != _L33_SCHEMA
|
||||
or l33_manifest.get("result_id") != l33_root.name
|
||||
or l33_identity.get("source_session_id")
|
||||
!= truth_source.get("session_id")
|
||||
or detector.get("architecture") != "YOLOX-S"
|
||||
or detector.get("model_sha256") != profile["candidate"]["model_sha256"]
|
||||
or semantic.get("minimum_detector_score")
|
||||
!= profile["candidate"]["minimum_score"]
|
||||
):
|
||||
raise L34RightYoloxTruthIslandError("L3.3 candidate identity is invalid")
|
||||
|
||||
references = tuple(
|
||||
_read_jsonl(truth_island.result_root / E46_REFERENCES_NAME)
|
||||
)
|
||||
target_indices = {
|
||||
_integer(reference.get("frame_index"), "truth frame index")
|
||||
for reference in references
|
||||
}
|
||||
frames = _selected_detector_frames(frames_path, target_indices)
|
||||
predictions = freeze_l34_candidate_predictions(
|
||||
references=references,
|
||||
detector_frames=frames,
|
||||
minimum_score=float(profile["candidate"]["minimum_score"]),
|
||||
)
|
||||
predictions_bytes = b"".join(
|
||||
_canonical_json(row) + b"\n" for row in predictions
|
||||
)
|
||||
predictions_sha256 = hashlib.sha256(predictions_bytes).hexdigest()
|
||||
class_counts = Counter(
|
||||
prediction["label"]
|
||||
for row in predictions
|
||||
for prediction in row["predictions"]
|
||||
)
|
||||
identity = {
|
||||
"schema_version": L34_RESULT_SCHEMA,
|
||||
"profile": profile,
|
||||
"source": {
|
||||
"session_id": truth_source["session_id"],
|
||||
"source_id": truth_source["source_id"],
|
||||
"mode": "recorded-replay-only",
|
||||
},
|
||||
"truth_island": {
|
||||
"result_id": truth_island.result_id,
|
||||
"manifest_sha256": _sha256(
|
||||
truth_island.result_root / E46_MANIFEST_NAME
|
||||
),
|
||||
"truth_state": "labels-unavailable",
|
||||
},
|
||||
"candidate": {
|
||||
"l33_result_id": l33_root.name,
|
||||
"l33_manifest_sha256": _sha256(l33_manifest_path),
|
||||
"qualification_sha256": _sha256(qualification_path),
|
||||
"detector_frames_sha256": _sha256(frames_path),
|
||||
"prediction_rows_sha256": predictions_sha256,
|
||||
},
|
||||
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
result_id = f"l34-right-yolox-truth-island-freeze-{identity_sha256}"
|
||||
destination = output_root.expanduser().absolute() / result_id
|
||||
if destination.exists():
|
||||
return read_l34_right_yolox_truth_island_freeze(destination)
|
||||
|
||||
report = {
|
||||
"schema_version": L34_REPORT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"status": "predictions-frozen-awaiting-independent-truth",
|
||||
"profile_id": profile["profile_id"],
|
||||
"pipeline_id": profile["pipeline_id"],
|
||||
"source_session_id": truth_source["session_id"],
|
||||
"camera_source_id": truth_source["source_id"],
|
||||
"metrics": {
|
||||
"frame_count": len(predictions),
|
||||
"temporal_group_count": len(
|
||||
{str(row["group_id"]) for row in predictions}
|
||||
),
|
||||
"prediction_count": sum(
|
||||
len(row["predictions"]) for row in predictions
|
||||
),
|
||||
"frames_with_predictions": sum(
|
||||
bool(row["predictions"]) for row in predictions
|
||||
),
|
||||
"class_counts": {
|
||||
label: class_counts.get(label, 0) for label in _TARGET_CLASSES
|
||||
},
|
||||
"accuracy_metrics_available": False,
|
||||
},
|
||||
"decision": {
|
||||
"candidate_predictions_frozen": True,
|
||||
"truth_labels_read": False,
|
||||
"candidate_accepted": False,
|
||||
"model_retraining_authorized": False,
|
||||
"next_gate": (
|
||||
"complete two independent blind reviews, seal adjudicated "
|
||||
"truth, then evaluate these exact predictions"
|
||||
),
|
||||
},
|
||||
"limitations": [
|
||||
"RAVNOVES00 source-scoped benchmark; no cross-route claim",
|
||||
"accuracy remains unavailable until independent truth is sealed",
|
||||
"recorded replay only; live transport and hardware are out of scope",
|
||||
"only sensor.camera.right is admitted",
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
"access": "read-only",
|
||||
}
|
||||
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
(staging / L34_PREDICTIONS_NAME).write_bytes(predictions_bytes)
|
||||
_write_json(staging / L34_REPORT_NAME, report)
|
||||
manifest = {
|
||||
"schema_version": L34_RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": _utc_now(),
|
||||
"ground_truth": False,
|
||||
"acceptance_state": "prepared-awaiting-independent-human-truth",
|
||||
"artifacts": [
|
||||
_artifact(staging / L34_REPORT_NAME, "benchmark-report"),
|
||||
_artifact(
|
||||
staging / L34_PREDICTIONS_NAME,
|
||||
"frozen-candidate-predictions",
|
||||
),
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
_write_json(staging / L34_MANIFEST_NAME, manifest)
|
||||
os.replace(staging, destination)
|
||||
except Exception:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
return read_l34_right_yolox_truth_island_freeze(destination)
|
||||
|
||||
|
||||
def read_l34_right_yolox_truth_island_freeze(root: Path) -> L34RightYoloxTruthIsland:
|
||||
resolved = _directory(root)
|
||||
manifest = _read_json(resolved / L34_MANIFEST_NAME)
|
||||
report = _read_json(resolved / L34_REPORT_NAME)
|
||||
predictions = tuple(_read_jsonl(resolved / L34_PREDICTIONS_NAME))
|
||||
identity = _object(manifest.get("identity"), "L34 identity")
|
||||
identity_sha256 = manifest.get("identity_sha256")
|
||||
if (
|
||||
manifest.get("schema_version") != L34_RESULT_SCHEMA
|
||||
or not _RESULT_ID.fullmatch(resolved.name)
|
||||
or manifest.get("result_id") != resolved.name
|
||||
or not isinstance(identity_sha256, str)
|
||||
or resolved.name != f"l34-right-yolox-truth-island-freeze-{identity_sha256}"
|
||||
or hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
!= identity_sha256
|
||||
or manifest.get("ground_truth") is not False
|
||||
or manifest.get("authority") != _AUTHORITY
|
||||
or identity.get("authority") != _AUTHORITY
|
||||
):
|
||||
raise L34RightYoloxTruthIslandError("L34 manifest identity is invalid")
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(artifacts, list) or len(artifacts) != 2:
|
||||
raise L34RightYoloxTruthIslandError("L34 artifacts are invalid")
|
||||
for item in artifacts:
|
||||
artifact = _object(item, "L34 artifact")
|
||||
path = resolved / str(artifact.get("path"))
|
||||
if (
|
||||
path.parent != resolved
|
||||
or not path.is_file()
|
||||
or path.is_symlink()
|
||||
or path.stat().st_size != artifact.get("byte_length")
|
||||
or _sha256(path) != artifact.get("sha256")
|
||||
):
|
||||
raise L34RightYoloxTruthIslandError("L34 artifact changed")
|
||||
if (
|
||||
report.get("schema_version") != L34_REPORT_SCHEMA
|
||||
or report.get("result_id") != resolved.name
|
||||
or report.get("identity_sha256") != identity_sha256
|
||||
or report.get("status")
|
||||
!= "predictions-frozen-awaiting-independent-truth"
|
||||
or report.get("camera_source_id") != "sensor.camera.right"
|
||||
or _object(report.get("decision"), "L34 decision").get(
|
||||
"truth_labels_read"
|
||||
)
|
||||
is not False
|
||||
):
|
||||
raise L34RightYoloxTruthIslandError("L34 result contract is invalid")
|
||||
sequences: list[int] = []
|
||||
for row in predictions:
|
||||
sequence = _integer(
|
||||
row.get("truth_island_sequence"),
|
||||
"prediction truth island sequence",
|
||||
)
|
||||
source_image_sha256 = row.get("source_image_sha256")
|
||||
if (
|
||||
row.get("schema_version") != L34_PREDICTION_SCHEMA
|
||||
or row.get("candidate_id") != _CANDIDATE_ID
|
||||
or row.get("truth_joined") is not False
|
||||
or not isinstance(source_image_sha256, str)
|
||||
or not _SHA256.fullmatch(source_image_sha256)
|
||||
or not isinstance(row.get("session_seconds"), (int, float))
|
||||
or isinstance(row.get("session_seconds"), bool)
|
||||
or not math.isfinite(float(row["session_seconds"]))
|
||||
or not isinstance(row.get("predictions"), list)
|
||||
):
|
||||
raise L34RightYoloxTruthIslandError(
|
||||
"L34 prediction identity is invalid"
|
||||
)
|
||||
sequences.append(sequence)
|
||||
if len(set(sequences)) != len(sequences):
|
||||
raise L34RightYoloxTruthIslandError(
|
||||
"L34 prediction sequence is duplicated"
|
||||
)
|
||||
metrics = _object(report.get("metrics"), "L34 metrics")
|
||||
if (
|
||||
metrics.get("frame_count") != len(predictions)
|
||||
or metrics.get("accuracy_metrics_available") is not False
|
||||
):
|
||||
raise L34RightYoloxTruthIslandError("L34 metrics are invalid")
|
||||
return L34RightYoloxTruthIsland(
|
||||
result_id=resolved.name,
|
||||
result_root=resolved,
|
||||
manifest=manifest,
|
||||
report=report,
|
||||
predictions=predictions,
|
||||
)
|
||||
|
||||
|
||||
def _read_profile(path: Path) -> dict[str, Any]:
|
||||
profile = _read_json(path.expanduser().resolve(strict=True))
|
||||
candidate = _object(profile.get("candidate"), "benchmark candidate")
|
||||
if (
|
||||
profile.get("schema_version") != L34_PROFILE_SCHEMA
|
||||
or profile.get("profile_id") != "RAVNOVES00_RIGHT_YOLOX_TRUTH_ISLAND_V1"
|
||||
or profile.get("pipeline_id")
|
||||
!= "kb4-core3-yolox-eomt-k1-lidar-e23-temporal/v1"
|
||||
or profile.get("mode") != "recorded-replay-only"
|
||||
or profile.get("source_session_id") != "20260720T065719Z_viewer_live"
|
||||
or profile.get("camera_source_id") != "sensor.camera.right"
|
||||
or profile.get("target_classes") != list(_TARGET_CLASSES)
|
||||
or candidate.get("architecture") != "YOLOX-S"
|
||||
or candidate.get("candidate_id") != _CANDIDATE_ID
|
||||
or not isinstance(candidate.get("model_sha256"), str)
|
||||
or candidate.get("minimum_score") != 0.25
|
||||
or profile.get("authority") != _AUTHORITY
|
||||
):
|
||||
raise L34RightYoloxTruthIslandError("L34 profile is invalid")
|
||||
return profile
|
||||
|
||||
|
||||
def _selected_detector_frames(
|
||||
path: Path,
|
||||
target_indices: set[int],
|
||||
) -> dict[int, dict[str, Any]]:
|
||||
selected: dict[int, dict[str, Any]] = {}
|
||||
for row in _read_jsonl(path):
|
||||
frame_index = _integer(row.get("frame_index"), "detector frame index")
|
||||
if frame_index not in target_indices:
|
||||
continue
|
||||
if (
|
||||
row.get("schema_version") != _DETECTOR_FRAME_SCHEMA
|
||||
or frame_index in selected
|
||||
or not isinstance(row.get("detections"), list)
|
||||
):
|
||||
raise L34RightYoloxTruthIslandError("detector frame is invalid")
|
||||
selected[frame_index] = row
|
||||
if set(selected) != target_indices:
|
||||
raise L34RightYoloxTruthIslandError("detector frame coverage is incomplete")
|
||||
return selected
|
||||
|
||||
|
||||
def freeze_l34_candidate_predictions(
|
||||
*,
|
||||
references: tuple[dict[str, Any], ...],
|
||||
detector_frames: dict[int, dict[str, Any]],
|
||||
minimum_score: float,
|
||||
) -> tuple[dict[str, Any], ...]:
|
||||
"""Create the deterministic prediction freeze for an exact blind island."""
|
||||
|
||||
if not 0.0 < minimum_score < 1.0:
|
||||
raise L34RightYoloxTruthIslandError("minimum score is invalid")
|
||||
target_indices = {
|
||||
_integer(reference.get("frame_index"), "frame index")
|
||||
for reference in references
|
||||
}
|
||||
if set(detector_frames) != target_indices:
|
||||
raise L34RightYoloxTruthIslandError("detector frame coverage is incomplete")
|
||||
return tuple(
|
||||
_prediction_row(
|
||||
reference=reference,
|
||||
frame=detector_frames[
|
||||
_integer(reference.get("frame_index"), "frame index")
|
||||
],
|
||||
minimum_score=minimum_score,
|
||||
)
|
||||
for reference in references
|
||||
)
|
||||
|
||||
|
||||
def _prediction_row(
|
||||
*,
|
||||
reference: dict[str, Any],
|
||||
frame: dict[str, Any],
|
||||
minimum_score: float,
|
||||
) -> dict[str, Any]:
|
||||
predictions: list[dict[str, Any]] = []
|
||||
for raw in frame["detections"]:
|
||||
detection = _object(raw, "detector prediction")
|
||||
label = str(detection.get("label"))
|
||||
score = _number(detection.get("score"), "detector score")
|
||||
if label not in _DETECTOR_LABELS or score < minimum_score:
|
||||
continue
|
||||
bbox = detection.get("bbox_xyxy")
|
||||
if (
|
||||
not isinstance(bbox, list)
|
||||
or len(bbox) != 4
|
||||
or any(not isinstance(value, (int, float)) for value in bbox)
|
||||
):
|
||||
raise L34RightYoloxTruthIslandError("detector box is invalid")
|
||||
predictions.append(
|
||||
{
|
||||
"label": "heavy_vehicle" if label in {"truck", "bus"} else label,
|
||||
"score": score,
|
||||
"bbox_xyxy": [float(value) for value in bbox],
|
||||
}
|
||||
)
|
||||
predictions.sort(
|
||||
key=lambda item: (-float(item["score"]), str(item["label"]))
|
||||
)
|
||||
return {
|
||||
"schema_version": L34_PREDICTION_SCHEMA,
|
||||
"candidate_id": _CANDIDATE_ID,
|
||||
"truth_island_sequence": _integer(
|
||||
reference.get("truth_island_sequence"),
|
||||
"truth island sequence",
|
||||
),
|
||||
"image_id": _integer(reference.get("image_id"), "image id"),
|
||||
"frame_index": _integer(reference.get("frame_index"), "frame index"),
|
||||
"session_seconds": _number(
|
||||
reference.get("session_seconds"),
|
||||
"session seconds",
|
||||
),
|
||||
"source_image_sha256": _sha256_text(
|
||||
reference.get("sha256"),
|
||||
"source image sha256",
|
||||
),
|
||||
"group_id": str(reference.get("group_id")),
|
||||
"predictions": predictions,
|
||||
"truth_joined": False,
|
||||
}
|
||||
|
||||
|
||||
def _directory(path: Path) -> Path:
|
||||
candidate = path.expanduser().absolute()
|
||||
if candidate.is_symlink():
|
||||
raise L34RightYoloxTruthIslandError("source directory is invalid")
|
||||
try:
|
||||
resolved = candidate.resolve(strict=True)
|
||||
except OSError as reason:
|
||||
raise L34RightYoloxTruthIslandError("source directory is unavailable") from reason
|
||||
if not resolved.is_dir():
|
||||
raise L34RightYoloxTruthIslandError("source directory is invalid")
|
||||
return resolved
|
||||
|
||||
|
||||
def _artifact(path: Path, kind: str) -> dict[str, object]:
|
||||
return {
|
||||
"kind": kind,
|
||||
"path": path.name,
|
||||
"byte_length": path.stat().st_size,
|
||||
"sha256": _sha256(path),
|
||||
}
|
||||
|
||||
|
||||
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 _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def _write_json(path: Path, value: object) -> None:
|
||||
path.write_text(
|
||||
json.dumps(value, ensure_ascii=False, sort_keys=True, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as reason:
|
||||
raise L34RightYoloxTruthIslandError(f"invalid JSON: {path.name}") from reason
|
||||
return _object(value, path.name)
|
||||
|
||||
|
||||
def _read_jsonl(path: Path) -> list[dict[str, Any]]:
|
||||
rows: list[dict[str, Any]] = []
|
||||
try:
|
||||
with path.open("r", encoding="utf-8") as stream:
|
||||
for line in stream:
|
||||
if line.strip():
|
||||
rows.append(_object(json.loads(line), path.name))
|
||||
except (OSError, json.JSONDecodeError) as reason:
|
||||
raise L34RightYoloxTruthIslandError(f"invalid JSONL: {path.name}") from reason
|
||||
return rows
|
||||
|
||||
|
||||
def _object(value: object, label: str) -> dict[str, Any]:
|
||||
if not isinstance(value, dict):
|
||||
raise L34RightYoloxTruthIslandError(f"{label} must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def _integer(value: object, label: str) -> int:
|
||||
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
||||
raise L34RightYoloxTruthIslandError(f"{label} must be an integer")
|
||||
return value
|
||||
|
||||
|
||||
def _number(value: object, label: str) -> float:
|
||||
if (
|
||||
not isinstance(value, (int, float))
|
||||
or isinstance(value, bool)
|
||||
or not math.isfinite(float(value))
|
||||
):
|
||||
raise L34RightYoloxTruthIslandError(f"{label} must be numeric")
|
||||
return float(value)
|
||||
|
||||
|
||||
def _sha256_text(value: object, label: str) -> str:
|
||||
if not isinstance(value, str) or not _SHA256.fullmatch(value):
|
||||
raise L34RightYoloxTruthIslandError(f"{label} must be a SHA-256 digest")
|
||||
return value
|
||||
|
||||
|
||||
def _utc_now() -> str:
|
||||
return datetime.now(UTC).isoformat(timespec="milliseconds").replace(
|
||||
"+00:00",
|
||||
"Z",
|
||||
)
|
||||
@@ -0,0 +1,781 @@
|
||||
"""Build a deterministic error audit against one assisted L3.4 review.
|
||||
|
||||
The result is deliberately not ground truth. It compares the immutable L3.4
|
||||
candidate freeze with a complete, candidate-seeded annotation session so that
|
||||
engineering failure modes can be inspected without opening the independent
|
||||
E48/L3.5 acceptance gate.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
from .l34_right_yolox_truth_island_freeze import (
|
||||
L34_MANIFEST_NAME,
|
||||
L34RightYoloxTruthIslandError,
|
||||
read_l34_right_yolox_truth_island_freeze,
|
||||
)
|
||||
|
||||
L34A_RESULT_SCHEMA: Final = "missioncore.l34a-assisted-yolox-error-audit/v1"
|
||||
L34A_REPORT_SCHEMA: Final = "missioncore.l34a-assisted-yolox-error-report/v1"
|
||||
L34A_CASE_SCHEMA: Final = "missioncore.l34a-assisted-yolox-error-case/v1"
|
||||
L34A_MANIFEST_NAME: Final = "manifest.json"
|
||||
L34A_REPORT_NAME: Final = "assisted-error-report.json"
|
||||
L34A_CASES_NAME: Final = "assisted-error-cases.jsonl"
|
||||
|
||||
_ANNOTATION_SCHEMA: Final = "missioncore.l34-annotation-session/v3"
|
||||
_RESULT_ID = re.compile(r"^l34a-assisted-yolox-error-audit-[a-f0-9]{64}$")
|
||||
_SESSION_ID = re.compile(r"^l34-annotation-session-[a-f0-9]{64}$")
|
||||
_IOU_THRESHOLD: Final = 0.5
|
||||
_AUTHORITY: Final = {
|
||||
"ground_truth": False,
|
||||
"candidate_accepted": False,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
|
||||
class L34AAssistedYoloxErrorAuditError(RuntimeError):
|
||||
"""The assisted audit input or immutable result is invalid."""
|
||||
|
||||
|
||||
def build_l34a_assisted_yolox_error_audit(
|
||||
*,
|
||||
l34_freeze_root: Path,
|
||||
annotation_session_path: Path,
|
||||
output_root: Path,
|
||||
) -> dict[str, Any]:
|
||||
"""Compare the exact L3.4 freeze with a complete assisted review."""
|
||||
|
||||
try:
|
||||
freeze = read_l34_right_yolox_truth_island_freeze(l34_freeze_root)
|
||||
except L34RightYoloxTruthIslandError as reason:
|
||||
raise L34AAssistedYoloxErrorAuditError(
|
||||
"L3.4 candidate freeze is invalid"
|
||||
) from reason
|
||||
session_path = annotation_session_path.expanduser().resolve(strict=True)
|
||||
if not session_path.is_file() or session_path.is_symlink():
|
||||
raise L34AAssistedYoloxErrorAuditError(
|
||||
"assisted annotation session is unavailable"
|
||||
)
|
||||
session = _read_json(session_path)
|
||||
_validate_session(session, freeze_result_id=freeze.result_id)
|
||||
|
||||
predictions_by_sequence = {
|
||||
_integer(row.get("truth_island_sequence"), "prediction sequence"): row
|
||||
for row in freeze.predictions
|
||||
}
|
||||
frames = session["frames"]
|
||||
if set(predictions_by_sequence) != {
|
||||
_integer(frame.get("truth_island_sequence"), "annotation sequence")
|
||||
for frame in frames
|
||||
}:
|
||||
raise L34AAssistedYoloxErrorAuditError(
|
||||
"candidate and annotation coverage differ"
|
||||
)
|
||||
|
||||
cases = tuple(
|
||||
_audit_case(
|
||||
prediction_row=predictions_by_sequence[
|
||||
_integer(frame.get("truth_island_sequence"), "annotation sequence")
|
||||
],
|
||||
annotation_frame=frame,
|
||||
)
|
||||
for frame in sorted(
|
||||
frames,
|
||||
key=lambda value: _integer(
|
||||
value.get("truth_island_sequence"),
|
||||
"annotation sequence",
|
||||
),
|
||||
)
|
||||
)
|
||||
aggregate = _aggregate(cases)
|
||||
per_class = _per_class(cases)
|
||||
report_basis = {
|
||||
"schema_version": L34A_REPORT_SCHEMA,
|
||||
"status": "completed-assisted-candidate-error-audit-not-truth",
|
||||
"profile": {
|
||||
"profile_id": "l34a-assisted-yolox-error-audit/v1",
|
||||
"matcher": "greedy-maximum-iou",
|
||||
"iou_threshold": _IOU_THRESHOLD,
|
||||
"duplicate_rule": "same-class-iou-0.30-or-overlap-over-smaller-0.70",
|
||||
"class_policy": "spatial-match-first-then-class-verdict",
|
||||
"mismatch_accounting": "one-false-positive-plus-one-false-negative",
|
||||
"score_visibility": "candidate-scores-used-for-display-and-ordering",
|
||||
},
|
||||
"metrics": {
|
||||
**aggregate,
|
||||
"per_class": per_class,
|
||||
},
|
||||
"case_order": [
|
||||
case["truth_island_sequence"]
|
||||
for case in sorted(
|
||||
cases,
|
||||
key=lambda item: (
|
||||
-int(item["summary"]["severity_score"]),
|
||||
int(item["truth_island_sequence"]),
|
||||
),
|
||||
)
|
||||
],
|
||||
"decision": {
|
||||
"assisted_alignment_available": True,
|
||||
"blind_accuracy_available": False,
|
||||
"postprocessing_issue_confirmed": aggregate["duplicate_false_positive"] > 0,
|
||||
"ontology_gap_confirmed": aggregate["custom_reference_count"] > 0,
|
||||
"candidate_accepted": False,
|
||||
"model_retraining_authorized": False,
|
||||
"l35_blind_gate_open": False,
|
||||
"next_action": (
|
||||
"use the visual FP/FN/mismatch audit to scope NMS, class mapping "
|
||||
"and detector-data work without claiming independent accuracy"
|
||||
),
|
||||
},
|
||||
"limitations": [
|
||||
"the review was seeded from the same frozen candidate and is not independent truth",
|
||||
"precision, recall and F1 are assisted diagnostic alignment metrics, not acceptance metrics",
|
||||
"custom labels are retained as proposed ontology terms and were not adjudicated",
|
||||
"the result is source-scoped to 32 RAVNOVES00 right-camera frames",
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
"ground_truth": False,
|
||||
}
|
||||
l34_identity = _object(freeze.manifest.get("identity"), "L3.4 identity")
|
||||
method = {
|
||||
"schema_version": "missioncore.laboratory-method/v1",
|
||||
"completeness": "complete",
|
||||
"execution_class": "deterministic",
|
||||
"pipeline_id": "ravnoves00-right-yolox-assisted-error-audit/v1",
|
||||
"components": [
|
||||
{
|
||||
"kind": "source",
|
||||
"name": freeze.result_id,
|
||||
"version": "L3.4 immutable YOLOX candidate freeze",
|
||||
"role": "prediction substrate",
|
||||
"identity_sha256": _sha256(
|
||||
freeze.result_root / L34_MANIFEST_NAME
|
||||
),
|
||||
},
|
||||
{
|
||||
"kind": "source",
|
||||
"name": session["session_id"],
|
||||
"version": "candidate-seeded assisted review; not truth",
|
||||
"role": "engineering reference annotations",
|
||||
"identity_sha256": _sha256(session_path),
|
||||
},
|
||||
{
|
||||
"kind": "algorithm",
|
||||
"name": "greedy-maximum-iou-diagnostic-matcher",
|
||||
"version": f"v1-iou-{_IOU_THRESHOLD:.2f}",
|
||||
"role": "TP, FP, FN, duplicate and class-mismatch accounting",
|
||||
"identity_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
},
|
||||
],
|
||||
}
|
||||
identity = {
|
||||
"schema_version": L34A_RESULT_SCHEMA,
|
||||
"l34_freeze": {
|
||||
"result_id": freeze.result_id,
|
||||
"manifest_sha256": _sha256(freeze.result_root / L34_MANIFEST_NAME),
|
||||
"prediction_rows_sha256": _object(
|
||||
l34_identity.get("candidate"),
|
||||
"L3.4 candidate identity",
|
||||
).get("prediction_rows_sha256"),
|
||||
},
|
||||
"assisted_annotation": {
|
||||
"session_id": session["session_id"],
|
||||
"session_sha256": _sha256(session_path),
|
||||
"revision": session["revision"],
|
||||
"updated_at_utc": session["updated_at_utc"],
|
||||
"independent_truth_eligible": False,
|
||||
},
|
||||
"method": method,
|
||||
"report_sha256": hashlib.sha256(_canonical_json(report_basis)).hexdigest(),
|
||||
"cases_sha256": hashlib.sha256(_canonical_json(cases)).hexdigest(),
|
||||
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
result_id = f"l34a-assisted-yolox-error-audit-{identity_sha256}"
|
||||
destination = output_root.expanduser().absolute() / result_id
|
||||
if destination.exists():
|
||||
return read_l34a_assisted_yolox_error_audit(destination)
|
||||
|
||||
created_at_utc = _utc_now()
|
||||
report = {
|
||||
**report_basis,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"created_at_utc": created_at_utc,
|
||||
"source_session_id": "RAVNOVES00",
|
||||
"camera_source_id": "sensor.camera.right",
|
||||
"method": method,
|
||||
}
|
||||
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
_write_json(staging / L34A_REPORT_NAME, report)
|
||||
_write_jsonl(staging / L34A_CASES_NAME, cases)
|
||||
manifest = {
|
||||
"schema_version": L34A_RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": created_at_utc,
|
||||
"acceptance_state": "accepted-assisted-diagnostic-not-truth",
|
||||
"ground_truth": False,
|
||||
"artifacts": [
|
||||
_artifact(staging / L34A_REPORT_NAME, "assisted-error-report"),
|
||||
_artifact(staging / L34A_CASES_NAME, "assisted-error-cases"),
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
_write_json(staging / L34A_MANIFEST_NAME, manifest)
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
return read_l34a_assisted_yolox_error_audit(destination)
|
||||
|
||||
|
||||
def read_l34a_assisted_yolox_error_audit(root: Path) -> dict[str, Any]:
|
||||
"""Read and fully revalidate one immutable assisted audit."""
|
||||
|
||||
resolved = root.resolve(strict=True)
|
||||
manifest = _read_json(resolved / L34A_MANIFEST_NAME)
|
||||
identity = _object(manifest.get("identity"), "L3.4A identity")
|
||||
identity_sha256 = manifest.get("identity_sha256")
|
||||
if (
|
||||
manifest.get("schema_version") != L34A_RESULT_SCHEMA
|
||||
or not isinstance(identity_sha256, str)
|
||||
or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256
|
||||
or manifest.get("result_id")
|
||||
!= f"l34a-assisted-yolox-error-audit-{identity_sha256}"
|
||||
or resolved.name != manifest.get("result_id")
|
||||
or _RESULT_ID.fullmatch(resolved.name) is None
|
||||
or manifest.get("acceptance_state")
|
||||
!= "accepted-assisted-diagnostic-not-truth"
|
||||
or manifest.get("ground_truth") is not False
|
||||
or manifest.get("authority") != _AUTHORITY
|
||||
):
|
||||
raise L34AAssistedYoloxErrorAuditError("L3.4A identity is invalid")
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(artifacts, list) or len(artifacts) != 2:
|
||||
raise L34AAssistedYoloxErrorAuditError("L3.4A artifacts are invalid")
|
||||
artifact_by_role = {
|
||||
_text(item.get("role"), "artifact role"): _object(item, "artifact")
|
||||
for item in artifacts
|
||||
if isinstance(item, dict)
|
||||
}
|
||||
report_path = _validated_artifact(
|
||||
resolved,
|
||||
artifact_by_role.get("assisted-error-report"),
|
||||
)
|
||||
cases_path = _validated_artifact(
|
||||
resolved,
|
||||
artifact_by_role.get("assisted-error-cases"),
|
||||
)
|
||||
report = _read_json(report_path)
|
||||
cases = tuple(_read_jsonl(cases_path))
|
||||
if (
|
||||
report.get("schema_version") != L34A_REPORT_SCHEMA
|
||||
or report.get("result_id") != resolved.name
|
||||
or report.get("identity_sha256") != identity_sha256
|
||||
or report.get("status")
|
||||
!= "completed-assisted-candidate-error-audit-not-truth"
|
||||
or report.get("ground_truth") is not False
|
||||
or report.get("authority") != _AUTHORITY
|
||||
or len(cases) != 32
|
||||
or any(case.get("schema_version") != L34A_CASE_SCHEMA for case in cases)
|
||||
or hashlib.sha256(_canonical_json(cases)).hexdigest()
|
||||
!= identity.get("cases_sha256")
|
||||
):
|
||||
raise L34AAssistedYoloxErrorAuditError("L3.4A result changed")
|
||||
return {
|
||||
"result_id": resolved.name,
|
||||
"result_root": resolved,
|
||||
"manifest": manifest,
|
||||
"report": report,
|
||||
"cases": cases,
|
||||
}
|
||||
|
||||
|
||||
def _validate_session(session: dict[str, Any], *, freeze_result_id: str) -> None:
|
||||
frames = session.get("frames")
|
||||
progress = session.get("progress")
|
||||
if (
|
||||
session.get("schema_version") != _ANNOTATION_SCHEMA
|
||||
or not isinstance(session.get("session_id"), str)
|
||||
or _SESSION_ID.fullmatch(session["session_id"]) is None
|
||||
or session.get("result_id") != freeze_result_id
|
||||
or session.get("state") != "saved"
|
||||
or not isinstance(session.get("revision"), int)
|
||||
or session["revision"] < 1
|
||||
or _object(session.get("assistance"), "assistance").get(
|
||||
"independent_truth_eligible"
|
||||
)
|
||||
is not False
|
||||
or _object(session.get("authority"), "authority") != _AUTHORITY
|
||||
or not isinstance(frames, list)
|
||||
or len(frames) != 32
|
||||
or (progress is not None and not isinstance(progress, dict))
|
||||
or not isinstance(session.get("updated_at_utc"), str)
|
||||
):
|
||||
raise L34AAssistedYoloxErrorAuditError(
|
||||
"assisted annotation session contract is invalid"
|
||||
)
|
||||
sequences: set[int] = set()
|
||||
for frame in frames:
|
||||
item = _object(frame, "annotation frame")
|
||||
sequence = _integer(item.get("truth_island_sequence"), "annotation sequence")
|
||||
objects = item.get("objects")
|
||||
if (
|
||||
not 1 <= sequence <= 32
|
||||
or sequence in sequences
|
||||
or item.get("reviewed") is not True
|
||||
or not isinstance(objects, list)
|
||||
):
|
||||
raise L34AAssistedYoloxErrorAuditError(
|
||||
"assisted annotation coverage is incomplete"
|
||||
)
|
||||
sequences.add(sequence)
|
||||
for value in objects:
|
||||
obj = _object(value, "annotation object")
|
||||
category = obj.get("category")
|
||||
proposed = obj.get("proposed_label")
|
||||
if (
|
||||
not isinstance(obj.get("object_id"), str)
|
||||
or not isinstance(category, str)
|
||||
or not _valid_box(obj.get("box_xyxy"))
|
||||
or obj.get("origin") not in {"manual", "frozen_candidate_seed"}
|
||||
or (category == "unmapped" and not isinstance(proposed, str))
|
||||
or (category != "unmapped" and proposed is not None)
|
||||
):
|
||||
raise L34AAssistedYoloxErrorAuditError(
|
||||
"assisted annotation object is invalid"
|
||||
)
|
||||
|
||||
|
||||
def _audit_case(
|
||||
*,
|
||||
prediction_row: dict[str, Any],
|
||||
annotation_frame: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
sequence = _integer(
|
||||
prediction_row.get("truth_island_sequence"),
|
||||
"prediction sequence",
|
||||
)
|
||||
if (
|
||||
annotation_frame.get("truth_island_sequence") != sequence
|
||||
or annotation_frame.get("image_id") != prediction_row.get("image_id")
|
||||
or annotation_frame.get("frame_index") != prediction_row.get("frame_index")
|
||||
or annotation_frame.get("source_sha256")
|
||||
!= prediction_row.get("source_image_sha256")
|
||||
):
|
||||
raise L34AAssistedYoloxErrorAuditError("case source identity differs")
|
||||
raw_predictions = prediction_row.get("predictions")
|
||||
raw_annotations = annotation_frame.get("objects")
|
||||
if not isinstance(raw_predictions, list) or not isinstance(raw_annotations, list):
|
||||
raise L34AAssistedYoloxErrorAuditError("case objects are unavailable")
|
||||
|
||||
predictions = [
|
||||
{
|
||||
"prediction_index": index,
|
||||
"category": _text(item.get("label"), "prediction category"),
|
||||
"score": _finite(item.get("score"), "prediction score"),
|
||||
"box_xyxy": _box(item.get("bbox_xyxy"), "prediction box"),
|
||||
}
|
||||
for index, item in enumerate(
|
||||
(_object(value, "prediction") for value in raw_predictions),
|
||||
start=1,
|
||||
)
|
||||
]
|
||||
annotations = [
|
||||
{
|
||||
"object_id": _text(item.get("object_id"), "annotation object id"),
|
||||
"category": _text(item.get("category"), "annotation category"),
|
||||
"proposed_label": item.get("proposed_label"),
|
||||
"display_category": _display_reference_category(item),
|
||||
"origin": _text(item.get("origin"), "annotation origin"),
|
||||
"box_xyxy": _box(item.get("box_xyxy"), "annotation box"),
|
||||
"occluded": item.get("occluded") is True,
|
||||
"truncated": item.get("truncated") is True,
|
||||
}
|
||||
for item in (_object(value, "annotation") for value in raw_annotations)
|
||||
]
|
||||
candidates = sorted(
|
||||
(
|
||||
(_iou(prediction["box_xyxy"], annotation["box_xyxy"]), p_index, a_index)
|
||||
for p_index, prediction in enumerate(predictions)
|
||||
for a_index, annotation in enumerate(annotations)
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
matched_predictions: set[int] = set()
|
||||
matched_annotations: set[int] = set()
|
||||
matches: list[dict[str, Any]] = []
|
||||
for iou, prediction_index, annotation_index in candidates:
|
||||
if (
|
||||
iou < _IOU_THRESHOLD
|
||||
or prediction_index in matched_predictions
|
||||
or annotation_index in matched_annotations
|
||||
):
|
||||
continue
|
||||
matched_predictions.add(prediction_index)
|
||||
matched_annotations.add(annotation_index)
|
||||
prediction = predictions[prediction_index]
|
||||
annotation = annotations[annotation_index]
|
||||
verdict = (
|
||||
"true_positive"
|
||||
if prediction["category"] == annotation["category"]
|
||||
else "class_mismatch"
|
||||
)
|
||||
prediction.update(
|
||||
verdict=verdict,
|
||||
matched_object_id=annotation["object_id"],
|
||||
match_iou=iou,
|
||||
)
|
||||
annotation.update(
|
||||
verdict=verdict,
|
||||
matched_prediction_index=prediction["prediction_index"],
|
||||
match_iou=iou,
|
||||
)
|
||||
matches.append(
|
||||
{
|
||||
"prediction_index": prediction["prediction_index"],
|
||||
"object_id": annotation["object_id"],
|
||||
"iou": iou,
|
||||
"verdict": verdict,
|
||||
}
|
||||
)
|
||||
for index, prediction in enumerate(predictions):
|
||||
if index in matched_predictions:
|
||||
continue
|
||||
duplicate = any(
|
||||
prediction["category"] == annotation["category"]
|
||||
and (
|
||||
_iou(prediction["box_xyxy"], annotation["box_xyxy"]) >= 0.3
|
||||
or _overlap_over_smaller(
|
||||
prediction["box_xyxy"],
|
||||
annotation["box_xyxy"],
|
||||
)
|
||||
>= 0.7
|
||||
)
|
||||
for annotation in annotations
|
||||
)
|
||||
prediction.update(
|
||||
verdict="duplicate_false_positive" if duplicate else "false_positive",
|
||||
matched_object_id=None,
|
||||
match_iou=None,
|
||||
)
|
||||
for index, annotation in enumerate(annotations):
|
||||
if index in matched_annotations:
|
||||
continue
|
||||
annotation.update(
|
||||
verdict="false_negative",
|
||||
matched_prediction_index=None,
|
||||
match_iou=None,
|
||||
)
|
||||
|
||||
true_positive = sum(
|
||||
prediction["verdict"] == "true_positive" for prediction in predictions
|
||||
)
|
||||
class_mismatch = sum(
|
||||
prediction["verdict"] == "class_mismatch" for prediction in predictions
|
||||
)
|
||||
duplicate_false_positive = sum(
|
||||
prediction["verdict"] == "duplicate_false_positive"
|
||||
for prediction in predictions
|
||||
)
|
||||
unmatched_false_positive = sum(
|
||||
prediction["verdict"] == "false_positive" for prediction in predictions
|
||||
)
|
||||
unmatched_false_negative = sum(
|
||||
annotation["verdict"] == "false_negative" for annotation in annotations
|
||||
)
|
||||
false_positive = unmatched_false_positive + duplicate_false_positive + class_mismatch
|
||||
false_negative = unmatched_false_negative + class_mismatch
|
||||
summary = {
|
||||
"prediction_count": len(predictions),
|
||||
"reference_count": len(annotations),
|
||||
"true_positive": true_positive,
|
||||
"false_positive": false_positive,
|
||||
"false_negative": false_negative,
|
||||
"class_mismatch": class_mismatch,
|
||||
"duplicate_false_positive": duplicate_false_positive,
|
||||
"unmatched_false_positive": unmatched_false_positive,
|
||||
"unmatched_false_negative": unmatched_false_negative,
|
||||
"severity_score": (
|
||||
class_mismatch * 3
|
||||
+ duplicate_false_positive * 2
|
||||
+ unmatched_false_positive
|
||||
+ unmatched_false_negative * 2
|
||||
),
|
||||
}
|
||||
return {
|
||||
"schema_version": L34A_CASE_SCHEMA,
|
||||
"truth_island_sequence": sequence,
|
||||
"image_id": prediction_row["image_id"],
|
||||
"frame_index": prediction_row["frame_index"],
|
||||
"group_id": prediction_row["group_id"],
|
||||
"session_seconds": prediction_row["session_seconds"],
|
||||
"source_image_sha256": prediction_row["source_image_sha256"],
|
||||
"camera": {"width": 800, "height": 600},
|
||||
"predictions": predictions,
|
||||
"annotations": annotations,
|
||||
"matches": matches,
|
||||
"summary": summary,
|
||||
}
|
||||
|
||||
|
||||
def _aggregate(cases: tuple[dict[str, Any], ...]) -> dict[str, Any]:
|
||||
totals: defaultdict[str, int] = defaultdict(int)
|
||||
for case in cases:
|
||||
for key, value in _object(case.get("summary"), "case summary").items():
|
||||
if key != "severity_score":
|
||||
totals[key] += _integer(value, f"case summary {key}")
|
||||
true_positive = totals["true_positive"]
|
||||
false_positive = totals["false_positive"]
|
||||
false_negative = totals["false_negative"]
|
||||
precision = _ratio(true_positive, true_positive + false_positive)
|
||||
recall = _ratio(true_positive, true_positive + false_negative)
|
||||
f1 = _ratio(2 * precision * recall, precision + recall)
|
||||
custom_reference_count = sum(
|
||||
annotation["category"] == "unmapped"
|
||||
for case in cases
|
||||
for annotation in case["annotations"]
|
||||
)
|
||||
error_case_count = sum(
|
||||
case["summary"]["false_positive"] > 0
|
||||
or case["summary"]["false_negative"] > 0
|
||||
for case in cases
|
||||
)
|
||||
return {
|
||||
"frame_count": len(cases),
|
||||
**dict(totals),
|
||||
"precision_iou50": precision,
|
||||
"recall_iou50": recall,
|
||||
"f1_iou50": f1,
|
||||
"error_case_count": error_case_count,
|
||||
"custom_reference_count": custom_reference_count,
|
||||
}
|
||||
|
||||
|
||||
def _per_class(cases: tuple[dict[str, Any], ...]) -> dict[str, dict[str, Any]]:
|
||||
counts: defaultdict[str, defaultdict[str, int]] = defaultdict(
|
||||
lambda: defaultdict(int)
|
||||
)
|
||||
for case in cases:
|
||||
for prediction in case["predictions"]:
|
||||
category = prediction["category"]
|
||||
verdict = prediction["verdict"]
|
||||
if verdict == "true_positive":
|
||||
counts[category]["true_positive"] += 1
|
||||
elif verdict in {
|
||||
"false_positive",
|
||||
"duplicate_false_positive",
|
||||
"class_mismatch",
|
||||
}:
|
||||
counts[category]["false_positive"] += 1
|
||||
for annotation in case["annotations"]:
|
||||
category = annotation["display_category"]
|
||||
verdict = annotation["verdict"]
|
||||
counts[category]["reference_count"] += 1
|
||||
if verdict in {"false_negative", "class_mismatch"}:
|
||||
counts[category]["false_negative"] += 1
|
||||
projected: dict[str, dict[str, Any]] = {}
|
||||
for category, values in sorted(counts.items()):
|
||||
true_positive = values["true_positive"]
|
||||
false_positive = values["false_positive"]
|
||||
false_negative = values["false_negative"]
|
||||
precision = _ratio(true_positive, true_positive + false_positive)
|
||||
recall = _ratio(true_positive, true_positive + false_negative)
|
||||
projected[category] = {
|
||||
"reference_count": values["reference_count"],
|
||||
"true_positive": true_positive,
|
||||
"false_positive": false_positive,
|
||||
"false_negative": false_negative,
|
||||
"precision_iou50": precision,
|
||||
"recall_iou50": recall,
|
||||
}
|
||||
return projected
|
||||
|
||||
|
||||
def _display_reference_category(item: dict[str, Any]) -> str:
|
||||
if item.get("category") == "unmapped":
|
||||
return f"unmapped:{_text(item.get('proposed_label'), 'proposed label')}"
|
||||
return _text(item.get("category"), "annotation category")
|
||||
|
||||
|
||||
def _iou(left: list[float], right: list[float]) -> float:
|
||||
intersection_width = max(0.0, min(left[2], right[2]) - max(left[0], right[0]))
|
||||
intersection_height = max(0.0, min(left[3], right[3]) - max(left[1], right[1]))
|
||||
intersection = intersection_width * intersection_height
|
||||
left_area = (left[2] - left[0]) * (left[3] - left[1])
|
||||
right_area = (right[2] - right[0]) * (right[3] - right[1])
|
||||
union = left_area + right_area - intersection
|
||||
return intersection / union if union > 0 else 0.0
|
||||
|
||||
|
||||
def _overlap_over_smaller(left: list[float], right: list[float]) -> float:
|
||||
intersection_width = max(0.0, min(left[2], right[2]) - max(left[0], right[0]))
|
||||
intersection_height = max(0.0, min(left[3], right[3]) - max(left[1], right[1]))
|
||||
intersection = intersection_width * intersection_height
|
||||
smaller = min(
|
||||
(left[2] - left[0]) * (left[3] - left[1]),
|
||||
(right[2] - right[0]) * (right[3] - right[1]),
|
||||
)
|
||||
return intersection / smaller if smaller > 0 else 0.0
|
||||
|
||||
|
||||
def _ratio(numerator: float, denominator: float) -> float:
|
||||
return numerator / denominator if denominator > 0 else 0.0
|
||||
|
||||
|
||||
def _valid_box(value: object) -> bool:
|
||||
try:
|
||||
box = _box(value, "box")
|
||||
except L34AAssistedYoloxErrorAuditError:
|
||||
return False
|
||||
return 0 <= box[0] < box[2] <= 800 and 0 <= box[1] < box[3] <= 600
|
||||
|
||||
|
||||
def _box(value: object, label: str) -> list[float]:
|
||||
if not isinstance(value, list) or len(value) != 4:
|
||||
raise L34AAssistedYoloxErrorAuditError(f"{label} is invalid")
|
||||
box = [_finite(item, label) for item in value]
|
||||
if not (0 <= box[0] < box[2] <= 800 and 0 <= box[1] < box[3] <= 600):
|
||||
raise L34AAssistedYoloxErrorAuditError(f"{label} is invalid")
|
||||
return box
|
||||
|
||||
|
||||
def _finite(value: object, label: str) -> float:
|
||||
if (
|
||||
not isinstance(value, (int, float))
|
||||
or isinstance(value, bool)
|
||||
or not math.isfinite(float(value))
|
||||
):
|
||||
raise L34AAssistedYoloxErrorAuditError(f"{label} is invalid")
|
||||
return float(value)
|
||||
|
||||
|
||||
def _integer(value: object, label: str) -> int:
|
||||
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
||||
raise L34AAssistedYoloxErrorAuditError(f"{label} is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _text(value: object, label: str) -> str:
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
raise L34AAssistedYoloxErrorAuditError(f"{label} is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _object(value: object, label: str) -> dict[str, Any]:
|
||||
if not isinstance(value, dict):
|
||||
raise L34AAssistedYoloxErrorAuditError(f"{label} is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _validated_artifact(root: Path, artifact: dict[str, Any] | None) -> Path:
|
||||
if artifact is None:
|
||||
raise L34AAssistedYoloxErrorAuditError("L3.4A artifact is missing")
|
||||
path = (root / _text(artifact.get("path"), "artifact path")).resolve()
|
||||
if (
|
||||
path.parent != root
|
||||
or path.is_symlink()
|
||||
or not path.is_file()
|
||||
or path.stat().st_size != artifact.get("byte_length")
|
||||
or _sha256(path) != artifact.get("sha256")
|
||||
):
|
||||
raise L34AAssistedYoloxErrorAuditError("L3.4A artifact changed")
|
||||
return path
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError) as reason:
|
||||
raise L34AAssistedYoloxErrorAuditError(
|
||||
f"cannot read {path.name}"
|
||||
) from reason
|
||||
return _object(value, path.name)
|
||||
|
||||
|
||||
def _read_jsonl(path: Path) -> list[dict[str, Any]]:
|
||||
try:
|
||||
rows = [
|
||||
json.loads(line)
|
||||
for line in path.read_text(encoding="utf-8").splitlines()
|
||||
if line
|
||||
]
|
||||
except (OSError, ValueError) as reason:
|
||||
raise L34AAssistedYoloxErrorAuditError(
|
||||
f"cannot read {path.name}"
|
||||
) from reason
|
||||
if any(not isinstance(row, dict) for row in rows):
|
||||
raise L34AAssistedYoloxErrorAuditError(f"{path.name} is invalid")
|
||||
return rows
|
||||
|
||||
|
||||
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 _write_jsonl(path: Path, rows: tuple[dict[str, Any], ...]) -> None:
|
||||
path.write_text(
|
||||
"".join(
|
||||
json.dumps(row, ensure_ascii=False, sort_keys=True) + "\n"
|
||||
for row in rows
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def _artifact(path: Path, role: str) -> dict[str, object]:
|
||||
return {
|
||||
"role": role,
|
||||
"path": path.name,
|
||||
"media_type": (
|
||||
"application/x-ndjson" if path.suffix == ".jsonl" else "application/json"
|
||||
),
|
||||
"byte_length": path.stat().st_size,
|
||||
"sha256": _sha256(path),
|
||||
}
|
||||
|
||||
|
||||
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 _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def _utc_now() -> str:
|
||||
return datetime.now(tz=UTC).isoformat(timespec="milliseconds").replace(
|
||||
"+00:00",
|
||||
"Z",
|
||||
)
|
||||
@@ -0,0 +1,623 @@
|
||||
"""Build an immutable L3.4B nested-box consolidation shadow.
|
||||
|
||||
The shadow applies one deliberately narrow post-processing rule to the exact
|
||||
L3.4 freeze: predictions of the same normalized category are consolidated only
|
||||
when at least 95 percent of the smaller box is covered by the other box. It is
|
||||
an assisted engineering diagnostic, never independent truth or an acceptance
|
||||
result.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
from .l34_right_yolox_truth_island_freeze import (
|
||||
L34_MANIFEST_NAME,
|
||||
L34RightYoloxTruthIslandError,
|
||||
read_l34_right_yolox_truth_island_freeze,
|
||||
)
|
||||
from .l34a_assisted_yolox_error_audit import (
|
||||
L34A_MANIFEST_NAME,
|
||||
L34AAssistedYoloxErrorAuditError,
|
||||
_aggregate,
|
||||
_audit_case,
|
||||
_overlap_over_smaller,
|
||||
_validate_session,
|
||||
read_l34a_assisted_yolox_error_audit,
|
||||
)
|
||||
|
||||
L34B_RESULT_SCHEMA: Final = "missioncore.l34b-nested-box-consolidation-shadow/v1"
|
||||
L34B_REPORT_SCHEMA: Final = "missioncore.l34b-nested-box-consolidation-report/v1"
|
||||
L34B_CASE_SCHEMA: Final = "missioncore.l34b-nested-box-consolidation-case/v1"
|
||||
L34B_MANIFEST_NAME: Final = "manifest.json"
|
||||
L34B_REPORT_NAME: Final = "nested-box-consolidation-report.json"
|
||||
L34B_CASES_NAME: Final = "nested-box-consolidation-cases.jsonl"
|
||||
L34B_OVERLAP_THRESHOLD: Final = 0.95
|
||||
|
||||
_RESULT_ID = re.compile(r"^l34b-nested-box-consolidation-shadow-[a-f0-9]{64}$")
|
||||
_AUTHORITY: Final = {
|
||||
"ground_truth": False,
|
||||
"candidate_accepted": False,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
|
||||
class L34BNestedBoxConsolidationError(RuntimeError):
|
||||
"""An L3.4B input or immutable result is invalid."""
|
||||
|
||||
|
||||
def consolidate_l34b_prediction_row(
|
||||
prediction_row: dict[str, Any],
|
||||
) -> tuple[dict[str, Any], tuple[dict[str, Any], ...]]:
|
||||
"""Consolidate connected same-category nested boxes deterministically."""
|
||||
|
||||
raw_predictions = prediction_row.get("predictions")
|
||||
if not isinstance(raw_predictions, list):
|
||||
raise L34BNestedBoxConsolidationError("L3.4 predictions are unavailable")
|
||||
predictions = [_prediction(value, index) for index, value in enumerate(raw_predictions)]
|
||||
parents = list(range(len(predictions)))
|
||||
|
||||
def find(index: int) -> int:
|
||||
while parents[index] != index:
|
||||
parents[index] = parents[parents[index]]
|
||||
index = parents[index]
|
||||
return index
|
||||
|
||||
def union(left: int, right: int) -> None:
|
||||
left_root = find(left)
|
||||
right_root = find(right)
|
||||
if left_root == right_root:
|
||||
return
|
||||
lower, upper = sorted((left_root, right_root))
|
||||
parents[upper] = lower
|
||||
|
||||
for left_index, left in enumerate(predictions):
|
||||
for right_index in range(left_index + 1, len(predictions)):
|
||||
right = predictions[right_index]
|
||||
if (
|
||||
left["label"] == right["label"]
|
||||
and _overlap_over_smaller(
|
||||
left["bbox_xyxy"],
|
||||
right["bbox_xyxy"],
|
||||
)
|
||||
>= L34B_OVERLAP_THRESHOLD
|
||||
):
|
||||
union(left_index, right_index)
|
||||
|
||||
grouped: dict[int, list[int]] = {}
|
||||
for index in range(len(predictions)):
|
||||
grouped.setdefault(find(index), []).append(index)
|
||||
|
||||
output_predictions: list[dict[str, Any]] = []
|
||||
consolidations: list[dict[str, Any]] = []
|
||||
for component in sorted(grouped.values(), key=min):
|
||||
members = [predictions[index] for index in component]
|
||||
source_indices = [index + 1 for index in component]
|
||||
if len(component) == 1:
|
||||
output = copy.deepcopy(members[0])
|
||||
else:
|
||||
boxes = [member["bbox_xyxy"] for member in members]
|
||||
output = {
|
||||
"label": members[0]["label"],
|
||||
"score": max(member["score"] for member in members),
|
||||
"bbox_xyxy": [
|
||||
min(box[0] for box in boxes),
|
||||
min(box[1] for box in boxes),
|
||||
max(box[2] for box in boxes),
|
||||
max(box[3] for box in boxes),
|
||||
],
|
||||
}
|
||||
consolidations.append(
|
||||
{
|
||||
"category": output["label"],
|
||||
"source_prediction_indices": source_indices,
|
||||
"source_scores": [member["score"] for member in members],
|
||||
"source_boxes_xyxy": copy.deepcopy(boxes),
|
||||
"merged_score": output["score"],
|
||||
"merged_box_xyxy": copy.deepcopy(output["bbox_xyxy"]),
|
||||
"minimum_overlap_over_smaller": min(
|
||||
_overlap_over_smaller(
|
||||
members[left]["bbox_xyxy"],
|
||||
members[right]["bbox_xyxy"],
|
||||
)
|
||||
for left in range(len(members))
|
||||
for right in range(left + 1, len(members))
|
||||
),
|
||||
"output_prediction_index": len(output_predictions) + 1,
|
||||
}
|
||||
)
|
||||
output["source_prediction_indices"] = source_indices
|
||||
output_predictions.append(output)
|
||||
|
||||
projected = copy.deepcopy(prediction_row)
|
||||
projected["predictions"] = output_predictions
|
||||
return projected, tuple(consolidations)
|
||||
|
||||
|
||||
def evaluate_l34b_shadow(
|
||||
*,
|
||||
prediction_rows: tuple[dict[str, Any], ...],
|
||||
annotation_frames: tuple[dict[str, Any], ...],
|
||||
before_cases: tuple[dict[str, Any], ...],
|
||||
) -> tuple[tuple[dict[str, Any], ...], dict[str, Any]]:
|
||||
"""Apply the rule and return source-bound before/after cases and metrics."""
|
||||
|
||||
annotations_by_sequence = {
|
||||
_integer(frame.get("truth_island_sequence"), "annotation sequence"): frame
|
||||
for frame in annotation_frames
|
||||
}
|
||||
before_by_sequence = {
|
||||
_integer(case.get("truth_island_sequence"), "before sequence"): case
|
||||
for case in before_cases
|
||||
}
|
||||
if (
|
||||
len(prediction_rows) != 32
|
||||
or len(annotations_by_sequence) != 32
|
||||
or len(before_by_sequence) != 32
|
||||
):
|
||||
raise L34BNestedBoxConsolidationError("L3.4B requires 32 bound cases")
|
||||
|
||||
cases: list[dict[str, Any]] = []
|
||||
after_audits: list[dict[str, Any]] = []
|
||||
for row in prediction_rows:
|
||||
sequence = _integer(row.get("truth_island_sequence"), "prediction sequence")
|
||||
projected, consolidations = consolidate_l34b_prediction_row(row)
|
||||
after = _audit_case(
|
||||
prediction_row=projected,
|
||||
annotation_frame=annotations_by_sequence[sequence],
|
||||
)
|
||||
for prediction, source in zip(
|
||||
after["predictions"],
|
||||
projected["predictions"],
|
||||
strict=True,
|
||||
):
|
||||
prediction["source_prediction_indices"] = copy.deepcopy(
|
||||
source["source_prediction_indices"]
|
||||
)
|
||||
before = before_by_sequence[sequence]
|
||||
if (
|
||||
before.get("source_image_sha256") != after.get("source_image_sha256")
|
||||
or before.get("frame_index") != after.get("frame_index")
|
||||
or before.get("image_id") != after.get("image_id")
|
||||
):
|
||||
raise L34BNestedBoxConsolidationError("L3.4B case binding changed")
|
||||
cases.append(
|
||||
{
|
||||
"schema_version": L34B_CASE_SCHEMA,
|
||||
"truth_island_sequence": sequence,
|
||||
"image_id": after["image_id"],
|
||||
"frame_index": after["frame_index"],
|
||||
"group_id": after["group_id"],
|
||||
"session_seconds": after["session_seconds"],
|
||||
"source_image_sha256": after["source_image_sha256"],
|
||||
"camera": copy.deepcopy(after["camera"]),
|
||||
"before_predictions": copy.deepcopy(before["predictions"]),
|
||||
"after_predictions": copy.deepcopy(after["predictions"]),
|
||||
"annotations": copy.deepcopy(after["annotations"]),
|
||||
"consolidations": list(consolidations),
|
||||
"before_summary": copy.deepcopy(before["summary"]),
|
||||
"after_summary": copy.deepcopy(after["summary"]),
|
||||
}
|
||||
)
|
||||
after_audits.append(after)
|
||||
|
||||
before_metrics = _aggregate(tuple(before_cases))
|
||||
after_metrics = _aggregate(tuple(after_audits))
|
||||
delta = {
|
||||
key: after_metrics[key] - before_metrics[key]
|
||||
for key in (
|
||||
"prediction_count",
|
||||
"true_positive",
|
||||
"false_positive",
|
||||
"false_negative",
|
||||
"class_mismatch",
|
||||
"duplicate_false_positive",
|
||||
"unmatched_false_positive",
|
||||
"unmatched_false_negative",
|
||||
"precision_iou50",
|
||||
"recall_iou50",
|
||||
"f1_iou50",
|
||||
"error_case_count",
|
||||
)
|
||||
}
|
||||
consolidation_count = sum(len(case["consolidations"]) for case in cases)
|
||||
affected_case_count = sum(bool(case["consolidations"]) for case in cases)
|
||||
regression_free = (
|
||||
consolidation_count > 0
|
||||
and delta["true_positive"] >= 0
|
||||
and delta["false_positive"] < 0
|
||||
and delta["false_negative"] <= 0
|
||||
and delta["class_mismatch"] <= 0
|
||||
)
|
||||
metrics = {
|
||||
"before": before_metrics,
|
||||
"after": after_metrics,
|
||||
"delta": delta,
|
||||
"consolidation_count": consolidation_count,
|
||||
"affected_case_count": affected_case_count,
|
||||
"assisted_regression_free": regression_free,
|
||||
}
|
||||
return tuple(cases), metrics
|
||||
|
||||
|
||||
def build_l34b_nested_box_consolidation_shadow(
|
||||
*,
|
||||
l34_freeze_root: Path,
|
||||
l34a_audit_root: Path,
|
||||
annotation_session_path: Path,
|
||||
output_root: Path,
|
||||
) -> dict[str, Any]:
|
||||
"""Build and publish one immutable L3.4B result."""
|
||||
|
||||
try:
|
||||
freeze = read_l34_right_yolox_truth_island_freeze(l34_freeze_root)
|
||||
l34a = read_l34a_assisted_yolox_error_audit(l34a_audit_root)
|
||||
except (L34RightYoloxTruthIslandError, L34AAssistedYoloxErrorAuditError) as reason:
|
||||
raise L34BNestedBoxConsolidationError("L3.4/L3.4A input is invalid") from reason
|
||||
session_path = annotation_session_path.expanduser().resolve(strict=True)
|
||||
if not session_path.is_file() or session_path.is_symlink():
|
||||
raise L34BNestedBoxConsolidationError("annotation session is unavailable")
|
||||
session = _read_json(session_path)
|
||||
try:
|
||||
_validate_session(session, freeze_result_id=freeze.result_id)
|
||||
except L34AAssistedYoloxErrorAuditError as reason:
|
||||
raise L34BNestedBoxConsolidationError("annotation session is invalid") from reason
|
||||
l34a_identity = _object(l34a["manifest"].get("identity"), "L3.4A identity")
|
||||
l34a_freeze = _object(l34a_identity.get("l34_freeze"), "L3.4A freeze")
|
||||
l34a_annotation = _object(
|
||||
l34a_identity.get("assisted_annotation"),
|
||||
"L3.4A annotation",
|
||||
)
|
||||
if (
|
||||
l34a_freeze.get("result_id") != freeze.result_id
|
||||
or l34a_annotation.get("session_id") != session.get("session_id")
|
||||
or l34a_annotation.get("session_sha256") != _sha256(session_path)
|
||||
):
|
||||
raise L34BNestedBoxConsolidationError("L3.4B lineage differs")
|
||||
|
||||
cases, metrics = evaluate_l34b_shadow(
|
||||
prediction_rows=freeze.predictions,
|
||||
annotation_frames=tuple(session["frames"]),
|
||||
before_cases=l34a["cases"],
|
||||
)
|
||||
affected_sequences = [
|
||||
case["truth_island_sequence"] for case in cases if case["consolidations"]
|
||||
]
|
||||
case_order = affected_sequences + [
|
||||
case["truth_island_sequence"]
|
||||
for case in sorted(
|
||||
(item for item in cases if not item["consolidations"]),
|
||||
key=lambda item: (
|
||||
-int(item["after_summary"]["severity_score"]),
|
||||
int(item["truth_island_sequence"]),
|
||||
),
|
||||
)
|
||||
]
|
||||
report_basis = {
|
||||
"schema_version": L34B_REPORT_SCHEMA,
|
||||
"status": "completed-nested-box-consolidation-shadow-not-truth",
|
||||
"profile": {
|
||||
"profile_id": "l34b-nested-box-consolidation-shadow/v1",
|
||||
"category_policy": "same-normalized-category-only",
|
||||
"overlap_metric": "intersection-over-smaller-box-area",
|
||||
"overlap_threshold": L34B_OVERLAP_THRESHOLD,
|
||||
"geometry_policy": "union-box",
|
||||
"score_policy": "maximum-source-score",
|
||||
"scope": "frozen-prediction-postprocessing-only",
|
||||
},
|
||||
"metrics": metrics,
|
||||
"case_order": case_order,
|
||||
"decision": {
|
||||
"shadow_policy_accepted": metrics["assisted_regression_free"],
|
||||
"assisted_alignment_available": True,
|
||||
"blind_accuracy_available": False,
|
||||
"candidate_accepted": False,
|
||||
"model_retraining_authorized": False,
|
||||
"l35_blind_gate_open": False,
|
||||
"classic_iou_nms_fix_rejected": True,
|
||||
"remaining_l34a_duplicate_signals": metrics["after"][
|
||||
"duplicate_false_positive"
|
||||
],
|
||||
"next_action": (
|
||||
"preserve rectification_tile provenance and evaluate temporal "
|
||||
"left/front seam stitching; do not lower global IoU NMS"
|
||||
),
|
||||
},
|
||||
"limitations": [
|
||||
(
|
||||
"the comparison uses the same candidate-seeded assisted review "
|
||||
"as L3.4A and is not independent truth"
|
||||
),
|
||||
(
|
||||
"the accepted shadow rule changes one nested same-category pair "
|
||||
"on this 32-frame source scope"
|
||||
),
|
||||
(
|
||||
"four left/front tile seam splits remain and cannot be safely "
|
||||
"solved by lowering global IoU NMS"
|
||||
),
|
||||
"the L3.4 freeze does not retain rectification_tile on each prediction",
|
||||
"no live transport, hardware, LiDAR range, navigation or safety claim is made",
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
"ground_truth": False,
|
||||
}
|
||||
method = {
|
||||
"schema_version": "missioncore.laboratory-method/v1",
|
||||
"completeness": "complete",
|
||||
"execution_class": "deterministic",
|
||||
"pipeline_id": "ravnoves00-right-yolox-nested-box-consolidation-shadow/v1",
|
||||
"components": [
|
||||
{
|
||||
"kind": "source",
|
||||
"name": freeze.result_id,
|
||||
"version": "L3.4 immutable candidate freeze",
|
||||
"role": "prediction substrate",
|
||||
"identity_sha256": _sha256(freeze.result_root / L34_MANIFEST_NAME),
|
||||
},
|
||||
{
|
||||
"kind": "source",
|
||||
"name": l34a["result_id"],
|
||||
"version": "L3.4A assisted diagnostic; not truth",
|
||||
"role": "before-state and engineering comparison",
|
||||
"identity_sha256": _sha256(l34a["result_root"] / L34A_MANIFEST_NAME),
|
||||
},
|
||||
{
|
||||
"kind": "algorithm",
|
||||
"name": "same-category-nested-box-union",
|
||||
"version": f"v1-overlap-{L34B_OVERLAP_THRESHOLD:.2f}",
|
||||
"role": "bounded post-normalization consolidation",
|
||||
"identity_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
},
|
||||
],
|
||||
}
|
||||
identity = {
|
||||
"schema_version": L34B_RESULT_SCHEMA,
|
||||
"l34_freeze": {
|
||||
"result_id": freeze.result_id,
|
||||
"manifest_sha256": _sha256(freeze.result_root / L34_MANIFEST_NAME),
|
||||
},
|
||||
"l34a_audit": {
|
||||
"result_id": l34a["result_id"],
|
||||
"manifest_sha256": _sha256(l34a["result_root"] / L34A_MANIFEST_NAME),
|
||||
},
|
||||
"assisted_annotation": {
|
||||
"session_id": session["session_id"],
|
||||
"session_sha256": _sha256(session_path),
|
||||
"independent_truth_eligible": False,
|
||||
},
|
||||
"method": method,
|
||||
"report_sha256": hashlib.sha256(_canonical_json(report_basis)).hexdigest(),
|
||||
"cases_sha256": hashlib.sha256(_canonical_json(cases)).hexdigest(),
|
||||
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
result_id = f"l34b-nested-box-consolidation-shadow-{identity_sha256}"
|
||||
destination = output_root.expanduser().absolute() / result_id
|
||||
if destination.exists():
|
||||
return read_l34b_nested_box_consolidation_shadow(destination)
|
||||
|
||||
created_at_utc = _utc_now()
|
||||
report = {
|
||||
**report_basis,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"created_at_utc": created_at_utc,
|
||||
"source_session_id": "RAVNOVES00",
|
||||
"camera_source_id": "sensor.camera.right",
|
||||
"method": method,
|
||||
}
|
||||
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
_write_json(staging / L34B_REPORT_NAME, report)
|
||||
_write_jsonl(staging / L34B_CASES_NAME, cases)
|
||||
manifest = {
|
||||
"schema_version": L34B_RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": created_at_utc,
|
||||
"acceptance_state": "accepted-assisted-shadow-not-truth",
|
||||
"ground_truth": False,
|
||||
"artifacts": [
|
||||
_artifact(staging / L34B_REPORT_NAME, "nested-box-report"),
|
||||
_artifact(staging / L34B_CASES_NAME, "nested-box-cases"),
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
_write_json(staging / L34B_MANIFEST_NAME, manifest)
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
return read_l34b_nested_box_consolidation_shadow(destination)
|
||||
|
||||
|
||||
def read_l34b_nested_box_consolidation_shadow(root: Path) -> dict[str, Any]:
|
||||
"""Read and fully revalidate one immutable L3.4B result."""
|
||||
|
||||
resolved = root.resolve(strict=True)
|
||||
manifest = _read_json(resolved / L34B_MANIFEST_NAME)
|
||||
identity = _object(manifest.get("identity"), "L3.4B identity")
|
||||
identity_sha256 = manifest.get("identity_sha256")
|
||||
if (
|
||||
manifest.get("schema_version") != L34B_RESULT_SCHEMA
|
||||
or not isinstance(identity_sha256, str)
|
||||
or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256
|
||||
or manifest.get("result_id") != f"l34b-nested-box-consolidation-shadow-{identity_sha256}"
|
||||
or resolved.name != manifest.get("result_id")
|
||||
or _RESULT_ID.fullmatch(resolved.name) is None
|
||||
or manifest.get("acceptance_state") != "accepted-assisted-shadow-not-truth"
|
||||
or manifest.get("ground_truth") is not False
|
||||
or manifest.get("authority") != _AUTHORITY
|
||||
):
|
||||
raise L34BNestedBoxConsolidationError("L3.4B identity is invalid")
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(artifacts, list) or len(artifacts) != 2:
|
||||
raise L34BNestedBoxConsolidationError("L3.4B artifacts are invalid")
|
||||
artifact_by_role = {
|
||||
_text(item.get("role"), "artifact role"): _object(item, "artifact")
|
||||
for item in artifacts
|
||||
if isinstance(item, dict)
|
||||
}
|
||||
report_path = _validated_artifact(resolved, artifact_by_role.get("nested-box-report"))
|
||||
cases_path = _validated_artifact(resolved, artifact_by_role.get("nested-box-cases"))
|
||||
report = _read_json(report_path)
|
||||
cases = tuple(_read_jsonl(cases_path))
|
||||
if (
|
||||
report.get("schema_version") != L34B_REPORT_SCHEMA
|
||||
or report.get("result_id") != resolved.name
|
||||
or report.get("identity_sha256") != identity_sha256
|
||||
or report.get("status") != "completed-nested-box-consolidation-shadow-not-truth"
|
||||
or report.get("ground_truth") is not False
|
||||
or report.get("authority") != _AUTHORITY
|
||||
or len(cases) != 32
|
||||
or any(case.get("schema_version") != L34B_CASE_SCHEMA for case in cases)
|
||||
or hashlib.sha256(_canonical_json(cases)).hexdigest() != identity.get("cases_sha256")
|
||||
):
|
||||
raise L34BNestedBoxConsolidationError("L3.4B result changed")
|
||||
return {
|
||||
"result_id": resolved.name,
|
||||
"result_root": resolved,
|
||||
"manifest": manifest,
|
||||
"report": report,
|
||||
"cases": cases,
|
||||
}
|
||||
|
||||
|
||||
def _prediction(value: object, index: int) -> dict[str, Any]:
|
||||
item = _object(value, f"prediction {index + 1}")
|
||||
return {
|
||||
"label": _text(item.get("label"), "prediction label"),
|
||||
"score": _finite(item.get("score"), "prediction score"),
|
||||
"bbox_xyxy": _box(item.get("bbox_xyxy"), "prediction box"),
|
||||
}
|
||||
|
||||
|
||||
def _box(value: object, label: str) -> list[float]:
|
||||
if not isinstance(value, list) or len(value) != 4:
|
||||
raise L34BNestedBoxConsolidationError(f"{label} is invalid")
|
||||
box = [_finite(item, label) for item in value]
|
||||
if not (0 <= box[0] < box[2] <= 800 and 0 <= box[1] < box[3] <= 600):
|
||||
raise L34BNestedBoxConsolidationError(f"{label} is invalid")
|
||||
return box
|
||||
|
||||
|
||||
def _finite(value: object, label: str) -> float:
|
||||
if (
|
||||
not isinstance(value, (int, float))
|
||||
or isinstance(value, bool)
|
||||
or not math.isfinite(float(value))
|
||||
):
|
||||
raise L34BNestedBoxConsolidationError(f"{label} is invalid")
|
||||
return float(value)
|
||||
|
||||
|
||||
def _integer(value: object, label: str) -> int:
|
||||
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
||||
raise L34BNestedBoxConsolidationError(f"{label} is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _text(value: object, label: str) -> str:
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
raise L34BNestedBoxConsolidationError(f"{label} is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _object(value: object, label: str) -> dict[str, Any]:
|
||||
if not isinstance(value, dict):
|
||||
raise L34BNestedBoxConsolidationError(f"{label} is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _validated_artifact(root: Path, artifact: dict[str, Any] | None) -> Path:
|
||||
if artifact is None:
|
||||
raise L34BNestedBoxConsolidationError("L3.4B artifact is missing")
|
||||
path = (root / _text(artifact.get("path"), "artifact path")).resolve()
|
||||
if (
|
||||
path.parent != root
|
||||
or path.is_symlink()
|
||||
or not path.is_file()
|
||||
or path.stat().st_size != artifact.get("byte_length")
|
||||
or _sha256(path) != artifact.get("sha256")
|
||||
):
|
||||
raise L34BNestedBoxConsolidationError("L3.4B artifact changed")
|
||||
return path
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError) as reason:
|
||||
raise L34BNestedBoxConsolidationError(f"cannot read {path.name}") from reason
|
||||
return _object(value, path.name)
|
||||
|
||||
|
||||
def _read_jsonl(path: Path) -> list[dict[str, Any]]:
|
||||
try:
|
||||
rows = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line]
|
||||
except (OSError, ValueError) as reason:
|
||||
raise L34BNestedBoxConsolidationError(f"cannot read {path.name}") from reason
|
||||
if any(not isinstance(row, dict) for row in rows):
|
||||
raise L34BNestedBoxConsolidationError(f"{path.name} is invalid")
|
||||
return rows
|
||||
|
||||
|
||||
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 _write_jsonl(path: Path, rows: tuple[dict[str, Any], ...]) -> None:
|
||||
path.write_text(
|
||||
"".join(json.dumps(row, ensure_ascii=False, sort_keys=True) + "\n" for row in rows),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def _artifact(path: Path, role: str) -> dict[str, object]:
|
||||
return {
|
||||
"role": role,
|
||||
"path": path.name,
|
||||
"media_type": "application/x-ndjson" if path.suffix == ".jsonl" else "application/json",
|
||||
"byte_length": path.stat().st_size,
|
||||
"sha256": _sha256(path),
|
||||
}
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as source:
|
||||
for chunk in iter(lambda: source.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
separators=(",", ":"),
|
||||
sort_keys=True,
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def _utc_now() -> str:
|
||||
return datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,796 @@
|
||||
"""Freeze the cumulative L3.4B + L3.4C post-processing candidate.
|
||||
|
||||
The candidate composes only operations already admitted by the immutable
|
||||
L3.4B and L3.4C shadows. Both operation sets remain expressed against the
|
||||
original L3.4 prediction indices. Any overlap, lineage drift or operation
|
||||
payload mismatch fails closed instead of introducing an ordering policy.
|
||||
Assisted annotations are joined only after the deterministic projection and
|
||||
remain ineligible as independent truth.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
from .l34a_assisted_yolox_error_audit import (
|
||||
L34A_MANIFEST_NAME,
|
||||
L34AAssistedYoloxErrorAuditError,
|
||||
_aggregate,
|
||||
_audit_case,
|
||||
_validate_session,
|
||||
read_l34a_assisted_yolox_error_audit,
|
||||
)
|
||||
from .l34b_nested_box_consolidation_shadow import (
|
||||
L34B_MANIFEST_NAME,
|
||||
L34BNestedBoxConsolidationError,
|
||||
read_l34b_nested_box_consolidation_shadow,
|
||||
)
|
||||
from .l34c_tile_seam_stitch_shadow import (
|
||||
L34C_MANIFEST_NAME,
|
||||
L34CTileSeamStitchError,
|
||||
_artifact,
|
||||
_canonical_json,
|
||||
_finite,
|
||||
_integer,
|
||||
_list,
|
||||
_object,
|
||||
_prediction,
|
||||
_read_json,
|
||||
_read_jsonl,
|
||||
_sha256,
|
||||
_text,
|
||||
_union_box,
|
||||
_utc_now,
|
||||
_validated_artifact,
|
||||
_write_json,
|
||||
_write_jsonl,
|
||||
read_l34c_tile_seam_stitch_shadow,
|
||||
)
|
||||
|
||||
L34D_RESULT_SCHEMA: Final = (
|
||||
"missioncore.l34d-cumulative-postprocessing-candidate/v1"
|
||||
)
|
||||
L34D_REPORT_SCHEMA: Final = (
|
||||
"missioncore.l34d-cumulative-postprocessing-report/v1"
|
||||
)
|
||||
L34D_CASE_SCHEMA: Final = (
|
||||
"missioncore.l34d-cumulative-postprocessing-case/v1"
|
||||
)
|
||||
L34D_MANIFEST_NAME: Final = "manifest.json"
|
||||
L34D_REPORT_NAME: Final = "cumulative-postprocessing-report.json"
|
||||
L34D_CASES_NAME: Final = "cumulative-postprocessing-cases.jsonl"
|
||||
|
||||
_RESULT_ID = re.compile(
|
||||
r"^l34d-cumulative-postprocessing-candidate-[a-f0-9]{64}$"
|
||||
)
|
||||
_COUNT_METRICS: Final = (
|
||||
"prediction_count",
|
||||
"true_positive",
|
||||
"false_positive",
|
||||
"false_negative",
|
||||
"class_mismatch",
|
||||
"duplicate_false_positive",
|
||||
"unmatched_false_positive",
|
||||
"unmatched_false_negative",
|
||||
"error_case_count",
|
||||
)
|
||||
_DELTA_METRICS: Final = (
|
||||
*_COUNT_METRICS[:-1],
|
||||
"precision_iou50",
|
||||
"recall_iou50",
|
||||
"f1_iou50",
|
||||
"error_case_count",
|
||||
)
|
||||
_AUTHORITY: Final = {
|
||||
"ground_truth": False,
|
||||
"candidate_accepted": False,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
|
||||
class L34DCumulativeCandidateError(RuntimeError):
|
||||
"""An L3.4D input, composition or immutable output is invalid."""
|
||||
|
||||
|
||||
def compose_l34d_prediction_row(
|
||||
provenance_row: dict[str, Any],
|
||||
*,
|
||||
consolidations: tuple[dict[str, Any], ...],
|
||||
stitches: tuple[dict[str, Any], ...],
|
||||
) -> tuple[dict[str, Any], tuple[dict[str, Any], ...]]:
|
||||
"""Compose source-indexed B/C operations with a fail-closed conflict gate."""
|
||||
|
||||
raw_predictions = _list(
|
||||
provenance_row.get("predictions"),
|
||||
"L3.4D provenance predictions",
|
||||
)
|
||||
predictions = [
|
||||
_prediction(value, index)
|
||||
for index, value in enumerate(raw_predictions, start=1)
|
||||
]
|
||||
normalized_operations = [
|
||||
_validated_operation(
|
||||
raw,
|
||||
predictions=predictions,
|
||||
provenance_predictions=raw_predictions,
|
||||
operation_type="nested-box-consolidation",
|
||||
)
|
||||
for raw in consolidations
|
||||
] + [
|
||||
_validated_operation(
|
||||
raw,
|
||||
predictions=predictions,
|
||||
provenance_predictions=raw_predictions,
|
||||
operation_type="temporal-tile-seam-stitch",
|
||||
)
|
||||
for raw in stitches
|
||||
]
|
||||
|
||||
consumed: set[int] = set()
|
||||
operation_by_first: dict[int, dict[str, Any]] = {}
|
||||
for operation in normalized_operations:
|
||||
source_indices = operation["source_prediction_indices"]
|
||||
overlap = consumed.intersection(source_indices)
|
||||
if overlap:
|
||||
raise L34DCumulativeCandidateError(
|
||||
"L3.4B/L3.4C operation sets overlap"
|
||||
)
|
||||
consumed.update(source_indices)
|
||||
operation_by_first[min(source_indices)] = operation
|
||||
|
||||
output: list[dict[str, Any]] = []
|
||||
for index, raw_prediction in enumerate(raw_predictions, start=1):
|
||||
if index in consumed and index not in operation_by_first:
|
||||
continue
|
||||
operation = operation_by_first.get(index)
|
||||
if operation is None:
|
||||
prediction = copy.deepcopy(_object(raw_prediction, "prediction"))
|
||||
prediction["source_prediction_indices"] = [index]
|
||||
prediction["source_rectification_tiles"] = [
|
||||
_text(
|
||||
prediction.get("rectification_tile"),
|
||||
"prediction rectification tile",
|
||||
)
|
||||
]
|
||||
prediction["operation_types"] = []
|
||||
output.append(prediction)
|
||||
continue
|
||||
output.append(
|
||||
{
|
||||
"label": operation["category"],
|
||||
"score": operation["merged_score"],
|
||||
"bbox_xyxy": copy.deepcopy(operation["merged_box_xyxy"]),
|
||||
"source_prediction_indices": copy.deepcopy(
|
||||
operation["source_prediction_indices"]
|
||||
),
|
||||
"source_rectification_tiles": copy.deepcopy(
|
||||
operation["source_tiles"]
|
||||
),
|
||||
"operation_types": [operation["operation_type"]],
|
||||
**(
|
||||
{
|
||||
"temporal_run_id": operation["temporal_run_id"],
|
||||
"temporal_run_length": operation["temporal_run_length"],
|
||||
}
|
||||
if operation["operation_type"]
|
||||
== "temporal-tile-seam-stitch"
|
||||
else {}
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
projected = copy.deepcopy(provenance_row)
|
||||
projected["predictions"] = output
|
||||
return projected, tuple(normalized_operations)
|
||||
|
||||
|
||||
def evaluate_l34d_cumulative_candidate(
|
||||
*,
|
||||
provenance_rows: tuple[dict[str, Any], ...],
|
||||
annotation_frames: tuple[dict[str, Any], ...],
|
||||
before_cases: tuple[dict[str, Any], ...],
|
||||
l34b_cases: tuple[dict[str, Any], ...],
|
||||
l34c_cases: tuple[dict[str, Any], ...],
|
||||
l34b_metrics: dict[str, Any],
|
||||
l34c_metrics: dict[str, Any],
|
||||
) -> tuple[tuple[dict[str, Any], ...], dict[str, Any]]:
|
||||
"""Project and compare the cumulative candidate against assisted review."""
|
||||
|
||||
annotations_by_sequence = _sequence_map(annotation_frames, "annotation")
|
||||
before_by_sequence = _sequence_map(before_cases, "before")
|
||||
l34b_by_sequence = _sequence_map(l34b_cases, "L3.4B")
|
||||
l34c_by_sequence = _sequence_map(l34c_cases, "L3.4C")
|
||||
if (
|
||||
len(provenance_rows) != 32
|
||||
or len(annotations_by_sequence) != 32
|
||||
or len(before_by_sequence) != 32
|
||||
or len(l34b_by_sequence) != 32
|
||||
or len(l34c_by_sequence) != 32
|
||||
):
|
||||
raise L34DCumulativeCandidateError(
|
||||
"L3.4D requires 32 source-bound cases"
|
||||
)
|
||||
|
||||
cases: list[dict[str, Any]] = []
|
||||
after_audits: list[dict[str, Any]] = []
|
||||
for row in provenance_rows:
|
||||
sequence = _integer(
|
||||
row.get("truth_island_sequence"),
|
||||
"prediction sequence",
|
||||
)
|
||||
b_case = l34b_by_sequence[sequence]
|
||||
c_case = l34c_by_sequence[sequence]
|
||||
_validate_case_binding(row, b_case, "L3.4B")
|
||||
_validate_case_binding(row, c_case, "L3.4C")
|
||||
projected, operations = compose_l34d_prediction_row(
|
||||
row,
|
||||
consolidations=tuple(
|
||||
_object(value, "L3.4B consolidation")
|
||||
for value in _list(
|
||||
b_case.get("consolidations"),
|
||||
"L3.4B consolidations",
|
||||
)
|
||||
),
|
||||
stitches=tuple(
|
||||
_object(value, "L3.4C stitch")
|
||||
for value in _list(c_case.get("stitches"), "L3.4C stitches")
|
||||
),
|
||||
)
|
||||
after = _audit_case(
|
||||
prediction_row=projected,
|
||||
annotation_frame=annotations_by_sequence[sequence],
|
||||
)
|
||||
for prediction, source in zip(
|
||||
after["predictions"],
|
||||
projected["predictions"],
|
||||
strict=True,
|
||||
):
|
||||
prediction["source_prediction_indices"] = copy.deepcopy(
|
||||
source["source_prediction_indices"]
|
||||
)
|
||||
prediction["source_rectification_tiles"] = copy.deepcopy(
|
||||
source["source_rectification_tiles"]
|
||||
)
|
||||
prediction["operation_types"] = copy.deepcopy(
|
||||
source["operation_types"]
|
||||
)
|
||||
|
||||
provenance_by_index = {
|
||||
_integer(
|
||||
_object(value, "provenance prediction").get(
|
||||
"source_prediction_index"
|
||||
),
|
||||
"source prediction index",
|
||||
): _object(value, "provenance prediction")
|
||||
for value in _list(row.get("predictions"), "provenance predictions")
|
||||
}
|
||||
before = copy.deepcopy(before_by_sequence[sequence])
|
||||
for prediction in _list(
|
||||
before.get("predictions"),
|
||||
"before predictions",
|
||||
):
|
||||
current = _object(prediction, "before prediction")
|
||||
provenance = provenance_by_index.get(
|
||||
_integer(current.get("prediction_index"), "prediction index")
|
||||
)
|
||||
if provenance is None:
|
||||
raise L34DCumulativeCandidateError(
|
||||
"before prediction lost provenance"
|
||||
)
|
||||
current["rectification_tile"] = provenance["rectification_tile"]
|
||||
current["raw_label"] = provenance["raw_label"]
|
||||
current["class_id"] = provenance["class_id"]
|
||||
current["raw_center_xy"] = copy.deepcopy(provenance["raw_center_xy"])
|
||||
|
||||
_validate_case_binding(row, after, "L3.4D after")
|
||||
cases.append(
|
||||
{
|
||||
"schema_version": L34D_CASE_SCHEMA,
|
||||
"truth_island_sequence": sequence,
|
||||
"image_id": after["image_id"],
|
||||
"frame_index": after["frame_index"],
|
||||
"group_id": after["group_id"],
|
||||
"session_seconds": after["session_seconds"],
|
||||
"source_image_sha256": after["source_image_sha256"],
|
||||
"camera": copy.deepcopy(after["camera"]),
|
||||
"before_predictions": copy.deepcopy(before["predictions"]),
|
||||
"after_predictions": copy.deepcopy(after["predictions"]),
|
||||
"annotations": copy.deepcopy(after["annotations"]),
|
||||
"operations": list(operations),
|
||||
"before_summary": copy.deepcopy(before["summary"]),
|
||||
"after_summary": copy.deepcopy(after["summary"]),
|
||||
}
|
||||
)
|
||||
after_audits.append(after)
|
||||
|
||||
before_metrics = _aggregate(tuple(before_cases))
|
||||
after_metrics = _aggregate(tuple(after_audits))
|
||||
delta = {
|
||||
key: after_metrics[key] - before_metrics[key]
|
||||
for key in _DELTA_METRICS
|
||||
}
|
||||
operation_types = [
|
||||
operation["operation_type"]
|
||||
for case in cases
|
||||
for operation in case["operations"]
|
||||
]
|
||||
nested_count = operation_types.count("nested-box-consolidation")
|
||||
stitch_count = operation_types.count("temporal-tile-seam-stitch")
|
||||
expected_count_delta = {
|
||||
key: int(l34b_metrics["delta"][key])
|
||||
+ int(l34c_metrics["delta"][key])
|
||||
for key in _COUNT_METRICS
|
||||
}
|
||||
count_effects_additive = all(
|
||||
int(delta[key]) == expected_count_delta[key]
|
||||
for key in _COUNT_METRICS
|
||||
)
|
||||
regression_free = (
|
||||
nested_count > 0
|
||||
and stitch_count > 0
|
||||
and count_effects_additive
|
||||
and delta["true_positive"] >= 0
|
||||
and delta["false_positive"] < 0
|
||||
and delta["false_negative"] <= 0
|
||||
and delta["class_mismatch"] <= 0
|
||||
)
|
||||
return tuple(cases), {
|
||||
"before": before_metrics,
|
||||
"after": after_metrics,
|
||||
"delta": delta,
|
||||
"nested_consolidation_count": nested_count,
|
||||
"temporal_stitch_count": stitch_count,
|
||||
"cumulative_operation_count": len(operation_types),
|
||||
"affected_case_count": sum(bool(case["operations"]) for case in cases),
|
||||
"operation_conflict_count": 0,
|
||||
"count_effects_additive": count_effects_additive,
|
||||
"assisted_regression_free": regression_free,
|
||||
}
|
||||
|
||||
|
||||
def build_l34d_cumulative_postprocessing_candidate(
|
||||
*,
|
||||
l34a_audit_root: Path,
|
||||
l34b_shadow_root: Path,
|
||||
l34c_shadow_root: Path,
|
||||
annotation_session_path: Path,
|
||||
output_root: Path,
|
||||
) -> dict[str, Any]:
|
||||
"""Build and publish one immutable cumulative candidate freeze."""
|
||||
|
||||
try:
|
||||
l34a = read_l34a_assisted_yolox_error_audit(l34a_audit_root)
|
||||
l34b = read_l34b_nested_box_consolidation_shadow(l34b_shadow_root)
|
||||
l34c = read_l34c_tile_seam_stitch_shadow(l34c_shadow_root)
|
||||
except (
|
||||
L34AAssistedYoloxErrorAuditError,
|
||||
L34BNestedBoxConsolidationError,
|
||||
L34CTileSeamStitchError,
|
||||
) as reason:
|
||||
raise L34DCumulativeCandidateError(
|
||||
"L3.4A/B/C input is invalid"
|
||||
) from reason
|
||||
|
||||
session_path = annotation_session_path.expanduser().resolve(strict=True)
|
||||
if not session_path.is_file() or session_path.is_symlink():
|
||||
raise L34DCumulativeCandidateError(
|
||||
"annotation session is unavailable"
|
||||
)
|
||||
session = _read_json(session_path)
|
||||
l34b_identity = _object(l34b["manifest"].get("identity"), "L3.4B identity")
|
||||
l34c_identity = _object(l34c["manifest"].get("identity"), "L3.4C identity")
|
||||
l34a_id = l34a["result_id"]
|
||||
freeze_id = _object(
|
||||
l34c_identity.get("l34_freeze"),
|
||||
"L3.4C freeze",
|
||||
).get("result_id")
|
||||
try:
|
||||
_validate_session(session, freeze_result_id=_text(freeze_id, "freeze id"))
|
||||
except L34AAssistedYoloxErrorAuditError as reason:
|
||||
raise L34DCumulativeCandidateError(
|
||||
"annotation session is invalid"
|
||||
) from reason
|
||||
if (
|
||||
_object(l34b_identity.get("l34_freeze"), "L3.4B freeze")
|
||||
!= _object(l34c_identity.get("l34_freeze"), "L3.4C freeze")
|
||||
or _object(l34b_identity.get("l34a_audit"), "L3.4B audit").get(
|
||||
"result_id"
|
||||
)
|
||||
!= l34a_id
|
||||
or _object(l34c_identity.get("l34a_audit"), "L3.4C audit").get(
|
||||
"result_id"
|
||||
)
|
||||
!= l34a_id
|
||||
or _object(
|
||||
l34b_identity.get("assisted_annotation"),
|
||||
"L3.4B annotation",
|
||||
).get("session_sha256")
|
||||
!= _sha256(session_path)
|
||||
or _object(
|
||||
l34c_identity.get("assisted_annotation"),
|
||||
"L3.4C annotation",
|
||||
).get("session_sha256")
|
||||
!= _sha256(session_path)
|
||||
or l34b["report"]["decision"].get("shadow_policy_accepted") is not True
|
||||
or l34c["report"]["decision"].get("shadow_policy_accepted") is not True
|
||||
):
|
||||
raise L34DCumulativeCandidateError("L3.4D lineage differs")
|
||||
|
||||
cases, metrics = evaluate_l34d_cumulative_candidate(
|
||||
provenance_rows=l34c["provenance"],
|
||||
annotation_frames=tuple(session["frames"]),
|
||||
before_cases=l34a["cases"],
|
||||
l34b_cases=l34b["cases"],
|
||||
l34c_cases=l34c["cases"],
|
||||
l34b_metrics=l34b["report"]["metrics"],
|
||||
l34c_metrics=l34c["report"]["metrics"],
|
||||
)
|
||||
if not metrics["assisted_regression_free"]:
|
||||
raise L34DCumulativeCandidateError(
|
||||
"cumulative candidate failed the assisted regression gate"
|
||||
)
|
||||
affected_sequences = [
|
||||
case["truth_island_sequence"] for case in cases if case["operations"]
|
||||
]
|
||||
case_order = affected_sequences + [
|
||||
case["truth_island_sequence"]
|
||||
for case in sorted(
|
||||
(item for item in cases if not item["operations"]),
|
||||
key=lambda item: (
|
||||
-int(item["after_summary"]["severity_score"]),
|
||||
int(item["truth_island_sequence"]),
|
||||
),
|
||||
)
|
||||
]
|
||||
|
||||
method = {
|
||||
"schema_version": "missioncore.laboratory-method/v1",
|
||||
"completeness": "complete",
|
||||
"execution_class": "deterministic",
|
||||
"pipeline_id": "ravnoves00-right-yolox-cumulative-postprocessing/v1",
|
||||
"components": [
|
||||
{
|
||||
"kind": "source",
|
||||
"name": l34b["result_id"],
|
||||
"version": "accepted L3.4B nested-box shadow",
|
||||
"role": "source-indexed nested-box operations",
|
||||
"identity_sha256": _sha256(
|
||||
l34b["result_root"] / L34B_MANIFEST_NAME
|
||||
),
|
||||
},
|
||||
{
|
||||
"kind": "source",
|
||||
"name": l34c["result_id"],
|
||||
"version": "accepted L3.4C temporal seam shadow",
|
||||
"role": "tile provenance and source-indexed seam operations",
|
||||
"identity_sha256": _sha256(
|
||||
l34c["result_root"] / L34C_MANIFEST_NAME
|
||||
),
|
||||
},
|
||||
{
|
||||
"kind": "source",
|
||||
"name": l34a_id,
|
||||
"version": "L3.4A assisted diagnostic; not truth",
|
||||
"role": "after-freeze engineering comparison",
|
||||
"identity_sha256": _sha256(
|
||||
l34a["result_root"] / L34A_MANIFEST_NAME
|
||||
),
|
||||
},
|
||||
{
|
||||
"kind": "algorithm",
|
||||
"name": "source-indexed-order-independent-composition",
|
||||
"version": "v1-fail-closed-on-conflict",
|
||||
"role": "compose accepted B/C operations and freeze one candidate",
|
||||
"identity_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
},
|
||||
],
|
||||
}
|
||||
report_basis = {
|
||||
"schema_version": L34D_REPORT_SCHEMA,
|
||||
"status": "completed-cumulative-postprocessing-candidate-freeze-not-truth",
|
||||
"profile": {
|
||||
"profile_id": "l34d-source-indexed-cumulative-candidate/v1",
|
||||
"operation_order": "simultaneous-original-source-indices",
|
||||
"conflict_policy": "reject-any-overlapping-source-index",
|
||||
"geometry_policy": "accepted-l34b-and-l34c-union-boxes-only",
|
||||
"score_policy": "accepted-maximum-source-score",
|
||||
"scope": "recorded-right-camera-frozen-prediction-candidate-only",
|
||||
},
|
||||
"metrics": metrics,
|
||||
"case_order": case_order,
|
||||
"decision": {
|
||||
"cumulative_shadow_accepted": True,
|
||||
"candidate_frozen": True,
|
||||
"candidate_accepted": False,
|
||||
"prediction_provenance_preserved": True,
|
||||
"operation_sets_disjoint": True,
|
||||
"global_nms_unchanged": True,
|
||||
"independent_truth_available": False,
|
||||
"l35_blind_gate_open": False,
|
||||
"next_action": (
|
||||
"collect prediction-hidden independent labels against this exact "
|
||||
"frozen candidate, then evaluate once without retuning"
|
||||
),
|
||||
},
|
||||
"limitations": [
|
||||
"the comparison reuses candidate-seeded assisted review and is not independent truth",
|
||||
"the frozen candidate changes only five of 32 RIGHT-camera cases on one route",
|
||||
"the composition is valid only while L3.4B and L3.4C source-index sets remain disjoint",
|
||||
"no model weights, global NMS or detector inference are changed",
|
||||
(
|
||||
"no live transport, hardware, left camera, LiDAR range, "
|
||||
"navigation or safety claim is made"
|
||||
),
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
"ground_truth": False,
|
||||
}
|
||||
identity = {
|
||||
"schema_version": L34D_RESULT_SCHEMA,
|
||||
"l34_freeze": copy.deepcopy(l34c_identity["l34_freeze"]),
|
||||
"l34a_audit": {
|
||||
"result_id": l34a_id,
|
||||
"manifest_sha256": _sha256(
|
||||
l34a["result_root"] / L34A_MANIFEST_NAME
|
||||
),
|
||||
},
|
||||
"l34b_shadow": {
|
||||
"result_id": l34b["result_id"],
|
||||
"manifest_sha256": _sha256(
|
||||
l34b["result_root"] / L34B_MANIFEST_NAME
|
||||
),
|
||||
},
|
||||
"l34c_shadow": {
|
||||
"result_id": l34c["result_id"],
|
||||
"manifest_sha256": _sha256(
|
||||
l34c["result_root"] / L34C_MANIFEST_NAME
|
||||
),
|
||||
},
|
||||
"assisted_annotation": {
|
||||
"session_id": session["session_id"],
|
||||
"session_sha256": _sha256(session_path),
|
||||
"independent_truth_eligible": False,
|
||||
},
|
||||
"method": method,
|
||||
"report_sha256": hashlib.sha256(
|
||||
_canonical_json(report_basis)
|
||||
).hexdigest(),
|
||||
"cases_sha256": hashlib.sha256(_canonical_json(cases)).hexdigest(),
|
||||
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
result_id = f"l34d-cumulative-postprocessing-candidate-{identity_sha256}"
|
||||
destination = output_root.expanduser().absolute() / result_id
|
||||
if destination.exists():
|
||||
return read_l34d_cumulative_postprocessing_candidate(destination)
|
||||
|
||||
created_at_utc = _utc_now()
|
||||
report = {
|
||||
**report_basis,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"created_at_utc": created_at_utc,
|
||||
"source_session_id": "RAVNOVES00",
|
||||
"camera_source_id": "sensor.camera.right",
|
||||
"method": method,
|
||||
}
|
||||
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
_write_json(staging / L34D_REPORT_NAME, report)
|
||||
_write_jsonl(staging / L34D_CASES_NAME, cases)
|
||||
manifest = {
|
||||
"schema_version": L34D_RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": created_at_utc,
|
||||
"acceptance_state": "frozen-assisted-cumulative-candidate-not-truth",
|
||||
"ground_truth": False,
|
||||
"artifacts": [
|
||||
_artifact(staging / L34D_REPORT_NAME, "cumulative-report"),
|
||||
_artifact(staging / L34D_CASES_NAME, "cumulative-cases"),
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
_write_json(staging / L34D_MANIFEST_NAME, manifest)
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
return read_l34d_cumulative_postprocessing_candidate(destination)
|
||||
|
||||
|
||||
def read_l34d_cumulative_postprocessing_candidate(
|
||||
root: Path,
|
||||
) -> dict[str, Any]:
|
||||
"""Read and fully revalidate one immutable L3.4D candidate."""
|
||||
|
||||
resolved = root.resolve(strict=True)
|
||||
manifest = _read_json(resolved / L34D_MANIFEST_NAME)
|
||||
identity = _object(manifest.get("identity"), "L3.4D identity")
|
||||
identity_sha256 = manifest.get("identity_sha256")
|
||||
if (
|
||||
manifest.get("schema_version") != L34D_RESULT_SCHEMA
|
||||
or not isinstance(identity_sha256, str)
|
||||
or hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
!= identity_sha256
|
||||
or manifest.get("result_id")
|
||||
!= f"l34d-cumulative-postprocessing-candidate-{identity_sha256}"
|
||||
or resolved.name != manifest.get("result_id")
|
||||
or _RESULT_ID.fullmatch(resolved.name) is None
|
||||
or manifest.get("acceptance_state")
|
||||
!= "frozen-assisted-cumulative-candidate-not-truth"
|
||||
or manifest.get("ground_truth") is not False
|
||||
or manifest.get("authority") != _AUTHORITY
|
||||
):
|
||||
raise L34DCumulativeCandidateError("L3.4D identity is invalid")
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(artifacts, list) or len(artifacts) != 2:
|
||||
raise L34DCumulativeCandidateError("L3.4D artifacts are invalid")
|
||||
artifact_by_role = {
|
||||
_text(item.get("role"), "artifact role"): _object(item, "artifact")
|
||||
for item in artifacts
|
||||
if isinstance(item, dict)
|
||||
}
|
||||
report = _read_json(
|
||||
_validated_artifact(
|
||||
resolved,
|
||||
artifact_by_role.get("cumulative-report"),
|
||||
)
|
||||
)
|
||||
cases = tuple(
|
||||
_read_jsonl(
|
||||
_validated_artifact(
|
||||
resolved,
|
||||
artifact_by_role.get("cumulative-cases"),
|
||||
)
|
||||
)
|
||||
)
|
||||
if (
|
||||
report.get("schema_version") != L34D_REPORT_SCHEMA
|
||||
or report.get("result_id") != resolved.name
|
||||
or report.get("identity_sha256") != identity_sha256
|
||||
or report.get("status")
|
||||
!= "completed-cumulative-postprocessing-candidate-freeze-not-truth"
|
||||
or report.get("ground_truth") is not False
|
||||
or report.get("authority") != _AUTHORITY
|
||||
or len(cases) != 32
|
||||
or any(case.get("schema_version") != L34D_CASE_SCHEMA for case in cases)
|
||||
or hashlib.sha256(_canonical_json(cases)).hexdigest()
|
||||
!= identity.get("cases_sha256")
|
||||
):
|
||||
raise L34DCumulativeCandidateError("L3.4D result changed")
|
||||
return {
|
||||
"result_id": resolved.name,
|
||||
"result_root": resolved,
|
||||
"manifest": manifest,
|
||||
"report": report,
|
||||
"cases": cases,
|
||||
}
|
||||
|
||||
|
||||
def _validated_operation(
|
||||
raw: dict[str, Any],
|
||||
*,
|
||||
predictions: list[dict[str, Any]],
|
||||
provenance_predictions: list[Any],
|
||||
operation_type: str,
|
||||
) -> dict[str, Any]:
|
||||
operation = copy.deepcopy(_object(raw, "L3.4D operation"))
|
||||
raw_indices = _list(
|
||||
operation.get("source_prediction_indices"),
|
||||
"operation source indices",
|
||||
)
|
||||
source_indices = sorted(
|
||||
_integer(value, "operation source index") for value in raw_indices
|
||||
)
|
||||
if (
|
||||
len(source_indices) < 2
|
||||
or len(set(source_indices)) != len(source_indices)
|
||||
or source_indices[0] < 1
|
||||
or source_indices[-1] > len(predictions)
|
||||
):
|
||||
raise L34DCumulativeCandidateError(
|
||||
"operation source indices are invalid"
|
||||
)
|
||||
members = [predictions[index - 1] for index in source_indices]
|
||||
category = _text(operation.get("category"), "operation category")
|
||||
if any(member["label"] != category for member in members):
|
||||
raise L34DCumulativeCandidateError("operation category changed")
|
||||
source_scores = [
|
||||
_finite(value, "operation source score")
|
||||
for value in _list(operation.get("source_scores"), "operation scores")
|
||||
]
|
||||
source_boxes = [
|
||||
[
|
||||
_finite(coordinate, "operation source box")
|
||||
for coordinate in _list(value, "operation source box")
|
||||
]
|
||||
for value in _list(operation.get("source_boxes_xyxy"), "operation boxes")
|
||||
]
|
||||
if (
|
||||
source_scores != [member["score"] for member in members]
|
||||
or source_boxes != [member["bbox_xyxy"] for member in members]
|
||||
):
|
||||
raise L34DCumulativeCandidateError("operation source payload changed")
|
||||
merged_box = source_boxes[0]
|
||||
for source_box in source_boxes[1:]:
|
||||
merged_box = _union_box(merged_box, source_box)
|
||||
declared_merged_box = [
|
||||
_finite(value, "operation merged box")
|
||||
for value in _list(operation.get("merged_box_xyxy"), "merged box")
|
||||
]
|
||||
merged_score = _finite(operation.get("merged_score"), "merged score")
|
||||
if declared_merged_box != merged_box or merged_score != max(source_scores):
|
||||
raise L34DCumulativeCandidateError("operation projection changed")
|
||||
source_tiles = [
|
||||
_text(
|
||||
_object(provenance_predictions[index - 1], "provenance prediction").get(
|
||||
"rectification_tile"
|
||||
),
|
||||
"source tile",
|
||||
)
|
||||
for index in source_indices
|
||||
]
|
||||
if operation_type == "temporal-tile-seam-stitch":
|
||||
if source_tiles != _list(operation.get("source_tiles"), "stitch tiles"):
|
||||
raise L34DCumulativeCandidateError("stitch tile provenance changed")
|
||||
operation["temporal_run_id"] = _text(
|
||||
operation.get("temporal_run_id"),
|
||||
"temporal run id",
|
||||
)
|
||||
operation["temporal_run_length"] = _integer(
|
||||
operation.get("temporal_run_length"),
|
||||
"temporal run length",
|
||||
)
|
||||
operation["source_prediction_indices"] = source_indices
|
||||
operation["source_tiles"] = source_tiles
|
||||
operation["source_scores"] = source_scores
|
||||
operation["source_boxes_xyxy"] = source_boxes
|
||||
operation["merged_box_xyxy"] = declared_merged_box
|
||||
operation["merged_score"] = merged_score
|
||||
operation["operation_type"] = operation_type
|
||||
operation.pop("output_prediction_index", None)
|
||||
return operation
|
||||
|
||||
|
||||
def _sequence_map(
|
||||
values: tuple[dict[str, Any], ...],
|
||||
label: str,
|
||||
) -> dict[int, dict[str, Any]]:
|
||||
result = {
|
||||
_integer(value.get("truth_island_sequence"), f"{label} sequence"): value
|
||||
for value in values
|
||||
}
|
||||
if len(result) != len(values):
|
||||
raise L34DCumulativeCandidateError(f"{label} sequences are not unique")
|
||||
return result
|
||||
|
||||
|
||||
def _validate_case_binding(
|
||||
source: dict[str, Any],
|
||||
candidate: dict[str, Any],
|
||||
label: str,
|
||||
) -> None:
|
||||
if any(
|
||||
source.get(key) != candidate.get(key)
|
||||
for key in (
|
||||
"truth_island_sequence",
|
||||
"image_id",
|
||||
"frame_index",
|
||||
"group_id",
|
||||
"source_image_sha256",
|
||||
)
|
||||
):
|
||||
raise L34DCumulativeCandidateError(f"{label} case binding changed")
|
||||
@@ -0,0 +1,827 @@
|
||||
"""Build the L3.4E diagnostic comparison against a manual self-review.
|
||||
|
||||
L3.4E deliberately does not promote the self-review to ground truth. The
|
||||
reviewer has seen the candidate identity and the boxes are coarse enough that
|
||||
strict IoU50 alone would confuse annotation geometry with detector quality.
|
||||
The artifact therefore preserves the strict metric for reproducibility and
|
||||
adds a second, explicitly diagnostic association pass for visual triage.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
from .l34a_assisted_yolox_error_audit import (
|
||||
L34AAssistedYoloxErrorAuditError,
|
||||
_aggregate,
|
||||
_audit_case,
|
||||
_iou,
|
||||
_overlap_over_smaller,
|
||||
_per_class,
|
||||
)
|
||||
from .l34c_tile_seam_stitch_shadow import (
|
||||
_artifact,
|
||||
_canonical_json,
|
||||
_integer,
|
||||
_list,
|
||||
_object,
|
||||
_read_json,
|
||||
_read_jsonl,
|
||||
_sha256,
|
||||
_text,
|
||||
_utc_now,
|
||||
_validated_artifact,
|
||||
_write_json,
|
||||
_write_jsonl,
|
||||
)
|
||||
from .l34d_cumulative_postprocessing_candidate import (
|
||||
L34D_MANIFEST_NAME,
|
||||
L34DCumulativeCandidateError,
|
||||
read_l34d_cumulative_postprocessing_candidate,
|
||||
)
|
||||
|
||||
L34E_RESULT_SCHEMA: Final = "missioncore.l34e-self-review-diagnostic/v1"
|
||||
L34E_REPORT_SCHEMA: Final = (
|
||||
"missioncore.l34e-self-review-diagnostic-report/v1"
|
||||
)
|
||||
L34E_CASE_SCHEMA: Final = "missioncore.l34e-self-review-diagnostic-case/v1"
|
||||
L34E_MANIFEST_NAME: Final = "manifest.json"
|
||||
L34E_REPORT_NAME: Final = "self-review-diagnostic-report.json"
|
||||
L34E_CASES_NAME: Final = "self-review-diagnostic-cases.jsonl"
|
||||
|
||||
_RESULT_ID = re.compile(r"^l34e-self-review-diagnostic-[a-f0-9]{64}$")
|
||||
_SESSION_ID = re.compile(r"^l34-annotation-session-[a-f0-9]{64}$")
|
||||
_ANNOTATION_SCHEMA: Final = "missioncore.l34-annotation-session/v3"
|
||||
_STRICT_IOU: Final = 0.5
|
||||
_LOOSE_IOU: Final = 0.1
|
||||
_LOOSE_OVERLAP: Final = 0.3
|
||||
_AUTHORITY: Final = {
|
||||
"ground_truth": False,
|
||||
"candidate_accepted": False,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
|
||||
class L34ESelfReviewDiagnosticError(RuntimeError):
|
||||
"""An L3.4E source, result identity or immutable artifact is invalid."""
|
||||
|
||||
|
||||
def evaluate_l34e_self_review_diagnostic(
|
||||
*,
|
||||
l34d_cases: tuple[dict[str, Any], ...],
|
||||
annotation_frames: tuple[dict[str, Any], ...],
|
||||
) -> tuple[tuple[dict[str, Any], ...], dict[str, Any]]:
|
||||
"""Evaluate strict IoU50 and diagnostic associations for 32 cases."""
|
||||
|
||||
if len(l34d_cases) != 32 or len(annotation_frames) != 32:
|
||||
raise L34ESelfReviewDiagnosticError(
|
||||
"L3.4E requires exactly 32 candidate and review frames"
|
||||
)
|
||||
candidate_by_sequence = _sequence_map(l34d_cases, "candidate")
|
||||
review_by_sequence = _sequence_map(annotation_frames, "review")
|
||||
if set(candidate_by_sequence) != set(range(1, 33)) or set(
|
||||
review_by_sequence
|
||||
) != set(range(1, 33)):
|
||||
raise L34ESelfReviewDiagnosticError("L3.4E frame coverage differs")
|
||||
|
||||
cases: list[dict[str, Any]] = []
|
||||
strict_cases: list[dict[str, Any]] = []
|
||||
for sequence in range(1, 33):
|
||||
source = candidate_by_sequence[sequence]
|
||||
review = review_by_sequence[sequence]
|
||||
prediction_row = {
|
||||
"truth_island_sequence": sequence,
|
||||
"image_id": source.get("image_id"),
|
||||
"frame_index": source.get("frame_index"),
|
||||
"group_id": source.get("group_id"),
|
||||
"session_seconds": source.get("session_seconds"),
|
||||
"source_image_sha256": source.get("source_image_sha256"),
|
||||
"predictions": [
|
||||
{
|
||||
"label": item.get("category"),
|
||||
"score": item.get("score"),
|
||||
"bbox_xyxy": copy.deepcopy(item.get("box_xyxy")),
|
||||
}
|
||||
for item in (
|
||||
_object(value, "L3.4D after prediction")
|
||||
for value in _list(
|
||||
source.get("after_predictions"),
|
||||
"L3.4D after predictions",
|
||||
)
|
||||
)
|
||||
],
|
||||
}
|
||||
try:
|
||||
strict = _audit_case(
|
||||
prediction_row=prediction_row,
|
||||
annotation_frame=review,
|
||||
)
|
||||
except L34AAssistedYoloxErrorAuditError as reason:
|
||||
raise L34ESelfReviewDiagnosticError(
|
||||
"L3.4E source binding or box contract is invalid"
|
||||
) from reason
|
||||
|
||||
source_predictions = _list(
|
||||
source.get("after_predictions"),
|
||||
"L3.4D after predictions",
|
||||
)
|
||||
for prediction, source_prediction in zip(
|
||||
strict["predictions"],
|
||||
source_predictions,
|
||||
strict=True,
|
||||
):
|
||||
provenance = _object(source_prediction, "L3.4D prediction")
|
||||
prediction["source_prediction_indices"] = copy.deepcopy(
|
||||
provenance.get("source_prediction_indices", [])
|
||||
)
|
||||
prediction["source_rectification_tiles"] = copy.deepcopy(
|
||||
provenance.get("source_rectification_tiles", [])
|
||||
)
|
||||
prediction["operation_types"] = copy.deepcopy(
|
||||
provenance.get("operation_types", [])
|
||||
)
|
||||
|
||||
associations = _diagnostic_associations(
|
||||
strict["predictions"],
|
||||
strict["annotations"],
|
||||
)
|
||||
summary = _diagnostic_case_summary(
|
||||
strict["predictions"],
|
||||
strict["annotations"],
|
||||
associations,
|
||||
)
|
||||
cases.append(
|
||||
{
|
||||
"schema_version": L34E_CASE_SCHEMA,
|
||||
"truth_island_sequence": sequence,
|
||||
"image_id": strict["image_id"],
|
||||
"frame_index": strict["frame_index"],
|
||||
"group_id": strict["group_id"],
|
||||
"session_seconds": strict["session_seconds"],
|
||||
"source_image_sha256": strict["source_image_sha256"],
|
||||
"camera": copy.deepcopy(strict["camera"]),
|
||||
"predictions": copy.deepcopy(strict["predictions"]),
|
||||
"references": copy.deepcopy(strict["annotations"]),
|
||||
"associations": associations,
|
||||
"strict_summary": copy.deepcopy(strict["summary"]),
|
||||
"diagnostic_summary": summary,
|
||||
}
|
||||
)
|
||||
strict_cases.append(strict)
|
||||
|
||||
strict_metrics = _aggregate(tuple(strict_cases))
|
||||
strict_metrics["per_class"] = _per_class(tuple(strict_cases))
|
||||
diagnostic = _aggregate_diagnostic(tuple(cases))
|
||||
temporal = _temporal_count_diagnostics(tuple(cases))
|
||||
return tuple(cases), {
|
||||
"strict_iou50": strict_metrics,
|
||||
"diagnostic_association": diagnostic,
|
||||
"frame_count_disagreement": {
|
||||
"candidate_surplus_lower_bound": sum(
|
||||
max(
|
||||
0,
|
||||
case["diagnostic_summary"]["prediction_count"]
|
||||
- case["diagnostic_summary"]["reference_count"],
|
||||
)
|
||||
for case in cases
|
||||
),
|
||||
"reference_surplus_lower_bound": sum(
|
||||
max(
|
||||
0,
|
||||
case["diagnostic_summary"]["reference_count"]
|
||||
- case["diagnostic_summary"]["prediction_count"],
|
||||
)
|
||||
for case in cases
|
||||
),
|
||||
"equal_count_frame_count": sum(
|
||||
case["diagnostic_summary"]["prediction_count"]
|
||||
== case["diagnostic_summary"]["reference_count"]
|
||||
for case in cases
|
||||
),
|
||||
},
|
||||
"temporal_groups": temporal,
|
||||
"reference_quality": "not-metric-grade-self-review",
|
||||
}
|
||||
|
||||
|
||||
def build_l34e_self_review_diagnostic(
|
||||
*,
|
||||
l34d_candidate_root: Path,
|
||||
annotation_session_path: Path,
|
||||
output_root: Path,
|
||||
) -> dict[str, Any]:
|
||||
"""Build and publish one immutable L3.4E diagnostic result."""
|
||||
|
||||
try:
|
||||
candidate = read_l34d_cumulative_postprocessing_candidate(
|
||||
l34d_candidate_root
|
||||
)
|
||||
except L34DCumulativeCandidateError as reason:
|
||||
raise L34ESelfReviewDiagnosticError(
|
||||
"L3.4D candidate is invalid"
|
||||
) from reason
|
||||
session_path = annotation_session_path.expanduser().resolve(strict=True)
|
||||
if not session_path.is_file() or session_path.is_symlink():
|
||||
raise L34ESelfReviewDiagnosticError("self-review session is unavailable")
|
||||
session = _read_json(session_path)
|
||||
_validate_self_review_session(session, candidate["result_id"])
|
||||
|
||||
cases, metrics = evaluate_l34e_self_review_diagnostic(
|
||||
l34d_cases=candidate["cases"],
|
||||
annotation_frames=tuple(session["frames"]),
|
||||
)
|
||||
case_order = [
|
||||
case["truth_island_sequence"]
|
||||
for case in sorted(
|
||||
cases,
|
||||
key=lambda item: (
|
||||
-int(item["diagnostic_summary"]["severity_score"]),
|
||||
int(item["truth_island_sequence"]),
|
||||
),
|
||||
)
|
||||
]
|
||||
method = {
|
||||
"schema_version": "missioncore.laboratory-method/v1",
|
||||
"completeness": "complete",
|
||||
"execution_class": "deterministic",
|
||||
"pipeline_id": "ravnoves00-right-yolox-self-review-diagnostic/v1",
|
||||
"components": [
|
||||
{
|
||||
"kind": "source",
|
||||
"name": candidate["result_id"],
|
||||
"version": "L3.4D frozen cumulative candidate",
|
||||
"role": "candidate boxes and immutable source binding",
|
||||
"identity_sha256": _sha256(
|
||||
candidate["result_root"] / L34D_MANIFEST_NAME
|
||||
),
|
||||
},
|
||||
{
|
||||
"kind": "source",
|
||||
"name": session["session_id"],
|
||||
"version": "prediction-hidden manual self-review; not truth",
|
||||
"role": "coarse human reference boxes for diagnostic triage",
|
||||
"identity_sha256": _sha256(session_path),
|
||||
},
|
||||
{
|
||||
"kind": "algorithm",
|
||||
"name": "strict-plus-diagnostic-spatial-association",
|
||||
"version": "v1-iou50-then-iou10-or-overlap30",
|
||||
"role": "separate object association from localization disagreement",
|
||||
"identity_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
},
|
||||
],
|
||||
}
|
||||
report_basis = {
|
||||
"schema_version": L34E_REPORT_SCHEMA,
|
||||
"status": "completed-self-review-diagnostic-not-truth",
|
||||
"profile": {
|
||||
"profile_id": "l34e-self-review-diagnostic/v1",
|
||||
"strict_matcher": "greedy-maximum-iou-0.50",
|
||||
"diagnostic_matcher": (
|
||||
"strict-first-then-greedy-iou-0.10-or-overlap-over-smaller-0.30"
|
||||
),
|
||||
"class_policy": "spatial-association-first-then-class-verdict",
|
||||
"reference_policy": "manual-self-review-coarse-boxes-not-metric-grade",
|
||||
"scope": "recorded-right-camera-32-frozen-frames",
|
||||
},
|
||||
"metrics": metrics,
|
||||
"case_order": case_order,
|
||||
"decision": {
|
||||
"self_review_complete": True,
|
||||
"diagnostic_alignment_available": True,
|
||||
"metric_grade_reference_available": False,
|
||||
"independent_truth_available": False,
|
||||
"detector_retuning_authorized": False,
|
||||
"candidate_accepted": False,
|
||||
"l35_blind_gate_open": False,
|
||||
"next_action": (
|
||||
"refine and adjudicate the visual disagreement gallery, then obtain "
|
||||
"an independent reviewer before one-shot candidate acceptance"
|
||||
),
|
||||
},
|
||||
"limitations": [
|
||||
(
|
||||
"the reviewer had seen the candidate identity, so this result is "
|
||||
"diagnostic self-review and not independent truth"
|
||||
),
|
||||
(
|
||||
"manual boxes are coarse, especially for small distant vehicles; "
|
||||
"strict IoU50 is not detector accuracy"
|
||||
),
|
||||
(
|
||||
"the loose association pass is a visual-triage heuristic and must "
|
||||
"not be used as an acceptance metric"
|
||||
),
|
||||
"the sample contains only 32 recorded RIGHT-camera frames on one route",
|
||||
(
|
||||
"no live transport, hardware, left camera, LiDAR range, navigation "
|
||||
"or safety claim is made"
|
||||
),
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
"ground_truth": False,
|
||||
}
|
||||
identity = {
|
||||
"schema_version": L34E_RESULT_SCHEMA,
|
||||
"l34d_candidate": {
|
||||
"result_id": candidate["result_id"],
|
||||
"manifest_sha256": _sha256(
|
||||
candidate["result_root"] / L34D_MANIFEST_NAME
|
||||
),
|
||||
},
|
||||
"self_review": {
|
||||
"session_id": session["session_id"],
|
||||
"session_sha256": _sha256(session_path),
|
||||
"revision": session["revision"],
|
||||
"independent_truth_eligible": False,
|
||||
"metric_grade_reference": False,
|
||||
},
|
||||
"method": method,
|
||||
"report_sha256": hashlib.sha256(
|
||||
_canonical_json(report_basis)
|
||||
).hexdigest(),
|
||||
"cases_sha256": hashlib.sha256(_canonical_json(cases)).hexdigest(),
|
||||
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
result_id = f"l34e-self-review-diagnostic-{identity_sha256}"
|
||||
destination = output_root.expanduser().absolute() / result_id
|
||||
if destination.exists():
|
||||
return read_l34e_self_review_diagnostic(destination)
|
||||
|
||||
created_at_utc = _utc_now()
|
||||
report = {
|
||||
**report_basis,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"created_at_utc": created_at_utc,
|
||||
"source_session_id": "RAVNOVES00",
|
||||
"camera_source_id": "sensor.camera.right",
|
||||
"method": method,
|
||||
}
|
||||
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
_write_json(staging / L34E_REPORT_NAME, report)
|
||||
_write_jsonl(staging / L34E_CASES_NAME, cases)
|
||||
manifest = {
|
||||
"schema_version": L34E_RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": created_at_utc,
|
||||
"acceptance_state": "diagnostic-self-review-not-truth",
|
||||
"ground_truth": False,
|
||||
"artifacts": [
|
||||
_artifact(staging / L34E_REPORT_NAME, "diagnostic-report"),
|
||||
_artifact(staging / L34E_CASES_NAME, "diagnostic-cases"),
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
_write_json(staging / L34E_MANIFEST_NAME, manifest)
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
return read_l34e_self_review_diagnostic(destination)
|
||||
|
||||
|
||||
def read_l34e_self_review_diagnostic(root: Path) -> dict[str, Any]:
|
||||
"""Read and fully revalidate one immutable L3.4E result."""
|
||||
|
||||
resolved = root.resolve(strict=True)
|
||||
manifest = _read_json(resolved / L34E_MANIFEST_NAME)
|
||||
identity = _object(manifest.get("identity"), "L3.4E identity")
|
||||
identity_sha256 = manifest.get("identity_sha256")
|
||||
if (
|
||||
manifest.get("schema_version") != L34E_RESULT_SCHEMA
|
||||
or not isinstance(identity_sha256, str)
|
||||
or hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
!= identity_sha256
|
||||
or manifest.get("result_id")
|
||||
!= f"l34e-self-review-diagnostic-{identity_sha256}"
|
||||
or resolved.name != manifest.get("result_id")
|
||||
or _RESULT_ID.fullmatch(resolved.name) is None
|
||||
or manifest.get("acceptance_state") != "diagnostic-self-review-not-truth"
|
||||
or manifest.get("ground_truth") is not False
|
||||
or manifest.get("authority") != _AUTHORITY
|
||||
):
|
||||
raise L34ESelfReviewDiagnosticError("L3.4E identity is invalid")
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(artifacts, list) or len(artifacts) != 2:
|
||||
raise L34ESelfReviewDiagnosticError("L3.4E artifacts are invalid")
|
||||
artifact_by_role = {
|
||||
_text(item.get("role"), "artifact role"): _object(item, "artifact")
|
||||
for item in artifacts
|
||||
if isinstance(item, dict)
|
||||
}
|
||||
report = _read_json(
|
||||
_validated_artifact(
|
||||
resolved,
|
||||
artifact_by_role.get("diagnostic-report"),
|
||||
)
|
||||
)
|
||||
cases = tuple(
|
||||
_read_jsonl(
|
||||
_validated_artifact(
|
||||
resolved,
|
||||
artifact_by_role.get("diagnostic-cases"),
|
||||
)
|
||||
)
|
||||
)
|
||||
if (
|
||||
report.get("schema_version") != L34E_REPORT_SCHEMA
|
||||
or report.get("result_id") != resolved.name
|
||||
or report.get("identity_sha256") != identity_sha256
|
||||
or report.get("status") != "completed-self-review-diagnostic-not-truth"
|
||||
or report.get("ground_truth") is not False
|
||||
or report.get("authority") != _AUTHORITY
|
||||
or len(cases) != 32
|
||||
or any(case.get("schema_version") != L34E_CASE_SCHEMA for case in cases)
|
||||
or hashlib.sha256(_canonical_json(cases)).hexdigest()
|
||||
!= identity.get("cases_sha256")
|
||||
):
|
||||
raise L34ESelfReviewDiagnosticError("L3.4E result changed")
|
||||
return {
|
||||
"result_id": resolved.name,
|
||||
"result_root": resolved,
|
||||
"manifest": manifest,
|
||||
"report": report,
|
||||
"cases": cases,
|
||||
}
|
||||
|
||||
|
||||
def _validate_self_review_session(
|
||||
session: dict[str, Any],
|
||||
candidate_result_id: str,
|
||||
) -> None:
|
||||
assistance = _object(session.get("assistance"), "self-review assistance")
|
||||
blindness = _object(session.get("blindness"), "self-review blindness")
|
||||
frames = session.get("frames")
|
||||
if (
|
||||
session.get("schema_version") != _ANNOTATION_SCHEMA
|
||||
or not isinstance(session.get("session_id"), str)
|
||||
or _SESSION_ID.fullmatch(session["session_id"]) is None
|
||||
or session.get("result_id") != candidate_result_id
|
||||
or session.get("contract_id") != "l34d-prediction-hidden-review/v1"
|
||||
or session.get("state") != "saved"
|
||||
or not isinstance(session.get("revision"), int)
|
||||
or session["revision"] < 1
|
||||
or assistance
|
||||
!= {
|
||||
"mode": "prediction-hidden-manual",
|
||||
"independent_truth_eligible": False,
|
||||
}
|
||||
or blindness
|
||||
!= {
|
||||
"candidate_identity_seen": True,
|
||||
"model_prelabels_seen": False,
|
||||
"model_predictions_seen": False,
|
||||
"model_scores_seen": False,
|
||||
}
|
||||
or session.get("authority") != _AUTHORITY
|
||||
or not isinstance(frames, list)
|
||||
or len(frames) != 32
|
||||
):
|
||||
raise L34ESelfReviewDiagnosticError(
|
||||
"self-review session contract is invalid"
|
||||
)
|
||||
sequences: set[int] = set()
|
||||
for raw_frame in frames:
|
||||
frame = _object(raw_frame, "self-review frame")
|
||||
sequence = _integer(frame.get("truth_island_sequence"), "review sequence")
|
||||
objects = frame.get("objects")
|
||||
if (
|
||||
not 1 <= sequence <= 32
|
||||
or sequence in sequences
|
||||
or frame.get("reviewed") is not True
|
||||
or not isinstance(frame.get("source_sha256"), str)
|
||||
or not isinstance(objects, list)
|
||||
):
|
||||
raise L34ESelfReviewDiagnosticError(
|
||||
"self-review coverage is incomplete"
|
||||
)
|
||||
sequences.add(sequence)
|
||||
for raw_object in objects:
|
||||
item = _object(raw_object, "self-review object")
|
||||
if item.get("origin") != "manual":
|
||||
raise L34ESelfReviewDiagnosticError(
|
||||
"self-review contains seeded objects"
|
||||
)
|
||||
category = item.get("category")
|
||||
if (
|
||||
not isinstance(item.get("object_id"), str)
|
||||
or not isinstance(category, str)
|
||||
or not _valid_box(item.get("box_xyxy"))
|
||||
or (
|
||||
category == "unmapped"
|
||||
and not isinstance(item.get("proposed_label"), str)
|
||||
)
|
||||
or (
|
||||
category != "unmapped"
|
||||
and item.get("proposed_label") is not None
|
||||
)
|
||||
):
|
||||
raise L34ESelfReviewDiagnosticError(
|
||||
"self-review object contract is invalid"
|
||||
)
|
||||
|
||||
|
||||
def _diagnostic_associations(
|
||||
predictions: list[dict[str, Any]],
|
||||
references: list[dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
remaining_predictions = set(range(len(predictions)))
|
||||
remaining_references = set(range(len(references)))
|
||||
associations: list[dict[str, Any]] = []
|
||||
|
||||
strict_candidates = sorted(
|
||||
(
|
||||
(_iou(prediction["box_xyxy"], reference["box_xyxy"]), p, r)
|
||||
for p, prediction in enumerate(predictions)
|
||||
for r, reference in enumerate(references)
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
for iou, prediction_index, reference_index in strict_candidates:
|
||||
if iou < _STRICT_IOU:
|
||||
break
|
||||
if (
|
||||
prediction_index not in remaining_predictions
|
||||
or reference_index not in remaining_references
|
||||
):
|
||||
continue
|
||||
classification = (
|
||||
"strict_alignment"
|
||||
if predictions[prediction_index]["category"]
|
||||
== references[reference_index]["category"]
|
||||
else "strict_class_mismatch"
|
||||
)
|
||||
associations.append(
|
||||
_association(
|
||||
predictions,
|
||||
references,
|
||||
prediction_index,
|
||||
reference_index,
|
||||
classification,
|
||||
)
|
||||
)
|
||||
remaining_predictions.remove(prediction_index)
|
||||
remaining_references.remove(reference_index)
|
||||
|
||||
loose_candidates = sorted(
|
||||
(
|
||||
(
|
||||
max(
|
||||
_iou(
|
||||
predictions[prediction_index]["box_xyxy"],
|
||||
references[reference_index]["box_xyxy"],
|
||||
),
|
||||
_overlap_over_smaller(
|
||||
predictions[prediction_index]["box_xyxy"],
|
||||
references[reference_index]["box_xyxy"],
|
||||
),
|
||||
),
|
||||
prediction_index,
|
||||
reference_index,
|
||||
)
|
||||
for prediction_index in remaining_predictions
|
||||
for reference_index in remaining_references
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
for _, prediction_index, reference_index in loose_candidates:
|
||||
if (
|
||||
prediction_index not in remaining_predictions
|
||||
or reference_index not in remaining_references
|
||||
):
|
||||
continue
|
||||
prediction_box = predictions[prediction_index]["box_xyxy"]
|
||||
reference_box = references[reference_index]["box_xyxy"]
|
||||
iou = _iou(prediction_box, reference_box)
|
||||
overlap = _overlap_over_smaller(prediction_box, reference_box)
|
||||
if iou < _LOOSE_IOU and overlap < _LOOSE_OVERLAP:
|
||||
continue
|
||||
classification = (
|
||||
"localization_disagreement"
|
||||
if predictions[prediction_index]["category"]
|
||||
== references[reference_index]["category"]
|
||||
else "class_and_localization_disagreement"
|
||||
)
|
||||
associations.append(
|
||||
_association(
|
||||
predictions,
|
||||
references,
|
||||
prediction_index,
|
||||
reference_index,
|
||||
classification,
|
||||
)
|
||||
)
|
||||
remaining_predictions.remove(prediction_index)
|
||||
remaining_references.remove(reference_index)
|
||||
|
||||
for prediction in predictions:
|
||||
prediction["diagnostic_verdict"] = "prediction_only"
|
||||
prediction["associated_object_id"] = None
|
||||
prediction["association_iou"] = None
|
||||
prediction["association_overlap_over_smaller"] = None
|
||||
for reference in references:
|
||||
reference["diagnostic_verdict"] = "reference_only"
|
||||
reference["associated_prediction_index"] = None
|
||||
reference["association_iou"] = None
|
||||
reference["association_overlap_over_smaller"] = None
|
||||
for association in associations:
|
||||
prediction = predictions[association["prediction_index"] - 1]
|
||||
reference = next(
|
||||
item
|
||||
for item in references
|
||||
if item["object_id"] == association["object_id"]
|
||||
)
|
||||
prediction["diagnostic_verdict"] = association["classification"]
|
||||
prediction["associated_object_id"] = association["object_id"]
|
||||
prediction["association_iou"] = association["iou"]
|
||||
prediction["association_overlap_over_smaller"] = association[
|
||||
"overlap_over_smaller"
|
||||
]
|
||||
reference["diagnostic_verdict"] = association["classification"]
|
||||
reference["associated_prediction_index"] = association[
|
||||
"prediction_index"
|
||||
]
|
||||
reference["association_iou"] = association["iou"]
|
||||
reference["association_overlap_over_smaller"] = association[
|
||||
"overlap_over_smaller"
|
||||
]
|
||||
return sorted(associations, key=lambda item: item["prediction_index"])
|
||||
|
||||
|
||||
def _association(
|
||||
predictions: list[dict[str, Any]],
|
||||
references: list[dict[str, Any]],
|
||||
prediction_index: int,
|
||||
reference_index: int,
|
||||
classification: str,
|
||||
) -> dict[str, Any]:
|
||||
prediction = predictions[prediction_index]
|
||||
reference = references[reference_index]
|
||||
return {
|
||||
"prediction_index": prediction["prediction_index"],
|
||||
"object_id": reference["object_id"],
|
||||
"prediction_category": prediction["category"],
|
||||
"reference_category": reference["display_category"],
|
||||
"iou": _iou(prediction["box_xyxy"], reference["box_xyxy"]),
|
||||
"overlap_over_smaller": _overlap_over_smaller(
|
||||
prediction["box_xyxy"], reference["box_xyxy"]
|
||||
),
|
||||
"classification": classification,
|
||||
}
|
||||
|
||||
|
||||
def _diagnostic_case_summary(
|
||||
predictions: list[dict[str, Any]],
|
||||
references: list[dict[str, Any]],
|
||||
associations: list[dict[str, Any]],
|
||||
) -> dict[str, Any]:
|
||||
counts: defaultdict[str, int] = defaultdict(int)
|
||||
for association in associations:
|
||||
counts[association["classification"]] += 1
|
||||
prediction_only = sum(
|
||||
item["diagnostic_verdict"] == "prediction_only" for item in predictions
|
||||
)
|
||||
reference_only = sum(
|
||||
item["diagnostic_verdict"] == "reference_only" for item in references
|
||||
)
|
||||
associated = len(associations)
|
||||
return {
|
||||
"prediction_count": len(predictions),
|
||||
"reference_count": len(references),
|
||||
"associated_pair_count": associated,
|
||||
"strict_alignment": counts["strict_alignment"],
|
||||
"strict_class_mismatch": counts["strict_class_mismatch"],
|
||||
"localization_disagreement": counts["localization_disagreement"],
|
||||
"class_and_localization_disagreement": counts[
|
||||
"class_and_localization_disagreement"
|
||||
],
|
||||
"prediction_only": prediction_only,
|
||||
"reference_only": reference_only,
|
||||
"candidate_association_coverage": (
|
||||
associated / len(predictions) if predictions else 0.0
|
||||
),
|
||||
"reference_association_coverage": (
|
||||
associated / len(references) if references else 0.0
|
||||
),
|
||||
"severity_score": (
|
||||
prediction_only
|
||||
+ reference_only * 2
|
||||
+ counts["localization_disagreement"]
|
||||
+ counts["strict_class_mismatch"] * 3
|
||||
+ counts["class_and_localization_disagreement"] * 4
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _aggregate_diagnostic(cases: tuple[dict[str, Any], ...]) -> dict[str, Any]:
|
||||
keys = (
|
||||
"prediction_count",
|
||||
"reference_count",
|
||||
"associated_pair_count",
|
||||
"strict_alignment",
|
||||
"strict_class_mismatch",
|
||||
"localization_disagreement",
|
||||
"class_and_localization_disagreement",
|
||||
"prediction_only",
|
||||
"reference_only",
|
||||
)
|
||||
totals = {
|
||||
key: sum(int(case["diagnostic_summary"][key]) for case in cases)
|
||||
for key in keys
|
||||
}
|
||||
associated = totals["associated_pair_count"]
|
||||
return {
|
||||
**totals,
|
||||
"candidate_association_coverage": (
|
||||
associated / totals["prediction_count"]
|
||||
if totals["prediction_count"]
|
||||
else 0.0
|
||||
),
|
||||
"reference_association_coverage": (
|
||||
associated / totals["reference_count"]
|
||||
if totals["reference_count"]
|
||||
else 0.0
|
||||
),
|
||||
"error_case_count": sum(
|
||||
case["diagnostic_summary"]["strict_class_mismatch"] > 0
|
||||
or case["diagnostic_summary"]["localization_disagreement"] > 0
|
||||
or case["diagnostic_summary"]["class_and_localization_disagreement"]
|
||||
> 0
|
||||
or case["diagnostic_summary"]["prediction_only"] > 0
|
||||
or case["diagnostic_summary"]["reference_only"] > 0
|
||||
for case in cases
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _temporal_count_diagnostics(
|
||||
cases: tuple[dict[str, Any], ...],
|
||||
) -> list[dict[str, Any]]:
|
||||
grouped: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
||||
for case in cases:
|
||||
grouped[_text(case.get("group_id"), "group id")].append(case)
|
||||
result: list[dict[str, Any]] = []
|
||||
for group_id, members in sorted(grouped.items()):
|
||||
ordered = sorted(members, key=lambda item: item["truth_island_sequence"])
|
||||
candidate_counts = [
|
||||
item["diagnostic_summary"]["prediction_count"] for item in ordered
|
||||
]
|
||||
reference_counts = [
|
||||
item["diagnostic_summary"]["reference_count"] for item in ordered
|
||||
]
|
||||
result.append(
|
||||
{
|
||||
"group_id": group_id,
|
||||
"sequences": [item["truth_island_sequence"] for item in ordered],
|
||||
"candidate_counts": candidate_counts,
|
||||
"reference_counts": reference_counts,
|
||||
"candidate_count_range": max(candidate_counts)
|
||||
- min(candidate_counts),
|
||||
"reference_count_range": max(reference_counts)
|
||||
- min(reference_counts),
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _sequence_map(
|
||||
rows: tuple[dict[str, Any], ...],
|
||||
label: str,
|
||||
) -> dict[int, dict[str, Any]]:
|
||||
result: dict[int, dict[str, Any]] = {}
|
||||
for raw in rows:
|
||||
item = _object(raw, f"{label} row")
|
||||
sequence = _integer(item.get("truth_island_sequence"), f"{label} sequence")
|
||||
if sequence in result:
|
||||
raise L34ESelfReviewDiagnosticError(f"duplicate {label} sequence")
|
||||
result[sequence] = item
|
||||
return result
|
||||
|
||||
|
||||
def _valid_box(value: object) -> bool:
|
||||
if not isinstance(value, list) or len(value) != 4:
|
||||
return False
|
||||
if any(
|
||||
not isinstance(item, (int, float)) or isinstance(item, bool)
|
||||
for item in value
|
||||
):
|
||||
return False
|
||||
left, top, right, bottom = (float(item) for item in value)
|
||||
return 0 <= left < right <= 800 and 0 <= top < bottom <= 600
|
||||
@@ -0,0 +1,350 @@
|
||||
"""Freeze one candidate-visible L3.4F engineering reference.
|
||||
|
||||
The artifact records human adjudication of the L3.4E disagreement gallery.
|
||||
It is deliberately not independent truth and grants no detector, command,
|
||||
navigation, or safety authority.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
from k1link.compute.l34e_self_review_diagnostic import (
|
||||
L34E_MANIFEST_NAME,
|
||||
read_l34e_self_review_diagnostic,
|
||||
)
|
||||
|
||||
L34F_RESULT_SCHEMA: Final = "missioncore.l34f-adjudicated-reference/v1"
|
||||
L34F_REPORT_SCHEMA: Final = "missioncore.l34f-adjudicated-reference-report/v1"
|
||||
L34F_CASE_SCHEMA: Final = "missioncore.l34f-adjudicated-reference-case/v1"
|
||||
L34F_MANIFEST_NAME: Final = "manifest.json"
|
||||
L34F_REPORT_NAME: Final = "adjudicated-reference-report.json"
|
||||
L34F_CASES_NAME: Final = "adjudicated-reference-cases.jsonl"
|
||||
L34F_SESSION_SCHEMA: Final = "missioncore.l34f-adjudication-session/v1"
|
||||
|
||||
_RESULT_ID = re.compile(r"^l34f-adjudicated-reference-[a-f0-9]{64}$")
|
||||
_SESSION_ID = re.compile(r"^l34f-adjudication-session-[a-f0-9]{64}$")
|
||||
_AUTHORITY: Final = {
|
||||
"ground_truth": False,
|
||||
"independent_truth": False,
|
||||
"metric_grade_reference": False,
|
||||
"candidate_accepted": False,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
|
||||
class L34FAdjudicatedReferenceError(ValueError):
|
||||
"""Raised when an L3.4F source or immutable artifact is invalid."""
|
||||
|
||||
|
||||
def build_l34f_adjudicated_reference(
|
||||
*,
|
||||
diagnostic_root: Path,
|
||||
adjudication_session_path: Path,
|
||||
output_root: Path,
|
||||
) -> dict[str, Any]:
|
||||
diagnostic = read_l34e_self_review_diagnostic(diagnostic_root)
|
||||
session_path = adjudication_session_path.expanduser().resolve(strict=True)
|
||||
if not session_path.is_file() or session_path.is_symlink():
|
||||
raise L34FAdjudicatedReferenceError("adjudication session unavailable")
|
||||
session = _read_json(session_path)
|
||||
_validate_session(session, diagnostic["result_id"])
|
||||
|
||||
source_cases = {int(case["truth_island_sequence"]): case for case in diagnostic["cases"]}
|
||||
cases: list[dict[str, Any]] = []
|
||||
totals = {
|
||||
"frame_count": 32,
|
||||
"source_reference_count": 0,
|
||||
"adjudicated_reference_count": 0,
|
||||
"unchanged_object_count": 0,
|
||||
"geometry_changed_object_count": 0,
|
||||
"class_changed_object_count": 0,
|
||||
"attribute_changed_object_count": 0,
|
||||
"added_object_count": 0,
|
||||
"deleted_object_count": 0,
|
||||
"changed_frame_count": 0,
|
||||
}
|
||||
for frame in sorted(session["frames"], key=lambda row: row["truth_island_sequence"]):
|
||||
sequence = int(frame["truth_island_sequence"])
|
||||
source = source_cases[sequence]
|
||||
source_by_id = {item["object_id"]: item for item in source["references"]}
|
||||
final_by_id = {item["object_id"]: item for item in frame["objects"]}
|
||||
changes: list[dict[str, Any]] = []
|
||||
for object_id, original in source_by_id.items():
|
||||
final = final_by_id.get(object_id)
|
||||
if final is None:
|
||||
changes.append({"type": "delete", "object_id": object_id})
|
||||
totals["deleted_object_count"] += 1
|
||||
continue
|
||||
changed = False
|
||||
if original["box_xyxy"] != final["box_xyxy"]:
|
||||
changes.append(
|
||||
{
|
||||
"type": "geometry",
|
||||
"object_id": object_id,
|
||||
"before": original["box_xyxy"],
|
||||
"after": final["box_xyxy"],
|
||||
}
|
||||
)
|
||||
totals["geometry_changed_object_count"] += 1
|
||||
changed = True
|
||||
if original["category"] != final["category"] or original.get(
|
||||
"proposed_label"
|
||||
) != final.get("proposed_label"):
|
||||
changes.append(
|
||||
{
|
||||
"type": "class",
|
||||
"object_id": object_id,
|
||||
"before": {
|
||||
"category": original["category"],
|
||||
"proposed_label": original.get("proposed_label"),
|
||||
},
|
||||
"after": {
|
||||
"category": final["category"],
|
||||
"proposed_label": final.get("proposed_label"),
|
||||
},
|
||||
}
|
||||
)
|
||||
totals["class_changed_object_count"] += 1
|
||||
changed = True
|
||||
if bool(original["occluded"]) != bool(final["occluded"]) or bool(
|
||||
original["truncated"]
|
||||
) != bool(final["truncated"]):
|
||||
changes.append({"type": "attributes", "object_id": object_id})
|
||||
totals["attribute_changed_object_count"] += 1
|
||||
changed = True
|
||||
if not changed:
|
||||
totals["unchanged_object_count"] += 1
|
||||
for object_id in final_by_id.keys() - source_by_id.keys():
|
||||
changes.append({"type": "add", "object_id": object_id})
|
||||
totals["added_object_count"] += 1
|
||||
if changes:
|
||||
totals["changed_frame_count"] += 1
|
||||
totals["source_reference_count"] += len(source_by_id)
|
||||
totals["adjudicated_reference_count"] += len(final_by_id)
|
||||
cases.append(
|
||||
{
|
||||
"schema_version": L34F_CASE_SCHEMA,
|
||||
"truth_island_sequence": sequence,
|
||||
"image_id": int(source["image_id"]),
|
||||
"frame_index": int(source["frame_index"]),
|
||||
"group_id": str(source["group_id"]),
|
||||
"session_seconds": float(source["session_seconds"]),
|
||||
"source_image_sha256": str(source["source_image_sha256"]),
|
||||
"references": frame["objects"],
|
||||
"source_reference_count": len(source_by_id),
|
||||
"change_count": len(changes),
|
||||
"changes": changes,
|
||||
}
|
||||
)
|
||||
|
||||
report_basis = {
|
||||
"schema_version": L34F_REPORT_SCHEMA,
|
||||
"status": "completed-candidate-visible-adjudication-not-truth",
|
||||
"metrics": totals,
|
||||
"decision": {
|
||||
"adjudication_complete": True,
|
||||
"engineering_reference_available": True,
|
||||
"metric_grade_reference_available": False,
|
||||
"independent_truth_available": False,
|
||||
"candidate_accepted": False,
|
||||
"detector_retuning_authorized": False,
|
||||
"l35_blind_gate_open": False,
|
||||
"next_action": (
|
||||
"obtain two prediction-free independent reviewer submissions "
|
||||
"and explicit adjudication before E48 and one-shot L3.5"
|
||||
),
|
||||
},
|
||||
"limitations": [
|
||||
"candidate predictions were visible during adjudication",
|
||||
"the artifact is an engineering reference and not independent ground truth",
|
||||
"the sample contains 32 recorded RIGHT-camera frames on one route",
|
||||
"no live, left-camera, LiDAR-range, navigation, command, or safety claim is made",
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
"ground_truth": False,
|
||||
}
|
||||
identity = {
|
||||
"schema_version": L34F_RESULT_SCHEMA,
|
||||
"l34e_diagnostic": {
|
||||
"result_id": diagnostic["result_id"],
|
||||
"manifest_sha256": _sha256(diagnostic["result_root"] / L34E_MANIFEST_NAME),
|
||||
},
|
||||
"adjudication_session": {
|
||||
"session_id": session["session_id"],
|
||||
"session_sha256": _sha256(session_path),
|
||||
"revision": session["revision"],
|
||||
},
|
||||
"report_sha256": hashlib.sha256(_canonical_json(report_basis)).hexdigest(),
|
||||
"cases_sha256": hashlib.sha256(_canonical_json(cases)).hexdigest(),
|
||||
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
result_id = f"l34f-adjudicated-reference-{identity_sha256}"
|
||||
destination = output_root.expanduser().absolute() / result_id
|
||||
if destination.exists():
|
||||
return read_l34f_adjudicated_reference(destination)
|
||||
created_at_utc = _utc_now()
|
||||
report = {
|
||||
**report_basis,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"created_at_utc": created_at_utc,
|
||||
"source_session_id": "RAVNOVES00",
|
||||
"camera_source_id": "sensor.camera.right",
|
||||
}
|
||||
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
_write_json(staging / L34F_REPORT_NAME, report)
|
||||
_write_jsonl(staging / L34F_CASES_NAME, cases)
|
||||
artifacts = [
|
||||
_artifact(staging / L34F_REPORT_NAME, "adjudication-report"),
|
||||
_artifact(staging / L34F_CASES_NAME, "adjudicated-cases"),
|
||||
]
|
||||
_write_json(
|
||||
staging / L34F_MANIFEST_NAME,
|
||||
{
|
||||
"schema_version": L34F_RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": created_at_utc,
|
||||
"acceptance_state": "engineering-reference-not-truth",
|
||||
"ground_truth": False,
|
||||
"artifacts": artifacts,
|
||||
"authority": _AUTHORITY,
|
||||
},
|
||||
)
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
return read_l34f_adjudicated_reference(destination)
|
||||
|
||||
|
||||
def read_l34f_adjudicated_reference(root: Path) -> dict[str, Any]:
|
||||
resolved = root.resolve(strict=True)
|
||||
manifest = _read_json(resolved / L34F_MANIFEST_NAME)
|
||||
identity = manifest.get("identity")
|
||||
if not isinstance(identity, dict):
|
||||
raise L34FAdjudicatedReferenceError("L3.4F identity invalid")
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
if (
|
||||
manifest.get("schema_version") != L34F_RESULT_SCHEMA
|
||||
or manifest.get("identity_sha256") != identity_sha256
|
||||
or manifest.get("result_id") != f"l34f-adjudicated-reference-{identity_sha256}"
|
||||
or resolved.name != manifest.get("result_id")
|
||||
or _RESULT_ID.fullmatch(resolved.name) is None
|
||||
or manifest.get("acceptance_state") != "engineering-reference-not-truth"
|
||||
or manifest.get("authority") != _AUTHORITY
|
||||
or manifest.get("ground_truth") is not False
|
||||
):
|
||||
raise L34FAdjudicatedReferenceError("L3.4F identity invalid")
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(artifacts, list) or len(artifacts) != 2:
|
||||
raise L34FAdjudicatedReferenceError("L3.4F artifacts invalid")
|
||||
by_role = {item.get("role"): item for item in artifacts if isinstance(item, dict)}
|
||||
report = _read_json(_validated_artifact(resolved, by_role.get("adjudication-report")))
|
||||
cases = tuple(_read_jsonl(_validated_artifact(resolved, by_role.get("adjudicated-cases"))))
|
||||
if (
|
||||
report.get("schema_version") != L34F_REPORT_SCHEMA
|
||||
or report.get("result_id") != resolved.name
|
||||
or report.get("status") != "completed-candidate-visible-adjudication-not-truth"
|
||||
or report.get("authority") != _AUTHORITY
|
||||
or report.get("ground_truth") is not False
|
||||
or len(cases) != 32
|
||||
or any(case.get("schema_version") != L34F_CASE_SCHEMA for case in cases)
|
||||
or hashlib.sha256(_canonical_json(cases)).hexdigest() != identity.get("cases_sha256")
|
||||
):
|
||||
raise L34FAdjudicatedReferenceError("L3.4F result changed")
|
||||
return {
|
||||
"result_id": resolved.name,
|
||||
"result_root": resolved,
|
||||
"manifest": manifest,
|
||||
"report": report,
|
||||
"cases": cases,
|
||||
}
|
||||
|
||||
|
||||
def _validate_session(session: dict[str, Any], diagnostic_result_id: str) -> None:
|
||||
frames = session.get("frames")
|
||||
if (
|
||||
session.get("schema_version") != L34F_SESSION_SCHEMA
|
||||
or not isinstance(session.get("session_id"), str)
|
||||
or _SESSION_ID.fullmatch(session["session_id"]) is None
|
||||
or session.get("diagnostic_result_id") != diagnostic_result_id
|
||||
or session.get("state") != "saved"
|
||||
or not isinstance(session.get("revision"), int)
|
||||
or session["revision"] < 1
|
||||
or session.get("authority") != _AUTHORITY
|
||||
or not isinstance(frames, list)
|
||||
or len(frames) != 32
|
||||
or any(frame.get("reviewed") is not True for frame in frames if isinstance(frame, dict))
|
||||
):
|
||||
raise L34FAdjudicatedReferenceError("adjudication session incomplete")
|
||||
|
||||
|
||||
def _artifact(path: Path, role: str) -> dict[str, Any]:
|
||||
return {
|
||||
"path": path.name,
|
||||
"role": role,
|
||||
"byte_length": path.stat().st_size,
|
||||
"sha256": _sha256(path),
|
||||
}
|
||||
|
||||
|
||||
def _validated_artifact(root: Path, raw: object) -> Path:
|
||||
if not isinstance(raw, dict) or not isinstance(raw.get("path"), str):
|
||||
raise L34FAdjudicatedReferenceError("L3.4F artifact invalid")
|
||||
path = (root / raw["path"]).resolve(strict=True)
|
||||
if path.parent != root or path.is_symlink() or _sha256(path) != raw.get("sha256"):
|
||||
raise L34FAdjudicatedReferenceError("L3.4F artifact changed")
|
||||
return path
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False
|
||||
).encode()
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(value, dict):
|
||||
raise L34FAdjudicatedReferenceError("expected JSON object")
|
||||
return value
|
||||
|
||||
|
||||
def _read_jsonl(path: Path) -> list[dict[str, Any]]:
|
||||
values = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line]
|
||||
if any(not isinstance(value, dict) for value in values):
|
||||
raise L34FAdjudicatedReferenceError("expected JSONL objects")
|
||||
return values
|
||||
|
||||
|
||||
def _write_json(path: Path, value: object) -> None:
|
||||
path.write_bytes(_canonical_json(value) + b"\n")
|
||||
|
||||
|
||||
def _write_jsonl(path: Path, values: list[dict[str, Any]]) -> None:
|
||||
path.write_bytes(b"".join(_canonical_json(value) + b"\n" for value in values))
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def _utc_now() -> str:
|
||||
return datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
||||
@@ -0,0 +1,407 @@
|
||||
"""Evaluate the exact L3.4 YOLOX freeze after an accepted E48 truth seal."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
from .e46_detector_truth_island import (
|
||||
E46_MANIFEST_NAME,
|
||||
E46DetectorTruthIslandError,
|
||||
read_e46_detector_truth_island,
|
||||
)
|
||||
from .e48_detector_truth_seal import (
|
||||
E48_MANIFEST_NAME,
|
||||
E48DetectorTruthSealError,
|
||||
read_e48_detector_truth_seal,
|
||||
)
|
||||
from .e49_detector_truth_evaluation import (
|
||||
E49DetectorTruthEvaluationError,
|
||||
evaluate_frozen_detector_candidates,
|
||||
read_valid_fov_mask,
|
||||
)
|
||||
from .l34_right_yolox_truth_island_freeze import (
|
||||
L34_MANIFEST_NAME,
|
||||
L34_PREDICTION_SCHEMA,
|
||||
L34RightYoloxTruthIslandError,
|
||||
read_l34_right_yolox_truth_island_freeze,
|
||||
)
|
||||
|
||||
L35_RESULT_SCHEMA: Final = "missioncore.l35-right-yolox-truth-evaluation/v1"
|
||||
L35_REPORT_SCHEMA: Final = "missioncore.l35-right-yolox-evaluation-report/v1"
|
||||
L35_MANIFEST_NAME: Final = "manifest.json"
|
||||
L35_REPORT_NAME: Final = "right-yolox-evaluation-report.json"
|
||||
|
||||
_RESULT_ID: Final = re.compile(
|
||||
r"^l35-right-yolox-truth-evaluation-[a-f0-9]{64}$"
|
||||
)
|
||||
_AUTHORITY: Final = {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
|
||||
class L35RightYoloxTruthEvaluationError(RuntimeError):
|
||||
"""The L3.4 freeze cannot be joined to accepted independent truth."""
|
||||
|
||||
|
||||
def build_l35_right_yolox_truth_evaluation(
|
||||
*,
|
||||
truth_island_root: Path,
|
||||
truth_seal_root: Path,
|
||||
l34_freeze_root: Path,
|
||||
valid_fov_root: Path,
|
||||
output_root: Path,
|
||||
) -> dict[str, Any]:
|
||||
"""Join L3.4 to E48 only after truth was sealed after the exact freeze."""
|
||||
|
||||
try:
|
||||
truth_island = read_e46_detector_truth_island(truth_island_root)
|
||||
truth_seal = read_e48_detector_truth_seal(truth_seal_root)
|
||||
l34_freeze = read_l34_right_yolox_truth_island_freeze(l34_freeze_root)
|
||||
except (
|
||||
E46DetectorTruthIslandError,
|
||||
E48DetectorTruthSealError,
|
||||
L34RightYoloxTruthIslandError,
|
||||
) as reason:
|
||||
raise L35RightYoloxTruthEvaluationError(
|
||||
"L3.5 evaluation input is invalid"
|
||||
) from reason
|
||||
|
||||
seal_report = _object(truth_seal.get("report"), "E48 report")
|
||||
seal_identity = _object(
|
||||
_object(truth_seal.get("manifest"), "E48 manifest").get("identity"),
|
||||
"E48 identity",
|
||||
)
|
||||
sealed_island = _object(
|
||||
seal_identity.get("truth_island"),
|
||||
"E48 truth island",
|
||||
)
|
||||
l34_identity = _object(l34_freeze.manifest.get("identity"), "L3.4 identity")
|
||||
l34_island = _object(
|
||||
l34_identity.get("truth_island"),
|
||||
"L3.4 truth island",
|
||||
)
|
||||
if (
|
||||
seal_report.get("status") != "sealed-adjudicated-independent-truth"
|
||||
or _object(seal_report.get("decision"), "E48 decision").get(
|
||||
"candidate_comparison_authorized"
|
||||
)
|
||||
is not True
|
||||
or sealed_island.get("result_id") != truth_island.result_id
|
||||
or l34_island.get("result_id") != truth_island.result_id
|
||||
):
|
||||
raise L35RightYoloxTruthEvaluationError(
|
||||
"truth island, seal and L3.4 identities differ"
|
||||
)
|
||||
provenance = _object(truth_seal.get("provenance"), "E48 provenance")
|
||||
_require_freeze_before_truth_seal(
|
||||
freeze_created_at_utc=l34_freeze.manifest.get("created_at_utc"),
|
||||
truth_sealed_at_utc=provenance.get("sealed_at_utc"),
|
||||
)
|
||||
|
||||
source = _object(
|
||||
_object(truth_island.manifest.get("identity"), "E46 identity").get(
|
||||
"source"
|
||||
),
|
||||
"E46 source",
|
||||
)
|
||||
try:
|
||||
valid_fov = read_valid_fov_mask(
|
||||
valid_fov_root,
|
||||
calibration_sha256=str(source["calibration_sha256"]),
|
||||
calibration_slot=str(source["calibration_slot"]),
|
||||
)
|
||||
prediction_rows = l34_rows_for_sealed_truth(l34_freeze.predictions)
|
||||
metrics = evaluate_frozen_detector_candidates(
|
||||
truth_rows=tuple(truth_seal["truth_rows"]),
|
||||
prediction_rows=prediction_rows,
|
||||
valid_fov_mask=valid_fov["mask"],
|
||||
)
|
||||
except (KeyError, E49DetectorTruthEvaluationError) as reason:
|
||||
raise L35RightYoloxTruthEvaluationError(
|
||||
"L3.4 predictions cannot be evaluated against sealed truth"
|
||||
) from reason
|
||||
|
||||
profile = {
|
||||
"profile_id": "l35-ravnoves00-right-yolox-evaluation/v1",
|
||||
"metric_engine": "e49-frozen-detector-metrics/v1",
|
||||
"candidate_selection_policy": "no-automatic-winner",
|
||||
"model_retraining_authorized": False,
|
||||
}
|
||||
identity = {
|
||||
"schema_version": L35_RESULT_SCHEMA,
|
||||
"truth_island": {
|
||||
"result_id": truth_island.result_id,
|
||||
"manifest_sha256": _sha256(
|
||||
truth_island.result_root / E46_MANIFEST_NAME
|
||||
),
|
||||
},
|
||||
"truth_seal": {
|
||||
"result_id": truth_seal["result_id"],
|
||||
"manifest_sha256": _sha256(
|
||||
truth_seal["result_root"] / E48_MANIFEST_NAME
|
||||
),
|
||||
},
|
||||
"l34_freeze": {
|
||||
"result_id": l34_freeze.result_id,
|
||||
"manifest_sha256": _sha256(
|
||||
l34_freeze.result_root / L34_MANIFEST_NAME
|
||||
),
|
||||
"prediction_rows_sha256": _object(
|
||||
l34_identity.get("candidate"),
|
||||
"L3.4 candidate identity",
|
||||
)["prediction_rows_sha256"],
|
||||
},
|
||||
"valid_fov": valid_fov["identity"],
|
||||
"profile": profile,
|
||||
"metrics_sha256": hashlib.sha256(_canonical_json(metrics)).hexdigest(),
|
||||
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
result_id = f"l35-right-yolox-truth-evaluation-{identity_sha256}"
|
||||
destination = output_root.expanduser().absolute() / result_id
|
||||
if destination.exists():
|
||||
return read_l35_right_yolox_truth_evaluation(destination)
|
||||
|
||||
report = {
|
||||
"schema_version": L35_REPORT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"status": "completed-sealed-truth-right-yolox-evaluation",
|
||||
"frame_count": len(truth_seal["truth_rows"]),
|
||||
"candidate_count": len(metrics),
|
||||
"profile": profile,
|
||||
"candidates": metrics,
|
||||
"decision": {
|
||||
"truth_join_performed": True,
|
||||
"accuracy_metrics_available": True,
|
||||
"candidate_winner_selected": False,
|
||||
"model_retraining_authorized": False,
|
||||
"next_gate": (
|
||||
"review the preregistered metrics and explicitly accept or "
|
||||
"reject the frozen right-camera YOLOX candidate"
|
||||
),
|
||||
},
|
||||
"limitations": [
|
||||
"source-scoped to the 32-frame RAVNOVES00 right-camera Truth Island",
|
||||
"recorded replay only; live transport and hardware are out of scope",
|
||||
"truth evaluation does not authorize navigation or safety claims",
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
_write_json(staging / L35_REPORT_NAME, report)
|
||||
manifest = {
|
||||
"schema_version": L35_RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": _utc_now(),
|
||||
"acceptance_state": "accepted-metrics-only-no-candidate-decision",
|
||||
"artifacts": [
|
||||
_artifact(staging / L35_REPORT_NAME, "evaluation-report")
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
_write_json(staging / L35_MANIFEST_NAME, manifest)
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
return read_l35_right_yolox_truth_evaluation(destination)
|
||||
|
||||
|
||||
def l34_rows_for_sealed_truth(
|
||||
rows: tuple[dict[str, Any], ...],
|
||||
) -> tuple[dict[str, Any], ...]:
|
||||
"""Adapt immutable L3.4 rows to the shared E49 metric engine contract."""
|
||||
|
||||
converted: list[dict[str, Any]] = []
|
||||
for row in rows:
|
||||
predictions = row.get("predictions")
|
||||
if (
|
||||
row.get("schema_version") != L34_PREDICTION_SCHEMA
|
||||
or row.get("truth_joined") is not False
|
||||
or not isinstance(predictions, list)
|
||||
):
|
||||
raise L35RightYoloxTruthEvaluationError(
|
||||
"L3.4 prediction row is invalid"
|
||||
)
|
||||
converted.append(
|
||||
{
|
||||
"candidate_id": row.get("candidate_id"),
|
||||
"truth_island_sequence": row.get("truth_island_sequence"),
|
||||
"image_id": row.get("image_id"),
|
||||
"frame_index": row.get("frame_index"),
|
||||
"session_seconds": row.get("session_seconds"),
|
||||
"source_image_sha256": row.get("source_image_sha256"),
|
||||
"predictions": [
|
||||
{
|
||||
"category": prediction.get("label"),
|
||||
"score": prediction.get("score"),
|
||||
"box_xyxy": prediction.get("bbox_xyxy"),
|
||||
}
|
||||
for prediction in predictions
|
||||
if isinstance(prediction, dict)
|
||||
],
|
||||
"truth_joined": False,
|
||||
}
|
||||
)
|
||||
if len(converted[-1]["predictions"]) != len(predictions):
|
||||
raise L35RightYoloxTruthEvaluationError(
|
||||
"L3.4 prediction entry is invalid"
|
||||
)
|
||||
return tuple(converted)
|
||||
|
||||
|
||||
def read_l35_right_yolox_truth_evaluation(root: Path) -> dict[str, Any]:
|
||||
"""Read and revalidate an immutable L3.5 evaluation result."""
|
||||
|
||||
resolved = root.resolve(strict=True)
|
||||
manifest = _read_json(resolved / L35_MANIFEST_NAME)
|
||||
identity = _object(manifest.get("identity"), "L3.5 identity")
|
||||
identity_sha256 = manifest.get("identity_sha256")
|
||||
if (
|
||||
manifest.get("schema_version") != L35_RESULT_SCHEMA
|
||||
or not isinstance(identity_sha256, str)
|
||||
or hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
!= identity_sha256
|
||||
or manifest.get("result_id")
|
||||
!= f"l35-right-yolox-truth-evaluation-{identity_sha256}"
|
||||
or not _RESULT_ID.fullmatch(resolved.name)
|
||||
or resolved.name != manifest.get("result_id")
|
||||
or manifest.get("acceptance_state")
|
||||
!= "accepted-metrics-only-no-candidate-decision"
|
||||
or manifest.get("authority") != _AUTHORITY
|
||||
):
|
||||
raise L35RightYoloxTruthEvaluationError("L3.5 identity is invalid")
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(artifacts, list) or len(artifacts) != 1:
|
||||
raise L35RightYoloxTruthEvaluationError("L3.5 artifacts are invalid")
|
||||
artifact = _object(artifacts[0], "L3.5 report artifact")
|
||||
report_path = resolved / str(artifact.get("path"))
|
||||
if (
|
||||
report_path.parent != resolved
|
||||
or not report_path.is_file()
|
||||
or report_path.is_symlink()
|
||||
or artifact.get("byte_length") != report_path.stat().st_size
|
||||
or artifact.get("sha256") != _sha256(report_path)
|
||||
):
|
||||
raise L35RightYoloxTruthEvaluationError("L3.5 report changed")
|
||||
report = _read_json(report_path)
|
||||
if (
|
||||
report.get("schema_version") != L35_REPORT_SCHEMA
|
||||
or report.get("result_id") != resolved.name
|
||||
or report.get("identity_sha256") != identity_sha256
|
||||
or report.get("status")
|
||||
!= "completed-sealed-truth-right-yolox-evaluation"
|
||||
or hashlib.sha256(_canonical_json(report.get("candidates"))).hexdigest()
|
||||
!= identity.get("metrics_sha256")
|
||||
):
|
||||
raise L35RightYoloxTruthEvaluationError("L3.5 report is invalid")
|
||||
return {
|
||||
"result_id": resolved.name,
|
||||
"result_root": resolved,
|
||||
"manifest": manifest,
|
||||
"report": report,
|
||||
}
|
||||
|
||||
|
||||
def _require_freeze_before_truth_seal(
|
||||
*,
|
||||
freeze_created_at_utc: object,
|
||||
truth_sealed_at_utc: object,
|
||||
) -> None:
|
||||
freeze = _utc_timestamp(freeze_created_at_utc, "L3.4 created_at_utc")
|
||||
seal = _utc_timestamp(truth_sealed_at_utc, "E48 sealed_at_utc")
|
||||
if freeze > seal:
|
||||
raise L35RightYoloxTruthEvaluationError(
|
||||
"L3.4 prediction freeze postdates the independent truth seal"
|
||||
)
|
||||
|
||||
|
||||
def _utc_timestamp(value: object, field: str) -> datetime:
|
||||
if not isinstance(value, str) or not value.endswith("Z"):
|
||||
raise L35RightYoloxTruthEvaluationError(f"{field} is invalid")
|
||||
try:
|
||||
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
except ValueError as reason:
|
||||
raise L35RightYoloxTruthEvaluationError(f"{field} is invalid") from reason
|
||||
if parsed.tzinfo is None:
|
||||
raise L35RightYoloxTruthEvaluationError(f"{field} is invalid")
|
||||
return parsed.astimezone(UTC)
|
||||
|
||||
|
||||
def _object(value: object, field: str) -> dict[str, Any]:
|
||||
if not isinstance(value, dict):
|
||||
raise L35RightYoloxTruthEvaluationError(f"{field} must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError) as reason:
|
||||
raise L35RightYoloxTruthEvaluationError(
|
||||
f"cannot read {path.name}"
|
||||
) from reason
|
||||
return _object(value, path.name)
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).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 _artifact(path: Path, role: str) -> dict[str, Any]:
|
||||
return {
|
||||
"path": path.name,
|
||||
"role": role,
|
||||
"byte_length": path.stat().st_size,
|
||||
"sha256": _sha256(path),
|
||||
}
|
||||
|
||||
|
||||
def _write_json(path: Path, value: object) -> None:
|
||||
path.write_text(
|
||||
json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
indent=2,
|
||||
allow_nan=False,
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def _utc_now() -> str:
|
||||
return datetime.now(UTC).isoformat(timespec="milliseconds").replace(
|
||||
"+00:00", "Z"
|
||||
)
|
||||
@@ -568,21 +568,25 @@ def _semantic_support(
|
||||
reason = "camera-semantic-without-qualified-occupied-lidar-support"
|
||||
if occupied_count:
|
||||
ranges = projected.depths_m[clustered_rows]
|
||||
range_m = float(np.median(ranges))
|
||||
range_estimate_m = float(np.median(ranges))
|
||||
centroid = np.median(occupied_points, axis=0).astype(np.float64).tolist()
|
||||
height_range = [
|
||||
float(np.min(point_height_m[occupied_indices])),
|
||||
float(np.max(point_height_m[occupied_indices])),
|
||||
]
|
||||
else:
|
||||
range_m = None
|
||||
range_estimate_m = None
|
||||
centroid = None
|
||||
height_range = None
|
||||
range_m = range_estimate_m if support_agrees else None
|
||||
base.update(
|
||||
{
|
||||
"geometry_status": status,
|
||||
"geometry_reason": reason,
|
||||
"range_m": range_m,
|
||||
"range_estimate_m": range_estimate_m,
|
||||
"range_estimate_available": range_estimate_m is not None,
|
||||
"range_support_qualified": support_agrees,
|
||||
"occupied_centroid_map_xyz_m": centroid,
|
||||
"occupied_height_range_m": height_range,
|
||||
"support": {
|
||||
@@ -893,6 +897,9 @@ def _empty_geometry(status: str, reason: str) -> dict[str, object]:
|
||||
"geometry_status": status,
|
||||
"geometry_reason": reason,
|
||||
"range_m": None,
|
||||
"range_estimate_m": None,
|
||||
"range_estimate_available": False,
|
||||
"range_support_qualified": False,
|
||||
"occupied_centroid_map_xyz_m": None,
|
||||
"occupied_height_range_m": None,
|
||||
"support": {
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from fastapi import APIRouter
|
||||
from fastapi.routing import APIRoute
|
||||
|
||||
import k1link.compute.e46a_ai_engineering_preannotation as compute
|
||||
from k1link.web.e46a_ai_engineering_preannotation_api import (
|
||||
build_e46a_ai_engineering_preannotation_router,
|
||||
)
|
||||
from k1link.web.l34_annotation_api import (
|
||||
L34AnnotationCreateRequest,
|
||||
L34AnnotationFrameRequest,
|
||||
L34AnnotationObjectRequest,
|
||||
L34AnnotationSaveRequest,
|
||||
)
|
||||
|
||||
|
||||
def _endpoint(router: APIRouter, path: str, method: str = "GET") -> object:
|
||||
for route in router.routes:
|
||||
if (
|
||||
isinstance(route, APIRoute)
|
||||
and route.path == path
|
||||
and route.methods is not None
|
||||
and method in route.methods
|
||||
):
|
||||
return route.endpoint
|
||||
raise AssertionError(f"{method} {path} route is missing")
|
||||
|
||||
|
||||
def _build_fixture(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> dict[str, object]:
|
||||
e46_id = f"e46-detector-truth-island-{'a' * 64}"
|
||||
e46_root = tmp_path / "e46" / e46_id
|
||||
e46_root.mkdir(parents=True)
|
||||
(e46_root / "manifest.json").write_text("{}\n", encoding="utf-8")
|
||||
references = []
|
||||
cases = []
|
||||
for sequence in range(1, 33):
|
||||
source_sha = f"{sequence:064x}"
|
||||
references.append(
|
||||
{
|
||||
"truth_island_sequence": sequence,
|
||||
"image_id": sequence,
|
||||
"frame_index": sequence * 100,
|
||||
"group_id": f"group-{sequence:02d}",
|
||||
"sha256": source_sha,
|
||||
}
|
||||
)
|
||||
objects = [
|
||||
{
|
||||
"object_id": f"object-{sequence:02d}-car",
|
||||
"category": "car",
|
||||
"proposed_label": None,
|
||||
"origin": "self_review_seed",
|
||||
"box_xyxy": [10.0, 20.0, 100.0, 120.0],
|
||||
"occluded": False,
|
||||
"truncated": False,
|
||||
}
|
||||
]
|
||||
if sequence in {5, 29}:
|
||||
objects.append(
|
||||
{
|
||||
"object_id": f"object-{sequence:02d}-custom",
|
||||
"category": "unmapped",
|
||||
"proposed_label": (
|
||||
"Детская коляска" if sequence == 5 else "Ноутбук"
|
||||
),
|
||||
"origin": "self_review_seed",
|
||||
"box_xyxy": [120.0, 140.0, 280.0, 320.0],
|
||||
"occluded": False,
|
||||
"truncated": sequence == 29,
|
||||
}
|
||||
)
|
||||
cases.append(
|
||||
{
|
||||
"truth_island_sequence": sequence,
|
||||
"image_id": sequence,
|
||||
"frame_index": sequence * 100,
|
||||
"group_id": f"group-{sequence:02d}",
|
||||
"session_seconds": float(sequence),
|
||||
"source_image_sha256": source_sha,
|
||||
"references": objects,
|
||||
}
|
||||
)
|
||||
(e46_root / "image-references.jsonl").write_text(
|
||||
"".join(json.dumps(row) + "\n" for row in references),
|
||||
encoding="utf-8",
|
||||
)
|
||||
l34f_id = f"l34f-adjudicated-reference-{'b' * 64}"
|
||||
l34f_root = tmp_path / "l34f" / l34f_id
|
||||
l34f_root.mkdir(parents=True)
|
||||
(l34f_root / "manifest.json").write_text("{}\n", encoding="utf-8")
|
||||
e46 = SimpleNamespace(
|
||||
result_id=e46_id,
|
||||
result_root=e46_root,
|
||||
manifest={},
|
||||
report={},
|
||||
)
|
||||
l34f = {
|
||||
"result_id": l34f_id,
|
||||
"result_root": l34f_root,
|
||||
"manifest": {},
|
||||
"report": {},
|
||||
"cases": tuple(cases),
|
||||
}
|
||||
monkeypatch.setattr(compute, "read_e46_detector_truth_island", lambda _: e46)
|
||||
monkeypatch.setattr(compute, "read_l34f_adjudicated_reference", lambda _: l34f)
|
||||
return compute.build_e46a_ai_engineering_preannotation(
|
||||
e46_root=e46_root,
|
||||
l34f_root=l34f_root,
|
||||
output_root=tmp_path / "results",
|
||||
)
|
||||
|
||||
|
||||
def test_e46a_freezes_ai_preannotation_without_truth_authority(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
result = _build_fixture(tmp_path, monkeypatch)
|
||||
|
||||
assert result["report"]["metrics"]["frame_count"] == 32
|
||||
assert result["report"]["metrics"]["object_count"] == 34
|
||||
assert result["report"]["metrics"]["custom_class_relabel_count"] == 2
|
||||
assert result["report"]["metrics"]["independent_review_submission_count"] == 0
|
||||
assert result["report"]["decision"]["e48_truth_seal_open"] is False
|
||||
assert result["manifest"]["authority"]["independent_truth"] is False
|
||||
custom = {
|
||||
item["category"]
|
||||
for case in result["cases"]
|
||||
for item in case["objects"]
|
||||
if item["category"] in {"stroller", "laptop"}
|
||||
}
|
||||
assert custom == {"stroller", "laptop"}
|
||||
assert all(
|
||||
item["category"] != "unmapped"
|
||||
for case in result["cases"]
|
||||
for item in case["objects"]
|
||||
)
|
||||
|
||||
|
||||
def test_e46a_object_level_audit_deletes_snaps_and_relabels(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
result = _build_fixture(tmp_path, monkeypatch)
|
||||
cases = copy.deepcopy(list(result["cases"]))
|
||||
rows = tuple(
|
||||
{
|
||||
"candidate_id": "geometry-candidate",
|
||||
"truth_island_sequence": sequence,
|
||||
"source_image_sha256": cases[sequence - 1][
|
||||
"source_image_sha256"
|
||||
],
|
||||
"predictions": [
|
||||
{
|
||||
"category": "car",
|
||||
"score": 0.95,
|
||||
"box_xyxy": [12.0, 22.0, 98.0, 118.0],
|
||||
}
|
||||
],
|
||||
}
|
||||
for sequence in range(1, 33)
|
||||
)
|
||||
profile = compute.E46AVisualAuditProfile(
|
||||
profile_id="fixture-object-qa/v1",
|
||||
geometry_candidate_id="geometry-candidate",
|
||||
candidate_nms_iou=0.3,
|
||||
maximum_match_cost=1.3,
|
||||
expected_source_object_count=34,
|
||||
expected_final_object_count=33,
|
||||
expected_geometry_snapped_count=31,
|
||||
delete_object_ids=("object-01-car",),
|
||||
category_overrides=(("object-02-car", "heavy_vehicle"),),
|
||||
)
|
||||
|
||||
audit = compute._apply_visual_audit( # noqa: SLF001
|
||||
cases=cases,
|
||||
prediction_rows=rows,
|
||||
profile=profile,
|
||||
)
|
||||
|
||||
assert audit["deleted_false_box_count"] == 1
|
||||
assert audit["geometry_snapped_object_count"] == 31
|
||||
assert audit["source_geometry_retained_object_count"] == 2
|
||||
assert cases[0]["objects"] == []
|
||||
assert cases[0]["hard_negative"] is True
|
||||
assert cases[1]["objects"][0]["category"] == "heavy_vehicle"
|
||||
assert cases[1]["objects"][0]["box_xyxy"] == [12.0, 22.0, 98.0, 118.0]
|
||||
|
||||
|
||||
def test_e46a_api_projects_visual_cases_without_truth_escalation(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
result = _build_fixture(tmp_path, monkeypatch)
|
||||
router = build_e46a_ai_engineering_preannotation_router(
|
||||
root_provider=lambda: tmp_path / "results"
|
||||
)
|
||||
catalog = _endpoint(router, "/api/v1/laboratory/e46a/results")(limit=1) # type: ignore[operator]
|
||||
assert catalog["items"][0]["result_id"] == result["result_id"]
|
||||
assert catalog["items"][0]["ground_truth"] is False
|
||||
|
||||
case = _endpoint(
|
||||
router,
|
||||
"/api/v1/laboratory/e46a/results/{result_id}/cases/{sequence}",
|
||||
)(result_id=result["result_id"], sequence=5) # type: ignore[operator]
|
||||
assert case["independent_review"] is False
|
||||
assert case["ground_truth"] is False
|
||||
assert any(item["category"] == "stroller" for item in case["objects"])
|
||||
assert case["camera_url"].endswith("/annotation-source/frames/5/camera")
|
||||
|
||||
|
||||
def test_e46a_correction_session_starts_from_editable_ai_seed(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
result = _build_fixture(tmp_path, monkeypatch)
|
||||
router = build_e46a_ai_engineering_preannotation_router(
|
||||
root_provider=lambda: tmp_path / "results",
|
||||
annotation_root_provider=lambda: tmp_path / "annotations",
|
||||
)
|
||||
result_id = str(result["result_id"])
|
||||
|
||||
source = _endpoint(
|
||||
router,
|
||||
"/api/v1/laboratory/e46a/results/{result_id}/annotation-source",
|
||||
)(result_id=result_id) # type: ignore[operator]
|
||||
assert source["frame_count"] == 32
|
||||
assert source["contract"]["contract_id"] == (
|
||||
"e46a-ai-engineering-correction/v1"
|
||||
)
|
||||
assert source["candidate_identity_included"] is True
|
||||
assert source["candidate_predictions_included"] is False
|
||||
assert source["prelabels_included"] is False
|
||||
|
||||
seed = _endpoint(
|
||||
router,
|
||||
(
|
||||
"/api/v1/laboratory/e46a/results/{result_id}"
|
||||
"/annotation-seed/frames/{sequence}"
|
||||
),
|
||||
)(result_id=result_id, sequence=5) # type: ignore[operator]
|
||||
custom = next(
|
||||
item for item in seed["objects"] if item["category"] == "unmapped"
|
||||
)
|
||||
assert custom["proposed_label"] == "Коляска"
|
||||
assert custom["origin"] == "frozen_candidate_seed"
|
||||
|
||||
create = _endpoint(
|
||||
router,
|
||||
"/api/v1/laboratory/e46a/results/{result_id}/annotation-sessions",
|
||||
"POST",
|
||||
)
|
||||
created = create( # type: ignore[operator]
|
||||
result_id=result_id,
|
||||
request=L34AnnotationCreateRequest(idempotency_key="e46a-correction-1"),
|
||||
)
|
||||
assert created["contract_id"] == "e46a-ai-engineering-correction/v1"
|
||||
assert created["revision"] == 0
|
||||
assert created["frames"] == []
|
||||
|
||||
save = _endpoint(
|
||||
router,
|
||||
(
|
||||
"/api/v1/laboratory/e46a/results/{result_id}"
|
||||
"/annotation-sessions/{session_id}"
|
||||
),
|
||||
"PUT",
|
||||
)
|
||||
saved = save( # type: ignore[operator]
|
||||
result_id=result_id,
|
||||
session_id=created["session_id"],
|
||||
request=L34AnnotationSaveRequest(
|
||||
expected_revision=0,
|
||||
idempotency_key="e46a-save-1",
|
||||
title="E46A human correction",
|
||||
assistance_mode="frozen-candidate-seeded",
|
||||
frames=[
|
||||
L34AnnotationFrameRequest(
|
||||
truth_island_sequence=5,
|
||||
reviewed=True,
|
||||
hard_negative=False,
|
||||
objects=[
|
||||
L34AnnotationObjectRequest(
|
||||
object_id=str(custom["object_id"]),
|
||||
category="unmapped",
|
||||
proposed_label="Коляска",
|
||||
origin="frozen_candidate_seed",
|
||||
box_xyxy=[130.0, 150.0, 300.0, 340.0],
|
||||
occluded=False,
|
||||
truncated=False,
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
assert saved["revision"] == 1
|
||||
assert saved["frames"][0]["objects"][0]["box_xyxy"] == [
|
||||
130.0,
|
||||
150.0,
|
||||
300.0,
|
||||
340.0,
|
||||
]
|
||||
assert saved["authority"]["ground_truth"] is False
|
||||
|
||||
|
||||
def test_e46a_reader_rejects_tampered_cases(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
result = _build_fixture(tmp_path, monkeypatch)
|
||||
cases_path = Path(result["result_root"]) / compute.E46A_CASES_NAME
|
||||
cases_path.write_text(cases_path.read_text(encoding="utf-8") + "{}\n", encoding="utf-8")
|
||||
|
||||
with pytest.raises(compute.E46AAiEngineeringPreannotationError):
|
||||
compute.read_e46a_ai_engineering_preannotation(Path(result["result_root"]))
|
||||
@@ -0,0 +1,83 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from k1link.compute.e46d_temporal_failure_audit import analyze_temporal_frames
|
||||
|
||||
|
||||
def _object(
|
||||
track_id: int,
|
||||
*,
|
||||
box: list[float] | None = None,
|
||||
camera_current: bool = True,
|
||||
world_track_id: int | None = 240001,
|
||||
motion_state: str = "static",
|
||||
) -> dict[str, object]:
|
||||
return {
|
||||
"bbox_xyxy": box or [100.0, 100.0, 180.0, 220.0],
|
||||
"category": "car",
|
||||
"route_track_id": track_id,
|
||||
"world_track_id": world_track_id,
|
||||
"motion_state": motion_state,
|
||||
"camera_evidence_current": camera_current,
|
||||
}
|
||||
|
||||
|
||||
def _frames() -> list[dict[str, object]]:
|
||||
rows: list[dict[str, object]] = []
|
||||
for frame_index in range(30):
|
||||
rows.append(
|
||||
{
|
||||
"frame_index": frame_index,
|
||||
"session_seconds": 35.0 + frame_index * 0.1,
|
||||
"objects": [],
|
||||
}
|
||||
)
|
||||
for frame_index in (0, 1, 2, 6, 7, 8):
|
||||
rows[frame_index]["objects"] = [_object(10)]
|
||||
rows[3]["objects"] = []
|
||||
rows[4]["objects"] = []
|
||||
rows[5]["objects"] = []
|
||||
rows[10]["objects"] = [_object(20)]
|
||||
rows[11]["objects"] = [_object(21)]
|
||||
rows[12]["objects"] = [_object(30, box=[100.0, 100.0, 180.0, 220.0])]
|
||||
rows[13]["objects"] = [_object(30, box=[300.0, 100.0, 380.0, 220.0])]
|
||||
for offset, state in enumerate(("static", "unknown", "static", "unknown", "static")):
|
||||
rows[15 + offset]["objects"] = [
|
||||
_object(40, motion_state=state, world_track_id=240001 + offset % 2)
|
||||
]
|
||||
for frame_index in range(20, 25):
|
||||
rows[frame_index]["objects"] = [_object(50, camera_current=frame_index in {20, 24})]
|
||||
for offset, track_id in enumerate((60, 61, 62, 63)):
|
||||
rows[26 + offset]["objects"] = [_object(track_id)]
|
||||
return rows
|
||||
|
||||
|
||||
def test_e46d_scans_observable_temporal_failures_and_ranks_video_clips() -> None:
|
||||
signals, clips, metrics = analyze_temporal_frames(_frames())
|
||||
kinds = {signal["kind"] for signal in signals}
|
||||
|
||||
assert "layer-blackout" in kinds
|
||||
assert "route-layer-gap" in kinds
|
||||
assert "route-id-rebirth-candidate" in kinds
|
||||
assert "bbox-jump" in kinds
|
||||
assert "motion-state-flap" in kinds
|
||||
assert "world-binding-flap" in kinds
|
||||
assert "short-track-burst" in kinds
|
||||
assert metrics["temporal_continuity_passed"] is False
|
||||
assert metrics["failure_signal_count"] == len(signals)
|
||||
assert metrics["review_clip_count"] == len(clips)
|
||||
assert clips[0]["priority"] == "critical"
|
||||
assert all(clip["start_seconds"] <= clip["event_start_seconds"] for clip in clips)
|
||||
assert all(clip["event_end_seconds"] <= clip["end_seconds"] for clip in clips)
|
||||
|
||||
|
||||
def test_e46d_distinguishes_camera_hold_from_route_layer_loss() -> None:
|
||||
frames = _frames()
|
||||
signals, _, metrics = analyze_temporal_frames(frames)
|
||||
|
||||
held = [signal for signal in signals if signal["kind"] == "camera-evidence-hold"]
|
||||
gaps = [signal for signal in signals if signal["kind"] == "route-layer-gap"]
|
||||
assert held
|
||||
assert gaps
|
||||
assert all(signal["evidence"]["camera_evidence_current"] is False for signal in held)
|
||||
assert all(signal["evidence"]["same_route_identity_returned"] is True for signal in gaps)
|
||||
assert metrics["detector_hold_episode_count"] == len(held)
|
||||
@@ -0,0 +1,279 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.compute.e46e_ready_stack import (
|
||||
E46E_RUNTIME_SCHEMA,
|
||||
E46EReadyStackError,
|
||||
analyze_e46e_frames,
|
||||
build_e46e_ready_stack,
|
||||
read_e46e_ready_stack,
|
||||
)
|
||||
|
||||
|
||||
def test_e46e_projects_stock_deepstream_output_without_custom_tracking(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
source, profile = _source_and_profile(tmp_path, frame_count=6)
|
||||
raw = _raw_output(tmp_path, profile, frame_count=6)
|
||||
_detector(raw, 0, "car", (10, 20, 40, 60), 0.91)
|
||||
_tracker(raw, 0, 7, "car", (10, 20, 40, 60), 0.88)
|
||||
_tracker(raw, 1, 7, "car", (12, 20, 42, 60), 0.73)
|
||||
_detector(raw, 5, "person", (100, 80, 130, 160), 0.93)
|
||||
_tracker(raw, 5, 7, "person", (100, 80, 130, 160), 0.79)
|
||||
|
||||
result = build_e46e_ready_stack(
|
||||
source_job_root=source,
|
||||
raw_root=raw,
|
||||
profile_path=profile,
|
||||
output_root=tmp_path / "results",
|
||||
)
|
||||
metrics = result["report"]["metrics"]
|
||||
assert metrics["frame_count"] == 6
|
||||
assert metrics["detection_observation_count"] == 2
|
||||
assert metrics["track_observation_count"] == 3
|
||||
assert metrics["detection_box_clipped_count"] == 0
|
||||
assert metrics["track_box_clipped_count"] == 0
|
||||
assert metrics["tracker_recovered_frame_count"] == 1
|
||||
assert metrics["full_layer_blackout_event_count"] == 1
|
||||
assert metrics["route_id_gap_event_count"] == 1
|
||||
assert metrics["track_class_switch_count"] == 1
|
||||
assert result["frames"][1]["objects"][0]["object_id"] == "nvdcf-7"
|
||||
assert result["frames"][1]["objects"][0]["bbox"] == [12.0, 20.0, 30.0, 40.0]
|
||||
assert [
|
||||
(component["kind"], component["role"])
|
||||
for component in result["report"]["method"]["components"]
|
||||
] == [
|
||||
("source", "exact recorded camera evidence"),
|
||||
("model", "framewise traffic-object detection"),
|
||||
("tool", "official RT-DETR output decoding"),
|
||||
("algorithm", "route-local temporal association"),
|
||||
("runtime", "GPU inference and media pipeline"),
|
||||
]
|
||||
assert result["report"]["decision"]["custom_temporal_logic_used"] is False
|
||||
assert result["manifest"]["authority"]["navigation_or_safety_accepted"] is False
|
||||
|
||||
repeated = build_e46e_ready_stack(
|
||||
source_job_root=source,
|
||||
raw_root=raw,
|
||||
profile_path=profile,
|
||||
output_root=tmp_path / "results",
|
||||
)
|
||||
assert repeated["result_id"] == result["result_id"]
|
||||
|
||||
|
||||
def test_e46e_clips_only_display_geometry_at_the_source_plane(tmp_path: Path) -> None:
|
||||
source, profile = _source_and_profile(tmp_path, frame_count=1)
|
||||
raw = _raw_output(tmp_path, profile, frame_count=1)
|
||||
_tracker(raw, 0, 31, "person", (447, 547, 527, 608), 0.69)
|
||||
|
||||
result = build_e46e_ready_stack(
|
||||
source_job_root=source,
|
||||
raw_root=raw,
|
||||
profile_path=profile,
|
||||
output_root=tmp_path / "results",
|
||||
)
|
||||
|
||||
item = result["frames"][0]["objects"][0]
|
||||
assert item["bbox"] == [447.0, 547.0, 80.0, 53.0]
|
||||
assert item["source_bbox_ltrb"] == [447.0, 547.0, 527.0, 608.0]
|
||||
assert item["source_plane_clipped"] is True
|
||||
assert result["report"]["metrics"]["track_box_clipped_count"] == 1
|
||||
assert result["report"]["decision"]["custom_temporal_logic_used"] is False
|
||||
|
||||
|
||||
def test_e46e_rejects_tampered_immutable_artifact(tmp_path: Path) -> None:
|
||||
source, profile = _source_and_profile(tmp_path, frame_count=1)
|
||||
raw = _raw_output(tmp_path, profile, frame_count=1)
|
||||
result = build_e46e_ready_stack(
|
||||
source_job_root=source,
|
||||
raw_root=raw,
|
||||
profile_path=profile,
|
||||
output_root=tmp_path / "results",
|
||||
)
|
||||
(result["result_root"] / "overlay.mp4").write_bytes(b"changed")
|
||||
with pytest.raises(E46EReadyStackError, match="artifact changed"):
|
||||
read_e46e_ready_stack(result["result_root"])
|
||||
|
||||
|
||||
def test_e46e_frame_analyzer_requires_contiguous_source_order() -> None:
|
||||
with pytest.raises(E46EReadyStackError, match="not contiguous"):
|
||||
analyze_e46e_frames(
|
||||
[
|
||||
{
|
||||
"frame_index": 1,
|
||||
"session_seconds": 1.0,
|
||||
"detections": [],
|
||||
"objects": [],
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _source_and_profile(tmp_path: Path, *, frame_count: int) -> tuple[Path, Path]:
|
||||
source = tmp_path / "source-job"
|
||||
camera = source / "input" / "camera" / "sensor.camera.right" / "epoch-1"
|
||||
camera.mkdir(parents=True)
|
||||
index_rows = [
|
||||
{
|
||||
"schema_version": "missioncore.camera-recording-index/v1",
|
||||
"kind": "media",
|
||||
"sequence": sequence,
|
||||
"session_monotonic_ns": 1_000_000_000 + (sequence - 1) * 100_000_000,
|
||||
"sha256": hashlib.sha256(f"frame-{sequence}".encode()).hexdigest(),
|
||||
}
|
||||
for sequence in range(1, frame_count + 1)
|
||||
]
|
||||
index_path = camera / "index.jsonl"
|
||||
index_path.write_text(
|
||||
"".join(json.dumps(row, sort_keys=True) + "\n" for row in index_rows),
|
||||
encoding="utf-8",
|
||||
)
|
||||
stream_sha = "1" * 64
|
||||
summary_path = camera / "summary.json"
|
||||
summary_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": "missioncore.camera-recording/v1",
|
||||
"stream_sha256": stream_sha,
|
||||
"segment_count": frame_count,
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
index_sha = _sha(index_path)
|
||||
summary_sha = _sha(summary_path)
|
||||
job = {
|
||||
"schema_version": "missioncore.compute-job/v1",
|
||||
"job_id": "recorded-test",
|
||||
"input": {
|
||||
"session_id": "test-session",
|
||||
"source_id": "sensor.camera.right",
|
||||
"segment_count": frame_count,
|
||||
"archive_index_sha256": index_sha,
|
||||
"archive_summary_sha256": summary_sha,
|
||||
"timeline": {"start_seconds": 10.0, "end_seconds": 11.0},
|
||||
},
|
||||
}
|
||||
(source / "job.json").write_text(json.dumps(job, indent=2) + "\n", encoding="utf-8")
|
||||
profile_value = {
|
||||
"schema_version": "missioncore.e46e-ready-stack-profile/v1",
|
||||
"profile_id": "e46e-test/v1",
|
||||
"source": {
|
||||
"camera_source_id": "sensor.camera.right",
|
||||
"job_id": "recorded-test",
|
||||
"session_id": "test-session",
|
||||
"segment_count": frame_count,
|
||||
"stream_sha256": stream_sha,
|
||||
"archive_index_sha256": index_sha,
|
||||
"archive_summary_sha256": summary_sha,
|
||||
},
|
||||
"runtime": {
|
||||
"container_image": "nvcr.io/nvidia/deepstream:9.1-samples-multiarch@sha256:"
|
||||
+ "2" * 64,
|
||||
"deepstream_version": "9.1",
|
||||
},
|
||||
"detector": {
|
||||
"name": "NVIDIA TrafficCamNet Transformer Lite",
|
||||
"version": "test",
|
||||
"model_sha256": "3" * 64,
|
||||
"custom_postprocessing": False,
|
||||
},
|
||||
"parser": {
|
||||
"name": "NVIDIA DeepStream TAO custom bounding-box parser",
|
||||
"repository": "https://github.com/NVIDIA/DeepStream.git",
|
||||
"commit": "8" * 40,
|
||||
"symbol": "NvDsInferParseCustomDDETRTAO",
|
||||
"library_sha256": "9" * 64,
|
||||
"custom_mission_core_logic": False,
|
||||
},
|
||||
"tracker": {
|
||||
"name": "NVIDIA NvDCF",
|
||||
"configuration": "stock-accuracy",
|
||||
"custom_association": False,
|
||||
"custom_hold_or_stitch": False,
|
||||
},
|
||||
"output": {"frame_width": 800, "frame_height": 600},
|
||||
"authority": {
|
||||
"ground_truth": False,
|
||||
"candidate_accepted": False,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
}
|
||||
profile = tmp_path / "profile.json"
|
||||
profile.write_text(json.dumps(profile_value, indent=2) + "\n", encoding="utf-8")
|
||||
return source, profile
|
||||
|
||||
|
||||
def _raw_output(tmp_path: Path, profile_path: Path, *, frame_count: int) -> Path:
|
||||
raw = tmp_path / "raw"
|
||||
detections = raw / "detections"
|
||||
tracks = raw / "tracks"
|
||||
detections.mkdir(parents=True)
|
||||
tracks.mkdir()
|
||||
for frame in range(frame_count):
|
||||
(detections / f"00_000_{frame:06d}.txt").write_text("", encoding="utf-8")
|
||||
(tracks / f"00_000_{frame:06d}.txt").write_text("", encoding="utf-8")
|
||||
overlay = raw / "overlay.mp4"
|
||||
overlay.write_bytes(b"synthetic-overlay")
|
||||
(raw / "deepstream.log").write_text("synthetic success\n", encoding="utf-8")
|
||||
profile = json.loads(profile_path.read_text(encoding="utf-8"))
|
||||
image = profile["runtime"]["container_image"]
|
||||
runtime = {
|
||||
"schema_version": E46E_RUNTIME_SCHEMA,
|
||||
"status": "completed",
|
||||
"worker_host": "TEST-WORKER-006",
|
||||
"gpu_name": "Synthetic RTX",
|
||||
"container_image": image,
|
||||
"container_image_digest": image.rsplit("@sha256:", 1)[1],
|
||||
"model_sha256": profile["detector"]["model_sha256"],
|
||||
"model_engine_sha256": "4" * 64,
|
||||
"deepstream_config_sha256": "5" * 64,
|
||||
"detector_config_sha256": "6" * 64,
|
||||
"parser_library_sha256": profile["parser"]["library_sha256"],
|
||||
"tracker_config_sha256": "7" * 64,
|
||||
"input_stream_sha256": profile["source"]["stream_sha256"],
|
||||
"overlay_sha256": _sha(overlay),
|
||||
}
|
||||
(raw / "runtime.json").write_text(json.dumps(runtime, indent=2) + "\n", encoding="utf-8")
|
||||
return raw
|
||||
|
||||
|
||||
def _detector(
|
||||
raw: Path,
|
||||
frame: int,
|
||||
label: str,
|
||||
box: tuple[int, int, int, int],
|
||||
confidence: float,
|
||||
) -> None:
|
||||
left, top, right, bottom = box
|
||||
(raw / "detections" / f"00_000_{frame:06d}.txt").write_text(
|
||||
f"{label} 0.0 0 0.0 {left} {top} {right} {bottom} 0 0 0 0 0 0 0 {confidence}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def _tracker(
|
||||
raw: Path,
|
||||
frame: int,
|
||||
track_id: int,
|
||||
label: str,
|
||||
box: tuple[int, int, int, int],
|
||||
confidence: float,
|
||||
) -> None:
|
||||
left, top, right, bottom = box
|
||||
(raw / "tracks" / f"00_000_{frame:06d}.txt").write_text(
|
||||
f"{label} {track_id} 0.0 0 0.0 {left} {top} {right} {bottom} 0 0 0 0 0 0 0 {confidence}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def _sha(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
@@ -0,0 +1,84 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _module() -> object:
|
||||
path = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "prepare_e46e_worker_package.py"
|
||||
)
|
||||
spec = importlib.util.spec_from_file_location("e46e_worker_package_test", path)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_e46e_package_is_minimal_deterministic_and_content_addressed(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
module = _module()
|
||||
repository = Path(__file__).resolve().parents[1]
|
||||
profile, parser_library = _package_inputs(tmp_path, repository)
|
||||
kwargs = {
|
||||
"repository_root": repository,
|
||||
"profile_path": profile,
|
||||
"parser_library_path": parser_library,
|
||||
"output_root": tmp_path,
|
||||
}
|
||||
package = module.build_e46e_worker_package(**kwargs)
|
||||
repeated = module.build_e46e_worker_package(**kwargs)
|
||||
manifest = module.validate_e46e_worker_package(package)
|
||||
assert repeated == package
|
||||
assert package.name == f"e46e-worker-package-{manifest['identity_sha256']}"
|
||||
assert manifest["identity"]["classification"] == (
|
||||
"minimal-stock-nvidia-recorded-right-worker-package"
|
||||
)
|
||||
assert manifest["identity"]["tracker"]["custom_association"] is False
|
||||
assert manifest["identity"]["tracker"]["custom_hold_or_stitch"] is False
|
||||
assert manifest["identity"]["parser"]["custom_mission_core_logic"] is False
|
||||
assert len(manifest["artifacts"]) == 13
|
||||
|
||||
|
||||
def test_e46e_package_rejects_unexpected_member(tmp_path: Path) -> None:
|
||||
module = _module()
|
||||
repository = Path(__file__).resolve().parents[1]
|
||||
profile, parser_library = _package_inputs(tmp_path, repository)
|
||||
package = module.build_e46e_worker_package(
|
||||
repository_root=repository,
|
||||
profile_path=profile,
|
||||
parser_library_path=parser_library,
|
||||
output_root=tmp_path,
|
||||
)
|
||||
(package / "unexpected.txt").write_text("not admitted\n", encoding="utf-8")
|
||||
with pytest.raises(module.E46EWorkerPackageError, match="file set changed"):
|
||||
module.validate_e46e_worker_package(package)
|
||||
|
||||
|
||||
def _package_inputs(tmp_path: Path, repository: Path) -> tuple[Path, Path]:
|
||||
parser_library = tmp_path / "libnvds_infercustomparser_tao.so"
|
||||
parser_library.write_bytes(b"synthetic official parser fixture")
|
||||
profile_value = json.loads(
|
||||
(
|
||||
repository
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "e46e_ready_stack_profile.json"
|
||||
).read_text(encoding="utf-8")
|
||||
)
|
||||
profile_value["parser"]["library_sha256"] = hashlib.sha256(
|
||||
parser_library.read_bytes()
|
||||
).hexdigest()
|
||||
profile = tmp_path / "profile.json"
|
||||
profile.write_text(json.dumps(profile_value, indent=2) + "\n", encoding="utf-8")
|
||||
return profile, parser_library
|
||||
@@ -0,0 +1,227 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.compute.e46f_dashcam_bakeoff import (
|
||||
E46F_RUNTIME_SCHEMA,
|
||||
E46FDashCamBakeoffError,
|
||||
build_e46f_dashcam_bakeoff,
|
||||
read_e46f_dashcam_bakeoff,
|
||||
)
|
||||
|
||||
|
||||
def test_e46f_freezes_only_the_stock_detector_change(tmp_path: Path) -> None:
|
||||
source, profile = _source_and_profile(tmp_path, frame_count=3)
|
||||
raw = _raw_output(tmp_path, profile, frame_count=3)
|
||||
_observation(raw, "detections", 0, "car", (10, 20, 40, 60), 0.91)
|
||||
_observation(raw, "tracks", 0, "car", (10, 20, 40, 60), 0.88, track_id=7)
|
||||
_observation(raw, "tracks", 1, "car", (12, 20, 42, 60), 0.73, track_id=7)
|
||||
_observation(raw, "detections", 2, "person", (100, 80, 130, 160), 0.93)
|
||||
_observation(raw, "tracks", 2, "person", (100, 80, 130, 160), 0.79, track_id=8)
|
||||
|
||||
result = build_e46f_dashcam_bakeoff(
|
||||
source_job_root=source,
|
||||
raw_root=raw,
|
||||
profile_path=profile,
|
||||
output_root=tmp_path / "results",
|
||||
)
|
||||
|
||||
metrics = result["report"]["metrics"]
|
||||
assert metrics["frame_count"] == 3
|
||||
assert metrics["detection_observation_count"] == 2
|
||||
assert metrics["track_observation_count"] == 3
|
||||
assert metrics["tracker_recovered_frame_count"] == 1
|
||||
assert result["frames"][0]["detections"][0]["provenance"] == ("nvidia-dashcamnet-detectnet-v2")
|
||||
assert result["report"]["comparison_contract"] == {
|
||||
"baseline_result_id": f"e46e-ready-stack-{'a' * 64}",
|
||||
"controlled_change": "detector-only",
|
||||
"held_constant": ["recorded RIGHT source", "DeepStream", "FP16", "NvDCF"],
|
||||
}
|
||||
assert result["report"]["decision"]["custom_temporal_logic_used"] is False
|
||||
assert result["manifest"]["authority"]["navigation_or_safety_accepted"] is False
|
||||
|
||||
repeated = build_e46f_dashcam_bakeoff(
|
||||
source_job_root=source,
|
||||
raw_root=raw,
|
||||
profile_path=profile,
|
||||
output_root=tmp_path / "results",
|
||||
)
|
||||
assert repeated["result_id"] == result["result_id"]
|
||||
|
||||
|
||||
def test_e46f_rejects_tampered_immutable_video(tmp_path: Path) -> None:
|
||||
source, profile = _source_and_profile(tmp_path, frame_count=1)
|
||||
raw = _raw_output(tmp_path, profile, frame_count=1)
|
||||
result = build_e46f_dashcam_bakeoff(
|
||||
source_job_root=source,
|
||||
raw_root=raw,
|
||||
profile_path=profile,
|
||||
output_root=tmp_path / "results",
|
||||
)
|
||||
(result["result_root"] / "overlay.mp4").write_bytes(b"changed")
|
||||
with pytest.raises(E46FDashCamBakeoffError, match="artifact changed"):
|
||||
read_e46f_dashcam_bakeoff(result["result_root"])
|
||||
|
||||
|
||||
def _source_and_profile(tmp_path: Path, *, frame_count: int) -> tuple[Path, Path]:
|
||||
source = tmp_path / "source-job"
|
||||
camera = source / "input" / "camera" / "sensor.camera.right" / "epoch-1"
|
||||
camera.mkdir(parents=True)
|
||||
rows = [
|
||||
{
|
||||
"schema_version": "missioncore.camera-recording-index/v1",
|
||||
"kind": "media",
|
||||
"sequence": sequence,
|
||||
"session_monotonic_ns": 1_000_000_000 + (sequence - 1) * 100_000_000,
|
||||
"sha256": hashlib.sha256(f"frame-{sequence}".encode()).hexdigest(),
|
||||
}
|
||||
for sequence in range(1, frame_count + 1)
|
||||
]
|
||||
index_path = camera / "index.jsonl"
|
||||
index_path.write_text(
|
||||
"".join(json.dumps(row, sort_keys=True) + "\n" for row in rows),
|
||||
encoding="utf-8",
|
||||
)
|
||||
stream_sha = "1" * 64
|
||||
summary_path = camera / "summary.json"
|
||||
summary_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": "missioncore.camera-recording/v1",
|
||||
"stream_sha256": stream_sha,
|
||||
"segment_count": frame_count,
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
index_sha = _sha(index_path)
|
||||
summary_sha = _sha(summary_path)
|
||||
(source / "job.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": "missioncore.compute-job/v1",
|
||||
"job_id": "recorded-test",
|
||||
"input": {
|
||||
"session_id": "test-session",
|
||||
"source_id": "sensor.camera.right",
|
||||
"segment_count": frame_count,
|
||||
"archive_index_sha256": index_sha,
|
||||
"archive_summary_sha256": summary_sha,
|
||||
"timeline": {"start_seconds": 10.0, "end_seconds": 11.0},
|
||||
},
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
profile_value = {
|
||||
"schema_version": "missioncore.e46f-dashcam-bakeoff-profile/v1",
|
||||
"profile_id": "e46f-test/v1",
|
||||
"comparison_contract": {
|
||||
"baseline_result_id": f"e46e-ready-stack-{'a' * 64}",
|
||||
"controlled_change": "detector-only",
|
||||
"held_constant": ["recorded RIGHT source", "DeepStream", "FP16", "NvDCF"],
|
||||
},
|
||||
"source": {
|
||||
"camera_source_id": "sensor.camera.right",
|
||||
"job_id": "recorded-test",
|
||||
"session_id": "test-session",
|
||||
"segment_count": frame_count,
|
||||
"stream_sha256": stream_sha,
|
||||
"archive_index_sha256": index_sha,
|
||||
"archive_summary_sha256": summary_sha,
|
||||
},
|
||||
"runtime": {
|
||||
"container_image": "nvcr.io/nvidia/deepstream:9.1-samples-multiarch@sha256:" + "2" * 64,
|
||||
"deepstream_version": "9.1",
|
||||
},
|
||||
"detector": {
|
||||
"name": "NVIDIA DashCamNet",
|
||||
"version": "pruned_onnx_v1.0.4",
|
||||
"model_sha256": "3" * 64,
|
||||
"custom_postprocessing": False,
|
||||
},
|
||||
"postprocessor": {
|
||||
"name": "NVIDIA DeepStream built-in DetectNet_v2 parser and NMS",
|
||||
"cluster_mode": "NMS",
|
||||
"reference_commit": "4" * 40,
|
||||
"reference_config_sha256": "5" * 64,
|
||||
"custom_mission_core_logic": False,
|
||||
},
|
||||
"tracker": {
|
||||
"name": "NVIDIA NvDCF",
|
||||
"configuration": "stock-performance",
|
||||
"custom_association": False,
|
||||
"custom_hold_or_stitch": False,
|
||||
},
|
||||
"output": {"frame_width": 800, "frame_height": 600},
|
||||
"authority": {
|
||||
"ground_truth": False,
|
||||
"candidate_accepted": False,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
}
|
||||
profile = tmp_path / "profile.json"
|
||||
profile.write_text(json.dumps(profile_value, indent=2) + "\n", encoding="utf-8")
|
||||
return source, profile
|
||||
|
||||
|
||||
def _raw_output(tmp_path: Path, profile_path: Path, *, frame_count: int) -> Path:
|
||||
raw = tmp_path / "raw"
|
||||
for name in ("detections", "tracks"):
|
||||
directory = raw / name
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
for frame in range(frame_count):
|
||||
(directory / f"00_000_{frame:06d}.txt").write_text("", encoding="utf-8")
|
||||
overlay = raw / "overlay.mp4"
|
||||
overlay.write_bytes(b"synthetic-overlay")
|
||||
(raw / "deepstream.log").write_text("synthetic success\n", encoding="utf-8")
|
||||
profile = json.loads(profile_path.read_text(encoding="utf-8"))
|
||||
image = profile["runtime"]["container_image"]
|
||||
runtime = {
|
||||
"schema_version": E46F_RUNTIME_SCHEMA,
|
||||
"status": "completed",
|
||||
"worker_host": "TEST-WORKER-006",
|
||||
"gpu_name": "Synthetic RTX",
|
||||
"container_image": image,
|
||||
"container_image_digest": image.rsplit("@sha256:", 1)[1],
|
||||
"model_sha256": profile["detector"]["model_sha256"],
|
||||
"model_engine_sha256": "6" * 64,
|
||||
"deepstream_config_sha256": "7" * 64,
|
||||
"detector_config_sha256": "8" * 64,
|
||||
"tracker_config_sha256": "9" * 64,
|
||||
"input_stream_sha256": profile["source"]["stream_sha256"],
|
||||
"overlay_sha256": _sha(overlay),
|
||||
}
|
||||
(raw / "runtime.json").write_text(json.dumps(runtime, indent=2) + "\n", encoding="utf-8")
|
||||
return raw
|
||||
|
||||
|
||||
def _observation(
|
||||
raw: Path,
|
||||
directory: str,
|
||||
frame: int,
|
||||
label: str,
|
||||
box: tuple[int, int, int, int],
|
||||
confidence: float,
|
||||
*,
|
||||
track_id: int | None = None,
|
||||
) -> None:
|
||||
left, top, right, bottom = box
|
||||
identity = "" if track_id is None else f" {track_id}"
|
||||
(raw / directory / f"00_000_{frame:06d}.txt").write_text(
|
||||
f"{label}{identity} 0.0 0 0.0 {left} {top} {right} {bottom} 0 0 0 0 0 0 0 {confidence}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def _sha(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
@@ -0,0 +1,64 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _module() -> object:
|
||||
path = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "prepare_e46f_worker_package.py"
|
||||
)
|
||||
spec = importlib.util.spec_from_file_location("e46f_worker_package_test", path)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_e46f_package_is_minimal_deterministic_and_detector_only(tmp_path: Path) -> None:
|
||||
module = _module()
|
||||
repository = Path(__file__).resolve().parents[1]
|
||||
kwargs = {
|
||||
"repository_root": repository,
|
||||
"profile_path": repository
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "e46f_dashcam_bakeoff_profile.json",
|
||||
"output_root": tmp_path,
|
||||
}
|
||||
package = module.build_e46f_worker_package(**kwargs)
|
||||
repeated = module.build_e46f_worker_package(**kwargs)
|
||||
manifest = module.validate_e46f_worker_package(package)
|
||||
assert repeated == package
|
||||
assert package.name == f"e46f-worker-package-{manifest['identity_sha256']}"
|
||||
assert manifest["identity"]["classification"] == (
|
||||
"minimal-stock-nvidia-detector-only-bakeoff-package"
|
||||
)
|
||||
assert manifest["identity"]["baseline_result_id"].startswith("e46e-ready-stack-")
|
||||
assert manifest["identity"]["tracker"]["custom_association"] is False
|
||||
assert manifest["identity"]["tracker"]["custom_hold_or_stitch"] is False
|
||||
assert manifest["identity"]["postprocessor"]["custom_mission_core_logic"] is False
|
||||
assert len(manifest["artifacts"]) == 11
|
||||
|
||||
|
||||
def test_e46f_package_rejects_unexpected_member(tmp_path: Path) -> None:
|
||||
module = _module()
|
||||
repository = Path(__file__).resolve().parents[1]
|
||||
package = module.build_e46f_worker_package(
|
||||
repository_root=repository,
|
||||
profile_path=repository
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "e46f_dashcam_bakeoff_profile.json",
|
||||
output_root=tmp_path,
|
||||
)
|
||||
(package / "unexpected.txt").write_text("not admitted\n", encoding="utf-8")
|
||||
with pytest.raises(module.E46FWorkerPackageError, match="file set changed"):
|
||||
module.validate_e46f_worker_package(package)
|
||||
@@ -0,0 +1,258 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.compute.e46g_rectified_detector_bakeoff import (
|
||||
E46G_RUNTIME_SCHEMA,
|
||||
E46GRectifiedDetectorBakeoffError,
|
||||
build_e46g_rectified_detector_bakeoff,
|
||||
read_e46g_rectified_detector_bakeoff,
|
||||
)
|
||||
|
||||
|
||||
def test_e46g_freezes_same_calibrated_views_for_both_stock_detectors(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
source, profile = _source_and_profile(tmp_path)
|
||||
raw = _raw_output(tmp_path, profile)
|
||||
|
||||
result = build_e46g_rectified_detector_bakeoff(
|
||||
source_job_root=source,
|
||||
raw_root=raw,
|
||||
profile_path=profile,
|
||||
output_root=tmp_path / "results",
|
||||
)
|
||||
|
||||
report = result["report"]
|
||||
assert report["status"] == "completed-awaiting-visual-semantic-adjudication"
|
||||
assert report["acceptance"]["official_nvidia_dewarper_executed"] is True
|
||||
assert report["acceptance"]["same_views_and_frames_for_both_candidates"] is True
|
||||
assert report["decision"]["automatic_winner_selected"] is False
|
||||
assert report["decision"]["custom_detector_or_tracker_logic_used"] is False
|
||||
assert {component["kind"] for component in report["method"]["components"]} <= {
|
||||
"source",
|
||||
"tool",
|
||||
"model",
|
||||
"algorithm",
|
||||
"runtime",
|
||||
}
|
||||
assert set(report["metrics"]) == {"trafficcamnet", "dashcamnet"}
|
||||
assert report["metrics"]["trafficcamnet"]["view_frame_count"] == 6
|
||||
assert report["metrics"]["trafficcamnet"]["track_observation_count"] == 3
|
||||
assert report["metrics"]["dashcamnet"]["track_observation_count"] == 3
|
||||
assert result["frames"]["trafficcamnet-front"][0]["source_frame_index"] == 0
|
||||
assert (
|
||||
result["frames"]["dashcamnet-right"][0]["objects"][0]["object_id"]
|
||||
== "dashcamnet-right-nvdcf-7"
|
||||
)
|
||||
assert result["manifest"]["authority"]["candidate_accepted"] is False
|
||||
|
||||
repeated = build_e46g_rectified_detector_bakeoff(
|
||||
source_job_root=source,
|
||||
raw_root=raw,
|
||||
profile_path=profile,
|
||||
output_root=tmp_path / "results",
|
||||
)
|
||||
assert repeated["result_id"] == result["result_id"]
|
||||
|
||||
|
||||
def test_e46g_rejects_tampered_comparison_video(tmp_path: Path) -> None:
|
||||
source, profile = _source_and_profile(tmp_path)
|
||||
raw = _raw_output(tmp_path, profile)
|
||||
result = build_e46g_rectified_detector_bakeoff(
|
||||
source_job_root=source,
|
||||
raw_root=raw,
|
||||
profile_path=profile,
|
||||
output_root=tmp_path / "results",
|
||||
)
|
||||
result["comparison_paths"]["trafficcamnet"].write_bytes(b"changed")
|
||||
with pytest.raises(E46GRectifiedDetectorBakeoffError, match="artifact changed"):
|
||||
read_e46g_rectified_detector_bakeoff(result["result_root"])
|
||||
|
||||
|
||||
def _source_and_profile(tmp_path: Path) -> tuple[Path, Path]:
|
||||
repository_profile = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "e46g_rectified_detector_bakeoff_profile.json"
|
||||
)
|
||||
profile_value = json.loads(repository_profile.read_text(encoding="utf-8"))
|
||||
source = tmp_path / "source-job"
|
||||
camera = source / "input" / "camera" / "sensor.camera.right" / "epoch-1"
|
||||
camera.mkdir(parents=True)
|
||||
rows = [
|
||||
{
|
||||
"schema_version": "missioncore.camera-recording-index/v1",
|
||||
"kind": "media",
|
||||
"sequence": sequence,
|
||||
"session_monotonic_ns": 1_000_000_000 + (sequence - 1) * 100_000_000,
|
||||
"sha256": hashlib.sha256(f"frame-{sequence}".encode()).hexdigest(),
|
||||
}
|
||||
for sequence in range(1, 4490)
|
||||
]
|
||||
index_path = camera / "index.jsonl"
|
||||
index_path.write_text(
|
||||
"".join(json.dumps(row, sort_keys=True) + "\n" for row in rows),
|
||||
encoding="utf-8",
|
||||
)
|
||||
summary_path = camera / "summary.json"
|
||||
summary_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": "missioncore.camera-recording/v1",
|
||||
"stream_sha256": "1" * 64,
|
||||
"segment_count": 4489,
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
profile_value["source"].update(
|
||||
{
|
||||
"job_id": "recorded-test",
|
||||
"session_id": "test-session",
|
||||
"stream_sha256": "1" * 64,
|
||||
"archive_index_sha256": _sha(index_path),
|
||||
"archive_summary_sha256": _sha(summary_path),
|
||||
}
|
||||
)
|
||||
profile_value["selection"].update(
|
||||
{
|
||||
"first_source_frame_index": 0,
|
||||
"last_source_frame_index": 1,
|
||||
"frame_count": 2,
|
||||
}
|
||||
)
|
||||
(source / "job.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": "missioncore.compute-job/v1",
|
||||
"job_id": "recorded-test",
|
||||
"input": {
|
||||
"session_id": "test-session",
|
||||
"source_id": "sensor.camera.right",
|
||||
"segment_count": 4489,
|
||||
"archive_index_sha256": _sha(index_path),
|
||||
"archive_summary_sha256": _sha(summary_path),
|
||||
"timeline": {"start_seconds": 10.0, "end_seconds": 459.0},
|
||||
},
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
profile = tmp_path / "profile.json"
|
||||
profile.write_text(json.dumps(profile_value, indent=2) + "\n", encoding="utf-8")
|
||||
return source, profile
|
||||
|
||||
|
||||
def _raw_output(tmp_path: Path, profile_path: Path) -> Path:
|
||||
profile = json.loads(profile_path.read_text(encoding="utf-8"))
|
||||
raw = tmp_path / "raw"
|
||||
geometry_runtime: dict[str, object] = {}
|
||||
for view in ("left", "front", "right"):
|
||||
full = _file(raw / "geometry" / f"{view}.mp4", f"full-{view}")
|
||||
sample = _file(raw / "samples" / f"{view}.mp4", f"sample-{view}")
|
||||
geometry_runtime[view] = {
|
||||
"dewarper_config_sha256": profile["rectification"]["views"][view]["config_sha256"],
|
||||
"dewarper_log_sha256": "2" * 64,
|
||||
"full_rectified_video_path": f"geometry/{view}.mp4",
|
||||
"full_rectified_video_sha256": _sha(full),
|
||||
"sample_video_path": f"samples/{view}.mp4",
|
||||
"sample_video_sha256": _sha(sample),
|
||||
"full_frame_count": profile["rectification"]["expected_full_frame_count"],
|
||||
"retained_source_frame_index_range": profile["rectification"][
|
||||
"retained_source_frame_index_range"
|
||||
],
|
||||
"excluded_source_tail_frame_count": profile["rectification"][
|
||||
"excluded_source_tail_frame_count"
|
||||
],
|
||||
"sample_frame_count": 2,
|
||||
}
|
||||
candidates_runtime: dict[str, object] = {}
|
||||
for candidate in ("trafficcamnet", "dashcamnet"):
|
||||
runs: dict[str, object] = {}
|
||||
for view in ("left", "front", "right"):
|
||||
root = raw / "runs" / candidate / view
|
||||
for directory in ("detections", "tracks"):
|
||||
(root / directory).mkdir(parents=True, exist_ok=True)
|
||||
for frame in range(2):
|
||||
target = root / directory / f"00_000_{frame:06d}.txt"
|
||||
target.write_text(
|
||||
_kitti_row(directory == "tracks") if frame == 0 else "",
|
||||
encoding="utf-8",
|
||||
)
|
||||
overlay = _file(root / "overlay.mp4", f"overlay-{candidate}-{view}")
|
||||
log = _file(root / "deepstream.log", "success")
|
||||
runs[view] = {
|
||||
"overlay_path": f"runs/{candidate}/{view}/overlay.mp4",
|
||||
"overlay_sha256": _sha(overlay),
|
||||
"deepstream_log_path": f"runs/{candidate}/{view}/deepstream.log",
|
||||
"deepstream_log_sha256": _sha(log),
|
||||
"tracker_config_sha256": "3" * 64,
|
||||
"model_engine_sha256": "4" * 64,
|
||||
"frame_count": 2,
|
||||
"deepstream_exit_code": 0,
|
||||
}
|
||||
candidates_runtime[candidate] = {
|
||||
"model_sha256": profile["candidates"][candidate]["model_sha256"],
|
||||
"deepstream_app_config_sha256": profile["candidates"][candidate][
|
||||
"deepstream_app_config_sha256"
|
||||
],
|
||||
"detector_config_sha256": profile["candidates"][candidate]["detector_config_sha256"],
|
||||
"parser_library_sha256": (
|
||||
profile["trafficcamnet_parser"]["library_sha256"]
|
||||
if candidate == "trafficcamnet"
|
||||
else None
|
||||
),
|
||||
"runs": runs,
|
||||
}
|
||||
comparison_runtime: dict[str, object] = {}
|
||||
for candidate in ("trafficcamnet", "dashcamnet"):
|
||||
video = _file(raw / "comparison" / f"{candidate}.mp4", candidate)
|
||||
comparison_runtime[candidate] = {
|
||||
"video_path": f"comparison/{candidate}.mp4",
|
||||
"video_sha256": _sha(video),
|
||||
"frame_count": 2,
|
||||
"view_order": ["left", "front", "right"],
|
||||
}
|
||||
_file(raw / "worker.log", "completed")
|
||||
image = profile["runtime"]["container_image"]
|
||||
runtime = {
|
||||
"schema_version": E46G_RUNTIME_SCHEMA,
|
||||
"status": "completed",
|
||||
"worker_host": "TEST-WORKER-006",
|
||||
"gpu_name": "Synthetic RTX",
|
||||
"container_image": image,
|
||||
"container_image_digest": image.rsplit("@sha256:", 1)[1],
|
||||
"source_stream_sha256": profile["source"]["stream_sha256"],
|
||||
"first_source_frame_index": 0,
|
||||
"sample_frame_count": 2,
|
||||
"geometry": geometry_runtime,
|
||||
"candidates": candidates_runtime,
|
||||
"comparison": comparison_runtime,
|
||||
}
|
||||
(raw / "runtime.json").write_text(json.dumps(runtime, indent=2) + "\n", encoding="utf-8")
|
||||
return raw
|
||||
|
||||
|
||||
def _file(path: Path, value: str) -> Path:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(value.encode())
|
||||
return path
|
||||
|
||||
|
||||
def _kitti_row(tracked: bool) -> str:
|
||||
identity = " 7" if tracked else ""
|
||||
return f"car{identity} 0.0 0 0.0 10 20 300 400 0 0 0 0 0 0 0 0.91\n"
|
||||
|
||||
|
||||
def _sha(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
@@ -0,0 +1,87 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _module() -> object:
|
||||
path = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "prepare_e46g_worker_package.py"
|
||||
)
|
||||
spec = importlib.util.spec_from_file_location("e46g_worker_package_test", path)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_e46g_package_is_deterministic_calibrated_and_stock(tmp_path: Path) -> None:
|
||||
module = _module()
|
||||
repository = Path(__file__).resolve().parents[1]
|
||||
kwargs = {
|
||||
"repository_root": repository,
|
||||
"profile_path": repository
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "e46g_rectified_detector_bakeoff_profile.json",
|
||||
"parser_library_path": repository
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e46e"
|
||||
/ "parser"
|
||||
/ "a18d85dae674a088549c5f9b8fda53c640f4fcbd88a41f4c2cb1f4e3ea8878ee"
|
||||
/ "libnvds_infercustomparser_tao.so",
|
||||
"output_root": tmp_path,
|
||||
}
|
||||
package = module.build_e46g_worker_package(**kwargs)
|
||||
repeated = module.build_e46g_worker_package(**kwargs)
|
||||
manifest = module.validate_e46g_worker_package(package)
|
||||
assert repeated == package
|
||||
assert package.name == f"e46g-worker-package-{manifest['identity_sha256']}"
|
||||
identity = manifest["identity"]
|
||||
assert identity["classification"] == (
|
||||
"minimal-factory-kb4-stock-nvidia-detector-bakeoff-package"
|
||||
)
|
||||
assert identity["calibration"]["calibration_sha256"] == (
|
||||
"05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9"
|
||||
)
|
||||
assert identity["rectification"]["provider"] == "NVIDIA Gst-nvdewarper"
|
||||
assert identity["rectification"]["expected_full_frame_count"] == 4488
|
||||
assert identity["rectification"]["retained_source_frame_index_range"] == [0, 4487]
|
||||
assert identity["selection"]["frame_count"] == 600
|
||||
assert all(
|
||||
candidate["custom_postprocessing"] is False for candidate in identity["candidates"].values()
|
||||
)
|
||||
assert identity["tracker"]["custom_association"] is False
|
||||
assert identity["tracker"]["custom_hold_or_stitch"] is False
|
||||
assert len(manifest["artifacts"]) == 19
|
||||
|
||||
|
||||
def test_e46g_package_rejects_unexpected_member(tmp_path: Path) -> None:
|
||||
module = _module()
|
||||
repository = Path(__file__).resolve().parents[1]
|
||||
package = module.build_e46g_worker_package(
|
||||
repository_root=repository,
|
||||
profile_path=repository
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "e46g_rectified_detector_bakeoff_profile.json",
|
||||
parser_library_path=repository
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e46e"
|
||||
/ "parser"
|
||||
/ "a18d85dae674a088549c5f9b8fda53c640f4fcbd88a41f4c2cb1f4e3ea8878ee"
|
||||
/ "libnvds_infercustomparser_tao.so",
|
||||
output_root=tmp_path,
|
||||
)
|
||||
(package / "unexpected.txt").write_text("not admitted\n", encoding="utf-8")
|
||||
with pytest.raises(module.E46GWorkerPackageError, match="file set changed"):
|
||||
module.validate_e46g_worker_package(package)
|
||||
@@ -0,0 +1,167 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.compute.e46h_full_rectified_front_replay import (
|
||||
E46H_RUNTIME_SCHEMA,
|
||||
E46HFullRectifiedFrontReplayError,
|
||||
build_e46h_full_rectified_front_replay,
|
||||
read_e46h_full_rectified_front_replay,
|
||||
)
|
||||
|
||||
|
||||
def test_e46h_freezes_full_retained_front_route(tmp_path: Path) -> None:
|
||||
source, profile = _source_and_profile(tmp_path)
|
||||
raw = _raw_output(tmp_path, profile)
|
||||
result = build_e46h_full_rectified_front_replay(
|
||||
source_job_root=source,
|
||||
raw_root=raw,
|
||||
profile_path=profile,
|
||||
output_root=tmp_path / "results",
|
||||
)
|
||||
|
||||
report = result["report"]
|
||||
assert report["status"] == "completed-awaiting-full-route-visual-review"
|
||||
assert report["metrics"]["frame_count"] == 4488
|
||||
assert report["acceptance"]["retained_route_accounted"] is True
|
||||
assert report["acceptance"]["terminal_source_frame_excluded"] is True
|
||||
assert report["acceptance"]["full_visual_review_completed"] is False
|
||||
assert report["decision"]["selected_provider"] == "front-trafficcamnet-stock-nvdcf"
|
||||
assert report["decision"]["custom_detector_or_tracker_logic_used"] is False
|
||||
assert result["frames"][0]["source_frame_index"] == 0
|
||||
assert result["frames"][-1]["source_frame_index"] == 4487
|
||||
assert result["manifest"]["authority"]["candidate_accepted"] is False
|
||||
|
||||
result["overlay_path"].write_bytes(b"changed")
|
||||
with pytest.raises(E46HFullRectifiedFrontReplayError, match="artifact changed"):
|
||||
read_e46h_full_rectified_front_replay(result["result_root"])
|
||||
|
||||
|
||||
def _source_and_profile(tmp_path: Path) -> tuple[Path, Path]:
|
||||
repository = Path(__file__).resolve().parents[1]
|
||||
profile_value = json.loads(
|
||||
(
|
||||
repository
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "e46h_full_rectified_front_replay_profile.json"
|
||||
).read_text(encoding="utf-8")
|
||||
)
|
||||
source = tmp_path / "source-job"
|
||||
camera = source / "input" / "camera" / "sensor.camera.right" / "epoch-1"
|
||||
camera.mkdir(parents=True)
|
||||
rows = [
|
||||
{
|
||||
"schema_version": "missioncore.camera-recording-index/v1",
|
||||
"kind": "media",
|
||||
"sequence": sequence,
|
||||
"session_monotonic_ns": 1_000_000_000 + (sequence - 1) * 100_000_000,
|
||||
"sha256": hashlib.sha256(f"frame-{sequence}".encode()).hexdigest(),
|
||||
}
|
||||
for sequence in range(1, 4490)
|
||||
]
|
||||
index_path = camera / "index.jsonl"
|
||||
index_path.write_text(
|
||||
"".join(json.dumps(row, sort_keys=True) + "\n" for row in rows),
|
||||
encoding="utf-8",
|
||||
)
|
||||
summary_path = camera / "summary.json"
|
||||
summary_path.write_text(
|
||||
json.dumps({"stream_sha256": "1" * 64, "segment_count": 4489}) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
profile_value["source"].update(
|
||||
{
|
||||
"job_id": "recorded-test",
|
||||
"session_id": "test-session",
|
||||
"stream_sha256": "1" * 64,
|
||||
"archive_index_sha256": _sha(index_path),
|
||||
"archive_summary_sha256": _sha(summary_path),
|
||||
}
|
||||
)
|
||||
(source / "job.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": "missioncore.compute-job/v1",
|
||||
"job_id": "recorded-test",
|
||||
"input": {
|
||||
"session_id": "test-session",
|
||||
"source_id": "sensor.camera.right",
|
||||
"segment_count": 4489,
|
||||
"archive_index_sha256": _sha(index_path),
|
||||
"archive_summary_sha256": _sha(summary_path),
|
||||
"timeline": {"start_seconds": 10.0, "end_seconds": 459.0},
|
||||
},
|
||||
}
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
profile = tmp_path / "profile.json"
|
||||
profile.write_text(json.dumps(profile_value) + "\n", encoding="utf-8")
|
||||
return source, profile
|
||||
|
||||
|
||||
def _raw_output(tmp_path: Path, profile_path: Path) -> Path:
|
||||
profile = json.loads(profile_path.read_text(encoding="utf-8"))
|
||||
raw = tmp_path / "raw"
|
||||
front = _file(raw / "geometry" / "front.mp4", "front")
|
||||
front_log = _file(raw / "geometry" / "front.log", "dewarper")
|
||||
overlay = _file(raw / "run" / "overlay.mp4", "overlay")
|
||||
deepstream_log = _file(raw / "run" / "deepstream.log", "deepstream")
|
||||
_file(raw / "worker.log", "completed")
|
||||
for directory in (raw / "run" / "detections", raw / "run" / "tracks"):
|
||||
directory.mkdir(parents=True)
|
||||
for frame in range(4488):
|
||||
(directory / f"00_000_{frame:06d}.txt").touch()
|
||||
image = profile["runtime"]["container_image"]
|
||||
runtime = {
|
||||
"schema_version": E46H_RUNTIME_SCHEMA,
|
||||
"status": "completed",
|
||||
"worker_host": "TEST-WORKER-006",
|
||||
"gpu_name": "Synthetic RTX",
|
||||
"container_image": image,
|
||||
"container_image_digest": image.rsplit("@sha256:", 1)[1],
|
||||
"source_stream_sha256": profile["source"]["stream_sha256"],
|
||||
"frame_count": 4488,
|
||||
"retained_source_frame_index_range": [0, 4487],
|
||||
"geometry": {
|
||||
"video_path": "geometry/front.mp4",
|
||||
"video_sha256": _sha(front),
|
||||
"log_path": "geometry/front.log",
|
||||
"log_sha256": _sha(front_log),
|
||||
"config_sha256": profile["rectification"]["config_sha256"],
|
||||
"frame_count": 4488,
|
||||
},
|
||||
"run": {
|
||||
"overlay_path": "run/overlay.mp4",
|
||||
"overlay_sha256": _sha(overlay),
|
||||
"deepstream_log_path": "run/deepstream.log",
|
||||
"deepstream_log_sha256": _sha(deepstream_log),
|
||||
"model_sha256": profile["detector"]["model_sha256"],
|
||||
"model_engine_sha256": "2" * 64,
|
||||
"parser_library_sha256": profile["parser"]["library_sha256"],
|
||||
"deepstream_app_config_sha256": profile["detector"][
|
||||
"deepstream_app_config_sha256"
|
||||
],
|
||||
"detector_config_sha256": profile["detector"]["detector_config_sha256"],
|
||||
"tracker_config_sha256": "3" * 64,
|
||||
"frame_count": 4488,
|
||||
},
|
||||
}
|
||||
(raw / "runtime.json").write_text(json.dumps(runtime) + "\n", encoding="utf-8")
|
||||
return raw
|
||||
|
||||
|
||||
def _file(path: Path, value: str) -> Path:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(value, encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
def _sha(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
@@ -0,0 +1,53 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _module() -> object:
|
||||
path = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "prepare_e46h_worker_package.py"
|
||||
)
|
||||
spec = importlib.util.spec_from_file_location("e46h_worker_package_test", path)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_e46h_package_is_deterministic_front_only_and_stock(tmp_path: Path) -> None:
|
||||
module = _module()
|
||||
repository = Path(__file__).resolve().parents[1]
|
||||
kwargs = {
|
||||
"repository_root": repository,
|
||||
"profile_path": repository
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "e46h_full_rectified_front_replay_profile.json",
|
||||
"parser_library_path": repository
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e46e"
|
||||
/ "parser"
|
||||
/ "a18d85dae674a088549c5f9b8fda53c640f4fcbd88a41f4c2cb1f4e3ea8878ee"
|
||||
/ "libnvds_infercustomparser_tao.so",
|
||||
"output_root": tmp_path,
|
||||
}
|
||||
package = module.build_e46h_worker_package(**kwargs)
|
||||
repeated = module.build_e46h_worker_package(**kwargs)
|
||||
manifest = module.validate_e46h_worker_package(package)
|
||||
assert repeated == package
|
||||
assert package.name == f"e46h-worker-package-{manifest['identity_sha256']}"
|
||||
identity = manifest["identity"]
|
||||
assert identity["classification"] == "minimal-full-front-stock-nvidia-replay-package"
|
||||
assert identity["rectification"]["view"] == "front"
|
||||
assert identity["selection"]["frame_count"] == 4488
|
||||
assert identity["selection"]["last_source_frame_index"] == 4487
|
||||
assert identity["detector"]["custom_postprocessing"] is False
|
||||
assert identity["tracker"]["custom_association"] is False
|
||||
assert identity["tracker"]["custom_hold_or_stitch"] is False
|
||||
@@ -0,0 +1,90 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.compute.l34_right_yolox_truth_island_freeze import (
|
||||
L34_PREDICTION_SCHEMA,
|
||||
L34RightYoloxTruthIslandError,
|
||||
freeze_l34_candidate_predictions,
|
||||
)
|
||||
|
||||
|
||||
def _reference(frame_index: int, sequence: int) -> dict[str, object]:
|
||||
return {
|
||||
"truth_island_sequence": sequence,
|
||||
"image_id": sequence + 10,
|
||||
"frame_index": frame_index,
|
||||
"group_id": "temporal-a",
|
||||
"session_seconds": 42.5,
|
||||
"sha256": "a" * 64,
|
||||
}
|
||||
|
||||
|
||||
def _frame(frame_index: int) -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": "missioncore.rectified-yolox-frame/v1",
|
||||
"frame_index": frame_index,
|
||||
"detections": [
|
||||
{
|
||||
"label": "bus",
|
||||
"score": 0.91,
|
||||
"bbox_xyxy": [10.0, 20.0, 30.0, 40.0],
|
||||
},
|
||||
{
|
||||
"label": "person",
|
||||
"score": 0.72,
|
||||
"bbox_xyxy": [1.0, 2.0, 3.0, 4.0],
|
||||
},
|
||||
{
|
||||
"label": "car",
|
||||
"score": 0.24,
|
||||
"bbox_xyxy": [5.0, 6.0, 7.0, 8.0],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_freezes_only_admitted_right_camera_candidate_classes() -> None:
|
||||
predictions = freeze_l34_candidate_predictions(
|
||||
references=(_reference(70, 1),),
|
||||
detector_frames={70: _frame(70)},
|
||||
minimum_score=0.25,
|
||||
)
|
||||
|
||||
assert predictions == (
|
||||
{
|
||||
"schema_version": L34_PREDICTION_SCHEMA,
|
||||
"candidate_id": "yolox-s-kb4-core3",
|
||||
"truth_island_sequence": 1,
|
||||
"image_id": 11,
|
||||
"frame_index": 70,
|
||||
"session_seconds": 42.5,
|
||||
"source_image_sha256": "a" * 64,
|
||||
"group_id": "temporal-a",
|
||||
"predictions": [
|
||||
{
|
||||
"label": "heavy_vehicle",
|
||||
"score": 0.91,
|
||||
"bbox_xyxy": [10.0, 20.0, 30.0, 40.0],
|
||||
},
|
||||
{
|
||||
"label": "person",
|
||||
"score": 0.72,
|
||||
"bbox_xyxy": [1.0, 2.0, 3.0, 4.0],
|
||||
},
|
||||
],
|
||||
"truth_joined": False,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_freeze_fails_closed_on_incomplete_frame_coverage() -> None:
|
||||
with pytest.raises(
|
||||
L34RightYoloxTruthIslandError,
|
||||
match="coverage is incomplete",
|
||||
):
|
||||
freeze_l34_candidate_predictions(
|
||||
references=(_reference(70, 1), _reference(71, 2)),
|
||||
detector_frames={70: _frame(70)},
|
||||
minimum_score=0.25,
|
||||
)
|
||||
@@ -0,0 +1,115 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from k1link.compute.l34a_assisted_yolox_error_audit import (
|
||||
_aggregate,
|
||||
_audit_case,
|
||||
)
|
||||
|
||||
|
||||
def _prediction_row() -> dict[str, object]:
|
||||
return {
|
||||
"truth_island_sequence": 1,
|
||||
"image_id": 2,
|
||||
"frame_index": 70,
|
||||
"group_id": "anchor-a",
|
||||
"session_seconds": 4.2,
|
||||
"source_image_sha256": "a" * 64,
|
||||
"predictions": [
|
||||
{
|
||||
"label": "car",
|
||||
"score": 0.9,
|
||||
"bbox_xyxy": [100.0, 100.0, 300.0, 300.0],
|
||||
},
|
||||
{
|
||||
"label": "car",
|
||||
"score": 0.7,
|
||||
"bbox_xyxy": [120.0, 120.0, 280.0, 280.0],
|
||||
},
|
||||
{
|
||||
"label": "motorcycle",
|
||||
"score": 0.6,
|
||||
"bbox_xyxy": [400.0, 200.0, 520.0, 420.0],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _annotation_frame() -> dict[str, object]:
|
||||
return {
|
||||
"truth_island_sequence": 1,
|
||||
"image_id": 2,
|
||||
"frame_index": 70,
|
||||
"source_sha256": "a" * 64,
|
||||
"objects": [
|
||||
{
|
||||
"object_id": "car-1",
|
||||
"category": "car",
|
||||
"proposed_label": None,
|
||||
"origin": "frozen_candidate_seed",
|
||||
"box_xyxy": [100.0, 100.0, 300.0, 300.0],
|
||||
"occluded": False,
|
||||
"truncated": False,
|
||||
},
|
||||
{
|
||||
"object_id": "stroller-1",
|
||||
"category": "unmapped",
|
||||
"proposed_label": "Детская коляска",
|
||||
"origin": "manual",
|
||||
"box_xyxy": [400.0, 200.0, 520.0, 420.0],
|
||||
"occluded": False,
|
||||
"truncated": False,
|
||||
},
|
||||
{
|
||||
"object_id": "person-1",
|
||||
"category": "person",
|
||||
"proposed_label": None,
|
||||
"origin": "manual",
|
||||
"box_xyxy": [10.0, 10.0, 60.0, 160.0],
|
||||
"occluded": False,
|
||||
"truncated": False,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_assisted_audit_distinguishes_duplicate_mismatch_and_miss() -> None:
|
||||
case = _audit_case(
|
||||
prediction_row=_prediction_row(),
|
||||
annotation_frame=_annotation_frame(),
|
||||
)
|
||||
|
||||
assert case["summary"] == {
|
||||
"prediction_count": 3,
|
||||
"reference_count": 3,
|
||||
"true_positive": 1,
|
||||
"false_positive": 2,
|
||||
"false_negative": 2,
|
||||
"class_mismatch": 1,
|
||||
"duplicate_false_positive": 1,
|
||||
"unmatched_false_positive": 0,
|
||||
"unmatched_false_negative": 1,
|
||||
"severity_score": 7,
|
||||
}
|
||||
assert [item["verdict"] for item in case["predictions"]] == [
|
||||
"true_positive",
|
||||
"duplicate_false_positive",
|
||||
"class_mismatch",
|
||||
]
|
||||
assert case["annotations"][1]["display_category"] == (
|
||||
"unmapped:Детская коляска"
|
||||
)
|
||||
|
||||
|
||||
def test_assisted_alignment_metrics_remain_descriptive() -> None:
|
||||
case = _audit_case(
|
||||
prediction_row=_prediction_row(),
|
||||
annotation_frame=_annotation_frame(),
|
||||
)
|
||||
|
||||
metrics = _aggregate((case,))
|
||||
|
||||
assert metrics["precision_iou50"] == 1 / 3
|
||||
assert metrics["recall_iou50"] == 1 / 3
|
||||
assert metrics["f1_iou50"] == 1 / 3
|
||||
assert metrics["custom_reference_count"] == 1
|
||||
assert metrics["error_case_count"] == 1
|
||||
@@ -0,0 +1,100 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from k1link.compute.l34a_assisted_yolox_error_audit import _audit_case
|
||||
from k1link.compute.l34b_nested_box_consolidation_shadow import (
|
||||
consolidate_l34b_prediction_row,
|
||||
evaluate_l34b_shadow,
|
||||
)
|
||||
|
||||
|
||||
def _row(sequence: int = 1) -> dict[str, object]:
|
||||
return {
|
||||
"truth_island_sequence": sequence,
|
||||
"image_id": sequence,
|
||||
"frame_index": sequence + 10,
|
||||
"group_id": "nested-box",
|
||||
"session_seconds": float(sequence),
|
||||
"source_image_sha256": f"{sequence % 10}" * 64,
|
||||
"predictions": [
|
||||
{
|
||||
"label": "heavy_vehicle",
|
||||
"score": 0.71,
|
||||
"bbox_xyxy": [100.0, 100.0, 140.0, 190.0],
|
||||
},
|
||||
{
|
||||
"label": "heavy_vehicle",
|
||||
"score": 0.69,
|
||||
"bbox_xyxy": [90.0, 99.0, 141.0, 191.0],
|
||||
},
|
||||
{
|
||||
"label": "car",
|
||||
"score": 0.8,
|
||||
"bbox_xyxy": [300.0, 100.0, 360.0, 180.0],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _annotation(sequence: int = 1) -> dict[str, object]:
|
||||
return {
|
||||
"truth_island_sequence": sequence,
|
||||
"image_id": sequence,
|
||||
"frame_index": sequence + 10,
|
||||
"source_sha256": f"{sequence % 10}" * 64,
|
||||
"objects": [
|
||||
{
|
||||
"object_id": f"truck-{sequence}",
|
||||
"category": "heavy_vehicle",
|
||||
"proposed_label": None,
|
||||
"origin": "manual",
|
||||
"box_xyxy": [90.0, 99.0, 141.0, 191.0],
|
||||
"occluded": False,
|
||||
"truncated": False,
|
||||
},
|
||||
{
|
||||
"object_id": f"car-{sequence}",
|
||||
"category": "car",
|
||||
"proposed_label": None,
|
||||
"origin": "manual",
|
||||
"box_xyxy": [300.0, 100.0, 360.0, 180.0],
|
||||
"occluded": False,
|
||||
"truncated": False,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_consolidates_only_same_category_nested_boxes() -> None:
|
||||
projected, consolidations = consolidate_l34b_prediction_row(_row())
|
||||
|
||||
assert len(projected["predictions"]) == 2
|
||||
assert consolidations[0]["source_prediction_indices"] == [1, 2]
|
||||
assert projected["predictions"][0] == {
|
||||
"label": "heavy_vehicle",
|
||||
"score": 0.71,
|
||||
"bbox_xyxy": [90.0, 99.0, 141.0, 191.0],
|
||||
"source_prediction_indices": [1, 2],
|
||||
}
|
||||
assert projected["predictions"][1]["source_prediction_indices"] == [3]
|
||||
|
||||
|
||||
def test_shadow_reports_regression_free_false_positive_reduction() -> None:
|
||||
rows = tuple(_row(sequence) for sequence in range(1, 33))
|
||||
annotations = tuple(_annotation(sequence) for sequence in range(1, 33))
|
||||
before = tuple(
|
||||
_audit_case(prediction_row=row, annotation_frame=annotation)
|
||||
for row, annotation in zip(rows, annotations, strict=True)
|
||||
)
|
||||
|
||||
cases, metrics = evaluate_l34b_shadow(
|
||||
prediction_rows=rows,
|
||||
annotation_frames=annotations,
|
||||
before_cases=before,
|
||||
)
|
||||
|
||||
assert len(cases) == 32
|
||||
assert metrics["consolidation_count"] == 32
|
||||
assert metrics["delta"]["true_positive"] == 0
|
||||
assert metrics["delta"]["false_positive"] == -32
|
||||
assert metrics["delta"]["false_negative"] == 0
|
||||
assert metrics["assisted_regression_free"] is True
|
||||
@@ -0,0 +1,153 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.compute.l34c_tile_seam_stitch_shadow import (
|
||||
L34C_PROVENANCE_SCHEMA,
|
||||
L34CTileSeamStitchError,
|
||||
apply_l34c_stitches,
|
||||
bind_l34c_prediction_provenance,
|
||||
find_l34c_temporal_stitches,
|
||||
)
|
||||
|
||||
|
||||
def _frozen_row(*, sequence: int = 1, frame_index: int = 100) -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": "missioncore.l34-right-yolox-truth-island-prediction/v1",
|
||||
"candidate_id": "yolox-s-kb4-core3",
|
||||
"truth_island_sequence": sequence,
|
||||
"image_id": sequence,
|
||||
"frame_index": frame_index,
|
||||
"session_seconds": frame_index / 10.0,
|
||||
"source_image_sha256": "a" * 64,
|
||||
"group_id": "clip-close-car",
|
||||
"predictions": [
|
||||
{
|
||||
"label": "car",
|
||||
"score": 0.95,
|
||||
"bbox_xyxy": [230.0, 220.0, 385.0, 340.0],
|
||||
},
|
||||
{
|
||||
"label": "car",
|
||||
"score": 0.90,
|
||||
"bbox_xyxy": [182.0, 210.0, 266.0, 338.0],
|
||||
},
|
||||
],
|
||||
"truth_joined": False,
|
||||
}
|
||||
|
||||
|
||||
def _detector_frame(frame_index: int = 100) -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": "missioncore.rectified-yolox-frame/v1",
|
||||
"frame_index": frame_index,
|
||||
"detections": [
|
||||
{
|
||||
"label": "car",
|
||||
"class_id": 2,
|
||||
"score": 0.95,
|
||||
"bbox_xyxy": [230.0, 220.0, 385.0, 340.0],
|
||||
"raw_center_xy": [290.0, 280.0],
|
||||
"rectification_tile": "front",
|
||||
"valid_fov_fraction": 1.0,
|
||||
},
|
||||
{
|
||||
"label": "car",
|
||||
"class_id": 2,
|
||||
"score": 0.90,
|
||||
"bbox_xyxy": [182.0, 210.0, 266.0, 338.0],
|
||||
"raw_center_xy": [230.0, 275.0],
|
||||
"rectification_tile": "left",
|
||||
"valid_fov_fraction": 1.0,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _provenance_run(length: int) -> tuple[dict[str, object], ...]:
|
||||
frozen = tuple(
|
||||
_frozen_row(sequence=index + 1, frame_index=100 + index)
|
||||
for index in range(length)
|
||||
)
|
||||
detector = {
|
||||
100 + index: _detector_frame(100 + index)
|
||||
for index in range(length)
|
||||
}
|
||||
return bind_l34c_prediction_provenance(
|
||||
prediction_rows=frozen,
|
||||
detector_frames=detector,
|
||||
)
|
||||
|
||||
|
||||
def test_exact_join_preserves_tile_and_raw_detector_identity() -> None:
|
||||
rows = _provenance_run(1)
|
||||
|
||||
assert rows[0]["schema_version"] == L34C_PROVENANCE_SCHEMA
|
||||
assert rows[0]["provenance_join"] == "exact-label-score-bbox"
|
||||
assert rows[0]["predictions"][0]["rectification_tile"] == "front"
|
||||
assert rows[0]["predictions"][1]["rectification_tile"] == "left"
|
||||
assert rows[0]["predictions"][0]["class_id"] == 2
|
||||
assert rows[0]["predictions"][0]["raw_label"] == "car"
|
||||
|
||||
|
||||
def test_exact_join_rejects_missing_or_ambiguous_provenance() -> None:
|
||||
frame = _detector_frame()
|
||||
frame["detections"].append(copy.deepcopy(frame["detections"][0]))
|
||||
|
||||
with pytest.raises(L34CTileSeamStitchError, match="one exact"):
|
||||
bind_l34c_prediction_provenance(
|
||||
prediction_rows=(_frozen_row(),),
|
||||
detector_frames={100: frame},
|
||||
)
|
||||
|
||||
|
||||
def test_temporal_gate_admits_three_consecutive_frames_but_not_two() -> None:
|
||||
admitted, static_count = find_l34c_temporal_stitches(_provenance_run(3))
|
||||
rejected, rejected_static_count = find_l34c_temporal_stitches(
|
||||
_provenance_run(2)
|
||||
)
|
||||
|
||||
assert static_count == 3
|
||||
assert set(admitted) == {1, 2, 3}
|
||||
assert all(items[0]["temporal_run_length"] == 3 for items in admitted.values())
|
||||
assert rejected_static_count == 2
|
||||
assert rejected == {}
|
||||
|
||||
|
||||
def test_temporal_gate_rejects_same_tile_and_small_pairs() -> None:
|
||||
rows = list(_provenance_run(3))
|
||||
for row in rows:
|
||||
row["predictions"][1]["rectification_tile"] = "front"
|
||||
same_tile, same_tile_count = find_l34c_temporal_stitches(tuple(rows))
|
||||
|
||||
small_rows = list(_provenance_run(3))
|
||||
for row in small_rows:
|
||||
row["predictions"][0]["bbox_xyxy"] = [230.0, 220.0, 260.0, 250.0]
|
||||
row["predictions"][1]["bbox_xyxy"] = [220.0, 218.0, 245.0, 252.0]
|
||||
small, small_count = find_l34c_temporal_stitches(tuple(small_rows))
|
||||
|
||||
assert same_tile_count == 0
|
||||
assert same_tile == {}
|
||||
assert small_count == 0
|
||||
assert small == {}
|
||||
|
||||
|
||||
def test_apply_stitch_unions_geometry_and_preserves_source_tiles() -> None:
|
||||
row = _provenance_run(3)[0]
|
||||
admitted, _ = find_l34c_temporal_stitches(_provenance_run(3))
|
||||
|
||||
projected = apply_l34c_stitches(row, admitted[1])
|
||||
|
||||
assert projected["predictions"] == [
|
||||
{
|
||||
"label": "car",
|
||||
"score": 0.95,
|
||||
"bbox_xyxy": [182.0, 210.0, 385.0, 340.0],
|
||||
"source_prediction_indices": [1, 2],
|
||||
"source_rectification_tiles": ["front", "left"],
|
||||
"temporal_run_id": "clip-close-car:car:100-102",
|
||||
"temporal_run_length": 3,
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,124 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.compute.l34c_tile_seam_stitch_shadow import (
|
||||
bind_l34c_prediction_provenance,
|
||||
)
|
||||
from k1link.compute.l34d_cumulative_postprocessing_candidate import (
|
||||
L34DCumulativeCandidateError,
|
||||
compose_l34d_prediction_row,
|
||||
)
|
||||
|
||||
|
||||
def _row() -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": "missioncore.l34-right-yolox-truth-island-prediction/v1",
|
||||
"truth_island_sequence": 1,
|
||||
"image_id": 1,
|
||||
"frame_index": 100,
|
||||
"session_seconds": 1.0,
|
||||
"source_image_sha256": "a" * 64,
|
||||
"group_id": "composition",
|
||||
"predictions": [
|
||||
{"label": "heavy_vehicle", "score": 0.71, "bbox_xyxy": [10.0, 10.0, 30.0, 40.0]},
|
||||
{"label": "heavy_vehicle", "score": 0.69, "bbox_xyxy": [9.0, 9.0, 31.0, 41.0]},
|
||||
{"label": "car", "score": 0.95, "bbox_xyxy": [230.0, 220.0, 385.0, 340.0]},
|
||||
{"label": "car", "score": 0.90, "bbox_xyxy": [182.0, 210.0, 266.0, 338.0]},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _detector_frame() -> dict[str, object]:
|
||||
detections = []
|
||||
for index, prediction in enumerate(_row()["predictions"]):
|
||||
detections.append({
|
||||
**prediction,
|
||||
"class_id": 7 if index < 2 else 2,
|
||||
"raw_center_xy": [20.0 + index, 20.0 + index],
|
||||
"rectification_tile": "front" if index in (0, 2) else "left",
|
||||
"valid_fov_fraction": 1.0,
|
||||
})
|
||||
return {
|
||||
"schema_version": "missioncore.rectified-yolox-frame/v1",
|
||||
"frame_index": 100,
|
||||
"detections": detections,
|
||||
}
|
||||
|
||||
|
||||
def _provenance() -> dict[str, object]:
|
||||
return bind_l34c_prediction_provenance(
|
||||
prediction_rows=(_row(),),
|
||||
detector_frames={100: _detector_frame()},
|
||||
)[0]
|
||||
|
||||
|
||||
def _operation(indices: list[int], operation_type: str) -> dict[str, object]:
|
||||
predictions = _row()["predictions"]
|
||||
members = [predictions[index - 1] for index in indices]
|
||||
boxes = [member["bbox_xyxy"] for member in members]
|
||||
operation = {
|
||||
"category": members[0]["label"],
|
||||
"source_prediction_indices": indices,
|
||||
"source_scores": [member["score"] for member in members],
|
||||
"source_boxes_xyxy": boxes,
|
||||
"merged_score": max(member["score"] for member in members),
|
||||
"merged_box_xyxy": [
|
||||
min(box[0] for box in boxes),
|
||||
min(box[1] for box in boxes),
|
||||
max(box[2] for box in boxes),
|
||||
max(box[3] for box in boxes),
|
||||
],
|
||||
}
|
||||
if operation_type == "temporal-tile-seam-stitch":
|
||||
operation.update({
|
||||
"source_tiles": ["front", "left"],
|
||||
"temporal_run_id": "composition:car:100-102",
|
||||
"temporal_run_length": 3,
|
||||
})
|
||||
return operation
|
||||
|
||||
|
||||
def test_composes_disjoint_nested_and_seam_operations_once() -> None:
|
||||
projected, operations = compose_l34d_prediction_row(
|
||||
_provenance(),
|
||||
consolidations=(_operation([1, 2], "nested-box-consolidation"),),
|
||||
stitches=(_operation([3, 4], "temporal-tile-seam-stitch"),),
|
||||
)
|
||||
|
||||
assert len(projected["predictions"]) == 2
|
||||
assert [
|
||||
item["source_prediction_indices"] for item in projected["predictions"]
|
||||
] == [[1, 2], [3, 4]]
|
||||
assert [item["operation_types"] for item in projected["predictions"]] == [
|
||||
["nested-box-consolidation"],
|
||||
["temporal-tile-seam-stitch"],
|
||||
]
|
||||
assert {item["operation_type"] for item in operations} == {
|
||||
"nested-box-consolidation",
|
||||
"temporal-tile-seam-stitch",
|
||||
}
|
||||
|
||||
|
||||
def test_rejects_overlapping_operation_sets() -> None:
|
||||
with pytest.raises(L34DCumulativeCandidateError, match="overlap"):
|
||||
compose_l34d_prediction_row(
|
||||
_provenance(),
|
||||
consolidations=(_operation([1, 2], "nested-box-consolidation"),),
|
||||
stitches=(_operation([1, 2], "temporal-tile-seam-stitch"),),
|
||||
)
|
||||
|
||||
|
||||
def test_rejects_operation_payload_drift() -> None:
|
||||
operation = _operation([1, 2], "nested-box-consolidation")
|
||||
operation["source_boxes_xyxy"] = copy.deepcopy(operation["source_boxes_xyxy"])
|
||||
operation["source_boxes_xyxy"][0][0] += 1.0
|
||||
|
||||
with pytest.raises(L34DCumulativeCandidateError, match="payload"):
|
||||
compose_l34d_prediction_row(
|
||||
_provenance(),
|
||||
consolidations=(operation,),
|
||||
stitches=(),
|
||||
)
|
||||
@@ -0,0 +1,152 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.compute.l34e_self_review_diagnostic import (
|
||||
L34ESelfReviewDiagnosticError,
|
||||
evaluate_l34e_self_review_diagnostic,
|
||||
)
|
||||
|
||||
|
||||
def _candidate_case(
|
||||
sequence: int,
|
||||
*,
|
||||
category: str = "car",
|
||||
box: list[float] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
predictions = [] if box is None else [
|
||||
{
|
||||
"prediction_index": 1,
|
||||
"category": category,
|
||||
"score": 0.8,
|
||||
"box_xyxy": box,
|
||||
"source_prediction_indices": [1],
|
||||
"source_rectification_tiles": ["center"],
|
||||
"operation_types": [],
|
||||
}
|
||||
]
|
||||
return {
|
||||
"truth_island_sequence": sequence,
|
||||
"image_id": sequence,
|
||||
"frame_index": sequence * 10,
|
||||
"group_id": f"frame-{sequence:02d}",
|
||||
"session_seconds": float(sequence),
|
||||
"source_image_sha256": f"{sequence:064x}",
|
||||
"after_predictions": predictions,
|
||||
}
|
||||
|
||||
|
||||
def _review_frame(
|
||||
sequence: int,
|
||||
*,
|
||||
category: str = "car",
|
||||
box: list[float] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
objects = [] if box is None else [
|
||||
{
|
||||
"object_id": f"manual-{sequence:02d}",
|
||||
"category": category,
|
||||
"proposed_label": None,
|
||||
"origin": "manual",
|
||||
"box_xyxy": box,
|
||||
"occluded": False,
|
||||
"truncated": False,
|
||||
}
|
||||
]
|
||||
return {
|
||||
"truth_island_sequence": sequence,
|
||||
"image_id": sequence,
|
||||
"frame_index": sequence * 10,
|
||||
"source_sha256": f"{sequence:064x}",
|
||||
"reviewed": True,
|
||||
"objects": objects,
|
||||
}
|
||||
|
||||
|
||||
def _fixture() -> tuple[tuple[dict[str, Any], ...], tuple[dict[str, Any], ...]]:
|
||||
candidates = []
|
||||
reviews = []
|
||||
for sequence in range(1, 33):
|
||||
candidate_category = "car"
|
||||
review_category = "car"
|
||||
candidate_box: list[float] | None = [10.0, 10.0, 30.0, 30.0]
|
||||
review_box: list[float] | None = [10.0, 10.0, 30.0, 30.0]
|
||||
if sequence == 2:
|
||||
review_box = [8.0, 8.0, 40.0, 40.0]
|
||||
elif sequence == 3:
|
||||
review_box = [8.0, 8.0, 40.0, 40.0]
|
||||
review_category = "person"
|
||||
elif sequence == 4:
|
||||
review_category = "person"
|
||||
elif sequence == 5:
|
||||
review_box = None
|
||||
elif sequence == 6:
|
||||
candidate_box = None
|
||||
candidates.append(
|
||||
_candidate_case(
|
||||
sequence,
|
||||
category=candidate_category,
|
||||
box=candidate_box,
|
||||
)
|
||||
)
|
||||
reviews.append(
|
||||
_review_frame(
|
||||
sequence,
|
||||
category=review_category,
|
||||
box=review_box,
|
||||
)
|
||||
)
|
||||
return tuple(candidates), tuple(reviews)
|
||||
|
||||
|
||||
def test_evaluation_separates_localization_from_unmatched_objects() -> None:
|
||||
candidates, reviews = _fixture()
|
||||
|
||||
cases, metrics = evaluate_l34e_self_review_diagnostic(
|
||||
l34d_cases=candidates,
|
||||
annotation_frames=reviews,
|
||||
)
|
||||
|
||||
diagnostic = metrics["diagnostic_association"]
|
||||
assert diagnostic == {
|
||||
"prediction_count": 31,
|
||||
"reference_count": 31,
|
||||
"associated_pair_count": 30,
|
||||
"strict_alignment": 27,
|
||||
"strict_class_mismatch": 1,
|
||||
"localization_disagreement": 1,
|
||||
"class_and_localization_disagreement": 1,
|
||||
"prediction_only": 1,
|
||||
"reference_only": 1,
|
||||
"candidate_association_coverage": 30 / 31,
|
||||
"reference_association_coverage": 30 / 31,
|
||||
"error_case_count": 5,
|
||||
}
|
||||
strict = metrics["strict_iou50"]
|
||||
assert strict["true_positive"] == 27
|
||||
assert strict["false_positive"] == 4
|
||||
assert strict["false_negative"] == 4
|
||||
assert strict["class_mismatch"] == 1
|
||||
assert cases[1]["predictions"][0]["diagnostic_verdict"] == (
|
||||
"localization_disagreement"
|
||||
)
|
||||
assert cases[2]["references"][0]["diagnostic_verdict"] == (
|
||||
"class_and_localization_disagreement"
|
||||
)
|
||||
assert cases[4]["predictions"][0]["diagnostic_verdict"] == "prediction_only"
|
||||
assert cases[5]["references"][0]["diagnostic_verdict"] == "reference_only"
|
||||
|
||||
|
||||
def test_evaluation_rejects_incomplete_coverage() -> None:
|
||||
candidates, reviews = _fixture()
|
||||
|
||||
with pytest.raises(
|
||||
L34ESelfReviewDiagnosticError,
|
||||
match="exactly 32",
|
||||
):
|
||||
evaluate_l34e_self_review_diagnostic(
|
||||
l34d_cases=candidates[:-1],
|
||||
annotation_frames=reviews,
|
||||
)
|
||||
@@ -0,0 +1,111 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
from k1link.compute.e49_detector_truth_evaluation import (
|
||||
evaluate_frozen_detector_candidates,
|
||||
)
|
||||
from k1link.compute.l34_right_yolox_truth_island_freeze import (
|
||||
L34_PREDICTION_SCHEMA,
|
||||
)
|
||||
from k1link.compute.l35_right_yolox_truth_evaluation import (
|
||||
L35RightYoloxTruthEvaluationError,
|
||||
_require_freeze_before_truth_seal,
|
||||
l34_rows_for_sealed_truth,
|
||||
)
|
||||
|
||||
|
||||
def _l34_row() -> dict[str, Any]:
|
||||
return {
|
||||
"schema_version": L34_PREDICTION_SCHEMA,
|
||||
"candidate_id": "yolox-s-kb4-core3",
|
||||
"truth_island_sequence": 1,
|
||||
"image_id": 11,
|
||||
"frame_index": 70,
|
||||
"session_seconds": 42.5,
|
||||
"source_image_sha256": "a" * 64,
|
||||
"group_id": "anchor-011",
|
||||
"predictions": [
|
||||
{
|
||||
"label": "car",
|
||||
"score": 0.9,
|
||||
"bbox_xyxy": [10.0, 10.0, 110.0, 110.0],
|
||||
}
|
||||
],
|
||||
"truth_joined": False,
|
||||
}
|
||||
|
||||
|
||||
def _truth_row() -> dict[str, Any]:
|
||||
return {
|
||||
"truth_island_sequence": 1,
|
||||
"image_id": 11,
|
||||
"frame_index": 70,
|
||||
"session_seconds": 42.5,
|
||||
"role": "anchor",
|
||||
"group_id": "anchor-011",
|
||||
"source_image_sha256": "a" * 64,
|
||||
"hard_negative": False,
|
||||
"objects": [
|
||||
{
|
||||
"object_id": "car-1",
|
||||
"category": "car",
|
||||
"box_xyxy": [10.0, 10.0, 110.0, 110.0],
|
||||
"occluded": False,
|
||||
"truncated": False,
|
||||
"notes": None,
|
||||
}
|
||||
],
|
||||
"adjudicated": True,
|
||||
}
|
||||
|
||||
|
||||
def test_l35_adapts_l34_without_changing_identity_or_boxes() -> None:
|
||||
rows = l34_rows_for_sealed_truth((_l34_row(),))
|
||||
|
||||
assert rows == (
|
||||
{
|
||||
"candidate_id": "yolox-s-kb4-core3",
|
||||
"truth_island_sequence": 1,
|
||||
"image_id": 11,
|
||||
"frame_index": 70,
|
||||
"session_seconds": 42.5,
|
||||
"source_image_sha256": "a" * 64,
|
||||
"predictions": [
|
||||
{
|
||||
"category": "car",
|
||||
"score": 0.9,
|
||||
"box_xyxy": [10.0, 10.0, 110.0, 110.0],
|
||||
}
|
||||
],
|
||||
"truth_joined": False,
|
||||
},
|
||||
)
|
||||
metrics = evaluate_frozen_detector_candidates(
|
||||
truth_rows=(_truth_row(),),
|
||||
prediction_rows=rows,
|
||||
valid_fov_mask=Image.new("L", (800, 600), color=255),
|
||||
)
|
||||
assert metrics["yolox-s-kb4-core3"]["ap50"] == 1.0
|
||||
assert metrics["yolox-s-kb4-core3"]["candidate_winner_selected"] is False
|
||||
|
||||
|
||||
def test_l35_rejects_a_freeze_created_after_truth_was_sealed() -> None:
|
||||
with pytest.raises(
|
||||
L35RightYoloxTruthEvaluationError,
|
||||
match="postdates",
|
||||
):
|
||||
_require_freeze_before_truth_seal(
|
||||
freeze_created_at_utc="2026-08-01T02:00:00Z",
|
||||
truth_sealed_at_utc="2026-08-01T01:00:00Z",
|
||||
)
|
||||
|
||||
|
||||
def test_l35_accepts_a_freeze_created_before_truth_was_sealed() -> None:
|
||||
_require_freeze_before_truth_seal(
|
||||
freeze_created_at_utc="2026-08-01T00:30:00Z",
|
||||
truth_sealed_at_utc="2026-08-01T01:00:00Z",
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user