diff --git a/openpilot/selfdrive/ui/mici/onroad/model_renderer.py b/openpilot/selfdrive/ui/mici/onroad/model_renderer.py index ab52934366..5008437805 100644 --- a/openpilot/selfdrive/ui/mici/onroad/model_renderer.py +++ b/openpilot/selfdrive/ui/mici/onroad/model_renderer.py @@ -356,7 +356,8 @@ class ModelRenderer(Widget): path_pts = self._path.projected_points + np.array([self._rect.x, self._rect.y], dtype=np.float32) max_len = min(len(path_pts) // 2, len(self._acceleration_x)) - segment_colors = [] + hues = [] + saturations = [] gradient_stops = [] i = 0 @@ -371,23 +372,24 @@ class ModelRenderer(Widget): lin_grad_point = 1 - (track_y - self._rect.y) / self._rect.height # speed up: 120, slow down: 0 - path_hue = np.clip(60 + self._acceleration_x[i] * 35, 0, 120) + path_hue = 60 + self._acceleration_x[i] * 35 saturation = min(abs(self._acceleration_x[i] * 1.5), 1) - lightness = np.interp(saturation, [0.0, 1.0], [0.95, 0.62]) - alpha = np.interp(lin_grad_point, [0.75 / 2.0, 0.75], [0.4, 0.0]) - - # Use HSL to RGB conversion - color = self._hsla_to_color(path_hue / 360.0, saturation, lightness, alpha) - + hues.append(path_hue) + saturations.append(saturation) gradient_stops.append(lin_grad_point) - segment_colors.append(color) # Skip a point, unless next is last i += 1 + (1 if (i + 2) < max_len else 0) - # Store the gradient in the path object - self._exp_gradient.colors = segment_colors + if gradient_stops: + hues = np.clip(hues, 0, 120) / 360.0 + lightness = np.interp(saturations, [0.0, 1.0], [0.95, 0.62]) + alphas = np.interp(gradient_stops, [0.75 / 2.0, 0.75], [0.4, 0.0]) + self._exp_gradient.colors = [self._hsla_to_color(h, s, l, a) + for h, s, l, a in zip(hues, saturations, lightness, alphas, strict=True)] + else: + self._exp_gradient.colors = [] self._exp_gradient.stops = gradient_stops diff --git a/openpilot/starpilot/ui/tests/test_shared_render_hot_paths.py b/openpilot/starpilot/ui/tests/test_shared_render_hot_paths.py index 637f293c0c..36f7abda37 100644 --- a/openpilot/starpilot/ui/tests/test_shared_render_hot_paths.py +++ b/openpilot/starpilot/ui/tests/test_shared_render_hot_paths.py @@ -80,3 +80,81 @@ class TestModelLineEndpoint: expected = renderer._map_line_to_polygon(expanded, 0.9, 1.2, 2, distance) actual = renderer._map_line_to_polygon(line, 0.9, 1.2, 1, distance) np.testing.assert_allclose(actual, expected, rtol=1e-6, atol=1e-6) + + +class TestExperimentalGradient: + @staticmethod + def renderer(dtype, count, seed): + from openpilot.selfdrive.ui.mici.onroad.model_renderer import ModelRenderer + import pyray as rl + rng = np.random.default_rng(seed) + result = object.__new__(ModelRenderer) + result._experimental_mode = True + result._rect = rl.Rectangle(-12, 21, 536, 240) + result._path = SimpleNamespace(projected_points=rng.uniform(-200, 500, (count, 2)).astype(dtype)) + result._acceleration_x = rng.uniform(-3, 3, 33).astype(dtype) + result._exp_gradient = SimpleNamespace(colors=[], stops=[]) + return result + + @staticmethod + def reference(renderer): + points = renderer._path.projected_points + np.array([renderer._rect.x, renderer._rect.y], dtype=np.float32) + end = min(len(points) // 2, len(renderer._acceleration_x)) + colors, stops = [], [] + index = 0 + while index < end: + y = points[index][1] + if y < renderer._rect.y or y > renderer._rect.y + renderer._rect.height: + index += 1 + continue + stop = 1 - (y - renderer._rect.y) / renderer._rect.height + hue = np.clip(60 + renderer._acceleration_x[index] * 35, 0, 120) + saturation = min(abs(renderer._acceleration_x[index] * 1.5), 1) + lightness = np.interp(saturation, [0.0, 1.0], [0.95, 0.62]) + alpha = np.interp(stop, [0.75 / 2.0, 0.75], [0.4, 0.0]) + color = renderer._hsla_to_color(hue / 360.0, saturation, lightness, alpha) + colors.append((color.r, color.g, color.b, color.a)) + stops.append(stop) + index += 1 + (1 if index + 2 < end else 0) + return colors, stops + + @pytest.mark.parametrize('dtype', [np.float32, np.float64]) + @pytest.mark.parametrize('count', [0, 1, 2, 7, 33, 66]) + @pytest.mark.parametrize('seed', [0, 1, 9]) + def test_batched_gradient_matches_original_scalar_colors_and_clipping(self, dtype, count, seed): + renderer = self.renderer(dtype, count, seed) + expected_colors, expected_stops = self.reference(renderer) + renderer._update_experimental_gradient() + assert [(c.r, c.g, c.b, c.a) for c in renderer._exp_gradient.colors] == expected_colors + assert renderer._exp_gradient.stops == expected_stops + + def test_interpolation_is_batched_without_dropping_gradient_points(self, monkeypatch): + renderer = self.renderer(np.float32, 66, 0) + renderer._path.projected_points[:, 1] = np.linspace(0, 240, 66, dtype=np.float32) + expected_colors, expected_stops = self.reference(renderer) + assert len(expected_colors) > 2 + interp, calls = np.interp, [] + clip, clip_calls = np.clip, [] + + def observed(*args, **kwargs): + calls.append(np.asarray(args[0]).size) + return interp(*args, **kwargs) + + def observed_clip(*args, **kwargs): + clip_calls.append(np.asarray(args[0]).size) + return clip(*args, **kwargs) + + monkeypatch.setattr(np, 'interp', observed) + monkeypatch.setattr(np, 'clip', observed_clip) + renderer._update_experimental_gradient() + assert calls == [len(expected_colors), len(expected_colors)] + assert clip_calls == [len(expected_colors)] + assert [(c.r, c.g, c.b, c.a) for c in renderer._exp_gradient.colors] == expected_colors + assert renderer._exp_gradient.stops == expected_stops + + def test_classic_mode_preserves_existing_gradient(self): + renderer = self.renderer(np.float32, 66, 0) + colors, stops = renderer._exp_gradient.colors, renderer._exp_gradient.stops + renderer._experimental_mode = False + renderer._update_experimental_gradient() + assert renderer._exp_gradient.colors is colors and renderer._exp_gradient.stops is stops