feat(perception): add frozen yolox provider

This commit is contained in:
DCCONSTRUCTIONS
2026-08-05 14:09:51 +03:00
parent 029b486c67
commit a680debfaf
10 changed files with 965 additions and 11 deletions
+10 -7
View File
@@ -84,16 +84,19 @@ def test_new_perception_boundary_has_no_experiment_specific_imports() -> None:
assert violations == {}
def test_reference_graph_imports_only_an_admitted_compute_primitive() -> None:
def test_product_perception_imports_only_admitted_compute_primitives() -> None:
inventory = validate_reuse_inventory(REUSE_PATH)
admitted = {
item["module"]
for item in inventory["reusable_primitives"]
if isinstance(item, dict) and isinstance(item.get("module"), str)
}
compute_imports = {
module
for module in _imports(PERCEPTION_ROOT / "graph.py")
if module.startswith("k1link.compute")
}
assert compute_imports <= admitted
violations: dict[str, set[str]] = {}
for path in PERCEPTION_ROOT.glob("*.py"):
compute_imports = {
module for module in _imports(path) if module.startswith("k1link.compute")
}
unadmitted = compute_imports - admitted
if unadmitted:
violations[path.name] = unadmitted
assert violations == {}
+24
View File
@@ -50,6 +50,7 @@ from k1link.perception.providers import (
SourcePacket,
)
from k1link.perception.recorded_source import (
DecodedRecordedSource,
RecordedRavnoves00Source,
RecordedSourceError,
ReplayPacing,
@@ -529,6 +530,29 @@ def test_recorded_source_rejects_timeline_mismatch(tmp_path: Path) -> None:
list(source.packets(Event()))
def test_decoded_recorded_source_attaches_images_without_detector_logic(tmp_path: Path) -> None:
camera_path, timeline_path = _write_recorded_fixture(tmp_path)
source = RecordedRavnoves00Source(
camera_index_path=camera_path,
source_pack_path=timeline_path,
expected_frame_count=2,
expected_source_pack_sha256=None,
)
class Decoder:
def frames(self, stop_event: Event) -> Iterator[np.ndarray]:
for value in (3, 7):
if stop_event.is_set():
return
yield np.full((600, 800, 3), value, dtype=np.uint8)
packets = list(DecodedRecordedSource(source=source, decoder=Decoder()).packets(Event()))
assert len(packets) == 2
assert isinstance(packets[0].image_payload, np.ndarray)
assert int(packets[0].image_payload[0, 0, 0]) == 3
assert int(packets[1].image_payload[0, 0, 0]) == 7
def test_camera_only_path_never_invents_metric_occupancy_or_free_space() -> None:
result = _graph(_Source((_packet(0, lidar=False),))).run()
delivery = result.deliveries[0]
+214
View File
@@ -0,0 +1,214 @@
from __future__ import annotations
import json
import math
from pathlib import Path
from threading import Event
import numpy as np
import pytest
from numpy.typing import NDArray
from k1link.compute.yolox_object_detector import (
FrozenYoloxConfig,
YoloxDetection,
YoloxDetectorError,
postprocess_yolox,
preprocess_raw_kb4,
)
from k1link.perception.contracts import (
ClockBasis,
ModalityOutcome,
ModalityStatus,
SourceEnvelope,
TimestampBundle,
)
from k1link.perception.detector import (
FROZEN_YOLOX_PROVIDER_ID,
DetectorProviderError,
FrozenYoloxDetectorProvider,
proposals_from_detections,
)
from k1link.perception.providers import SourcePacket
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
E46J_FRAMES = (
REPOSITORY_ROOT
/ ".runtime/compute-experiments/e46j/results"
/ "e46j-raw-fisheye-realtime-7119ce4344438eaa0e748db65aa044e9f9f4a0a226e5eea037a7180d0bc7ace7"
/ "frames.jsonl"
)
def _status() -> ModalityStatus:
return ModalityStatus(True, ModalityOutcome.AVAILABLE, "test-available")
def _packet(sequence: int, image: object) -> SourcePacket:
return SourcePacket(
envelope=SourceEnvelope(
source_id="RAVNOVES00",
session_id="20260720T065719Z_viewer_live",
frame_id=f"frame-{sequence:06d}",
sequence=sequence,
timestamps=TimestampBundle(
utc_ns=1_000 + sequence,
monotonic_ns=2_000 + sequence,
source_ns=3_000 + sequence,
clock_basis=ClockBasis.RECORDED_HOST,
),
source_age_ns=0,
binding_reason="test-recorded-source",
calibration_id="camera-1-kb4-test",
representation_id="registered-map-increment-v1",
image=_status(),
registered_point_increment=_status(),
pose=_status(),
),
image_payload=image,
registered_point_increment_payload=("points", sequence),
pose_payload=("pose", sequence),
)
class _Resizer:
def resize(
self,
image: NDArray[np.uint8],
width: int,
height: int,
) -> NDArray[np.uint8]:
assert image.shape == (600, 800, 3)
return np.zeros((height, width, 3), dtype=np.uint8)
class _Backend:
def __init__(self, output: NDArray[np.float32]) -> None:
self.output = output
self.calls = 0
def infer(self, tensor: NDArray[np.float32]) -> NDArray[np.float32]:
assert tensor.shape == (1, 3, 640, 640)
self.calls += 1
return self.output
def _one_person_output() -> NDArray[np.float32]:
output = np.zeros((1, 8400, 85), dtype=np.float32)
output[0, 0, :4] = [40.0, 30.0, math.log(10.0), math.log(10.0)]
output[0, 0, 4] = 0.9
output[0, 0, 5] = 0.9
return output
def test_frozen_preprocess_and_postprocess_match_the_e46j_contract() -> None:
image = np.full((600, 800, 3), 7, dtype=np.uint8)
mask = np.ones((600, 800), dtype=np.bool_)
tensor = preprocess_raw_kb4(image, mask, resizer=_Resizer())
result = postprocess_yolox(_one_person_output(), mask)
assert tensor.shape == (1, 3, 640, 640)
assert np.all(tensor[:, :, 480:, :] == 114)
assert result.rejected == ()
assert len(result.detections) == 1
assert result.detections[0].label == "person"
assert result.detections[0].score == pytest.approx(0.81)
assert result.detections[0].bbox_xyxy == pytest.approx((350.0, 250.0, 450.0, 350.0))
def test_provider_emits_class_optional_product_proposals_and_metrics() -> None:
backend = _Backend(_one_person_output())
provider = FrozenYoloxDetectorProvider(
mask=np.ones((600, 800), dtype=np.bool_),
backend=backend,
resizer=_Resizer(),
clock_ns=iter((10, 20)).__next__,
)
packet = _packet(7, np.zeros((600, 800, 3), dtype=np.uint8))
proposals = provider.detect(packet)
assert backend.calls == 1
assert len(proposals) == 1
assert proposals[0].proposal_id == "proposal-7-0"
assert proposals[0].provider_id == FROZEN_YOLOX_PROVIDER_ID
assert proposals[0].semantic_hint == "person"
assert proposals[0].provider_tracklet is None
assert provider.snapshot().proposal_count == 1
assert provider.snapshot().core_duration_ns == 10
def test_provider_accounts_zero_pathological_and_failed_frames() -> None:
mask = np.ones((600, 800), dtype=np.bool_)
zero = FrozenYoloxDetectorProvider(
mask=mask,
backend=_Backend(np.zeros((1, 8400, 85), dtype=np.float32)),
resizer=_Resizer(),
)
assert zero.detect(_packet(0, np.zeros((600, 800, 3), dtype=np.uint8))) == ()
assert zero.snapshot().zero_proposal_frames == 1
pathological_output = _one_person_output()
pathological_output[0, 0, 2:4] = 1000.0
pathological = FrozenYoloxDetectorProvider(
mask=mask,
backend=_Backend(pathological_output),
resizer=_Resizer(),
)
assert pathological.detect(_packet(1, np.zeros((600, 800, 3), dtype=np.uint8))) == ()
assert dict(pathological.snapshot().rejected)["nonfinite"] == 1
failed = FrozenYoloxDetectorProvider(
mask=mask,
backend=_Backend(_one_person_output()),
resizer=_Resizer(),
)
with pytest.raises(DetectorProviderError, match="decoded BGR"):
failed.detect(_packet(2, "opaque-reference"))
assert failed.snapshot().failed_frames == 1
def test_frozen_profile_rejects_in_place_threshold_tuning() -> None:
with pytest.raises(YoloxDetectorError, match="cannot be tuned"):
FrozenYoloxConfig(minimum_score=0.51)
def test_all_4489_accepted_e46j_frames_map_to_product_contract_without_class_routing() -> None:
assert E46J_FRAMES.is_file()
image = np.zeros((600, 800, 3), dtype=np.uint8)
frame_count = 0
proposal_count = 0
zero_frames = 0
for line in E46J_FRAMES.read_text("utf-8").splitlines():
row = json.loads(line)
detections = tuple(
YoloxDetection(
class_id=item["class_id"],
label=item["label"],
score=item["score"],
bbox_xyxy=tuple(item["bbox_xyxy"]),
valid_fov_fraction=item["valid_fov_fraction"],
)
for item in row["detections"]
)
proposals = proposals_from_detections(_packet(frame_count, image), detections)
assert all(proposal.source_id == "RAVNOVES00" for proposal in proposals)
assert all(proposal.provider_tracklet is None for proposal in proposals)
assert len({proposal.proposal_id for proposal in proposals}) == len(proposals)
proposal_count += len(proposals)
zero_frames += not proposals
frame_count += 1
assert frame_count == 4489
assert proposal_count == 15499
assert zero_frames == 181
def test_source_packet_requires_a_decoded_image_before_real_provider_execution() -> None:
provider = FrozenYoloxDetectorProvider(
mask=np.ones((600, 800), dtype=np.bool_),
backend=_Backend(_one_person_output()),
resizer=_Resizer(),
)
with pytest.raises(DetectorProviderError, match="decoded BGR"):
provider.detect(_packet(0, Event()))