Files
StarPilot/openpilot/starpilot/speed_limits/vision/tests/test_proposal_decode.py
T
2026-10-23 08:34:37 -05:00

76 lines
3.0 KiB
Python

import unittest
from types import SimpleNamespace
from unittest.mock import patch
import numpy as np
from openpilot.starpilot.speed_limits.vision import model
class TestProposalDecode(unittest.TestCase):
@staticmethod
def decode(predictions, threshold=0.06):
core = model.VisionModelCore.__new__(model.VisionModelCore)
core.detector_input_size = 256
core.last_detector_forward_count = 0
core.last_detector_forward_duration_s = 0.0
core.net = SimpleNamespace(setInput=lambda _: None, forward=lambda: predictions)
backend = SimpleNamespace(dnn=SimpleNamespace(blobFromImage=lambda *a, **kw: None))
with patch.object(model, 'cv2', backend), patch.object(core, '_letterbox', return_value=(None, 1.0, 0.0, 0.0)):
result = core._collect_detector_classifier_proposals_from_region(
np.zeros((1080, 1920, 3), dtype=np.uint8), 10, 20, 1920, 1080, threshold)
assert core.last_detector_forward_count == 1
return result
def test_scores_keep_row_order_supported_classes_and_first_tie(self):
rows = np.zeros((16, 9), dtype=np.float32)
rows[:, :4] = [1000, 300, 40, 70]
rows[:, 4] = 0.01
rows[0, 4:6] = 0.9
rows[2, 6] = 0.8
rows[4, 7] = 0.99 # An unsupported class cannot become a sign proposal.
rows[6, 5] = 0.7
expected = [(float(rows[i, 4 + c]), c, (990, 285, 1030, 355)) for i, c in ((0, 0), (2, 2), (6, 1))]
self.assertEqual(self.decode(rows), expected)
self.assertEqual(self.decode(rows.T), expected)
def test_threshold_uses_python_float_precision(self):
for dtype in (np.float16, np.float32, np.float64):
with self.subTest(dtype=dtype):
rows = np.zeros((8, 5), dtype=dtype)
rows[:, :4] = [1000, 300, 40, 70]
rows[:, 4] = dtype(0.06)
score = float(rows[0, 4])
self.assertEqual(len(self.decode(rows, score)), 8)
self.assertEqual(self.decode(rows, np.nextafter(score, np.inf)), [])
self.assertEqual(len(self.decode(rows, np.nextafter(score, -np.inf))), 8)
def test_nonfinite_scores_preserve_existing_decoder_behavior(self):
rows = np.zeros((8, 5), dtype=np.float32)
rows[:, :4] = [1000, 300, 40, 70]
rows[:, 4] = -np.inf
rows[1, 4] = np.nan
rows[5, 4] = np.inf
result = self.decode(rows)
self.assertEqual(len(result), 2)
self.assertTrue(np.isnan(result[0][0]))
self.assertEqual(result[1][0], np.inf)
self.assertTrue(all(row[1:] == (0, (990, 285, 1030, 355)) for row in result))
def test_invalid_rank_has_no_proposals(self):
self.assertEqual(self.decode(np.zeros((2, 2, 2))), [])
self.assertEqual(self.decode(np.zeros((1, 5))), [])
def test_geometry_filters_remain_after_score_admission(self):
rows = np.zeros((8, 5), dtype=np.float32)
rows[:, :4] = [1000, 300, 40, 70]
rows[:, 4] = 0.9
rows[1, 2] = 5
rows[2, 3] = 5
rows[3, 0] = 100
rows[4, 1] = 1000
rows[5, 2:4] = 1500
result = self.decode(rows)
self.assertEqual(len(result), 3)
self.assertTrue(all(row[2] == (990, 285, 1030, 355) for row in result))