Batch path gradient interpolation

This commit is contained in:
firestar5683
2026-11-01 12:17:17 -06:00
parent 195b7000aa
commit b70b7e16c4
2 changed files with 91 additions and 11 deletions
@@ -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
@@ -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