Separate stacked speed-limit panels before reading numbers

This commit is contained in:
firestar5683
2026-11-05 11:53:09 -06:00
parent ac2b90145f
commit faf81bb461
3 changed files with 81 additions and 1 deletions
@@ -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)
@@ -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()
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "46a4163eb60a227519f3e01c28cfad309e5eefd27132298487d072c43be4a31c", "schema_version": 1}
{"protected_inventory_sha256": "3f48edce7f387d0e45d93fd3e6dd8cdad10f0e88def45bb818e65882bf2d977e", "schema_version": 1}