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,463 @@
|
|
|
1
|
+
"""Track generation with bounds and centerline."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from matplotlib.axes import Axes
|
|
9
|
+
|
|
10
|
+
from simple_autonomous_car.car.car import CarState
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Track:
|
|
14
|
+
"""Represents a racing track with centerline and bounds."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
centerline: np.ndarray,
|
|
19
|
+
track_width: float = 5.0,
|
|
20
|
+
inner_bound: np.ndarray | None = None,
|
|
21
|
+
outer_bound: np.ndarray | None = None,
|
|
22
|
+
):
|
|
23
|
+
"""
|
|
24
|
+
Initialize track.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
centerline: Array of shape (N, 2) with [x, y] coordinates of centerline
|
|
28
|
+
track_width: Width of the track (used if bounds not provided)
|
|
29
|
+
inner_bound: Array of shape (N, 2) with inner boundary points
|
|
30
|
+
outer_bound: Array of shape (N, 2) with outer boundary points
|
|
31
|
+
"""
|
|
32
|
+
self.centerline = np.asarray(centerline, dtype=np.float64)
|
|
33
|
+
if self.centerline.ndim != 2 or self.centerline.shape[1] != 2:
|
|
34
|
+
raise ValueError("centerline must be shape (N, 2)")
|
|
35
|
+
|
|
36
|
+
self.track_width = track_width
|
|
37
|
+
|
|
38
|
+
if inner_bound is None or outer_bound is None:
|
|
39
|
+
self._generate_bounds()
|
|
40
|
+
else:
|
|
41
|
+
self.inner_bound = np.asarray(inner_bound, dtype=np.float64)
|
|
42
|
+
self.outer_bound = np.asarray(outer_bound, dtype=np.float64)
|
|
43
|
+
|
|
44
|
+
self._validate_bounds()
|
|
45
|
+
|
|
46
|
+
def _generate_bounds(self) -> None:
|
|
47
|
+
"""Generate inner and outer bounds from centerline and width."""
|
|
48
|
+
n_points = len(self.centerline)
|
|
49
|
+
inner_bound = np.zeros_like(self.centerline)
|
|
50
|
+
outer_bound = np.zeros_like(self.centerline)
|
|
51
|
+
|
|
52
|
+
for i in range(n_points):
|
|
53
|
+
# Get direction vector
|
|
54
|
+
if i == 0:
|
|
55
|
+
direction = self.centerline[1] - self.centerline[0]
|
|
56
|
+
elif i == n_points - 1:
|
|
57
|
+
direction = self.centerline[-1] - self.centerline[-2]
|
|
58
|
+
else:
|
|
59
|
+
direction = self.centerline[i + 1] - self.centerline[i - 1]
|
|
60
|
+
|
|
61
|
+
# Normalize and get perpendicular
|
|
62
|
+
direction_norm = np.linalg.norm(direction)
|
|
63
|
+
if direction_norm > 1e-6:
|
|
64
|
+
direction = direction / direction_norm
|
|
65
|
+
else:
|
|
66
|
+
direction = np.array([1.0, 0.0])
|
|
67
|
+
|
|
68
|
+
# Perpendicular vector (rotate 90 degrees)
|
|
69
|
+
perp = np.array([-direction[1], direction[0]])
|
|
70
|
+
|
|
71
|
+
# Generate bounds
|
|
72
|
+
half_width = self.track_width / 2.0
|
|
73
|
+
inner_bound[i] = self.centerline[i] - perp * half_width
|
|
74
|
+
outer_bound[i] = self.centerline[i] + perp * half_width
|
|
75
|
+
|
|
76
|
+
self.inner_bound = inner_bound
|
|
77
|
+
self.outer_bound = outer_bound
|
|
78
|
+
|
|
79
|
+
def _validate_bounds(self) -> None:
|
|
80
|
+
"""Validate that bounds have correct shape."""
|
|
81
|
+
if (
|
|
82
|
+
self.inner_bound.shape != self.centerline.shape
|
|
83
|
+
or self.outer_bound.shape != self.centerline.shape
|
|
84
|
+
):
|
|
85
|
+
raise ValueError("Bounds must have same shape as centerline")
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def create_oval_track(
|
|
89
|
+
cls,
|
|
90
|
+
length: float = 100.0,
|
|
91
|
+
width: float = 30.0,
|
|
92
|
+
track_width: float = 5.0,
|
|
93
|
+
num_points: int = 200,
|
|
94
|
+
) -> "Track":
|
|
95
|
+
"""
|
|
96
|
+
Create an oval-shaped track.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
length: Length of the straight sections
|
|
100
|
+
width: Width of the track (distance between straight sections)
|
|
101
|
+
track_width: Width of the track boundaries
|
|
102
|
+
num_points: Number of points along the centerline
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Track instance
|
|
106
|
+
"""
|
|
107
|
+
# Generate oval centerline
|
|
108
|
+
points = []
|
|
109
|
+
straight_points = int(num_points * 0.3)
|
|
110
|
+
|
|
111
|
+
# First straight
|
|
112
|
+
for i in range(straight_points):
|
|
113
|
+
x = -length / 2 + (i / straight_points) * length
|
|
114
|
+
y = -width / 2
|
|
115
|
+
points.append([x, y])
|
|
116
|
+
|
|
117
|
+
# First curve
|
|
118
|
+
curve_points = int(num_points * 0.2)
|
|
119
|
+
for i in range(curve_points):
|
|
120
|
+
angle = np.pi * (i / curve_points)
|
|
121
|
+
x = length / 2 + (width / 2) * np.cos(angle)
|
|
122
|
+
y = -width / 2 + (width / 2) * np.sin(angle)
|
|
123
|
+
points.append([x, y])
|
|
124
|
+
|
|
125
|
+
# Second straight
|
|
126
|
+
for i in range(straight_points):
|
|
127
|
+
x = length / 2 - (i / straight_points) * length
|
|
128
|
+
y = width / 2
|
|
129
|
+
points.append([x, y])
|
|
130
|
+
|
|
131
|
+
# Second curve
|
|
132
|
+
for i in range(curve_points):
|
|
133
|
+
angle = np.pi + np.pi * (i / curve_points)
|
|
134
|
+
x = -length / 2 + (width / 2) * np.cos(angle)
|
|
135
|
+
y = width / 2 + (width / 2) * np.sin(angle)
|
|
136
|
+
points.append([x, y])
|
|
137
|
+
|
|
138
|
+
return cls(np.array(points), track_width=track_width)
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def create_simple_track(
|
|
142
|
+
cls,
|
|
143
|
+
length: float = 100.0,
|
|
144
|
+
width: float = 50.0,
|
|
145
|
+
track_width: float = 5.0,
|
|
146
|
+
num_points: int = 200,
|
|
147
|
+
) -> "Track":
|
|
148
|
+
"""
|
|
149
|
+
Create a simple rounded rectangle track with gentle curves (no 180 or 360 degree turns).
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
length: Overall length of the track
|
|
153
|
+
width: Overall width of the track
|
|
154
|
+
track_width: Width of the track boundaries
|
|
155
|
+
num_points: Number of points along the centerline
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Track instance
|
|
159
|
+
"""
|
|
160
|
+
points = []
|
|
161
|
+
|
|
162
|
+
# Create a rounded rectangle with very gentle corners
|
|
163
|
+
# Use large radius for corners to avoid sharp turns
|
|
164
|
+
corner_radius = min(length, width) * 0.3 # Large radius for gentle curves
|
|
165
|
+
|
|
166
|
+
# Calculate dimensions
|
|
167
|
+
straight_length = length - 2 * corner_radius
|
|
168
|
+
straight_width = width - 2 * corner_radius
|
|
169
|
+
|
|
170
|
+
# Distribute points: more on straights, fewer on gentle curves
|
|
171
|
+
straight_points = int(num_points * 0.35)
|
|
172
|
+
corner_points = int(num_points * 0.15)
|
|
173
|
+
|
|
174
|
+
# Bottom straight (left to right)
|
|
175
|
+
for i in range(straight_points):
|
|
176
|
+
x = -length / 2 + corner_radius + (i / straight_points) * straight_length
|
|
177
|
+
y = -width / 2
|
|
178
|
+
points.append([x, y])
|
|
179
|
+
|
|
180
|
+
# Bottom-right corner (gentle curve, max 90 degrees)
|
|
181
|
+
for i in range(corner_points):
|
|
182
|
+
angle = -np.pi / 2 + (i / corner_points) * (np.pi / 2) # 0 to 90 degrees
|
|
183
|
+
x = length / 2 - corner_radius + corner_radius * np.cos(angle)
|
|
184
|
+
y = -width / 2 + corner_radius + corner_radius * np.sin(angle)
|
|
185
|
+
points.append([x, y])
|
|
186
|
+
|
|
187
|
+
# Right straight (bottom to top)
|
|
188
|
+
for i in range(straight_points):
|
|
189
|
+
x = length / 2
|
|
190
|
+
y = -width / 2 + corner_radius + (i / straight_points) * straight_width
|
|
191
|
+
points.append([x, y])
|
|
192
|
+
|
|
193
|
+
# Top-right corner (gentle curve)
|
|
194
|
+
for i in range(corner_points):
|
|
195
|
+
angle = 0 + (i / corner_points) * (np.pi / 2) # 0 to 90 degrees
|
|
196
|
+
x = length / 2 - corner_radius + corner_radius * np.cos(angle)
|
|
197
|
+
y = width / 2 - corner_radius + corner_radius * np.sin(angle)
|
|
198
|
+
points.append([x, y])
|
|
199
|
+
|
|
200
|
+
# Top straight (right to left)
|
|
201
|
+
for i in range(straight_points):
|
|
202
|
+
x = length / 2 - corner_radius - (i / straight_points) * straight_length
|
|
203
|
+
y = width / 2
|
|
204
|
+
points.append([x, y])
|
|
205
|
+
|
|
206
|
+
# Top-left corner (gentle curve)
|
|
207
|
+
for i in range(corner_points):
|
|
208
|
+
angle = np.pi / 2 + (i / corner_points) * (np.pi / 2) # 90 to 180 degrees
|
|
209
|
+
x = -length / 2 + corner_radius + corner_radius * np.cos(angle)
|
|
210
|
+
y = width / 2 - corner_radius + corner_radius * np.sin(angle)
|
|
211
|
+
points.append([x, y])
|
|
212
|
+
|
|
213
|
+
# Left straight (top to bottom)
|
|
214
|
+
for i in range(straight_points):
|
|
215
|
+
x = -length / 2
|
|
216
|
+
y = width / 2 - corner_radius - (i / straight_points) * straight_width
|
|
217
|
+
points.append([x, y])
|
|
218
|
+
|
|
219
|
+
# Bottom-left corner (gentle curve) - close the loop
|
|
220
|
+
for i in range(corner_points):
|
|
221
|
+
angle = np.pi + (i / corner_points) * (np.pi / 2) # 180 to 270 degrees
|
|
222
|
+
x = -length / 2 + corner_radius + corner_radius * np.cos(angle)
|
|
223
|
+
y = -width / 2 + corner_radius + corner_radius * np.sin(angle)
|
|
224
|
+
points.append([x, y])
|
|
225
|
+
|
|
226
|
+
return cls(np.array(points), track_width=track_width)
|
|
227
|
+
|
|
228
|
+
@classmethod
|
|
229
|
+
def create_figure8_track(
|
|
230
|
+
cls,
|
|
231
|
+
size: float = 60.0,
|
|
232
|
+
track_width: float = 5.0,
|
|
233
|
+
num_points: int = 300,
|
|
234
|
+
) -> "Track":
|
|
235
|
+
"""
|
|
236
|
+
Create a figure-8 (lemniscate) shaped track.
|
|
237
|
+
|
|
238
|
+
The track forms a smooth figure-8 pattern with two loops that cross in the middle.
|
|
239
|
+
This creates a more challenging track with crossing paths and varying curvature.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
size: Overall size of the track (controls the scale, approximately the diameter)
|
|
243
|
+
track_width: Width of the track boundaries
|
|
244
|
+
num_points: Number of points along the centerline
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Track instance
|
|
248
|
+
"""
|
|
249
|
+
points = []
|
|
250
|
+
|
|
251
|
+
# Use parametric equations for a lemniscate of Bernoulli (figure-8 curve)
|
|
252
|
+
# Parametric form: x = a * sin(t) / (1 + cos^2(t)), y = a * sin(t) * cos(t) / (1 + cos^2(t))
|
|
253
|
+
# This creates a smooth, symmetric figure-8
|
|
254
|
+
|
|
255
|
+
# Generate parameter t from 0 to 2*pi
|
|
256
|
+
t_values = np.linspace(0, 2 * np.pi, num_points)
|
|
257
|
+
|
|
258
|
+
# Scale factor for the lemniscate (adjust to match desired size)
|
|
259
|
+
a = size / 2.5 # Adjusted for better size control
|
|
260
|
+
|
|
261
|
+
for t in t_values:
|
|
262
|
+
# Lemniscate parametric equations
|
|
263
|
+
# Denominator ensures smooth curve and proper figure-8 shape
|
|
264
|
+
denom = 1 + np.cos(t) ** 2
|
|
265
|
+
x = a * np.sin(t) / denom
|
|
266
|
+
y = a * np.sin(t) * np.cos(t) / denom
|
|
267
|
+
|
|
268
|
+
points.append([x, y])
|
|
269
|
+
|
|
270
|
+
return cls(np.array(points), track_width=track_width)
|
|
271
|
+
|
|
272
|
+
def get_point_at_distance(self, distance: float) -> tuple[np.ndarray, float]:
|
|
273
|
+
"""
|
|
274
|
+
Get point on centerline at given distance along track.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
distance: Distance along track from start
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Tuple of (point, heading_angle)
|
|
281
|
+
"""
|
|
282
|
+
cumulative_distances = np.zeros(len(self.centerline))
|
|
283
|
+
for i in range(1, len(self.centerline)):
|
|
284
|
+
dist = np.linalg.norm(self.centerline[i] - self.centerline[i - 1])
|
|
285
|
+
cumulative_distances[i] = cumulative_distances[i - 1] + dist
|
|
286
|
+
|
|
287
|
+
total_length = cumulative_distances[-1]
|
|
288
|
+
distance = distance % total_length # Wrap around
|
|
289
|
+
|
|
290
|
+
# Find segment
|
|
291
|
+
idx_int = np.searchsorted(cumulative_distances, distance)
|
|
292
|
+
idx: int = int(idx_int)
|
|
293
|
+
if idx == 0:
|
|
294
|
+
idx = 1
|
|
295
|
+
if idx >= len(self.centerline):
|
|
296
|
+
idx = len(self.centerline) - 1
|
|
297
|
+
|
|
298
|
+
# Interpolate
|
|
299
|
+
segment_dist = distance - cumulative_distances[idx - 1]
|
|
300
|
+
segment_length = cumulative_distances[idx] - cumulative_distances[idx - 1]
|
|
301
|
+
if segment_length > 1e-6:
|
|
302
|
+
t = segment_dist / segment_length
|
|
303
|
+
else:
|
|
304
|
+
t = 0.0
|
|
305
|
+
|
|
306
|
+
point = self.centerline[idx - 1] + t * (self.centerline[idx] - self.centerline[idx - 1])
|
|
307
|
+
|
|
308
|
+
# Calculate heading
|
|
309
|
+
direction = self.centerline[idx] - self.centerline[idx - 1]
|
|
310
|
+
heading = np.arctan2(direction[1], direction[0])
|
|
311
|
+
|
|
312
|
+
return point, heading
|
|
313
|
+
|
|
314
|
+
def visualize(
|
|
315
|
+
self,
|
|
316
|
+
ax: "Axes",
|
|
317
|
+
car_state: Optional["CarState"] = None,
|
|
318
|
+
frame: str = "global",
|
|
319
|
+
**kwargs: Any,
|
|
320
|
+
) -> None:
|
|
321
|
+
"""
|
|
322
|
+
Visualize track boundaries on the given axes.
|
|
323
|
+
|
|
324
|
+
Parameters
|
|
325
|
+
----------
|
|
326
|
+
ax : matplotlib.axes.Axes
|
|
327
|
+
Axes to plot on.
|
|
328
|
+
car_state : CarState, optional
|
|
329
|
+
Current car state (for frame transformations).
|
|
330
|
+
frame : str, default="global"
|
|
331
|
+
Frame to plot in: "global" or "ego".
|
|
332
|
+
**kwargs
|
|
333
|
+
Additional visualization arguments:
|
|
334
|
+
- show_centerline: bool, whether to show centerline
|
|
335
|
+
- show_bounds: bool, whether to show track boundaries
|
|
336
|
+
- centerline_color: str, color for centerline
|
|
337
|
+
- bounds_color: str, color for boundaries
|
|
338
|
+
- bounds_linewidth: float, linewidth for boundaries
|
|
339
|
+
- horizon: float, visualization horizon (for ego frame filtering)
|
|
340
|
+
"""
|
|
341
|
+
show_centerline = kwargs.pop("show_centerline", True)
|
|
342
|
+
show_bounds = kwargs.pop("show_bounds", True)
|
|
343
|
+
centerline_color = kwargs.pop("centerline_color", "b")
|
|
344
|
+
bounds_color = kwargs.pop("bounds_color", "k")
|
|
345
|
+
bounds_linewidth = kwargs.pop("bounds_linewidth", 2.5)
|
|
346
|
+
horizon = kwargs.pop("horizon", None)
|
|
347
|
+
|
|
348
|
+
if frame == "ego" and car_state is not None:
|
|
349
|
+
# Transform track to ego frame
|
|
350
|
+
if show_bounds:
|
|
351
|
+
inner_bound_ego = np.array(
|
|
352
|
+
[car_state.transform_to_car_frame(point) for point in self.inner_bound]
|
|
353
|
+
)
|
|
354
|
+
outer_bound_ego = np.array(
|
|
355
|
+
[car_state.transform_to_car_frame(point) for point in self.outer_bound]
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
# Filter points within horizon for performance
|
|
359
|
+
if horizon is not None:
|
|
360
|
+
mask_inner = (np.abs(inner_bound_ego[:, 0]) < horizon * 1.2) & (
|
|
361
|
+
np.abs(inner_bound_ego[:, 1]) < horizon * 1.2
|
|
362
|
+
)
|
|
363
|
+
mask_outer = (np.abs(outer_bound_ego[:, 0]) < horizon * 1.2) & (
|
|
364
|
+
np.abs(outer_bound_ego[:, 1]) < horizon * 1.2
|
|
365
|
+
)
|
|
366
|
+
else:
|
|
367
|
+
mask_inner = np.ones(len(inner_bound_ego), dtype=bool)
|
|
368
|
+
mask_outer = np.ones(len(outer_bound_ego), dtype=bool)
|
|
369
|
+
|
|
370
|
+
if np.any(mask_inner):
|
|
371
|
+
ax.plot(
|
|
372
|
+
inner_bound_ego[mask_inner, 0],
|
|
373
|
+
inner_bound_ego[mask_inner, 1],
|
|
374
|
+
"-",
|
|
375
|
+
color=bounds_color,
|
|
376
|
+
linewidth=bounds_linewidth,
|
|
377
|
+
label="Map",
|
|
378
|
+
alpha=0.9,
|
|
379
|
+
zorder=1,
|
|
380
|
+
**kwargs,
|
|
381
|
+
)
|
|
382
|
+
if np.any(mask_outer):
|
|
383
|
+
ax.plot(
|
|
384
|
+
outer_bound_ego[mask_outer, 0],
|
|
385
|
+
outer_bound_ego[mask_outer, 1],
|
|
386
|
+
"-",
|
|
387
|
+
color=bounds_color,
|
|
388
|
+
linewidth=bounds_linewidth,
|
|
389
|
+
alpha=0.9,
|
|
390
|
+
zorder=1,
|
|
391
|
+
**kwargs,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
if show_centerline:
|
|
395
|
+
centerline_ego = np.array(
|
|
396
|
+
[car_state.transform_to_car_frame(point) for point in self.centerline]
|
|
397
|
+
)
|
|
398
|
+
if horizon is not None:
|
|
399
|
+
mask = (np.abs(centerline_ego[:, 0]) < horizon * 1.2) & (
|
|
400
|
+
np.abs(centerline_ego[:, 1]) < horizon * 1.2
|
|
401
|
+
)
|
|
402
|
+
else:
|
|
403
|
+
mask = np.ones(len(centerline_ego), dtype=bool)
|
|
404
|
+
|
|
405
|
+
if np.any(mask):
|
|
406
|
+
ax.plot(
|
|
407
|
+
centerline_ego[mask, 0],
|
|
408
|
+
centerline_ego[mask, 1],
|
|
409
|
+
"--",
|
|
410
|
+
color=centerline_color,
|
|
411
|
+
linewidth=1.5,
|
|
412
|
+
alpha=0.5,
|
|
413
|
+
label="Centerline",
|
|
414
|
+
**kwargs,
|
|
415
|
+
)
|
|
416
|
+
else:
|
|
417
|
+
# Global frame
|
|
418
|
+
if show_bounds:
|
|
419
|
+
ax.plot(
|
|
420
|
+
self.inner_bound[:, 0],
|
|
421
|
+
self.inner_bound[:, 1],
|
|
422
|
+
"-",
|
|
423
|
+
color=bounds_color,
|
|
424
|
+
linewidth=bounds_linewidth,
|
|
425
|
+
label="Track Bounds",
|
|
426
|
+
**kwargs,
|
|
427
|
+
)
|
|
428
|
+
ax.plot(
|
|
429
|
+
self.outer_bound[:, 0],
|
|
430
|
+
self.outer_bound[:, 1],
|
|
431
|
+
"-",
|
|
432
|
+
color=bounds_color,
|
|
433
|
+
linewidth=bounds_linewidth,
|
|
434
|
+
**kwargs,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
if show_centerline:
|
|
438
|
+
ax.plot(
|
|
439
|
+
self.centerline[:, 0],
|
|
440
|
+
self.centerline[:, 1],
|
|
441
|
+
"--",
|
|
442
|
+
color=centerline_color,
|
|
443
|
+
linewidth=1.5,
|
|
444
|
+
alpha=0.5,
|
|
445
|
+
label="Centerline",
|
|
446
|
+
**kwargs,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
def get_bounds_at_point(self, point: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
|
450
|
+
"""
|
|
451
|
+
Get inner and outer boundary points closest to given point.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
point: Point [x, y]
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
Tuple of (inner_bound_point, outer_bound_point)
|
|
458
|
+
"""
|
|
459
|
+
# Find closest centerline point
|
|
460
|
+
distances = np.linalg.norm(self.centerline - point, axis=1)
|
|
461
|
+
idx = np.argmin(distances)
|
|
462
|
+
|
|
463
|
+
return self.inner_bound[idx], self.outer_bound[idx]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Visualization and animation utilities.
|
|
2
|
+
|
|
3
|
+
All visualization is now component-based. Use component.visualize() methods:
|
|
4
|
+
- track.visualize()
|
|
5
|
+
- planner.visualize()
|
|
6
|
+
- controller.visualize()
|
|
7
|
+
- costmap.visualize()
|
|
8
|
+
|
|
9
|
+
Utility functions are provided for non-component data (perception, car).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from simple_autonomous_car.control.controller_viz import (
|
|
13
|
+
plot_control_history,
|
|
14
|
+
plot_pure_pursuit_state,
|
|
15
|
+
)
|
|
16
|
+
from simple_autonomous_car.visualization.alert_viz import AlertVisualizer
|
|
17
|
+
from simple_autonomous_car.visualization.utils import plot_car, plot_perception
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"AlertVisualizer",
|
|
21
|
+
"plot_perception",
|
|
22
|
+
"plot_car",
|
|
23
|
+
"plot_pure_pursuit_state",
|
|
24
|
+
"plot_control_history",
|
|
25
|
+
]
|