Mici customize?

This commit is contained in:
firestar5683
2026-04-19 00:59:39 -05:00
parent ad19135aa5
commit 979015cd20
4 changed files with 206 additions and 54 deletions
+92
View File
@@ -0,0 +1,92 @@
from __future__ import annotations
import json
from pathlib import Path
import pyray as rl
from openpilot.common.basedir import BASEDIR
ACTIVE_THEME_COLORS_PATH = Path(BASEDIR) / "starpilot/assets/active_theme/colors/colors.json"
STOCK_THEME_COLORS_PATH = Path(BASEDIR) / "starpilot/assets/stock_theme/colors/colors.json"
_FALLBACK_THEME_COLORS = {
"LaneLines": (255, 255, 255, 178),
"LeadMarker": (201, 34, 49, 255),
"Path": (48, 255, 156, 255),
"PathEdge": (38, 209, 125, 255),
"Sidebar1": (255, 255, 255, 178),
"Sidebar2": (255, 255, 255, 255),
"Sidebar3": (255, 255, 255, 255),
}
_THEME_COLOR_CACHE: dict[str, object] = {
"stamp": None,
"colors": None,
}
def _file_stamp(path: Path) -> tuple[str, int | None, int | None]:
try:
stat = path.stat()
return (str(path), stat.st_mtime_ns, stat.st_size)
except OSError:
return (str(path), None, None)
def _clamp_channel(value: object, fallback: int) -> int:
try:
return max(0, min(255, int(value)))
except (TypeError, ValueError):
return fallback
def _load_color_map(path: Path) -> dict[str, dict[str, int]]:
try:
with path.open() as f:
data = json.load(f)
if isinstance(data, dict):
return data
except (OSError, ValueError, TypeError):
pass
return {}
def _build_color(entry: object, fallback: tuple[int, int, int, int]) -> rl.Color:
if isinstance(entry, dict):
red, green, blue, alpha = fallback
return rl.Color(
_clamp_channel(entry.get("red"), red),
_clamp_channel(entry.get("green"), green),
_clamp_channel(entry.get("blue"), blue),
_clamp_channel(entry.get("alpha"), alpha),
)
return rl.Color(*fallback)
def _load_theme_colors() -> dict[str, rl.Color]:
stamp = (_file_stamp(STOCK_THEME_COLORS_PATH), _file_stamp(ACTIVE_THEME_COLORS_PATH))
if stamp == _THEME_COLOR_CACHE["stamp"] and _THEME_COLOR_CACHE["colors"] is not None:
return _THEME_COLOR_CACHE["colors"] # type: ignore[return-value]
stock_colors = _load_color_map(STOCK_THEME_COLORS_PATH)
active_colors = _load_color_map(ACTIVE_THEME_COLORS_PATH)
colors = {}
for key, fallback in _FALLBACK_THEME_COLORS.items():
colors[key] = _build_color(active_colors.get(key, stock_colors.get(key)), fallback)
_THEME_COLOR_CACHE["stamp"] = stamp
_THEME_COLOR_CACHE["colors"] = colors
return colors
def get_theme_color(key: str, fallback: rl.Color | None = None) -> rl.Color:
color = _load_theme_colors().get(key)
if color is not None:
return color
return fallback if fallback is not None else rl.WHITE
def with_alpha(color: rl.Color, alpha: int) -> rl.Color:
return rl.Color(color.r, color.g, color.b, max(0, min(255, int(alpha))))
+57 -10
View File
@@ -4,8 +4,10 @@ import pyray as rl
from cereal import messaging, car
from dataclasses import dataclass, field
from openpilot.common.params import Params
from openpilot.common.constants import CV
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT
from openpilot.selfdrive.ui.lib.starpilot_theme import get_theme_color, with_alpha
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
from openpilot.selfdrive.ui.mici.onroad import blend_colors
from openpilot.selfdrive.ui.mici.onroad.starpilot_status import get_border_color, get_path_edge_color
@@ -79,7 +81,8 @@ class ModelRenderer(Widget):
)
# Get longitudinal control setting from car parameters
if car_params := Params().get("CarParams"):
self._params = Params()
if car_params := self._params.get("CarParams"):
cp = messaging.log_from_bytes(car_params, car.CarParams)
self._longitudinal_control = cp.openpilotLongitudinalControl
@@ -184,21 +187,36 @@ class ModelRenderer(Widget):
def _update_model(self, lead, path_x_array):
"""Update model visualization data based on model message"""
model_ui_enabled = self._params.get_bool("ModelUI", default=True)
if model_ui_enabled:
path_width = self._path_width_to_half_m(self._params.get_float("PathWidth", return_default=True, default=6.1))
lane_line_width = self._small_distance_to_half_m(self._params.get_float("LaneLinesWidth", return_default=True, default=4.0))
road_edge_width = self._small_distance_to_half_m(self._params.get_float("RoadEdgesWidth", return_default=True, default=2.0))
else:
path_width = 0.9
lane_line_width = 0.025
road_edge_width = 0.025
if model_ui_enabled and self._params.get_bool("DynamicPathWidth", default=False):
if ui_state.status == UIStatus.ENGAGED:
path_width *= 1.0
elif ui_state.always_on_lateral_active:
path_width *= 0.75
else:
path_width *= 0.50
max_distance = np.clip(path_x_array[-1], MIN_DRAW_DISTANCE, MAX_DRAW_DISTANCE)
max_idx = self._get_path_length_idx(self._lane_lines[0].raw_points[:, 0], max_distance)
# Update lane lines using raw points
line_width_factor = 0.12
for i, lane_line in enumerate(self._lane_lines):
if i in (1, 2):
line_width_factor = 0.16
lane_line.projected_points = self._map_line_to_polygon(
lane_line.raw_points, line_width_factor * self._lane_line_probs[i], 0.0, max_idx
lane_line.raw_points, lane_line_width * self._lane_line_probs[i], 0.0, max_idx
)
# Update road edges using raw points
for road_edge in self._road_edges:
road_edge.projected_points = self._map_line_to_polygon(road_edge.raw_points, line_width_factor, 0.0, max_idx)
road_edge.projected_points = self._map_line_to_polygon(road_edge.raw_points, road_edge_width, 0.0, max_idx)
# Update path using raw points
if lead and lead.status:
@@ -214,7 +232,7 @@ class ModelRenderer(Widget):
high_pass_acceleration = self._acceleration_x_filter.x - self._acceleration_x_filter2.x
y_off = np.interp(high_pass_acceleration, [-1, 0, 1], [0.9 * 2, 0.9, 0.9 / 2])
else:
y_off = 0.9
y_off = path_width
max_idx = self._get_path_length_idx(path_x_array, max_distance)
self._path.projected_points = self._map_line_to_polygon(
@@ -225,7 +243,7 @@ class ModelRenderer(Widget):
def _update_experimental_gradient(self):
"""Pre-calculate experimental mode gradient colors"""
if not self._experimental_mode:
if not (self._experimental_mode or self._params.get_bool("AccelerationPath", default=True)):
return
max_len = min(len(self._path.projected_points) // 2, len(self._acceleration_x))
@@ -304,7 +322,8 @@ class ModelRenderer(Widget):
np.interp(abs(torque), [0.6, 0.8], [0.0, 1.0])
)
else:
color = rl.Color(255, 255, 255, int(alpha * 255))
lane_lines_color = get_theme_color("LaneLines", rl.WHITE)
color = with_alpha(lane_lines_color, int(alpha * lane_lines_color.a))
return color
@@ -334,14 +353,28 @@ class ModelRenderer(Widget):
lateral_ui_active = ui_state.status == UIStatus.ENGAGED or ui_state.always_on_lateral_active
allow_throttle = sm['longitudinalPlan'].allowThrottle or not self._longitudinal_control or ui_state.always_on_lateral_active
self._blend_filter.update(int(allow_throttle))
custom_theme_selected = (self._params.get("ColorScheme", encoding="utf-8", default="stock") or "stock").lower() != "stock"
if self._experimental_mode:
if self._experimental_mode or self._params.get_bool("AccelerationPath", default=True):
# Draw with acceleration coloring
if len(self._exp_gradient.colors) > 1:
draw_polygon(self._rect, self._path.projected_points, gradient=self._exp_gradient)
else:
fallback = get_border_color(ui_state)
draw_polygon(self._rect, self._path.projected_points, rl.Color(fallback.r, fallback.g, fallback.b, 90))
elif custom_theme_selected:
path_color = get_theme_color("Path", rl.Color(48, 255, 156, 255))
gradient = Gradient(
start=(0.0, 1.0),
end=(0.0, 0.0),
colors=[
with_alpha(path_color, path_color.a),
with_alpha(path_color, int(path_color.a * 0.55)),
with_alpha(path_color, int(path_color.a * 0.10)),
],
stops=[0.0, 0.5, 1.0],
)
draw_polygon(self._rect, self._path.projected_points, gradient=gradient)
else:
# Blend throttle/no throttle colors based on transition
blend_factor = round(self._blend_filter.x * 100) / 100
@@ -479,3 +512,17 @@ class ModelRenderer(Widget):
int(inv_t * start.b + t * end.b),
int(inv_t * start.a + t * end.a)
) for start, end in zip(begin_colors, end_colors, strict=True)]
def _small_distance_to_half_m(self, value: float) -> float:
if value <= 0:
return 0.0
if self._params.get_bool("IsMetric"):
return value / 200.0
return value * (CV.INCH_TO_CM / 100.0) / 2.0
def _path_width_to_half_m(self, value: float) -> float:
if value <= 0:
return 0.0
if self._params.get_bool("IsMetric"):
return value / 2.0
return value * CV.FOOT_TO_METER / 2.0
+49 -27
View File
@@ -5,7 +5,9 @@ from cereal import messaging, car
from dataclasses import dataclass, field
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.params import Params
from openpilot.common.constants import CV
from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT
from openpilot.selfdrive.ui.lib.starpilot_theme import get_theme_color, with_alpha
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient
@@ -185,28 +187,24 @@ class ModelRenderer(Widget):
def _update_model(self, lead, path_x_array):
"""Update model visualization data based on model message"""
path_width = self._params.get_float('PathWidth') # stored in feet, convert to meters
if path_width <= 0:
path_width = 0.9 # default in meters
lane_line_width = self._params.get_int('LaneLinesWidth') # stored in inches, convert to meters
if lane_line_width <= 0:
lane_line_width = int(0.025 / 0.0254) # default ~1 inch
lane_line_width_m = lane_line_width * 0.0254
road_edge_width = self._params.get_int('RoadEdgesWidth') # stored in inches
if road_edge_width <= 0:
road_edge_width = int(0.025 / 0.0254)
road_edge_width_m = road_edge_width * 0.0254
path_edge_width_pct = self._params.get_int('PathEdgeWidth') / 100.0
model_ui_enabled = self._params.get_bool('ModelUI', default=True)
if model_ui_enabled:
path_width = self._path_width_to_half_m(self._params.get_float('PathWidth', return_default=True, default=6.1))
lane_line_width_m = self._small_distance_to_half_m(self._params.get_float('LaneLinesWidth', return_default=True, default=4.0))
road_edge_width_m = self._small_distance_to_half_m(self._params.get_float('RoadEdgesWidth', return_default=True, default=2.0))
path_edge_width_pct = np.clip(self._params.get_float('PathEdgeWidth', return_default=True, default=20.0) / 100.0, 0.0, 1.0)
else:
path_width = 0.9
lane_line_width_m = 0.025
road_edge_width_m = 0.025
path_edge_width_pct = 0.0
# Dynamic path width
if self._params.get_bool('DynamicPathWidth'):
if model_ui_enabled and self._params.get_bool('DynamicPathWidth', default=False):
status = ui_state.status
if status == UIStatus.ENGAGED:
path_width *= 1.0
elif status == UIStatus.ALWAYS_ON_LATERAL:
elif ui_state.always_on_lateral_active:
path_width *= 0.75
else:
path_width *= 0.50
@@ -246,7 +244,7 @@ class ModelRenderer(Widget):
def _update_experimental_gradient(self):
"""Pre-calculate experimental mode gradient colors"""
use_acceleration = self._experimental_mode or self._params.get_bool('AccelerationPath')
use_acceleration = self._experimental_mode or self._params.get_bool('AccelerationPath', default=True)
use_rainbow = self._params.get_bool('RainbowPath') and not use_acceleration
if not use_acceleration and not use_rainbow:
@@ -333,19 +331,14 @@ class ModelRenderer(Widget):
def _draw_lane_lines(self):
"""Draw lane lines and road edges"""
color_scheme = self._params.get('ColorScheme', encoding='utf-8') or 'stock'
lane_lines_color = self._params.get('LaneLinesColor', encoding='utf-8')
lane_lines_color = get_theme_color("LaneLines", rl.WHITE)
for i, lane_line in enumerate(self._lane_lines):
if lane_line.projected_points.size == 0:
continue
alpha = np.clip(self._lane_line_probs[i], 0.0, 0.7)
if color_scheme != 'stock' and lane_lines_color:
base_color = self._hex_to_color(lane_lines_color)
color = rl.Color(base_color.r, base_color.g, base_color.b, int(alpha * 255))
else:
color = rl.Color(255, 255, 255, int(alpha * 255))
color = with_alpha(lane_lines_color, int(alpha * lane_lines_color.a))
draw_polygon(self._rect, lane_line.projected_points, color)
for i, road_edge in enumerate(self._road_edges):
@@ -364,12 +357,26 @@ class ModelRenderer(Widget):
allow_throttle = sm['longitudinalPlan'].allowThrottle or not self._longitudinal_control
self._blend_filter.update(int(allow_throttle))
use_accel_path = self._params.get_bool('AccelerationPath')
use_accel_path = self._params.get_bool('AccelerationPath', default=True)
custom_theme_selected = (self._params.get('ColorScheme', encoding='utf-8', default='stock') or 'stock').lower() != 'stock'
if self._experimental_mode or use_accel_path:
if len(self._exp_gradient.colors) > 1:
draw_polygon(self._rect, self._path.projected_points, gradient=self._exp_gradient)
else:
draw_polygon(self._rect, self._path.projected_points, rl.Color(255, 255, 255, 30))
elif custom_theme_selected:
path_color = get_theme_color("Path", rl.Color(48, 255, 156, 255))
gradient = Gradient(
start=(0.0, 1.0),
end=(0.0, 0.0),
colors=[
with_alpha(path_color, path_color.a),
with_alpha(path_color, int(path_color.a * 0.55)),
with_alpha(path_color, int(path_color.a * 0.10)),
],
stops=[0.0, 0.5, 1.0],
)
draw_polygon(self._rect, self._path.projected_points, gradient=gradient)
else:
# Blend throttle/no throttle colors based on transition
blend_factor = round(self._blend_filter.x * 100) / 100
@@ -384,12 +391,13 @@ class ModelRenderer(Widget):
def _draw_lead_indicator(self):
# Draw lead vehicles if available
lead_color = get_theme_color("LeadMarker", rl.Color(201, 34, 49, 255))
for lead in self._lead_vehicles:
if not lead.glow or not lead.chevron:
continue
rl.draw_triangle_fan(lead.glow, len(lead.glow), rl.Color(218, 202, 37, 255))
rl.draw_triangle_fan(lead.chevron, len(lead.chevron), rl.Color(201, 34, 49, lead.fill_alpha))
rl.draw_triangle_fan(lead.chevron, len(lead.chevron), with_alpha(lead_color, lead.fill_alpha))
def _update_adjacent_paths(self, max_idx: int, max_distance: float):
"""Compute adjacent lane path polygons by averaging lane line pairs."""
@@ -631,6 +639,20 @@ class ModelRenderer(Widget):
int(a * 255)
)
def _small_distance_to_half_m(self, value: float) -> float:
if value <= 0:
return 0.0
if self._params.get_bool("IsMetric"):
return value / 200.0
return value * (CV.INCH_TO_CM / 100.0) / 2.0
def _path_width_to_half_m(self, value: float) -> float:
if value <= 0:
return 0.0
if self._params.get_bool("IsMetric"):
return value / 2.0
return value * CV.FOOT_TO_METER / 2.0
@staticmethod
def _blend_colors(begin_colors, end_colors, t):
if t >= 1.0:
+8 -17
View File
@@ -6,6 +6,7 @@ import numpy as np
import pyray as rl
from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state
from openpilot.selfdrive.ui.lib.starpilot_theme import get_theme_color, with_alpha
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient
@@ -176,20 +177,10 @@ def render_path_edges(renderer) -> None:
elif ui_state.traffic_mode_enabled:
base_color = rl.Color(201, 34, 49, 241)
else:
color_scheme = renderer._params.get("ColorScheme")
if color_scheme and color_scheme.decode("utf-8") != "stock":
path_edges_color = renderer._params.get("PathEdgesColor")
if path_edges_color:
hex_str = path_edges_color.decode("utf-8").lstrip("#")
if len(hex_str) == 6:
r = int(hex_str[0:2], 16)
g = int(hex_str[2:4], 16)
b = int(hex_str[4:6], 16)
base_color = rl.Color(r, g, b, 241)
else:
base_color = rl.Color(23, 134, 68, 241)
else:
base_color = rl.Color(23, 134, 68, 241)
color_scheme = renderer._params.get("ColorScheme", encoding="utf-8", default="stock")
if color_scheme != "stock":
theme_color = get_theme_color("PathEdge", rl.Color(23, 134, 68, 241))
base_color = rl.Color(theme_color.r, theme_color.g, theme_color.b, 241)
else:
base_color = None
@@ -198,9 +189,9 @@ def render_path_edges(renderer) -> None:
start=(0.0, 1.0),
end=(0.0, 0.0),
colors=[
rl.Color(base_color.r, base_color.g, base_color.b, int(255 * 0.4)),
rl.Color(base_color.r, base_color.g, base_color.b, int(255 * 0.35)),
rl.Color(base_color.r, base_color.g, base_color.b, 0),
with_alpha(base_color, int(255 * 0.4)),
with_alpha(base_color, int(255 * 0.35)),
with_alpha(base_color, 0),
],
stops=[0.0, 0.5, 1.0],
)