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,187 @@
|
|
|
1
|
+
"""Base costmap class for obstacle representation."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from simple_autonomous_car.car.car import CarState
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BaseCostmap(ABC):
|
|
12
|
+
"""
|
|
13
|
+
Base class for all costmaps.
|
|
14
|
+
|
|
15
|
+
Costmaps represent the cost of traversing different areas in the environment,
|
|
16
|
+
typically derived from perception data (obstacles, track boundaries, etc.).
|
|
17
|
+
|
|
18
|
+
Attributes
|
|
19
|
+
----------
|
|
20
|
+
resolution : float
|
|
21
|
+
Resolution of the costmap (meters per cell).
|
|
22
|
+
inflation_radius : float
|
|
23
|
+
Inflation radius for obstacles (meters).
|
|
24
|
+
enabled : bool
|
|
25
|
+
Whether the costmap is enabled.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
resolution: float = 0.5,
|
|
31
|
+
inflation_radius: float = 1.0,
|
|
32
|
+
enabled: bool = True,
|
|
33
|
+
):
|
|
34
|
+
"""
|
|
35
|
+
Initialize base costmap.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
resolution : float, default=0.5
|
|
40
|
+
Resolution of the costmap in meters per cell.
|
|
41
|
+
inflation_radius : float, default=1.0
|
|
42
|
+
Inflation radius for obstacles in meters.
|
|
43
|
+
enabled : bool, default=True
|
|
44
|
+
Whether the costmap is enabled.
|
|
45
|
+
"""
|
|
46
|
+
self.resolution = resolution
|
|
47
|
+
self.inflation_radius = inflation_radius
|
|
48
|
+
self.enabled = enabled
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def update(
|
|
52
|
+
self,
|
|
53
|
+
perception_data: dict | None = None,
|
|
54
|
+
car_state: CarState | None = None,
|
|
55
|
+
static_obstacles: np.ndarray | None = None,
|
|
56
|
+
frame: str = "global",
|
|
57
|
+
) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Update costmap from perception data or static obstacles.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
perception_data : dict, optional
|
|
64
|
+
Dictionary of perception data from sensors.
|
|
65
|
+
Keys are sensor names, values are PerceptionPoints.
|
|
66
|
+
car_state : CarState, optional
|
|
67
|
+
Current car state (required for ego frame or perception_data).
|
|
68
|
+
static_obstacles : np.ndarray, optional
|
|
69
|
+
Static obstacles as array of shape (N, 2) with [x, y] positions in global frame.
|
|
70
|
+
Used for static maps when perception_data is not available.
|
|
71
|
+
frame : str, default="global"
|
|
72
|
+
Frame to use for costmap ("global" or "ego").
|
|
73
|
+
"""
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
def get_cost(
|
|
78
|
+
self, position: np.ndarray, frame: str = "global", car_state: CarState | None = None
|
|
79
|
+
) -> float:
|
|
80
|
+
"""
|
|
81
|
+
Get cost at a specific position.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
position : np.ndarray
|
|
86
|
+
Position as [x, y] array.
|
|
87
|
+
frame : str, default="global"
|
|
88
|
+
Frame of the position ("global" or "ego").
|
|
89
|
+
car_state : CarState, optional
|
|
90
|
+
Car state for frame conversion (if needed).
|
|
91
|
+
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
float
|
|
95
|
+
Cost value (0.0 = free, 1.0 = occupied, 0.0-1.0 = inflated).
|
|
96
|
+
"""
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
@abstractmethod
|
|
100
|
+
def get_cost_region(
|
|
101
|
+
self,
|
|
102
|
+
center: np.ndarray,
|
|
103
|
+
size: float,
|
|
104
|
+
frame: str = "global",
|
|
105
|
+
) -> np.ndarray:
|
|
106
|
+
"""
|
|
107
|
+
Get cost values in a region.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
center : np.ndarray
|
|
112
|
+
Center position as [x, y].
|
|
113
|
+
size : float
|
|
114
|
+
Size of the region in meters.
|
|
115
|
+
frame : str, default="global"
|
|
116
|
+
Frame of the center position.
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
np.ndarray
|
|
121
|
+
2D array of cost values.
|
|
122
|
+
"""
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
def is_enabled(self) -> bool:
|
|
126
|
+
"""Check if costmap is enabled."""
|
|
127
|
+
return self.enabled
|
|
128
|
+
|
|
129
|
+
def enable(self) -> None:
|
|
130
|
+
"""Enable the costmap."""
|
|
131
|
+
self.enabled = True
|
|
132
|
+
|
|
133
|
+
def disable(self) -> None:
|
|
134
|
+
"""Disable the costmap."""
|
|
135
|
+
self.enabled = False
|
|
136
|
+
|
|
137
|
+
def get_visualization_data(
|
|
138
|
+
self, car_state: CarState | None = None, **kwargs: Any
|
|
139
|
+
) -> dict[str, Any]:
|
|
140
|
+
"""
|
|
141
|
+
Get visualization data for this costmap.
|
|
142
|
+
|
|
143
|
+
This method should be overridden by subclasses to provide
|
|
144
|
+
costmap-specific visualization data (e.g., costmap array, bounds, origin).
|
|
145
|
+
|
|
146
|
+
Parameters
|
|
147
|
+
----------
|
|
148
|
+
car_state : CarState, optional
|
|
149
|
+
Current car state (for frame transformations).
|
|
150
|
+
**kwargs
|
|
151
|
+
Additional arguments.
|
|
152
|
+
|
|
153
|
+
Returns
|
|
154
|
+
-------
|
|
155
|
+
Dict
|
|
156
|
+
Dictionary containing visualization data. Default implementation
|
|
157
|
+
returns basic info. Subclasses should override to provide costmap data.
|
|
158
|
+
"""
|
|
159
|
+
return {
|
|
160
|
+
"enabled": self.enabled,
|
|
161
|
+
"resolution": self.resolution,
|
|
162
|
+
"inflation_radius": self.inflation_radius,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
def visualize(
|
|
166
|
+
self, ax: Any, car_state: CarState | None = None, frame: str = "global", **kwargs: Any
|
|
167
|
+
) -> None:
|
|
168
|
+
"""
|
|
169
|
+
Visualize costmap on the given axes.
|
|
170
|
+
|
|
171
|
+
This method should be overridden by subclasses to plot
|
|
172
|
+
costmap-specific visualizations.
|
|
173
|
+
|
|
174
|
+
Parameters
|
|
175
|
+
----------
|
|
176
|
+
ax : matplotlib.axes.Axes
|
|
177
|
+
Axes to plot on.
|
|
178
|
+
car_state : CarState, optional
|
|
179
|
+
Current car state (for frame transformations).
|
|
180
|
+
frame : str, default="global"
|
|
181
|
+
Frame to plot in: "global" or "ego".
|
|
182
|
+
**kwargs
|
|
183
|
+
Additional visualization arguments (colors, alpha, etc.).
|
|
184
|
+
"""
|
|
185
|
+
# Default implementation does nothing
|
|
186
|
+
# Subclasses should override to provide visualization
|
|
187
|
+
pass
|
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
"""Grid-based costmap implementation."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from simple_autonomous_car.car.car import CarState
|
|
8
|
+
from simple_autonomous_car.costmap.base_costmap import BaseCostmap
|
|
9
|
+
from simple_autonomous_car.costmap.inflation import inflate_obstacles
|
|
10
|
+
from simple_autonomous_car.perception.perception import PerceptionPoints
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from simple_autonomous_car.footprint.base_footprint import BaseFootprint
|
|
14
|
+
from simple_autonomous_car.constants import (
|
|
15
|
+
COST_FREE,
|
|
16
|
+
COST_OCCUPIED,
|
|
17
|
+
DEFAULT_COSTMAP_ALPHA,
|
|
18
|
+
DEFAULT_COSTMAP_HEIGHT,
|
|
19
|
+
DEFAULT_COSTMAP_RESOLUTION,
|
|
20
|
+
DEFAULT_COSTMAP_WIDTH,
|
|
21
|
+
DEFAULT_COSTMAP_ZORDER,
|
|
22
|
+
DEFAULT_INFLATION_RADIUS,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class GridCostmap(BaseCostmap):
|
|
27
|
+
"""
|
|
28
|
+
Grid-based costmap for obstacle representation.
|
|
29
|
+
|
|
30
|
+
This costmap represents the environment as a 2D grid where each cell
|
|
31
|
+
has a cost value (0.0 = free, 1.0 = occupied, 0.0-1.0 = inflated).
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
width : float
|
|
36
|
+
Width of costmap in meters.
|
|
37
|
+
height : float
|
|
38
|
+
Height of costmap in meters.
|
|
39
|
+
resolution : float, default=0.5
|
|
40
|
+
Resolution in meters per cell.
|
|
41
|
+
inflation_radius : float, optional
|
|
42
|
+
Inflation radius for obstacles in meters.
|
|
43
|
+
If None and footprint provided, uses footprint bounding radius + padding.
|
|
44
|
+
If None and no footprint, uses DEFAULT_INFLATION_RADIUS.
|
|
45
|
+
footprint : BaseFootprint, optional
|
|
46
|
+
Vehicle footprint for accurate collision checking.
|
|
47
|
+
If provided, inflation_radius is calculated from footprint.
|
|
48
|
+
footprint_padding : float, default=0.0
|
|
49
|
+
Additional safety padding around footprint in meters.
|
|
50
|
+
origin : np.ndarray, optional
|
|
51
|
+
Origin position [x, y] in global frame. If None, uses car position.
|
|
52
|
+
frame : str, default="ego"
|
|
53
|
+
Frame for costmap: "ego" (car-centered) or "global".
|
|
54
|
+
enabled : bool, default=True
|
|
55
|
+
Whether costmap is enabled.
|
|
56
|
+
|
|
57
|
+
Attributes
|
|
58
|
+
----------
|
|
59
|
+
costmap : np.ndarray
|
|
60
|
+
2D array of cost values.
|
|
61
|
+
origin : np.ndarray
|
|
62
|
+
Origin position in global frame.
|
|
63
|
+
width_pixels : int
|
|
64
|
+
Width in pixels.
|
|
65
|
+
height_pixels : int
|
|
66
|
+
Height in pixels.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
width: float = DEFAULT_COSTMAP_WIDTH,
|
|
72
|
+
height: float = DEFAULT_COSTMAP_HEIGHT,
|
|
73
|
+
resolution: float = DEFAULT_COSTMAP_RESOLUTION,
|
|
74
|
+
inflation_radius: float | None = None,
|
|
75
|
+
footprint: Optional["BaseFootprint"] = None,
|
|
76
|
+
footprint_padding: float = 0.0,
|
|
77
|
+
origin: np.ndarray | None = None,
|
|
78
|
+
frame: str = "ego",
|
|
79
|
+
enabled: bool = True,
|
|
80
|
+
):
|
|
81
|
+
# Determine inflation radius: use footprint if provided, otherwise use provided or default
|
|
82
|
+
self.footprint = footprint
|
|
83
|
+
self.footprint_padding = footprint_padding
|
|
84
|
+
|
|
85
|
+
if inflation_radius is None:
|
|
86
|
+
if footprint is not None:
|
|
87
|
+
# Use footprint bounding radius + padding
|
|
88
|
+
inflation_radius = footprint.get_inflation_radius(padding=footprint_padding)
|
|
89
|
+
else:
|
|
90
|
+
# Use default
|
|
91
|
+
inflation_radius = DEFAULT_INFLATION_RADIUS
|
|
92
|
+
|
|
93
|
+
super().__init__(resolution=resolution, inflation_radius=inflation_radius, enabled=enabled)
|
|
94
|
+
self.width = width
|
|
95
|
+
self.height = height
|
|
96
|
+
self.width_pixels = int(width / resolution)
|
|
97
|
+
self.height_pixels = int(height / resolution)
|
|
98
|
+
self.frame = frame
|
|
99
|
+
self.origin = origin if origin is not None else np.array([0.0, 0.0])
|
|
100
|
+
|
|
101
|
+
# Initialize costmap (0.0 = free space)
|
|
102
|
+
self.costmap = np.zeros((self.height_pixels, self.width_pixels), dtype=np.float32)
|
|
103
|
+
|
|
104
|
+
def _world_to_grid(
|
|
105
|
+
self, position: np.ndarray, car_state: CarState | None = None
|
|
106
|
+
) -> tuple[int, int]:
|
|
107
|
+
"""
|
|
108
|
+
Convert world position to grid coordinates.
|
|
109
|
+
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
position : np.ndarray
|
|
113
|
+
Position in world frame [x, y].
|
|
114
|
+
car_state : CarState, optional
|
|
115
|
+
Car state for ego frame conversion.
|
|
116
|
+
|
|
117
|
+
Returns
|
|
118
|
+
-------
|
|
119
|
+
Tuple[int, int]
|
|
120
|
+
Grid coordinates (row, col).
|
|
121
|
+
"""
|
|
122
|
+
if self.frame == "ego" and car_state is not None:
|
|
123
|
+
# Convert to ego frame
|
|
124
|
+
position_ego = car_state.transform_to_car_frame(position)
|
|
125
|
+
x, y = position_ego[0], position_ego[1]
|
|
126
|
+
else:
|
|
127
|
+
# Global frame
|
|
128
|
+
x = position[0] - self.origin[0]
|
|
129
|
+
y = position[1] - self.origin[1]
|
|
130
|
+
|
|
131
|
+
col = int((x + self.width / 2) / self.resolution)
|
|
132
|
+
row = int((y + self.height / 2) / self.resolution)
|
|
133
|
+
|
|
134
|
+
return row, col
|
|
135
|
+
|
|
136
|
+
def _grid_to_world(self, row: int, col: int, car_state: CarState | None = None) -> np.ndarray:
|
|
137
|
+
"""
|
|
138
|
+
Convert grid coordinates to world position.
|
|
139
|
+
|
|
140
|
+
Parameters
|
|
141
|
+
----------
|
|
142
|
+
row : int
|
|
143
|
+
Grid row.
|
|
144
|
+
col : int
|
|
145
|
+
Grid column.
|
|
146
|
+
car_state : CarState, optional
|
|
147
|
+
Car state for ego frame conversion.
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
np.ndarray
|
|
152
|
+
Position in world frame [x, y].
|
|
153
|
+
"""
|
|
154
|
+
x = (col * self.resolution) - (self.width / 2)
|
|
155
|
+
y = (row * self.resolution) - (self.height / 2)
|
|
156
|
+
|
|
157
|
+
if self.frame == "ego" and car_state is not None:
|
|
158
|
+
# Convert from ego to global
|
|
159
|
+
position_ego = np.array([x, y])
|
|
160
|
+
position = car_state.transform_to_world_frame(position_ego)
|
|
161
|
+
else:
|
|
162
|
+
position = np.array([x + self.origin[0], y + self.origin[1]])
|
|
163
|
+
|
|
164
|
+
return position
|
|
165
|
+
|
|
166
|
+
def update(
|
|
167
|
+
self,
|
|
168
|
+
perception_data: dict[str, PerceptionPoints] | None = None,
|
|
169
|
+
car_state: CarState | None = None,
|
|
170
|
+
static_obstacles: np.ndarray | None = None,
|
|
171
|
+
frame: str = "global",
|
|
172
|
+
) -> None:
|
|
173
|
+
"""
|
|
174
|
+
Update costmap from perception data or static obstacles.
|
|
175
|
+
|
|
176
|
+
Parameters
|
|
177
|
+
----------
|
|
178
|
+
perception_data : dict, optional
|
|
179
|
+
Dictionary of perception data from sensors.
|
|
180
|
+
If None and static_obstacles provided, uses static obstacles.
|
|
181
|
+
car_state : CarState, optional
|
|
182
|
+
Current car state (required if using perception_data or ego frame).
|
|
183
|
+
static_obstacles : np.ndarray, optional
|
|
184
|
+
Static obstacles as array of shape (N, 2) with [x, y] positions in global frame.
|
|
185
|
+
Used when perception_data is None (e.g., for static maps).
|
|
186
|
+
frame : str, default="global"
|
|
187
|
+
Frame to use (not used if costmap has fixed frame).
|
|
188
|
+
"""
|
|
189
|
+
if not self.enabled:
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
if car_state is None and (self.frame == "ego" or perception_data is not None):
|
|
193
|
+
raise ValueError("car_state is required for ego frame or perception_data updates")
|
|
194
|
+
|
|
195
|
+
# Update origin if ego frame (but keep grid size fixed)
|
|
196
|
+
if self.frame == "ego" and car_state is not None:
|
|
197
|
+
# Store new origin but grid dimensions stay the same
|
|
198
|
+
# The grid is always centered at origin in ego frame
|
|
199
|
+
self.origin = car_state.position().copy()
|
|
200
|
+
|
|
201
|
+
# Reset costmap (grid size is fixed, only data changes)
|
|
202
|
+
self.costmap.fill(COST_FREE)
|
|
203
|
+
|
|
204
|
+
# Mark obstacles from static obstacles (if provided)
|
|
205
|
+
if static_obstacles is not None and len(static_obstacles) > 0:
|
|
206
|
+
for obstacle_pos in static_obstacles:
|
|
207
|
+
row, col = self._world_to_grid(obstacle_pos, car_state)
|
|
208
|
+
if 0 <= row < self.height_pixels and 0 <= col < self.width_pixels:
|
|
209
|
+
self.costmap[row, col] = COST_OCCUPIED
|
|
210
|
+
|
|
211
|
+
# Mark obstacles from perception data (if provided)
|
|
212
|
+
if perception_data is not None:
|
|
213
|
+
for sensor_name, perception_points in perception_data.items():
|
|
214
|
+
if perception_points is None or len(perception_points.points) == 0:
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
# Convert to global frame if needed
|
|
218
|
+
if perception_points.frame != "global" and car_state is not None:
|
|
219
|
+
points_global = perception_points.to_global_frame(car_state).points
|
|
220
|
+
else:
|
|
221
|
+
points_global = perception_points.points
|
|
222
|
+
|
|
223
|
+
# Mark obstacles in costmap
|
|
224
|
+
for point in points_global:
|
|
225
|
+
row, col = self._world_to_grid(point, car_state)
|
|
226
|
+
if 0 <= row < self.height_pixels and 0 <= col < self.width_pixels:
|
|
227
|
+
self.costmap[row, col] = COST_OCCUPIED
|
|
228
|
+
|
|
229
|
+
# Apply inflation (in-place to preserve array shape)
|
|
230
|
+
if self.inflation_radius > 0:
|
|
231
|
+
inflated = inflate_obstacles(
|
|
232
|
+
self.costmap, self.inflation_radius, self.resolution, method="linear"
|
|
233
|
+
)
|
|
234
|
+
# Ensure we keep the same array shape (inflation should not change dimensions)
|
|
235
|
+
if inflated.shape == self.costmap.shape:
|
|
236
|
+
self.costmap[:] = inflated
|
|
237
|
+
else:
|
|
238
|
+
# Fallback: if shape changed, resize to original
|
|
239
|
+
self.costmap.fill(0.0)
|
|
240
|
+
min_rows = min(self.costmap.shape[0], inflated.shape[0])
|
|
241
|
+
min_cols = min(self.costmap.shape[1], inflated.shape[1])
|
|
242
|
+
self.costmap[:min_rows, :min_cols] = inflated[:min_rows, :min_cols]
|
|
243
|
+
|
|
244
|
+
def get_cost(
|
|
245
|
+
self, position: np.ndarray, frame: str = "global", car_state: CarState | None = None
|
|
246
|
+
) -> float:
|
|
247
|
+
"""
|
|
248
|
+
Get cost at a specific position.
|
|
249
|
+
|
|
250
|
+
Parameters
|
|
251
|
+
----------
|
|
252
|
+
position : np.ndarray
|
|
253
|
+
Position as [x, y] array.
|
|
254
|
+
frame : str, default="global"
|
|
255
|
+
Frame of the position.
|
|
256
|
+
car_state : CarState, optional
|
|
257
|
+
Car state for frame conversion.
|
|
258
|
+
|
|
259
|
+
Returns
|
|
260
|
+
-------
|
|
261
|
+
float
|
|
262
|
+
Cost value (0.0 = free, 1.0 = occupied).
|
|
263
|
+
"""
|
|
264
|
+
if not self.enabled:
|
|
265
|
+
return COST_FREE
|
|
266
|
+
|
|
267
|
+
# Convert to global if needed
|
|
268
|
+
if frame != "global" and car_state is not None:
|
|
269
|
+
if frame == "ego":
|
|
270
|
+
position = car_state.transform_to_world_frame(position)
|
|
271
|
+
|
|
272
|
+
row, col = self._world_to_grid(position, car_state)
|
|
273
|
+
if 0 <= row < self.height_pixels and 0 <= col < self.width_pixels:
|
|
274
|
+
return float(self.costmap[row, col])
|
|
275
|
+
else:
|
|
276
|
+
return COST_FREE # Outside costmap = free
|
|
277
|
+
|
|
278
|
+
def get_cost_region(
|
|
279
|
+
self,
|
|
280
|
+
center: np.ndarray,
|
|
281
|
+
size: float,
|
|
282
|
+
frame: str = "global",
|
|
283
|
+
car_state: CarState | None = None,
|
|
284
|
+
) -> np.ndarray:
|
|
285
|
+
"""
|
|
286
|
+
Get cost values in a region.
|
|
287
|
+
|
|
288
|
+
Parameters
|
|
289
|
+
----------
|
|
290
|
+
center : np.ndarray
|
|
291
|
+
Center position as [x, y].
|
|
292
|
+
size : float
|
|
293
|
+
Size of the region in meters.
|
|
294
|
+
frame : str, default="global"
|
|
295
|
+
Frame of the center position.
|
|
296
|
+
car_state : CarState, optional
|
|
297
|
+
Car state for frame conversion.
|
|
298
|
+
|
|
299
|
+
Returns
|
|
300
|
+
-------
|
|
301
|
+
np.ndarray
|
|
302
|
+
2D array of cost values.
|
|
303
|
+
"""
|
|
304
|
+
if not self.enabled:
|
|
305
|
+
return np.zeros((1, 1))
|
|
306
|
+
|
|
307
|
+
# Convert to global if needed
|
|
308
|
+
if frame != "global" and car_state is not None:
|
|
309
|
+
if frame == "ego":
|
|
310
|
+
center = car_state.transform_to_world_frame(center)
|
|
311
|
+
|
|
312
|
+
center_row, center_col = self._world_to_grid(center, car_state)
|
|
313
|
+
half_size_pixels = int(size / (2 * self.resolution))
|
|
314
|
+
|
|
315
|
+
row_start = max(0, center_row - half_size_pixels)
|
|
316
|
+
row_end = min(self.height_pixels, center_row + half_size_pixels)
|
|
317
|
+
col_start = max(0, center_col - half_size_pixels)
|
|
318
|
+
col_end = min(self.width_pixels, center_col + half_size_pixels)
|
|
319
|
+
|
|
320
|
+
return np.array(self.costmap[row_start:row_end, col_start:col_end].copy(), dtype=np.float64)
|
|
321
|
+
|
|
322
|
+
def get_visualization_data(
|
|
323
|
+
self, car_state: CarState | None = None, **kwargs: Any
|
|
324
|
+
) -> dict[str, Any]:
|
|
325
|
+
"""
|
|
326
|
+
Get visualization data for GridCostmap.
|
|
327
|
+
|
|
328
|
+
Returns costmap array, bounds, origin, and frame information.
|
|
329
|
+
|
|
330
|
+
Parameters
|
|
331
|
+
----------
|
|
332
|
+
car_state : CarState, optional
|
|
333
|
+
Current car state (for frame transformations).
|
|
334
|
+
**kwargs
|
|
335
|
+
Additional arguments.
|
|
336
|
+
|
|
337
|
+
Returns
|
|
338
|
+
-------
|
|
339
|
+
Dict
|
|
340
|
+
Dictionary with keys:
|
|
341
|
+
- "costmap": np.ndarray, 2D costmap array
|
|
342
|
+
- "width": float, width in meters
|
|
343
|
+
- "height": float, height in meters
|
|
344
|
+
- "resolution": float, resolution in meters per cell
|
|
345
|
+
- "origin": np.ndarray, origin position [x, y]
|
|
346
|
+
- "frame": str, frame type ("ego" or "global")
|
|
347
|
+
- "enabled": bool, whether costmap is enabled
|
|
348
|
+
"""
|
|
349
|
+
return {
|
|
350
|
+
"costmap": self.get_full_costmap(),
|
|
351
|
+
"width": self.width,
|
|
352
|
+
"height": self.height,
|
|
353
|
+
"resolution": self.resolution,
|
|
354
|
+
"origin": self.origin.copy(),
|
|
355
|
+
"frame": self.frame,
|
|
356
|
+
"enabled": self.enabled,
|
|
357
|
+
"width_pixels": self.width_pixels,
|
|
358
|
+
"height_pixels": self.height_pixels,
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
def get_full_costmap(self) -> np.ndarray:
|
|
362
|
+
"""
|
|
363
|
+
Get full costmap array.
|
|
364
|
+
|
|
365
|
+
Returns
|
|
366
|
+
-------
|
|
367
|
+
np.ndarray
|
|
368
|
+
Full 2D costmap array.
|
|
369
|
+
"""
|
|
370
|
+
return np.array(self.costmap.copy(), dtype=np.float64)
|
|
371
|
+
|
|
372
|
+
def visualize(
|
|
373
|
+
self, ax: Any, car_state: CarState | None = None, frame: str = "global", **kwargs: Any
|
|
374
|
+
) -> None:
|
|
375
|
+
"""
|
|
376
|
+
Visualize costmap on the given axes.
|
|
377
|
+
|
|
378
|
+
Parameters
|
|
379
|
+
----------
|
|
380
|
+
ax : matplotlib.axes.Axes
|
|
381
|
+
Axes to plot on.
|
|
382
|
+
car_state : CarState, optional
|
|
383
|
+
Current car state (for frame transformations).
|
|
384
|
+
frame : str, default="global"
|
|
385
|
+
Frame to plot in: "global" or "ego".
|
|
386
|
+
**kwargs
|
|
387
|
+
Additional visualization arguments:
|
|
388
|
+
- alpha: float, transparency of costmap
|
|
389
|
+
- cmap: str, colormap name
|
|
390
|
+
- show_car: bool, whether to show car position
|
|
391
|
+
"""
|
|
392
|
+
if not self.enabled:
|
|
393
|
+
return
|
|
394
|
+
|
|
395
|
+
import numpy as np
|
|
396
|
+
|
|
397
|
+
# Extract visualization parameters
|
|
398
|
+
alpha = kwargs.pop("alpha", DEFAULT_COSTMAP_ALPHA)
|
|
399
|
+
cmap = kwargs.pop("cmap", "RdYlGn_r")
|
|
400
|
+
show_car = kwargs.pop("show_car", False)
|
|
401
|
+
|
|
402
|
+
# Get costmap data
|
|
403
|
+
costmap_data = self.get_full_costmap()
|
|
404
|
+
|
|
405
|
+
# Mask out empty space (zero-cost cells) to save computation
|
|
406
|
+
# Only mask if there's actual data to show
|
|
407
|
+
if np.any(costmap_data > COST_FREE):
|
|
408
|
+
costmap_data = np.ma.masked_where(costmap_data == COST_FREE, costmap_data)
|
|
409
|
+
else:
|
|
410
|
+
# Costmap is empty, nothing to visualize
|
|
411
|
+
return
|
|
412
|
+
|
|
413
|
+
# Use imshow for optimal performance
|
|
414
|
+
if frame == "ego" and self.frame == "ego" and car_state is not None:
|
|
415
|
+
# Costmap is in ego frame, plot in ego frame
|
|
416
|
+
origin_x = -self.width / 2
|
|
417
|
+
origin_y = -self.height / 2
|
|
418
|
+
extent = [
|
|
419
|
+
origin_x,
|
|
420
|
+
origin_x + self.width,
|
|
421
|
+
origin_y,
|
|
422
|
+
origin_y + self.height,
|
|
423
|
+
]
|
|
424
|
+
ax.imshow(
|
|
425
|
+
costmap_data,
|
|
426
|
+
extent=extent,
|
|
427
|
+
origin="lower",
|
|
428
|
+
cmap=cmap,
|
|
429
|
+
vmin=COST_FREE,
|
|
430
|
+
vmax=COST_OCCUPIED,
|
|
431
|
+
interpolation="nearest",
|
|
432
|
+
alpha=alpha,
|
|
433
|
+
zorder=DEFAULT_COSTMAP_ZORDER,
|
|
434
|
+
**kwargs,
|
|
435
|
+
)
|
|
436
|
+
elif self.frame == "ego" and car_state is not None and frame == "global":
|
|
437
|
+
# Costmap is in ego frame, but we want to plot in global frame
|
|
438
|
+
# Use matplotlib transform instead of rotating array data for smoother visualization
|
|
439
|
+
# This avoids jerky artifacts from rotating the array every frame
|
|
440
|
+
car_pos = car_state.position()
|
|
441
|
+
car_heading = car_state.heading
|
|
442
|
+
|
|
443
|
+
from matplotlib.transforms import Affine2D
|
|
444
|
+
|
|
445
|
+
# Use transform to rotate/translate at rendering time (smooth, no array manipulation)
|
|
446
|
+
# This is the key difference: ego frame doesn't rotate array, it uses transforms
|
|
447
|
+
# Transform order: rotate around origin (0,0) first, then translate to car position
|
|
448
|
+
# This correctly transforms ego frame (centered at origin) to global frame
|
|
449
|
+
trans = (
|
|
450
|
+
Affine2D()
|
|
451
|
+
.rotate(car_heading) # Rotate around origin (0,0) in ego frame
|
|
452
|
+
.translate(car_pos[0], car_pos[1]) # Then translate to car position
|
|
453
|
+
+ ax.transData
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
# Extent in ego frame (centered at origin)
|
|
457
|
+
extent = [
|
|
458
|
+
-self.width / 2,
|
|
459
|
+
self.width / 2,
|
|
460
|
+
-self.height / 2,
|
|
461
|
+
self.height / 2,
|
|
462
|
+
]
|
|
463
|
+
|
|
464
|
+
# Plot with transform - matplotlib handles rotation smoothly at render time
|
|
465
|
+
# No array rotation = no jerky artifacts
|
|
466
|
+
ax.imshow(
|
|
467
|
+
costmap_data,
|
|
468
|
+
extent=extent,
|
|
469
|
+
origin="lower",
|
|
470
|
+
cmap=cmap,
|
|
471
|
+
vmin=COST_FREE,
|
|
472
|
+
vmax=COST_OCCUPIED,
|
|
473
|
+
interpolation="nearest",
|
|
474
|
+
alpha=alpha,
|
|
475
|
+
zorder=DEFAULT_COSTMAP_ZORDER,
|
|
476
|
+
transform=trans,
|
|
477
|
+
**kwargs,
|
|
478
|
+
)
|
|
479
|
+
else:
|
|
480
|
+
# Global frame costmap in global frame
|
|
481
|
+
origin = self.origin
|
|
482
|
+
extent = [
|
|
483
|
+
origin[0],
|
|
484
|
+
origin[0] + self.width,
|
|
485
|
+
origin[1],
|
|
486
|
+
origin[1] + self.height,
|
|
487
|
+
]
|
|
488
|
+
ax.imshow(
|
|
489
|
+
costmap_data,
|
|
490
|
+
extent=extent,
|
|
491
|
+
origin="lower",
|
|
492
|
+
cmap=cmap,
|
|
493
|
+
vmin=COST_FREE,
|
|
494
|
+
vmax=COST_OCCUPIED,
|
|
495
|
+
interpolation="nearest",
|
|
496
|
+
alpha=alpha,
|
|
497
|
+
zorder=DEFAULT_COSTMAP_ZORDER,
|
|
498
|
+
**kwargs,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
# Show car position if requested
|
|
502
|
+
if show_car and car_state is not None:
|
|
503
|
+
car_pos = car_state.position()
|
|
504
|
+
if frame == "global":
|
|
505
|
+
ax.plot(car_pos[0], car_pos[1], "bo", markersize=8, label="Car")
|
|
506
|
+
else:
|
|
507
|
+
ax.plot(0, 0, "bo", markersize=8, label="Car")
|