| """ |
| Configurable sensor and vehicle configuration for the FSD model. |
| Supports arbitrary sensor counts, types, and placements. |
| """ |
|
|
| import math |
| import json |
| from dataclasses import dataclass, field, asdict |
| from typing import List, Optional, Tuple, Dict, Any |
| from enum import Enum |
|
|
|
|
| class SensorType(Enum): |
| CAMERA = "camera" |
| ULTRASONIC = "ultrasonic" |
| LIDAR = "lidar" |
| RADAR = "radar" |
|
|
|
|
| class CameraPosition(Enum): |
| FRONT_LEFT = "front_left" |
| FRONT_RIGHT = "front_right" |
| REAR_LEFT = "rear_left" |
| REAR_RIGHT = "rear_right" |
| LEFT_MIRROR = "left_mirror" |
| RIGHT_MIRROR = "right_mirror" |
| FRONT_CENTER = "front_center" |
| REAR_CENTER = "rear_center" |
| ROOF_FRONT = "roof_front" |
| ROOF_REAR = "roof_rear" |
| BUMPER_LEFT = "bumper_left" |
| BUMPER_RIGHT = "bumper_right" |
|
|
|
|
| class UltrasonicZone(Enum): |
| FRONT_LEFT_CORNER = "front_left_corner" |
| FRONT_LEFT = "front_left" |
| FRONT_CENTER_LEFT = "front_center_left" |
| FRONT_CENTER = "front_center" |
| FRONT_CENTER_RIGHT = "front_center_right" |
| FRONT_RIGHT = "front_right" |
| FRONT_RIGHT_CORNER = "front_right_corner" |
| LEFT_FRONT = "left_front" |
| LEFT_CENTER = "left_center" |
| LEFT_REAR = "left_rear" |
| RIGHT_FRONT = "right_front" |
| RIGHT_CENTER = "right_center" |
| RIGHT_REAR = "right_rear" |
| REAR_LEFT_CORNER = "rear_left_corner" |
| REAR_LEFT = "rear_left" |
| REAR_CENTER_LEFT = "rear_center_left" |
| REAR_CENTER = "rear_center" |
| REAR_CENTER_RIGHT = "rear_center_right" |
| REAR_RIGHT = "rear_right" |
| REAR_RIGHT_CORNER = "rear_right_corner" |
|
|
|
|
| @dataclass |
| class SensorPlacement: |
| x: float |
| y: float |
| z: float |
| yaw: float = 0.0 |
| pitch: float = 0.0 |
| roll: float = 0.0 |
|
|
| def to_transform_matrix(self): |
| import numpy as np |
| yaw_r = math.radians(self.yaw) |
| pitch_r = math.radians(self.pitch) |
| roll_r = math.radians(self.roll) |
| Rz = np.array([ |
| [math.cos(yaw_r), -math.sin(yaw_r), 0], |
| [math.sin(yaw_r), math.cos(yaw_r), 0], |
| [0, 0, 1] |
| ]) |
| Ry = np.array([ |
| [math.cos(pitch_r), 0, math.sin(pitch_r)], |
| [0, 1, 0], |
| [-math.sin(pitch_r), 0, math.cos(pitch_r)] |
| ]) |
| Rx = np.array([ |
| [1, 0, 0], |
| [0, math.cos(roll_r), -math.sin(roll_r)], |
| [0, math.sin(roll_r), math.cos(roll_r)] |
| ]) |
| R = Rz @ Ry @ Rx |
| T = np.eye(4) |
| T[:3, :3] = R |
| T[:3, 3] = [self.x, self.y, self.z] |
| return T |
|
|
|
|
| @dataclass |
| class CameraSensorConfig: |
| name: str |
| position: CameraPosition |
| placement: SensorPlacement |
| resolution: Tuple[int, int] = (640, 480) |
| fov_horizontal: float = 120.0 |
| fov_vertical: float = 90.0 |
| fps: int = 30 |
| encoding: str = "rgb" |
| distortion_model: str = "pinhole" |
| fx: Optional[float] = None |
| fy: Optional[float] = None |
| cx: Optional[float] = None |
| cy: Optional[float] = None |
|
|
| def __post_init__(self): |
| if self.fx is None: |
| self.fx = self.resolution[0] / (2 * math.tan(math.radians(self.fov_horizontal / 2))) |
| if self.fy is None: |
| self.fy = self.resolution[1] / (2 * math.tan(math.radians(self.fov_vertical / 2))) |
| if self.cx is None: |
| self.cx = self.resolution[0] / 2 |
| if self.cy is None: |
| self.cy = self.resolution[1] / 2 |
|
|
| def get_intrinsic_matrix(self): |
| import numpy as np |
| return np.array([ |
| [self.fx, 0, self.cx], |
| [0, self.fy, self.cy], |
| [0, 0, 1] |
| ]) |
|
|
|
|
| @dataclass |
| class UltrasonicSensorConfig: |
| name: str |
| zone: UltrasonicZone |
| placement: SensorPlacement |
| max_range: float = 5.0 |
| min_range: float = 0.02 |
| beam_angle: float = 30.0 |
| frequency: float = 40000.0 |
| update_rate: float = 20.0 |
| accuracy: float = 0.01 |
| noise_std: float = 0.005 |
|
|
|
|
| @dataclass |
| class SensorConfig: |
| cameras: List[CameraSensorConfig] = field(default_factory=list) |
| ultrasonics: List[UltrasonicSensorConfig] = field(default_factory=list) |
|
|
| @property |
| def num_cameras(self) -> int: |
| return len(self.cameras) |
|
|
| @property |
| def num_ultrasonics(self) -> int: |
| return len(self.ultrasonics) |
|
|
| @property |
| def total_sensors(self) -> int: |
| return self.num_cameras + self.num_ultrasonics |
|
|
| def validate(self): |
| issues = [] |
| if self.num_cameras == 0: |
| issues.append("WARNING: No cameras configured") |
| if self.num_ultrasonics == 0: |
| issues.append("WARNING: No ultrasonic sensors configured") |
| yaw_angles = sorted([c.placement.yaw for c in self.cameras]) |
| if len(yaw_angles) >= 2: |
| gaps = [] |
| for i in range(len(yaw_angles)): |
| next_i = (i + 1) % len(yaw_angles) |
| gap = (yaw_angles[next_i] - yaw_angles[i]) % 360 |
| if gap == 0 and next_i != 0: |
| continue |
| gaps.append(gap) |
| max_gap = max(gaps) if gaps else 360 |
| if max_gap > 120: |
| issues.append(f"WARNING: Camera coverage gap of {max_gap:.0f} degrees") |
| return issues |
|
|
| def get_sensor_summary(self) -> str: |
| lines = [f"Sensor Configuration Summary:"] |
| lines.append(f" Total sensors: {self.total_sensors}") |
| lines.append(f" Cameras: {self.num_cameras}") |
| for cam in self.cameras: |
| lines.append(f" - {cam.name}: {cam.position.value} | {cam.resolution[0]}x{cam.resolution[1]} | FOV: {cam.fov_horizontal}") |
| lines.append(f" Ultrasonic sensors: {self.num_ultrasonics}") |
| for us in self.ultrasonics: |
| lines.append(f" - {us.name}: {us.zone.value} | Range: {us.min_range}-{us.max_range}m") |
| issues = self.validate() |
| if issues: |
| lines.append(" Validation issues:") |
| for issue in issues: |
| lines.append(f" {issue}") |
| return "\n".join(lines) |
|
|
| def to_dict(self) -> Dict[str, Any]: |
| return asdict(self) |
|
|
| def save(self, path: str): |
| with open(path, 'w') as f: |
| json.dump(self.to_dict(), f, indent=2, default=str) |
|
|
|
|
| @dataclass |
| class VehicleConfig: |
| name: str = "FSD_Vehicle" |
| length: float = 4.5 |
| width: float = 1.8 |
| height: float = 1.5 |
| wheelbase: float = 2.7 |
| track_width: float = 1.5 |
| max_speed_mph: float = 20.0 |
| max_speed_ms: float = 8.94 |
| max_steering_angle: float = 35.0 |
| max_acceleration: float = 3.0 |
| max_deceleration: float = 8.0 |
| sensor_config: Optional[SensorConfig] = None |
|
|
| def __post_init__(self): |
| self.max_speed_ms = self.max_speed_mph * 0.44704 |
| if self.sensor_config is None: |
| self.sensor_config = self._default_sensor_config() |
|
|
| def _default_sensor_config(self) -> SensorConfig: |
| cameras = self._create_default_cameras() |
| ultrasonics = self._create_default_ultrasonics() |
| return SensorConfig(cameras=cameras, ultrasonics=ultrasonics) |
|
|
| def _create_default_cameras(self) -> List[CameraSensorConfig]: |
| half_l = self.length / 2 |
| half_w = self.width / 2 |
| mirror_h = 1.1 |
| cameras = [ |
| CameraSensorConfig( |
| name="cam_front_left", position=CameraPosition.FRONT_LEFT, |
| placement=SensorPlacement(x=half_l - 0.3, y=half_w, z=0.8, yaw=-45, pitch=-5), |
| resolution=(640, 480), fov_horizontal=120, |
| ), |
| CameraSensorConfig( |
| name="cam_front_right", position=CameraPosition.FRONT_RIGHT, |
| placement=SensorPlacement(x=half_l - 0.3, y=-half_w, z=0.8, yaw=45, pitch=-5), |
| resolution=(640, 480), fov_horizontal=120, |
| ), |
| CameraSensorConfig( |
| name="cam_rear_left", position=CameraPosition.REAR_LEFT, |
| placement=SensorPlacement(x=-half_l + 0.3, y=half_w, z=0.8, yaw=-135, pitch=-5), |
| resolution=(640, 480), fov_horizontal=120, |
| ), |
| CameraSensorConfig( |
| name="cam_rear_right", position=CameraPosition.REAR_RIGHT, |
| placement=SensorPlacement(x=-half_l + 0.3, y=-half_w, z=0.8, yaw=135, pitch=-5), |
| resolution=(640, 480), fov_horizontal=120, |
| ), |
| CameraSensorConfig( |
| name="cam_left_mirror", position=CameraPosition.LEFT_MIRROR, |
| placement=SensorPlacement(x=0.8, y=half_w + 0.1, z=mirror_h, yaw=-90, pitch=-10), |
| resolution=(640, 480), fov_horizontal=90, |
| ), |
| CameraSensorConfig( |
| name="cam_right_mirror", position=CameraPosition.RIGHT_MIRROR, |
| placement=SensorPlacement(x=0.8, y=-(half_w + 0.1), z=mirror_h, yaw=90, pitch=-10), |
| resolution=(640, 480), fov_horizontal=90, |
| ), |
| ] |
| return cameras |
|
|
| def _create_default_ultrasonics(self) -> List[UltrasonicSensorConfig]: |
| half_l = self.length / 2 |
| half_w = self.width / 2 |
| bumper_h = 0.4 |
| zones_front = [ |
| UltrasonicZone.FRONT_LEFT_CORNER, UltrasonicZone.FRONT_LEFT, |
| UltrasonicZone.FRONT_CENTER_LEFT, UltrasonicZone.FRONT_CENTER, |
| UltrasonicZone.FRONT_CENTER_RIGHT, UltrasonicZone.FRONT_RIGHT, |
| UltrasonicZone.FRONT_RIGHT_CORNER, |
| ] |
| front_y = [half_w, half_w*0.66, half_w*0.33, 0.0, -half_w*0.33, -half_w*0.66, -half_w] |
| front_yaw = [-30, -15, -5, 0, 5, 15, 30] |
| ultrasonics = [] |
| for i, (zone, y_pos, yaw) in enumerate(zip(zones_front, front_y, front_yaw)): |
| ultrasonics.append(UltrasonicSensorConfig( |
| name=f"us_front_{i}", zone=zone, |
| placement=SensorPlacement(x=half_l, y=y_pos, z=bumper_h, yaw=yaw), |
| max_range=5.0, |
| )) |
| zones_rear = [ |
| UltrasonicZone.REAR_LEFT_CORNER, UltrasonicZone.REAR_LEFT, |
| UltrasonicZone.REAR_CENTER_LEFT, UltrasonicZone.REAR_CENTER, |
| UltrasonicZone.REAR_CENTER_RIGHT, UltrasonicZone.REAR_RIGHT, |
| UltrasonicZone.REAR_RIGHT_CORNER, |
| ] |
| rear_yaw = [-150, -165, -175, 180, 175, 165, 150] |
| for i, (zone, y_pos, yaw) in enumerate(zip(zones_rear, front_y, rear_yaw)): |
| ultrasonics.append(UltrasonicSensorConfig( |
| name=f"us_rear_{i}", zone=zone, |
| placement=SensorPlacement(x=-half_l, y=y_pos, z=bumper_h, yaw=yaw), |
| max_range=5.0, |
| )) |
| zones_left = [UltrasonicZone.LEFT_FRONT, UltrasonicZone.LEFT_CENTER, UltrasonicZone.LEFT_REAR] |
| left_x = [half_l * 0.5, 0.0, -half_l * 0.5] |
| for i, (zone, x_pos) in enumerate(zip(zones_left, left_x)): |
| ultrasonics.append(UltrasonicSensorConfig( |
| name=f"us_left_{i}", zone=zone, |
| placement=SensorPlacement(x=x_pos, y=half_w, z=bumper_h + 0.2, yaw=-90), |
| max_range=3.0, |
| )) |
| zones_right = [UltrasonicZone.RIGHT_FRONT, UltrasonicZone.RIGHT_CENTER, UltrasonicZone.RIGHT_REAR] |
| for i, (zone, x_pos) in enumerate(zip(zones_right, left_x)): |
| ultrasonics.append(UltrasonicSensorConfig( |
| name=f"us_right_{i}", zone=zone, |
| placement=SensorPlacement(x=x_pos, y=-half_w, z=bumper_h + 0.2, yaw=90), |
| max_range=3.0, |
| )) |
| return ultrasonics |
|
|
|
|
| def create_custom_config( |
| num_cameras=6, num_ultrasonics=20, |
| camera_placements=None, ultrasonic_placements=None, |
| max_speed_mph=20.0, **vehicle_kwargs |
| ) -> VehicleConfig: |
| config = VehicleConfig(max_speed_mph=max_speed_mph, **vehicle_kwargs) |
| if camera_placements is not None: |
| cameras = [] |
| positions = list(CameraPosition) |
| for i, cp in enumerate(camera_placements): |
| pos = cp.get("position", positions[i % len(positions)]) |
| if isinstance(pos, str): |
| pos = CameraPosition(pos) |
| placement = SensorPlacement(**cp.get("placement", {"x": 0, "y": 0, "z": 1.0})) |
| cameras.append(CameraSensorConfig( |
| name=cp.get("name", f"cam_{i}"), position=pos, placement=placement, |
| resolution=cp.get("resolution", (640, 480)), |
| fov_horizontal=cp.get("fov_horizontal", 120), |
| fov_vertical=cp.get("fov_vertical", 90), |
| )) |
| config.sensor_config.cameras = cameras |
| if ultrasonic_placements is not None: |
| ultrasonics = [] |
| zones = list(UltrasonicZone) |
| for i, up in enumerate(ultrasonic_placements): |
| zone = up.get("zone", zones[i % len(zones)]) |
| if isinstance(zone, str): |
| zone = UltrasonicZone(zone) |
| placement = SensorPlacement(**up.get("placement", {"x": 0, "y": 0, "z": 0.4})) |
| ultrasonics.append(UltrasonicSensorConfig( |
| name=up.get("name", f"us_{i}"), zone=zone, placement=placement, |
| max_range=up.get("max_range", 5.0), |
| beam_angle=up.get("beam_angle", 30.0), |
| )) |
| config.sensor_config.ultrasonics = ultrasonics |
| return config |
|
|