Files
StarPilot/openpilot/selfdrive/ui/onroad/augmented_road_view.py
T

268 lines
11 KiB
Python

import numpy as np
from openpilot.starpilot.ui.camera_selection import (selected_camera_stream, WIDE_CAM_MAX_SPEED as WIDE_CAM_MAX_SPEED,
ROAD_CAM_MIN_SPEED as ROAD_CAM_MIN_SPEED)
import pyray as rl
from openpilot.cereal import log
from openpilot.cereal.visionipc import VisionStreamType
from openpilot.selfdrive.ui import UI_BORDER_SIZE
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
from openpilot.selfdrive.ui.onroad.alert_renderer import AlertRenderer
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer
from openpilot.selfdrive.ui.onroad.hud_renderer import HudRenderer
from openpilot.selfdrive.ui.onroad.model_renderer import ModelRenderer
from openpilot.selfdrive.ui.onroad.cameraview import CameraView
from openpilot.system.ui.lib.application import gui_app
from openpilot.common.transformations.camera import DEVICE_CAMERAS, DeviceCameraConfig, view_frame_from_device_frame
from openpilot.common.transformations.orientation import rot_from_euler
OpState = log.SelfdriveState.OpenpilotState
CALIBRATED = log.ExtrinsicsCalibration.Status.calibrated
NARROW_ROAD_CAM = VisionStreamType.VISION_STREAM_NARROW_ROAD
WIDE_CAM = VisionStreamType.VISION_STREAM_WIDE_ROAD
DRIVER_CAM = VisionStreamType.VISION_STREAM_CABIN
CAMERA_VIEW_AUTO, CAMERA_VIEW_DRIVER, CAMERA_VIEW_STANDARD, CAMERA_VIEW_WIDE, CAMERA_VIEW_NONE = range(5)
DEFAULT_DEVICE_CAMERA = DEVICE_CAMERAS["tici", "ar0231"]
BORDER_COLORS = {
UIStatus.DISENGAGED: rl.Color(0x12, 0x28, 0x39, 0xFF), # Blue for disengaged state
UIStatus.OVERRIDE: rl.Color(0x89, 0x92, 0x8D, 0xFF), # Gray for override state
UIStatus.ENGAGED: rl.Color(0x16, 0x7F, 0x40, 0xFF), # Green for engaged state
}
INF_POINT = np.array([1000.0, 0.0, 0.0])
class AugmentedRoadView(CameraView):
def __init__(self, stream_type: VisionStreamType = VisionStreamType.VISION_STREAM_NARROW_ROAD):
super().__init__("camerad", stream_type)
self._set_placeholder_color(BORDER_COLORS[UIStatus.DISENGAGED])
self.device_camera: DeviceCameraConfig | None = None
self.view_from_calib = view_frame_from_device_frame.copy()
self.view_from_wide_calib = view_frame_from_device_frame.copy()
self._matrix_cache_key = (0, 0.0, 0.0, stream_type)
self._cached_matrix: np.ndarray | None = None
self._content_rect = rl.Rectangle()
self.model_renderer = ModelRenderer()
self._hud_renderer = HudRenderer()
self.alert_renderer = AlertRenderer()
self.driver_state_renderer = DriverStateRenderer()
def render_camera_model_layer(self, rect: rl.Rectangle, *, camera_view: int = CAMERA_VIEW_AUTO,
reverse_driver: bool = False, road_style: dict | None = None,
lead_display: tuple[bool, int, bool | None] | None = None, poll_camera: bool = True) -> None:
"""Draw the existing calibrated camera/model without stock HUD or alert."""
if not ui_state.started and ui_state.replay_clock is None:
return
if poll_camera and (camera_view == CAMERA_VIEW_DRIVER or reverse_driver):
self._ensure_connection()
target = self._switch_stream_if_needed(ui_state.sm, camera_view, reverse_driver)
if target is None:
rl.draw_rectangle_rec(rect, rl.BLACK)
return
driver_transition = DRIVER_CAM in (self.stream_type, target, self._target_stream_type)
if poll_camera and self._switching and driver_transition:
self._handle_switch()
if (target == DRIVER_CAM and DRIVER_CAM not in self.available_streams) or (driver_transition and self.stream_type != target):
rl.draw_rectangle_rec(rect, rl.BLACK)
return
self._update_calibration()
self._content_rect = rect
style = road_style or {}
if ("pathEdge" in getattr(self.model_renderer, "road_style", {})) != ("pathEdge" in style):
self.model_renderer._transform_dirty = True
self.model_renderer.road_style = style
self.model_renderer.lead_display = lead_display
gui_app.measure_frame_phase("camera", CameraView._render, self, rect, poll_camera=poll_camera)
if target != DRIVER_CAM:
gui_app.measure_frame_phase("model", self.model_renderer.render, rect)
def render_model_source_layer(self, rect: rl.Rectangle, *, show_long_indicator: bool | None = False) -> None:
self._hud_renderer._draw_model_source(rect)
def _render(self, rect, *, poll_camera: bool = True):
# Only render when system is started to avoid invalid data access
if not ui_state.started:
return
self._switch_stream_if_needed(ui_state.sm)
# Update calibration before rendering
self._update_calibration()
# Create inner content area with border padding
self._content_rect = rl.Rectangle(
rect.x + UI_BORDER_SIZE,
rect.y + UI_BORDER_SIZE,
rect.width - 2 * UI_BORDER_SIZE,
rect.height - 2 * UI_BORDER_SIZE,
)
# Enable scissor mode to clip all rendering within content rectangle boundaries
# This creates a rendering viewport that prevents graphics from drawing outside the border
rl.begin_scissor_mode(
int(self._content_rect.x),
int(self._content_rect.y),
int(self._content_rect.width),
int(self._content_rect.height)
)
# Render the base camera view
super()._render(self._content_rect, poll_camera=poll_camera)
# Draw all UI overlays
self.model_renderer.render(self._content_rect)
self._hud_renderer.render(self._content_rect)
self.alert_renderer.render(self._content_rect)
self.driver_state_renderer.render(self._content_rect)
# Custom UI extension point - add custom overlays here
# Use self._content_rect for positioning within camera bounds
# End clipping region
rl.end_scissor_mode()
# Draw colored border based on driving state
self._draw_border(rect)
def _handle_mouse_press(self, _):
if not self._hud_renderer.user_interacting() and self._click_callback is not None:
self._click_callback()
def _handle_mouse_release(self, _):
# We only call click callback on press if not interacting with HUD
pass
def _draw_border(self, rect: rl.Rectangle):
rl.draw_rectangle_lines_ex(rect, UI_BORDER_SIZE, rl.BLACK)
border_roundness = 0.12
border_color = BORDER_COLORS.get(ui_state.status, BORDER_COLORS[UIStatus.DISENGAGED])
border_rect = rl.Rectangle(rect.x + UI_BORDER_SIZE, rect.y + UI_BORDER_SIZE,
rect.width - 2 * UI_BORDER_SIZE, rect.height - 2 * UI_BORDER_SIZE)
rl.draw_rectangle_rounded_lines_ex(border_rect, border_roundness, 10, UI_BORDER_SIZE, border_color)
def _cancel_pending_switch(self) -> None:
self._target_client = None
self._target_stream_type = None
self._switching = False
def _switch_stream_if_needed(self, sm, camera_view: int = CAMERA_VIEW_AUTO,
reverse_driver: bool = False) -> VisionStreamType | None:
target = selected_camera_stream(camera_view, reverse_driver=reverse_driver, current=self.stream_type,
available=self.available_streams, experimental=sm['selfdriveState'].experimentalMode,
speed=sm['carState'].vEgo, narrow=NARROW_ROAD_CAM, wide=WIDE_CAM, driver=DRIVER_CAM)
if target is None or target == DRIVER_CAM and DRIVER_CAM not in self.available_streams:
self._cancel_pending_switch()
return target
if self._switching and self._target_stream_type != target:
# Revoke a queued Auto/temporary view before its first frame can paint.
self._cancel_pending_switch()
if self.stream_type != target:
self.switch_stream(target)
return target
def _update_calibration(self):
# Update device camera if not already set
sm = ui_state.sm
if not self.device_camera and sm.seen['narrowRoadCameraState'] and sm.seen['deviceState']:
self.device_camera = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['narrowRoadCameraState'].sensor))]
# Check if camera calibration data is available and valid
if not (sm.updated["extrinsicsCalibration"] and sm.valid['extrinsicsCalibration']):
return
calib = sm['extrinsicsCalibration']
if len(calib.rpyCalib) != 3 or calib.calStatus != CALIBRATED:
return
# Update view_from_calib matrix
device_from_calib = rot_from_euler(calib.rpyCalib)
self.view_from_calib = view_frame_from_device_frame @ device_from_calib
# Update wide calibration if available
if hasattr(calib, 'wideFromDeviceEuler') and len(calib.wideFromDeviceEuler) == 3:
wide_from_device = rot_from_euler(calib.wideFromDeviceEuler)
self.view_from_wide_calib = view_frame_from_device_frame @ wide_from_device @ device_from_calib
def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray:
if self.stream_type == DRIVER_CAM:
return CameraView._calc_frame_matrix(self, rect)
# Check if we can use cached matrix
cache_key = (
ui_state.sm.recv_frame['extrinsicsCalibration'],
self._content_rect.width,
self._content_rect.height,
self.stream_type
)
if cache_key == self._matrix_cache_key and self._cached_matrix is not None:
return self._cached_matrix
# Get camera configuration
device_camera = self.device_camera or DEFAULT_DEVICE_CAMERA
is_wide_camera = self.stream_type == WIDE_CAM
intrinsic = device_camera.wide_road.intrinsics if is_wide_camera else device_camera.narrow_road.intrinsics
calibration = self.view_from_wide_calib if is_wide_camera else self.view_from_calib
zoom = 2.0 if is_wide_camera else 1.1
# Calculate transforms for vanishing point
calib_transform = intrinsic @ calibration
kep = calib_transform @ INF_POINT
# Calculate center points and dimensions
x, y = self._content_rect.x, self._content_rect.y
w, h = self._content_rect.width, self._content_rect.height
cx, cy = intrinsic[0, 2], intrinsic[1, 2]
# Ensure zoom views the whole area
zoom = max(zoom, w / (2 * cx), h / (2 * cy))
# Calculate max allowed offsets with margins
margin = 5
max_x_offset = max(0.0, cx * zoom - w / 2 - margin)
max_y_offset = max(0.0, cy * zoom - h / 2 - margin)
# Calculate and clamp offsets to prevent out-of-bounds issues
try:
if abs(kep[2]) > 1e-6:
x_offset = np.clip((kep[0] / kep[2] - cx) * zoom, -max_x_offset, max_x_offset)
y_offset = np.clip((kep[1] / kep[2] - cy) * zoom, -max_y_offset, max_y_offset)
else:
x_offset, y_offset = 0, 0
except (ZeroDivisionError, OverflowError):
x_offset, y_offset = 0, 0
# Cache the computed transformation matrix to avoid recalculations
self._matrix_cache_key = cache_key
self._cached_matrix = np.array([
[zoom * 2 * cx / w, 0, -x_offset / w * 2],
[0, zoom * 2 * cy / h, -y_offset / h * 2],
[0, 0, 1.0]
])
video_transform = np.array([
[zoom, 0.0, (w / 2 + x - x_offset) - (cx * zoom)],
[0.0, zoom, (h / 2 + y - y_offset) - (cy * zoom)],
[0.0, 0.0, 1.0]
])
self.model_renderer.set_transform(video_transform @ calib_transform)
return self._cached_matrix
if __name__ == "__main__":
gui_app.init_window("OnRoad Camera View")
road_camera_view = AugmentedRoadView(NARROW_ROAD_CAM)
gui_app.push_widget(road_camera_view)
print("***press space to switch camera view***")
try:
for _ in gui_app.render():
ui_state.update()
if rl.is_key_released(rl.KeyboardKey.KEY_SPACE):
if WIDE_CAM in road_camera_view.available_streams:
stream = NARROW_ROAD_CAM if road_camera_view.stream_type == WIDE_CAM else WIDE_CAM
road_camera_view.switch_stream(stream)
finally:
road_camera_view.close()