From faf81bb4616fc537bfc0856bcde9376e49bee36c Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Thu, 5 Nov 2026 11:53:09 -0600 Subject: [PATCH] Separate stacked speed-limit panels before reading numbers --- .../starpilot/speed_limits/vision/model.py | 20 +++++++ .../vision/tests/test_speed_panels.py | 60 +++++++++++++++++++ prebuilt.json | 2 +- 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 openpilot/starpilot/speed_limits/vision/tests/test_speed_panels.py diff --git a/openpilot/starpilot/speed_limits/vision/model.py b/openpilot/starpilot/speed_limits/vision/model.py index 85dafa7685..e525f89780 100644 --- a/openpilot/starpilot/speed_limits/vision/model.py +++ b/openpilot/starpilot/speed_limits/vision/model.py @@ -267,6 +267,25 @@ class HistoryEntry: created_at: float strong_consensus: bool = False +def _primary_regulatory_panel(image): + height, width = image.shape[:2] + if image.dtype != np.uint8 or width < 24 or height < 1.8 * width: + return image + + gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + _, mask = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) + for foreground in (mask >= 128, mask < 128): + occupied = foreground[:, int(width * .2):int(width * .8)].mean(1) > .8 + edges = np.diff(np.r_[False, occupied, False].astype(np.int8)) + runs = list(zip(np.flatnonzero(edges == 1), np.flatnonzero(edges == -1), strict=False)) + for (top, bottom), (next_top, next_bottom) in zip(runs, runs[1:], strict=False): + if (top <= .3 * width and .75 * width <= bottom - top <= 1.6 * width and + next_bottom - next_top >= .5 * width and .02 * width <= next_top - bottom <= .15 * width): + # Keep a separate lower plate, such as MINIMUM, out of the number reader. + return image[:min(height, int(bottom + .03 * width))] + return image + + class VisionModelCore: """The frozen active detector/classifier path, with no camera or Params ownership.""" @@ -692,6 +711,7 @@ class VisionModelCore: speed_class_count = len(US_CLASSIFIER_SPEED_VALUES) input_size = self.classifier_input_size + sign_crop = _primary_regulatory_panel(sign_crop) padded_crop = self._square_resize(sign_crop, size=input_size) blob = cv2.dnn.blobFromImage(padded_crop, scalefactor=1 / 255.0, size=(input_size, input_size), swapRB=True, crop=False) self.classifier_net.setInput(blob) diff --git a/openpilot/starpilot/speed_limits/vision/tests/test_speed_panels.py b/openpilot/starpilot/speed_limits/vision/tests/test_speed_panels.py new file mode 100644 index 0000000000..651065791b --- /dev/null +++ b/openpilot/starpilot/speed_limits/vision/tests/test_speed_panels.py @@ -0,0 +1,60 @@ +import unittest + +import numpy as np + +from openpilot.starpilot.speed_limits.vision.model import ( + US_CLASSIFIER_SPEED_VALUES, VisionModelCore, _primary_regulatory_panel, +) + + +def stacked_panels(): + image = np.full((200, 100, 3), 200, dtype=np.uint8) + image[5:105, 10:90] = 20 + image[108:190, 10:90] = 20 + return image + + +class TestSpeedPanels(unittest.TestCase): + def test_separate_lower_plate_is_excluded(self): + image = stacked_panels() + crop = _primary_regulatory_panel(image) + self.assertGreaterEqual(crop.shape[0], 105) + self.assertLessEqual(crop.shape[0], 108) + np.testing.assert_array_equal(crop, image[:crop.shape[0]]) + + def test_single_plate_and_short_header_keep_original_input(self): + single = np.full((200, 100, 3), 200, dtype=np.uint8) + single[5:195, 10:90] = 20 + header = single.copy() + header[30:40] = 200 + for image in (single, header, stacked_panels().astype(np.float32), np.zeros((40, 30, 3), dtype=np.uint8)): + self.assertIs(_primary_regulatory_panel(image), image) + + def test_read_uses_one_existing_classifier_forward(self): + class Net: + calls = 0 + + def setInput(self, blob): + self.blob = blob + + def forward(self): + self.calls += 1 + scores = np.zeros((1, len(US_CLASSIFIER_SPEED_VALUES) + 1), dtype=np.float32) + scores[0, US_CLASSIFIER_SPEED_VALUES.index(70)] = .99 + return scores + + core = object.__new__(VisionModelCore) + core.classifier_net = Net() + core.classifier_input_size = 128 + core.classifier_logits_layer = None + core.last_classifier_forward_count = 0 + core.last_classifier_forward_duration_s = 0. + result = core._classify_speed_limit_from_model(stacked_panels()) + assert result is not None + self.assertEqual(result[0], 70) + self.assertEqual(core.classifier_net.calls, 1) + self.assertEqual(core.last_classifier_forward_count, 1) + + +if __name__ == '__main__': + unittest.main() diff --git a/prebuilt.json b/prebuilt.json index d652886fb4..e2fc2b882f 100644 --- a/prebuilt.json +++ b/prebuilt.json @@ -1 +1 @@ -{"protected_inventory_sha256": "46a4163eb60a227519f3e01c28cfad309e5eefd27132298487d072c43be4a31c", "schema_version": 1} +{"protected_inventory_sha256": "3f48edce7f387d0e45d93fd3e6dd8cdad10f0e88def45bb818e65882bf2d977e", "schema_version": 1}