feat(perception): freeze blind detector gates
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.compute.e45_binding_sensitivity import (
|
||||
E45_ROW_SCHEMA,
|
||||
E45BindingSensitivityError,
|
||||
E45BindingSensitivityProfile,
|
||||
analyze_binding_sensitivity,
|
||||
)
|
||||
|
||||
|
||||
def _row(
|
||||
item_id: str,
|
||||
*,
|
||||
radius: float,
|
||||
speed: float,
|
||||
angular: float,
|
||||
lidar_age: float,
|
||||
pose_age: float,
|
||||
residual: float,
|
||||
points: int,
|
||||
) -> dict[str, object]:
|
||||
age_seconds = (lidar_age + pose_age) / 1000.0
|
||||
return {
|
||||
"schema_version": E45_ROW_SCHEMA,
|
||||
"item_id": item_id,
|
||||
"image_radius_normalized": radius,
|
||||
"translation_speed_mps": speed,
|
||||
"angular_speed_deg_s": angular,
|
||||
"lidar_camera_age_ms": lidar_age,
|
||||
"pose_point_age_ms": pose_age,
|
||||
"motion_exposure_translation_m": speed * age_seconds,
|
||||
"motion_exposure_rotation_deg": angular * age_seconds,
|
||||
"centroid_residual_bbox_diagonal": residual,
|
||||
"occupied_points_in_bbox": points,
|
||||
"supported": points >= 2,
|
||||
}
|
||||
|
||||
|
||||
def test_e45_stratifies_existing_diagnostic_residual_without_target_claim() -> None:
|
||||
rows = [
|
||||
_row(
|
||||
"one",
|
||||
radius=0.2,
|
||||
speed=0.0,
|
||||
angular=0.0,
|
||||
lidar_age=5.0,
|
||||
pose_age=2.0,
|
||||
residual=0.1,
|
||||
points=5,
|
||||
),
|
||||
_row(
|
||||
"two",
|
||||
radius=0.7,
|
||||
speed=0.5,
|
||||
angular=6.0,
|
||||
lidar_age=40.0,
|
||||
pose_age=15.0,
|
||||
residual=0.2,
|
||||
points=8,
|
||||
),
|
||||
_row(
|
||||
"three",
|
||||
radius=1.0,
|
||||
speed=2.0,
|
||||
angular=20.0,
|
||||
lidar_age=80.0,
|
||||
pose_age=35.0,
|
||||
residual=0.4,
|
||||
points=12,
|
||||
),
|
||||
]
|
||||
|
||||
analysis = analyze_binding_sensitivity(rows)
|
||||
|
||||
assert analysis["correspondence_count"] == 3
|
||||
assert analysis["supported_fraction"] == 1.0
|
||||
assert [
|
||||
item["count"] for item in analysis["strata"]["image_radius"]
|
||||
] == [1, 1, 1]
|
||||
assert math.isclose(
|
||||
analysis["spearman_residual_correlation"][
|
||||
"image_radius_normalized"
|
||||
],
|
||||
1.0,
|
||||
)
|
||||
assert (
|
||||
analysis["measured_calibration_target_residual_available"] is False
|
||||
)
|
||||
assert analysis["physical_mount_inferred"] is False
|
||||
|
||||
|
||||
def test_e45_rejects_missing_support_in_accepted_correspondence() -> None:
|
||||
rows = [
|
||||
_row(
|
||||
"one",
|
||||
radius=0.2,
|
||||
speed=0.0,
|
||||
angular=0.0,
|
||||
lidar_age=5.0,
|
||||
pose_age=2.0,
|
||||
residual=0.1,
|
||||
points=1,
|
||||
)
|
||||
]
|
||||
|
||||
with pytest.raises(E45BindingSensitivityError, match="supported"):
|
||||
analyze_binding_sensitivity(rows)
|
||||
|
||||
|
||||
def test_e45_profile_rejects_overlapping_or_reversed_edges() -> None:
|
||||
with pytest.raises(E45BindingSensitivityError):
|
||||
E45BindingSensitivityProfile(image_radius_edges=(0.8, 0.4))
|
||||
@@ -0,0 +1,67 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.compute.e46_detector_truth_island import (
|
||||
E46DetectorTruthIslandError,
|
||||
E46DetectorTruthIslandProfile,
|
||||
select_truth_island_frames,
|
||||
)
|
||||
|
||||
|
||||
def _frames() -> list[dict[str, object]]:
|
||||
rows = []
|
||||
image_id = 1
|
||||
for frame_index in range(48):
|
||||
rows.append(
|
||||
{
|
||||
"image_id": image_id,
|
||||
"frame_index": frame_index * 10,
|
||||
"role": "anchor",
|
||||
"group_id": f"anchor-{image_id:03d}",
|
||||
}
|
||||
)
|
||||
image_id += 1
|
||||
for group_index, start in enumerate((500, 600, 700, 800), start=1):
|
||||
for offset in range(4):
|
||||
rows.append(
|
||||
{
|
||||
"image_id": image_id,
|
||||
"frame_index": start + offset,
|
||||
"role": "temporal",
|
||||
"group_id": f"clip-{group_index}",
|
||||
}
|
||||
)
|
||||
image_id += 1
|
||||
rows.sort(key=lambda row: int(row["frame_index"]))
|
||||
return rows
|
||||
|
||||
|
||||
def test_e46_selects_two_anchors_per_bin_and_all_temporal_groups() -> None:
|
||||
frames = _frames()
|
||||
|
||||
selected = select_truth_island_frames(frames)
|
||||
|
||||
assert len(selected) == 32
|
||||
assert sum(row["role"] == "anchor" for row in selected) == 16
|
||||
assert sum(row["role"] == "temporal" for row in selected) == 16
|
||||
assert {
|
||||
row["group_id"]
|
||||
for row in selected
|
||||
if row["role"] == "temporal"
|
||||
} == {"clip-1", "clip-2", "clip-3", "clip-4"}
|
||||
assert selected == select_truth_island_frames(frames)
|
||||
|
||||
|
||||
def test_e46_rejects_partial_or_nonconsecutive_temporal_group() -> None:
|
||||
frames = _frames()
|
||||
frames[-1]["frame_index"] = 900
|
||||
frames.sort(key=lambda row: int(row["frame_index"]))
|
||||
|
||||
with pytest.raises(E46DetectorTruthIslandError, match="consecutive"):
|
||||
select_truth_island_frames(frames)
|
||||
|
||||
|
||||
def test_e46_profile_requires_two_reviewers_and_no_prelabel_mode() -> None:
|
||||
with pytest.raises(E46DetectorTruthIslandError):
|
||||
E46DetectorTruthIslandProfile(independent_reviewers_required=1)
|
||||
@@ -0,0 +1,75 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.compute.e47_detector_candidate_freeze import (
|
||||
E47DetectorCandidateFreezeError,
|
||||
normalize_candidate_predictions,
|
||||
)
|
||||
|
||||
|
||||
def test_e47_normalizes_raw_predictions_and_drops_outside_ontology() -> None:
|
||||
result = normalize_candidate_predictions(
|
||||
instances=[
|
||||
{
|
||||
"label": "car",
|
||||
"score": 0.8,
|
||||
"box_xyxy": [10.0, 20.0, 30.0, 40.0],
|
||||
},
|
||||
{
|
||||
"label": "laptop",
|
||||
"score": 0.9,
|
||||
"box_xyxy": [20.0, 30.0, 40.0, 50.0],
|
||||
},
|
||||
],
|
||||
label_mapping={"car": 4},
|
||||
target_categories={4: "car"},
|
||||
source_kind="raw",
|
||||
)
|
||||
|
||||
assert result == (
|
||||
{
|
||||
"category_id": 4,
|
||||
"category": "car",
|
||||
"source_category": "car",
|
||||
"score": 0.8,
|
||||
"box_xyxy": [10.0, 20.0, 30.0, 40.0],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_e47_normalizes_fill_predictions_without_truth_fields() -> None:
|
||||
result = normalize_candidate_predictions(
|
||||
instances=[
|
||||
{
|
||||
"draft_category_id": 1,
|
||||
"source_model_category": "person",
|
||||
"score": 0.95,
|
||||
"box_xyxy": [1.0, 2.0, 3.0, 4.0],
|
||||
"review_state": "unreviewed-model-draft",
|
||||
}
|
||||
],
|
||||
label_mapping={},
|
||||
target_categories={1: "person"},
|
||||
source_kind="fill",
|
||||
)
|
||||
|
||||
assert result[0]["category"] == "person"
|
||||
assert "review_state" not in result[0]
|
||||
assert "truth" not in result[0]
|
||||
|
||||
|
||||
def test_e47_rejects_invalid_box() -> None:
|
||||
with pytest.raises(E47DetectorCandidateFreezeError, match="box"):
|
||||
normalize_candidate_predictions(
|
||||
instances=[
|
||||
{
|
||||
"label": "car",
|
||||
"score": 0.8,
|
||||
"box_xyxy": [30.0, 20.0, 10.0, 40.0],
|
||||
}
|
||||
],
|
||||
label_mapping={"car": 4},
|
||||
target_categories={4: "car"},
|
||||
source_kind="raw",
|
||||
)
|
||||
Reference in New Issue
Block a user