diff --git a/openpilot/starpilot/speed_limits/vision/model.py b/openpilot/starpilot/speed_limits/vision/model.py index af6ee32083..43955ee3a6 100644 --- a/openpilot/starpilot/speed_limits/vision/model.py +++ b/openpilot/starpilot/speed_limits/vision/model.py @@ -474,45 +474,53 @@ class VisionModelCore: white_mask = ((value >= REGULATORY_WHITE_VALUE_MIN) & (saturation <= REGULATORY_WHITE_SAT_MAX)).astype(np.uint8) dark_mask = ((value <= REGULATORY_DARK_VALUE_MAX) & (saturation <= REGULATORY_DARK_SAT_MAX)).astype(np.uint8) + + white_ratio = float(white_mask.mean()) + dark_ratio = float(dark_mask.mean()) + + if white_ratio < REGULATORY_MIN_WHITE_RATIO or dark_ratio < REGULATORY_MIN_DARK_RATIO: + return False + yellow_mask = ( (hue >= REGULATORY_YELLOW_HUE_MIN) & (hue <= REGULATORY_YELLOW_HUE_MAX) & (saturation >= REGULATORY_YELLOW_SAT_MIN) & (value >= REGULATORY_YELLOW_VALUE_MIN) ).astype(np.uint8) + yellow_ratio = float(yellow_mask.mean()) + + if yellow_ratio > REGULATORY_MAX_YELLOW_RATIO and yellow_ratio > white_ratio * 0.45: + return False + red_mask = ( (((hue <= REGULATORY_RED_LOW_HUE_MAX) | (hue >= REGULATORY_RED_HIGH_HUE_MIN))) & (saturation >= REGULATORY_RED_SAT_MIN) & (value >= REGULATORY_RED_VALUE_MIN) ).astype(np.uint8) + red_ratio = float(red_mask.mean()) + + if red_ratio > REGULATORY_MAX_RED_RATIO and red_ratio > white_ratio * 0.35: + return False + green_mask = ( (hue >= REGULATORY_GREEN_HUE_MIN) & (hue <= REGULATORY_GREEN_HUE_MAX) & (saturation >= REGULATORY_COLORED_SAT_MIN) & (value >= REGULATORY_COLORED_VALUE_MIN) ).astype(np.uint8) + green_ratio = float(green_mask.mean()) + + if green_ratio > REGULATORY_MAX_GREEN_RATIO and green_ratio > white_ratio * 0.60: + return False + blue_mask = ( (hue >= REGULATORY_BLUE_HUE_MIN) & (hue <= REGULATORY_BLUE_HUE_MAX) & (saturation >= REGULATORY_COLORED_SAT_MIN) & (value >= REGULATORY_COLORED_VALUE_MIN) ).astype(np.uint8) - - white_ratio = float(white_mask.mean()) - dark_ratio = float(dark_mask.mean()) - yellow_ratio = float(yellow_mask.mean()) - red_ratio = float(red_mask.mean()) - green_ratio = float(green_mask.mean()) blue_ratio = float(blue_mask.mean()) - if white_ratio < REGULATORY_MIN_WHITE_RATIO or dark_ratio < REGULATORY_MIN_DARK_RATIO: - return False - if yellow_ratio > REGULATORY_MAX_YELLOW_RATIO and yellow_ratio > white_ratio * 0.45: - return False - if red_ratio > REGULATORY_MAX_RED_RATIO and red_ratio > white_ratio * 0.35: - return False - if green_ratio > REGULATORY_MAX_GREEN_RATIO and green_ratio > white_ratio * 0.60: - return False if blue_ratio > REGULATORY_MAX_BLUE_RATIO and blue_ratio > white_ratio * 0.60: return False diff --git a/openpilot/starpilot/speed_limits/vision/tests/test_material_validation.py b/openpilot/starpilot/speed_limits/vision/tests/test_material_validation.py new file mode 100644 index 0000000000..c9fc5f1690 --- /dev/null +++ b/openpilot/starpilot/speed_limits/vision/tests/test_material_validation.py @@ -0,0 +1,70 @@ +"""Pure regulatory-material pixels; no model construction or inference.""" +import unittest +from unittest.mock import patch + +import numpy as np + +from openpilot.starpilot.speed_limits.vision import model + + +@unittest.skipIf(model.cv2 is None, 'OpenCV is optional') +class TestMaterialValidation(unittest.TestCase): + def validate(self, image): + return model.VisionModelCore._is_regulatory_speed_sign(None, image) + + def panel(self, hue=None): + hsv = np.zeros((100, 80, 3), np.uint8) + if hue is not None: + hsv[:] = (hue, 255, 255) + hsv[10:90, 15:65] = (0, 0, 255) + hsv[40:50, 25:55] = 0 + return model.cv2.cvtColor(hsv, model.cv2.COLOR_HSV2BGR) + + def test_plain_panel_and_each_color_veto(self): + self.assertTrue(self.validate(self.panel())) + for hue in (25, 0, 60, 110): + with self.subTest(hue=hue): + self.assertFalse(self.validate(self.panel(hue))) + + def test_empty_white_dark_and_component_rejections(self): + self.assertFalse(self.validate(np.empty((0, 0, 3), np.uint8))) + for value in (0, 255): + self.assertFalse(self.validate(np.full((100, 80, 3), value, np.uint8))) + image = np.zeros((100, 80, 3), np.uint8) + image[40:60, 5:75] = 255 + self.assertFalse(self.validate(image)) + + def test_early_rejection_does_not_evaluate_later_hue_masks(self): + operations = [] + + class Hue: + def __init__(self, values): + self.values = values + + def __ge__(self, value): + operations.append(('ge', value)) + return self.values >= value + + def __le__(self, value): + operations.append(('le', value)) + return self.values <= value + + class HSV: + def __init__(self, values): + self.values = values + + def __getitem__(self, key): + values = self.values[key] + return Hue(values) if key[2] == 0 else values + + for image, expected in ((np.zeros((100, 80, 3), np.uint8), []), + (self.panel(25), [('ge', 12), ('le', 45)])): + operations.clear() + hsv = model.cv2.cvtColor(image, model.cv2.COLOR_BGR2HSV) + with patch.object(model.cv2, 'cvtColor', return_value=HSV(hsv)): + self.assertFalse(self.validate(image)) + self.assertEqual(operations, expected) + + +if __name__ == '__main__': + unittest.main() diff --git a/prebuilt.json b/prebuilt.json index f7e9be68be..37fd254f15 100644 --- a/prebuilt.json +++ b/prebuilt.json @@ -1 +1 @@ -{"protected_inventory_sha256": "146409f6a49f160356ba631732972e7870ff88605cee0497e77787de656fb9bd", "schema_version": 1} +{"protected_inventory_sha256": "e2a34c53b21d7fe71b765a7f4889f8139ee4aa3a0c4d25c3bd41cf195ed94aa0", "schema_version": 1}