simple-autonomous-car 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- simple_autonomous_car/__init__.py +96 -0
- simple_autonomous_car/alerts/__init__.py +5 -0
- simple_autonomous_car/alerts/track_bounds_alert.py +276 -0
- simple_autonomous_car/car/__init__.py +5 -0
- simple_autonomous_car/car/car.py +234 -0
- simple_autonomous_car/constants.py +112 -0
- simple_autonomous_car/control/__init__.py +7 -0
- simple_autonomous_car/control/base_controller.py +152 -0
- simple_autonomous_car/control/controller_viz.py +282 -0
- simple_autonomous_car/control/pid_controller.py +153 -0
- simple_autonomous_car/control/pure_pursuit_controller.py +578 -0
- simple_autonomous_car/costmap/__init__.py +12 -0
- simple_autonomous_car/costmap/base_costmap.py +187 -0
- simple_autonomous_car/costmap/grid_costmap.py +507 -0
- simple_autonomous_car/costmap/inflation.py +126 -0
- simple_autonomous_car/detection/__init__.py +5 -0
- simple_autonomous_car/detection/error_detector.py +165 -0
- simple_autonomous_car/filters/__init__.py +7 -0
- simple_autonomous_car/filters/base_filter.py +119 -0
- simple_autonomous_car/filters/kalman_filter.py +131 -0
- simple_autonomous_car/filters/particle_filter.py +162 -0
- simple_autonomous_car/footprint/__init__.py +7 -0
- simple_autonomous_car/footprint/base_footprint.py +128 -0
- simple_autonomous_car/footprint/circular_footprint.py +73 -0
- simple_autonomous_car/footprint/rectangular_footprint.py +123 -0
- simple_autonomous_car/frames/__init__.py +21 -0
- simple_autonomous_car/frames/frenet.py +267 -0
- simple_autonomous_car/maps/__init__.py +9 -0
- simple_autonomous_car/maps/frenet_map.py +97 -0
- simple_autonomous_car/maps/grid_ground_truth_map.py +83 -0
- simple_autonomous_car/maps/grid_map.py +361 -0
- simple_autonomous_car/maps/ground_truth_map.py +64 -0
- simple_autonomous_car/maps/perceived_map.py +169 -0
- simple_autonomous_car/perception/__init__.py +5 -0
- simple_autonomous_car/perception/perception.py +107 -0
- simple_autonomous_car/planning/__init__.py +7 -0
- simple_autonomous_car/planning/base_planner.py +184 -0
- simple_autonomous_car/planning/goal_planner.py +261 -0
- simple_autonomous_car/planning/track_planner.py +199 -0
- simple_autonomous_car/sensors/__init__.py +6 -0
- simple_autonomous_car/sensors/base_sensor.py +105 -0
- simple_autonomous_car/sensors/lidar_sensor.py +145 -0
- simple_autonomous_car/track/__init__.py +5 -0
- simple_autonomous_car/track/track.py +463 -0
- simple_autonomous_car/visualization/__init__.py +25 -0
- simple_autonomous_car/visualization/alert_viz.py +316 -0
- simple_autonomous_car/visualization/utils.py +169 -0
- simple_autonomous_car-0.1.2.dist-info/METADATA +324 -0
- simple_autonomous_car-0.1.2.dist-info/RECORD +50 -0
- simple_autonomous_car-0.1.2.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Particle filter for state estimation."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from simple_autonomous_car.filters.base_filter import BaseFilter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ParticleFilter(BaseFilter):
|
|
9
|
+
"""
|
|
10
|
+
Particle filter for state estimation.
|
|
11
|
+
|
|
12
|
+
Uses a set of particles to represent the probability distribution
|
|
13
|
+
of the state. Good for non-linear systems and multi-modal distributions.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
initial_state : np.ndarray
|
|
18
|
+
Initial state vector (e.g., [x, y]).
|
|
19
|
+
initial_covariance : np.ndarray
|
|
20
|
+
Initial covariance matrix (used to initialize particles).
|
|
21
|
+
num_particles : int, default=100
|
|
22
|
+
Number of particles.
|
|
23
|
+
process_noise : np.ndarray
|
|
24
|
+
Process noise covariance matrix.
|
|
25
|
+
measurement_noise : np.ndarray
|
|
26
|
+
Measurement noise covariance matrix.
|
|
27
|
+
name : str, default="particle_filter"
|
|
28
|
+
Filter name.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
initial_state: np.ndarray,
|
|
34
|
+
initial_covariance: np.ndarray,
|
|
35
|
+
num_particles: int = 100,
|
|
36
|
+
process_noise: np.ndarray | None = None,
|
|
37
|
+
measurement_noise: np.ndarray | None = None,
|
|
38
|
+
name: str = "particle_filter",
|
|
39
|
+
):
|
|
40
|
+
super().__init__(name=name)
|
|
41
|
+
self.state_dim = len(initial_state)
|
|
42
|
+
self.num_particles = num_particles
|
|
43
|
+
self.process_noise = (
|
|
44
|
+
np.asarray(process_noise, dtype=np.float64)
|
|
45
|
+
if process_noise is not None
|
|
46
|
+
else np.eye(self.state_dim) * 0.1
|
|
47
|
+
)
|
|
48
|
+
self.measurement_noise = (
|
|
49
|
+
np.asarray(measurement_noise, dtype=np.float64)
|
|
50
|
+
if measurement_noise is not None
|
|
51
|
+
else np.eye(self.state_dim) * 0.1
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Initialize particles
|
|
55
|
+
self.particles = np.random.multivariate_normal(
|
|
56
|
+
initial_state, initial_covariance, num_particles
|
|
57
|
+
)
|
|
58
|
+
self.weights = np.ones(num_particles) / num_particles
|
|
59
|
+
|
|
60
|
+
def predict(self, dt: float, control: dict[str, float] | None = None) -> None:
|
|
61
|
+
"""
|
|
62
|
+
Predict state forward in time.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
dt : float
|
|
67
|
+
Time step in seconds.
|
|
68
|
+
control : dict, optional
|
|
69
|
+
Control inputs (e.g., {"velocity": 5.0, "heading": 0.5}).
|
|
70
|
+
"""
|
|
71
|
+
if not self.enabled:
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
# Add process noise to particles
|
|
75
|
+
noise = np.random.multivariate_normal(
|
|
76
|
+
np.zeros(self.state_dim), self.process_noise * dt, self.num_particles
|
|
77
|
+
)
|
|
78
|
+
self.particles += noise
|
|
79
|
+
|
|
80
|
+
# Apply control if provided (simple constant velocity model)
|
|
81
|
+
if control is not None:
|
|
82
|
+
if "velocity" in control and "heading" in control:
|
|
83
|
+
v = control["velocity"]
|
|
84
|
+
h = control["heading"]
|
|
85
|
+
self.particles[:, 0] += v * np.cos(h) * dt # x
|
|
86
|
+
self.particles[:, 1] += v * np.sin(h) * dt # y
|
|
87
|
+
|
|
88
|
+
def update(
|
|
89
|
+
self, measurement: np.ndarray, measurement_covariance: np.ndarray | None = None
|
|
90
|
+
) -> None:
|
|
91
|
+
"""
|
|
92
|
+
Update state estimate with measurement.
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
measurement : np.ndarray
|
|
97
|
+
Measurement vector.
|
|
98
|
+
measurement_covariance : np.ndarray, optional
|
|
99
|
+
Measurement noise covariance (uses default if not provided).
|
|
100
|
+
"""
|
|
101
|
+
if not self.enabled:
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
measurement = np.asarray(measurement, dtype=np.float64)
|
|
105
|
+
r_matrix = (
|
|
106
|
+
measurement_covariance if measurement_covariance is not None else self.measurement_noise
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Update weights based on measurement likelihood
|
|
110
|
+
for i in range(self.num_particles):
|
|
111
|
+
# Compute likelihood: p(measurement | particle)
|
|
112
|
+
residual = measurement - self.particles[i, : len(measurement)]
|
|
113
|
+
likelihood = np.exp(-0.5 * residual.T @ np.linalg.inv(r_matrix) @ residual)
|
|
114
|
+
self.weights[i] *= likelihood
|
|
115
|
+
|
|
116
|
+
# Normalize weights
|
|
117
|
+
self.weights /= np.sum(self.weights)
|
|
118
|
+
|
|
119
|
+
# Resample if effective number of particles is too low
|
|
120
|
+
effective_particles = 1.0 / np.sum(self.weights**2)
|
|
121
|
+
if effective_particles < self.num_particles / 2:
|
|
122
|
+
self._resample()
|
|
123
|
+
|
|
124
|
+
def _resample(self) -> None:
|
|
125
|
+
"""Resample particles based on weights."""
|
|
126
|
+
# Systematic resampling
|
|
127
|
+
cumulative_weights = np.cumsum(self.weights)
|
|
128
|
+
step = 1.0 / self.num_particles
|
|
129
|
+
u = np.random.uniform(0, step)
|
|
130
|
+
|
|
131
|
+
new_particles = np.zeros_like(self.particles)
|
|
132
|
+
j = 0
|
|
133
|
+
for i in range(self.num_particles):
|
|
134
|
+
while u > cumulative_weights[j]:
|
|
135
|
+
j += 1
|
|
136
|
+
new_particles[i] = self.particles[j]
|
|
137
|
+
u += step
|
|
138
|
+
|
|
139
|
+
self.particles = new_particles
|
|
140
|
+
self.weights = np.ones(self.num_particles) / self.num_particles
|
|
141
|
+
|
|
142
|
+
def get_state(self) -> np.ndarray:
|
|
143
|
+
"""Get current state estimate (weighted mean of particles)."""
|
|
144
|
+
result = np.average(self.particles, axis=0, weights=self.weights)
|
|
145
|
+
return np.asarray(result, dtype=np.float64)
|
|
146
|
+
|
|
147
|
+
def get_covariance(self) -> np.ndarray:
|
|
148
|
+
"""Get current state covariance."""
|
|
149
|
+
state = self.get_state()
|
|
150
|
+
diff = self.particles - state
|
|
151
|
+
return np.cov(diff.T, aweights=self.weights)
|
|
152
|
+
|
|
153
|
+
def reset(
|
|
154
|
+
self, initial_state: np.ndarray, initial_covariance: np.ndarray | None = None
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Reset filter to initial state."""
|
|
157
|
+
if initial_covariance is None:
|
|
158
|
+
initial_covariance = np.eye(self.state_dim) * 0.1
|
|
159
|
+
self.particles = np.random.multivariate_normal(
|
|
160
|
+
initial_state, initial_covariance, self.num_particles
|
|
161
|
+
)
|
|
162
|
+
self.weights = np.ones(self.num_particles) / self.num_particles
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Footprint module for vehicle shape representation."""
|
|
2
|
+
|
|
3
|
+
from simple_autonomous_car.footprint.base_footprint import BaseFootprint
|
|
4
|
+
from simple_autonomous_car.footprint.circular_footprint import CircularFootprint
|
|
5
|
+
from simple_autonomous_car.footprint.rectangular_footprint import RectangularFootprint
|
|
6
|
+
|
|
7
|
+
__all__ = ["BaseFootprint", "RectangularFootprint", "CircularFootprint"]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Base footprint class for vehicle shape representation."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseFootprint(ABC):
|
|
9
|
+
"""
|
|
10
|
+
Base class for vehicle footprints.
|
|
11
|
+
|
|
12
|
+
A footprint represents the shape of the vehicle in 2D space.
|
|
13
|
+
It's used to ensure the entire vehicle (not just a point) avoids obstacles.
|
|
14
|
+
|
|
15
|
+
Attributes
|
|
16
|
+
----------
|
|
17
|
+
name : str
|
|
18
|
+
Footprint name/identifier.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, name: str = "footprint"):
|
|
22
|
+
"""
|
|
23
|
+
Initialize base footprint.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
name : str, default="footprint"
|
|
28
|
+
Footprint name/identifier.
|
|
29
|
+
"""
|
|
30
|
+
self.name = name
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def get_vertices(self, position: np.ndarray, heading: float) -> np.ndarray:
|
|
34
|
+
"""
|
|
35
|
+
Get footprint vertices in world frame.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
position : np.ndarray
|
|
40
|
+
Vehicle position [x, y] in world frame.
|
|
41
|
+
heading : float
|
|
42
|
+
Vehicle heading angle in radians.
|
|
43
|
+
|
|
44
|
+
Returns
|
|
45
|
+
-------
|
|
46
|
+
np.ndarray
|
|
47
|
+
Array of shape (N, 2) with vertex positions [[x1, y1], [x2, y2], ...].
|
|
48
|
+
"""
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def get_bounding_radius(self) -> float:
|
|
53
|
+
"""
|
|
54
|
+
Get bounding radius (distance from center to farthest point).
|
|
55
|
+
|
|
56
|
+
This is used for efficient collision checking and costmap inflation.
|
|
57
|
+
|
|
58
|
+
Returns
|
|
59
|
+
-------
|
|
60
|
+
float
|
|
61
|
+
Bounding radius in meters.
|
|
62
|
+
"""
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def contains_point(self, point: np.ndarray, position: np.ndarray, heading: float) -> bool:
|
|
67
|
+
"""
|
|
68
|
+
Check if a point is inside the footprint.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
point : np.ndarray
|
|
73
|
+
Point [x, y] in world frame.
|
|
74
|
+
position : np.ndarray
|
|
75
|
+
Vehicle position [x, y] in world frame.
|
|
76
|
+
heading : float
|
|
77
|
+
Vehicle heading angle in radians.
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
bool
|
|
82
|
+
True if point is inside footprint, False otherwise.
|
|
83
|
+
"""
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
def get_inflation_radius(self, padding: float = 0.0) -> float:
|
|
87
|
+
"""
|
|
88
|
+
Get inflation radius for costmap (bounding radius + padding).
|
|
89
|
+
|
|
90
|
+
Parameters
|
|
91
|
+
----------
|
|
92
|
+
padding : float, default=0.0
|
|
93
|
+
Additional safety padding in meters.
|
|
94
|
+
|
|
95
|
+
Returns
|
|
96
|
+
-------
|
|
97
|
+
float
|
|
98
|
+
Inflation radius in meters.
|
|
99
|
+
"""
|
|
100
|
+
return self.get_bounding_radius() + padding
|
|
101
|
+
|
|
102
|
+
def check_collision(self, obstacles: np.ndarray, position: np.ndarray, heading: float) -> bool:
|
|
103
|
+
"""
|
|
104
|
+
Check if footprint collides with any obstacles.
|
|
105
|
+
|
|
106
|
+
Parameters
|
|
107
|
+
----------
|
|
108
|
+
obstacles : np.ndarray
|
|
109
|
+
Array of obstacle positions [[x1, y1], [x2, y2], ...].
|
|
110
|
+
position : np.ndarray
|
|
111
|
+
Vehicle position [x, y] in world frame.
|
|
112
|
+
heading : float
|
|
113
|
+
Vehicle heading angle in radians.
|
|
114
|
+
|
|
115
|
+
Returns
|
|
116
|
+
-------
|
|
117
|
+
bool
|
|
118
|
+
True if collision detected, False otherwise.
|
|
119
|
+
"""
|
|
120
|
+
if len(obstacles) == 0:
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
# Simple collision check: if any obstacle is inside footprint
|
|
124
|
+
for obstacle in obstacles:
|
|
125
|
+
if self.contains_point(obstacle, position, heading):
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
return False
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Circular footprint for vehicles."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from simple_autonomous_car.footprint.base_footprint import BaseFootprint
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CircularFootprint(BaseFootprint):
|
|
9
|
+
"""
|
|
10
|
+
Circular footprint for vehicles.
|
|
11
|
+
|
|
12
|
+
Represents a vehicle as a circle (simpler but less accurate for cars).
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
radius : float
|
|
17
|
+
Vehicle radius in meters.
|
|
18
|
+
name : str, default="circular_footprint"
|
|
19
|
+
Footprint name.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, radius: float, name: str = "circular_footprint"):
|
|
23
|
+
super().__init__(name=name)
|
|
24
|
+
self.radius = radius
|
|
25
|
+
|
|
26
|
+
def get_vertices(self, position: np.ndarray, heading: float) -> np.ndarray:
|
|
27
|
+
"""
|
|
28
|
+
Get circle vertices (approximated as polygon) in world frame.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
position : np.ndarray
|
|
33
|
+
Vehicle position [x, y] in world frame.
|
|
34
|
+
heading : float
|
|
35
|
+
Vehicle heading angle (not used for circle, but kept for interface compatibility).
|
|
36
|
+
|
|
37
|
+
Returns
|
|
38
|
+
-------
|
|
39
|
+
np.ndarray
|
|
40
|
+
Array of shape (N, 2) with circle vertices (approximated as 16-sided polygon).
|
|
41
|
+
"""
|
|
42
|
+
# Approximate circle as 16-sided polygon
|
|
43
|
+
num_vertices = 16
|
|
44
|
+
angles = np.linspace(0, 2 * np.pi, num_vertices, endpoint=False)
|
|
45
|
+
|
|
46
|
+
vertices = position + self.radius * np.column_stack([np.cos(angles), np.sin(angles)])
|
|
47
|
+
|
|
48
|
+
return np.asarray(vertices, dtype=np.float64)
|
|
49
|
+
|
|
50
|
+
def get_bounding_radius(self) -> float:
|
|
51
|
+
"""Get bounding radius (same as circle radius)."""
|
|
52
|
+
return self.radius
|
|
53
|
+
|
|
54
|
+
def contains_point(self, point: np.ndarray, position: np.ndarray, heading: float) -> bool:
|
|
55
|
+
"""
|
|
56
|
+
Check if point is inside circle.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
point : np.ndarray
|
|
61
|
+
Point [x, y] in world frame.
|
|
62
|
+
position : np.ndarray
|
|
63
|
+
Vehicle position [x, y] in world frame.
|
|
64
|
+
heading : float
|
|
65
|
+
Vehicle heading angle (not used for circle).
|
|
66
|
+
|
|
67
|
+
Returns
|
|
68
|
+
-------
|
|
69
|
+
bool
|
|
70
|
+
True if point is inside circle.
|
|
71
|
+
"""
|
|
72
|
+
distance = np.linalg.norm(point - position)
|
|
73
|
+
return bool(distance <= self.radius)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Rectangular footprint for vehicles."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from simple_autonomous_car.footprint.base_footprint import BaseFootprint
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RectangularFootprint(BaseFootprint):
|
|
9
|
+
"""
|
|
10
|
+
Rectangular footprint for vehicles.
|
|
11
|
+
|
|
12
|
+
Represents a vehicle as a rectangle (typical for cars).
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
length : float
|
|
17
|
+
Vehicle length in meters (front to back).
|
|
18
|
+
width : float
|
|
19
|
+
Vehicle width in meters (left to right).
|
|
20
|
+
center_offset : np.ndarray, optional
|
|
21
|
+
Offset of footprint center from vehicle position [x, y] in vehicle frame.
|
|
22
|
+
Default: [0, 0] (center at vehicle position).
|
|
23
|
+
name : str, default="rectangular_footprint"
|
|
24
|
+
Footprint name.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
length: float,
|
|
30
|
+
width: float,
|
|
31
|
+
center_offset: np.ndarray | None = None,
|
|
32
|
+
name: str = "rectangular_footprint",
|
|
33
|
+
):
|
|
34
|
+
super().__init__(name=name)
|
|
35
|
+
self.length = length
|
|
36
|
+
self.width = width
|
|
37
|
+
self.center_offset = center_offset if center_offset is not None else np.array([0.0, 0.0])
|
|
38
|
+
|
|
39
|
+
# Calculate bounding radius (half diagonal)
|
|
40
|
+
self._bounding_radius = np.sqrt((length / 2) ** 2 + (width / 2) ** 2)
|
|
41
|
+
|
|
42
|
+
def get_vertices(self, position: np.ndarray, heading: float) -> np.ndarray:
|
|
43
|
+
"""
|
|
44
|
+
Get rectangle vertices in world frame.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
position : np.ndarray
|
|
49
|
+
Vehicle position [x, y] in world frame.
|
|
50
|
+
heading : float
|
|
51
|
+
Vehicle heading angle in radians.
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
np.ndarray
|
|
56
|
+
Array of shape (4, 2) with rectangle corners.
|
|
57
|
+
"""
|
|
58
|
+
# Define rectangle corners in vehicle frame (centered at origin)
|
|
59
|
+
half_length = self.length / 2
|
|
60
|
+
half_width = self.width / 2
|
|
61
|
+
|
|
62
|
+
corners_vehicle = np.array(
|
|
63
|
+
[
|
|
64
|
+
[half_length, half_width], # Front-right
|
|
65
|
+
[-half_length, half_width], # Rear-right
|
|
66
|
+
[-half_length, -half_width], # Rear-left
|
|
67
|
+
[half_length, -half_width], # Front-left
|
|
68
|
+
]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Apply center offset
|
|
72
|
+
corners_vehicle += self.center_offset
|
|
73
|
+
|
|
74
|
+
# Rotate by heading
|
|
75
|
+
cos_h = np.cos(heading)
|
|
76
|
+
sin_h = np.sin(heading)
|
|
77
|
+
rotation_matrix = np.array([[cos_h, -sin_h], [sin_h, cos_h]])
|
|
78
|
+
corners_rotated = (rotation_matrix @ corners_vehicle.T).T
|
|
79
|
+
|
|
80
|
+
# Translate to world position
|
|
81
|
+
vertices = corners_rotated + position
|
|
82
|
+
|
|
83
|
+
return np.asarray(vertices, dtype=np.float64)
|
|
84
|
+
|
|
85
|
+
def get_bounding_radius(self) -> float:
|
|
86
|
+
"""Get bounding radius (half diagonal of rectangle)."""
|
|
87
|
+
return float(self._bounding_radius)
|
|
88
|
+
|
|
89
|
+
def contains_point(self, point: np.ndarray, position: np.ndarray, heading: float) -> bool:
|
|
90
|
+
"""
|
|
91
|
+
Check if point is inside rectangle.
|
|
92
|
+
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
point : np.ndarray
|
|
96
|
+
Point [x, y] in world frame.
|
|
97
|
+
position : np.ndarray
|
|
98
|
+
Vehicle position [x, y] in world frame.
|
|
99
|
+
heading : float
|
|
100
|
+
Vehicle heading angle in radians.
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
bool
|
|
105
|
+
True if point is inside rectangle.
|
|
106
|
+
"""
|
|
107
|
+
# Transform point to vehicle frame
|
|
108
|
+
point_relative = point - position
|
|
109
|
+
|
|
110
|
+
# Rotate by -heading to align with vehicle frame
|
|
111
|
+
cos_h = np.cos(-heading)
|
|
112
|
+
sin_h = np.sin(-heading)
|
|
113
|
+
rotation_matrix = np.array([[cos_h, -sin_h], [sin_h, cos_h]])
|
|
114
|
+
point_vehicle = rotation_matrix @ point_relative
|
|
115
|
+
|
|
116
|
+
# Apply center offset
|
|
117
|
+
point_vehicle -= self.center_offset
|
|
118
|
+
|
|
119
|
+
# Check if point is within rectangle bounds
|
|
120
|
+
half_length = self.length / 2
|
|
121
|
+
half_width = self.width / 2
|
|
122
|
+
|
|
123
|
+
return bool(abs(point_vehicle[0]) <= half_length and abs(point_vehicle[1]) <= half_width)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Frame conversion utilities for coordinate transformations."""
|
|
2
|
+
|
|
3
|
+
from simple_autonomous_car.frames.frenet import (
|
|
4
|
+
FrenetFrame,
|
|
5
|
+
ego_to_frenet,
|
|
6
|
+
ego_to_sensor,
|
|
7
|
+
frenet_to_ego,
|
|
8
|
+
frenet_to_global,
|
|
9
|
+
global_to_frenet,
|
|
10
|
+
sensor_to_ego,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"FrenetFrame",
|
|
15
|
+
"global_to_frenet",
|
|
16
|
+
"frenet_to_global",
|
|
17
|
+
"ego_to_frenet",
|
|
18
|
+
"frenet_to_ego",
|
|
19
|
+
"sensor_to_ego",
|
|
20
|
+
"ego_to_sensor",
|
|
21
|
+
]
|