avlite 0.4.0__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.
- avlite/__init__.py +19 -0
- avlite/__main__.py +31 -0
- avlite/c10_perception/__init__.py +0 -0
- avlite/c10_perception/c11_perception_model.py +470 -0
- avlite/c10_perception/c12_perception_strategy.py +176 -0
- avlite/c10_perception/c13_localization_strategy.py +76 -0
- avlite/c10_perception/c14_mapping_strategy.py +36 -0
- avlite/c10_perception/c15_perception_algs.py +387 -0
- avlite/c10_perception/c16_localization_algs.py +225 -0
- avlite/c10_perception/c17_mapping_algs.py +0 -0
- avlite/c10_perception/c18_hdmap_parser.py +512 -0
- avlite/c10_perception/c19_settings.py +47 -0
- avlite/c20_planning/__init__.py +0 -0
- avlite/c20_planning/c21_planning_model.py +166 -0
- avlite/c20_planning/c22_global_planning_strategy.py +51 -0
- avlite/c20_planning/c23_local_planning_strategy.py +353 -0
- avlite/c20_planning/c24_global_hdmap_planners.py +344 -0
- avlite/c20_planning/c25_global_race_planners.py +140 -0
- avlite/c20_planning/c26_local_path_planners.py +49 -0
- avlite/c20_planning/c27_local_behavioral_and_velocity_planners.py +306 -0
- avlite/c20_planning/c28_local_lattice_planners.py +823 -0
- avlite/c20_planning/c29_settings.py +50 -0
- avlite/c30_control/__init__.py +0 -0
- avlite/c30_control/c31_control_model.py +35 -0
- avlite/c30_control/c32_control_strategy.py +77 -0
- avlite/c30_control/c33_pid.py +128 -0
- avlite/c30_control/c34_stanley.py +140 -0
- avlite/c30_control/c38_control_mapping.py +31 -0
- avlite/c30_control/c39_settings.py +45 -0
- avlite/c40_execution/__init__.py +0 -0
- avlite/c40_execution/c41_world_bridge.py +182 -0
- avlite/c40_execution/c42_execution_strategy.py +330 -0
- avlite/c40_execution/c44_sync_executer.py +104 -0
- avlite/c40_execution/c45_async_threaded_executer.py +290 -0
- avlite/c40_execution/c46_basic_sim.py +249 -0
- avlite/c40_execution/c49_settings.py +66 -0
- avlite/c50_common/__init__.py +1 -0
- avlite/c50_common/c51_capabilities.py +77 -0
- avlite/c50_common/c52_sensor_datatypes.py +121 -0
- avlite/c50_common/c53_trajectory_tracker.py +856 -0
- avlite/c50_common/c54_collision_checking.py +185 -0
- avlite/c50_common/c55_fps_tracker.py +42 -0
- avlite/c60_apps/__init__.py +0 -0
- avlite/c60_apps/c61_app_strategy.py +89 -0
- avlite/c60_apps/c62_factory.py +365 -0
- avlite/c60_apps/c63_plugins.py +596 -0
- avlite/c60_apps/c64_settings_schema.py +251 -0
- avlite/c60_apps/c65_setting_utils.py +471 -0
- avlite/c60_apps/c68_paths.py +390 -0
- avlite/c60_apps/c69_settings.py +35 -0
- avlite/plugins/__init__.py +5 -0
- avlite/plugins/p60_headless_mode/__init__.py +8 -0
- avlite/plugins/p60_headless_mode/p61_headless.py +439 -0
- avlite/plugins/p60_headless_mode/settings.py +13 -0
- avlite/plugins/p60_setting_cli/__init__.py +8 -0
- avlite/plugins/p60_setting_cli/p61_setting_cli.py +222 -0
- avlite/plugins/p60_setting_cli/settings.py +8 -0
- avlite/plugins/p60_visualizer_tk/__init__.py +14 -0
- avlite/plugins/p60_visualizer_tk/p61_visualizer_app.py +596 -0
- avlite/plugins/p60_visualizer_tk/p62_setting_app.py +95 -0
- avlite/plugins/p60_visualizer_tk/p63_plugins_app.py +1958 -0
- avlite/plugins/p60_visualizer_tk/p64_setting_views.py +1316 -0
- avlite/plugins/p60_visualizer_tk/p65_ui_lib.py +846 -0
- avlite/plugins/p60_visualizer_tk/p66_plot_views.py +481 -0
- avlite/plugins/p60_visualizer_tk/p67_stack_views.py +922 -0
- avlite/plugins/p60_visualizer_tk/p68_log_view.py +511 -0
- avlite/plugins/p60_visualizer_tk/p69_plot_lib.py +1278 -0
- avlite/plugins/p60_visualizer_tk/settings.py +482 -0
- avlite-0.4.0.dist-info/METADATA +356 -0
- avlite-0.4.0.dist-info/RECORD +75 -0
- avlite-0.4.0.dist-info/WHEEL +5 -0
- avlite-0.4.0.dist-info/entry_points.txt +2 -0
- avlite-0.4.0.dist-info/licenses/LICENSE +21 -0
- avlite-0.4.0.dist-info/top_level.txt +2 -0
- scripts/migrate_configs.py +160 -0
avlite/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _package_version() -> str:
|
|
7
|
+
try:
|
|
8
|
+
return version("avlite")
|
|
9
|
+
except PackageNotFoundError:
|
|
10
|
+
pass
|
|
11
|
+
pyproject = Path(__file__).resolve().parent.parent / "pyproject.toml"
|
|
12
|
+
if pyproject.is_file():
|
|
13
|
+
match = re.search(r'^version\s*=\s*"([^"]+)"', pyproject.read_text(encoding="utf-8"), re.M)
|
|
14
|
+
if match:
|
|
15
|
+
return match.group(1)
|
|
16
|
+
return "unknown"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__version__ = _package_version()
|
avlite/__main__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from avlite.c60_apps.c61_app_strategy import bootstrap_apps, register_parsers, run_app
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main(argv: list[str] | None = None) -> None:
|
|
8
|
+
"""Main entry point for the AVLite application."""
|
|
9
|
+
parser = argparse.ArgumentParser(prog="avlite", description="AVLite")
|
|
10
|
+
sub = parser.add_subparsers(dest="command")
|
|
11
|
+
|
|
12
|
+
bootstrap_apps()
|
|
13
|
+
register_parsers(sub)
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
args, unknown = parser.parse_known_args(sys.argv[1:] if argv is None else argv)
|
|
17
|
+
except SystemExit as exc:
|
|
18
|
+
if exc.code not in (0, None):
|
|
19
|
+
sys.stderr.write("\nError parsing arguments. Use --help for usage.\n")
|
|
20
|
+
raise
|
|
21
|
+
|
|
22
|
+
if unknown:
|
|
23
|
+
sys.stderr.write(f"Ignoring unknown arguments: {unknown}\n")
|
|
24
|
+
|
|
25
|
+
exit_code = run_app(args.command, args, unknown)
|
|
26
|
+
if exit_code:
|
|
27
|
+
sys.exit(exit_code)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if __name__ == "__main__":
|
|
31
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from enum import Enum, auto
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import xml.etree.ElementTree as ET
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
import copy
|
|
13
|
+
import networkx as nx
|
|
14
|
+
import numpy as np
|
|
15
|
+
from scipy.spatial import KDTree
|
|
16
|
+
from shapely.geometry import Polygon
|
|
17
|
+
|
|
18
|
+
from avlite.c10_perception.c19_settings import PerceptionSettings
|
|
19
|
+
|
|
20
|
+
log = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
EGO_AGENT_ID: int = 0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class PerceptionModel:
|
|
29
|
+
static_obstacles: list[State] = field(default_factory=list)
|
|
30
|
+
agent_vehicles: list[AgentState] = field(default_factory=list)
|
|
31
|
+
ego_vehicle: EgoState= field(default_factory=lambda: EgoState())
|
|
32
|
+
max_agent_vehicles: int = field(default_factory=lambda: PerceptionSettings.c11_max_agents)
|
|
33
|
+
|
|
34
|
+
prediction: Optional[PredictionModelBase] = None
|
|
35
|
+
|
|
36
|
+
# Optional map (HDMap or RaceMap)
|
|
37
|
+
map: Optional[Map] = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Raw LiDAR points that passed segmentation + range gating (diagnostic overlay)
|
|
41
|
+
detection_clusters: Optional[np.ndarray] = None
|
|
42
|
+
|
|
43
|
+
def add_agent_vehicle(self, agent: AgentState) -> int: # return agent_id
|
|
44
|
+
if len(self.agent_vehicles) == self.max_agent_vehicles:
|
|
45
|
+
log.info("Max num of agent reached. Deleteing Old agents")
|
|
46
|
+
self.agent_vehicles = []
|
|
47
|
+
ids = {a.agent_id for a in self.agent_vehicles}
|
|
48
|
+
agent.agent_id = next(i for i in range(1, len(ids) + 2) if i not in ids)
|
|
49
|
+
self.agent_vehicles.append(agent)
|
|
50
|
+
log.info(f"Total agent vehicles {len(self.agent_vehicles)}")
|
|
51
|
+
|
|
52
|
+
return agent.agent_id
|
|
53
|
+
|
|
54
|
+
def reset(self):
|
|
55
|
+
self.static_obstacles = []
|
|
56
|
+
self.agent_vehicles = []
|
|
57
|
+
self.prediction = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class PredictionModelBase:
|
|
62
|
+
"""Shared metadata for prediction outputs on ``PerceptionModel.prediction``."""
|
|
63
|
+
|
|
64
|
+
# Seconds between consecutive forecast samples (t, t+dt, …).
|
|
65
|
+
predict_delta_t: float = field(
|
|
66
|
+
default_factory=lambda: PerceptionSettings.c11_predict_delta_t
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class SingleTrajectory(PredictionModelBase):
|
|
72
|
+
"""Deterministic (x, y) polyline per agent."""
|
|
73
|
+
|
|
74
|
+
# agent_id -> [n_steps, 2] world x,y [m]; step k at (k+1) * predict_delta_t.
|
|
75
|
+
trajectories: dict[int, np.ndarray] = field(default_factory=dict)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class GP(PredictionModelBase):
|
|
80
|
+
"""Gaussian-process forecast per agent (mean + joint covariance)."""
|
|
81
|
+
|
|
82
|
+
# agent_id -> [n_steps, 2] predictive mean trajectory.
|
|
83
|
+
means: dict[int, np.ndarray] = field(default_factory=dict)
|
|
84
|
+
# agent_id -> [2*n_steps, 2*n_steps] joint covariance; state order [x0,y0,x1,y1,...].
|
|
85
|
+
covariance: dict[int, np.ndarray] = field(default_factory=dict)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class GMM(PredictionModelBase):
|
|
90
|
+
"""Gaussian-mixture multi-modal forecast per agent."""
|
|
91
|
+
|
|
92
|
+
# agent_id -> [n_modes, n_steps, 2] mode means.
|
|
93
|
+
trajectories: dict[int, np.ndarray] = field(default_factory=dict)
|
|
94
|
+
# agent_id -> [n_modes] mode weights (sum ≈ 1).
|
|
95
|
+
weights: dict[int, np.ndarray] = field(default_factory=dict)
|
|
96
|
+
# agent_id -> [n_modes, n_steps, 2, 2] position covariance per mode/step.
|
|
97
|
+
covariances: dict[int, np.ndarray] = field(default_factory=dict)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
class OccupancyFlow(PredictionModelBase):
|
|
102
|
+
"""Per-agent occupancy grid sequences."""
|
|
103
|
+
|
|
104
|
+
# agent_id -> n_steps grids, each [grid_size, grid_size].
|
|
105
|
+
occupancy_flow: dict[int, list[np.ndarray]] = field(default_factory=dict)
|
|
106
|
+
grid_bounds: dict[str, float] = field(default_factory=dict)
|
|
107
|
+
grid_size: int = field(default_factory=lambda: PerceptionSettings.c11_prediction_grid_size)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class AggregatedOccupancyFlow(PredictionModelBase):
|
|
112
|
+
"""Lump-sum occupancy grids for all agents combined."""
|
|
113
|
+
|
|
114
|
+
occupancy_flow: list[np.ndarray] = field(default_factory=list)
|
|
115
|
+
grid_bounds: dict[str, float] = field(default_factory=dict)
|
|
116
|
+
grid_size: int = field(default_factory=lambda: PerceptionSettings.c11_prediction_grid_size)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class State:
|
|
121
|
+
x: float = 0.0
|
|
122
|
+
y: float = 0.0
|
|
123
|
+
z: float = 0.0
|
|
124
|
+
theta: float = PerceptionSettings.c11_state_default_heading
|
|
125
|
+
width: float = 2.0
|
|
126
|
+
length: float = 4.5
|
|
127
|
+
|
|
128
|
+
def __post_init__(self):
|
|
129
|
+
# initial x,y position, useful for reset
|
|
130
|
+
self.__init_x = self.x
|
|
131
|
+
self.__init_y = self.y
|
|
132
|
+
self.__init_theta = self.theta
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_bb_corners(self) -> np.ndarray:
|
|
136
|
+
"""Get the bounding box corners of the vehicle in world coordinates."""
|
|
137
|
+
corners_x = np.array(
|
|
138
|
+
[
|
|
139
|
+
-self.length / 2,
|
|
140
|
+
+self.length / 2,
|
|
141
|
+
+self.length / 2,
|
|
142
|
+
-self.length / 2,
|
|
143
|
+
]
|
|
144
|
+
)
|
|
145
|
+
corners_y = np.array(
|
|
146
|
+
[
|
|
147
|
+
-self.width / 2,
|
|
148
|
+
-self.width / 2,
|
|
149
|
+
+self.width / 2,
|
|
150
|
+
+self.width / 2,
|
|
151
|
+
]
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
rotation_matrix = np.array(
|
|
155
|
+
[
|
|
156
|
+
[np.cos(self.theta), -np.sin(self.theta)],
|
|
157
|
+
[np.sin(self.theta), np.cos(self.theta)],
|
|
158
|
+
]
|
|
159
|
+
)
|
|
160
|
+
rotated_corners = np.dot(rotation_matrix, np.array([corners_x, corners_y]))
|
|
161
|
+
|
|
162
|
+
rotated_corners_x = rotated_corners[0, :] + self.x
|
|
163
|
+
rotated_corners_y = rotated_corners[1, :] + self.y
|
|
164
|
+
|
|
165
|
+
return np.c_[rotated_corners_x, rotated_corners_y]
|
|
166
|
+
|
|
167
|
+
def reset(self):
|
|
168
|
+
self.x = self.__init_x
|
|
169
|
+
self.y = self.__init_y
|
|
170
|
+
self.theta = self.__init_theta
|
|
171
|
+
|
|
172
|
+
def get_copy(self):
|
|
173
|
+
return copy.deepcopy(self)
|
|
174
|
+
|
|
175
|
+
def get_bb_polygon(self):
|
|
176
|
+
return Polygon(self.get_bb_corners())
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class AgentType(Enum):
|
|
180
|
+
ACKERMANN = auto()
|
|
181
|
+
DIFF_DRIVE = auto()
|
|
182
|
+
AERIAL = auto()
|
|
183
|
+
SURFACE_VESSEL = auto() # boats, USVs — water surface
|
|
184
|
+
UNDERWATER = auto() # AUVs — subsurface
|
|
185
|
+
CYCLIST = auto()
|
|
186
|
+
PEDESTRIAN = auto()
|
|
187
|
+
DYNAMIC_OBJECT = auto()
|
|
188
|
+
|
|
189
|
+
@dataclass
|
|
190
|
+
class AgentState(State):
|
|
191
|
+
velocity: float = 0.0
|
|
192
|
+
agent_id: int = -1
|
|
193
|
+
agent_type: AgentType = AgentType.ACKERMANN
|
|
194
|
+
|
|
195
|
+
def __post_init__(self):
|
|
196
|
+
super().__post_init__()
|
|
197
|
+
self.__init_speed = self.velocity
|
|
198
|
+
|
|
199
|
+
def reset(self):
|
|
200
|
+
super().reset()
|
|
201
|
+
self.velocity = self.__init_speed
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@dataclass
|
|
206
|
+
class EgoState(AgentState):
|
|
207
|
+
"""Ego vehicle state with additional properties in future."""
|
|
208
|
+
agent_id: int = EGO_AGENT_ID
|
|
209
|
+
agent_type: AgentType = AgentType.ACKERMANN
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class Map(ABC):
|
|
213
|
+
"""Static world geometry with an optional WGS84 reference point."""
|
|
214
|
+
|
|
215
|
+
source_path: str
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
@abstractmethod
|
|
219
|
+
def reference_point(self) -> tuple[float, float] | None:
|
|
220
|
+
"""WGS84 (lat_deg, lon_deg) when available."""
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
@abstractmethod
|
|
224
|
+
def is_loadable(path: Path | str) -> bool:
|
|
225
|
+
"""Return True when *path* is a supported map file."""
|
|
226
|
+
|
|
227
|
+
@classmethod
|
|
228
|
+
@abstractmethod
|
|
229
|
+
def from_path(cls, path: Path | str) -> Map:
|
|
230
|
+
"""Load a map instance from *path*."""
|
|
231
|
+
|
|
232
|
+
@staticmethod
|
|
233
|
+
def open(path: Path | str) -> Map | None:
|
|
234
|
+
"""Dispatch to ``HDMap`` or ``RaceMap`` based on file format."""
|
|
235
|
+
path = Path(path)
|
|
236
|
+
if HDMap.is_loadable(path):
|
|
237
|
+
return HDMap.from_path(path)
|
|
238
|
+
if RaceMap.is_loadable(path):
|
|
239
|
+
return RaceMap.from_path(path)
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@dataclass
|
|
244
|
+
class RaceMap(Map):
|
|
245
|
+
"""Race corridor map from left/right boundary JSON."""
|
|
246
|
+
|
|
247
|
+
source_path: str
|
|
248
|
+
left_bound: np.ndarray = field(default_factory=lambda: np.array([]))
|
|
249
|
+
right_bound: np.ndarray = field(default_factory=lambda: np.array([]))
|
|
250
|
+
_reference_point: tuple[float, float] | None = None
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def reference_point(self) -> tuple[float, float] | None:
|
|
254
|
+
return self._reference_point
|
|
255
|
+
|
|
256
|
+
@staticmethod
|
|
257
|
+
def is_loadable(path: Path | str) -> bool:
|
|
258
|
+
"""True when *path* is a race-boundary JSON with bounds and ReferencePoint."""
|
|
259
|
+
path = Path(path)
|
|
260
|
+
if path.suffix.lower() != ".json" or not path.is_file():
|
|
261
|
+
return False
|
|
262
|
+
try:
|
|
263
|
+
with path.open(encoding="utf-8") as f:
|
|
264
|
+
data = json.load(f)
|
|
265
|
+
except (OSError, json.JSONDecodeError):
|
|
266
|
+
return False
|
|
267
|
+
if not all(k in data for k in ("LeftBound", "RightBound", "ReferencePoint")):
|
|
268
|
+
return False
|
|
269
|
+
left = data["LeftBound"]
|
|
270
|
+
right = data["RightBound"]
|
|
271
|
+
if not left or not right:
|
|
272
|
+
return False
|
|
273
|
+
if not isinstance(left[0], list) or not isinstance(right[0], list):
|
|
274
|
+
return False
|
|
275
|
+
ref = data["ReferencePoint"]
|
|
276
|
+
if not isinstance(ref, list) or len(ref) < 2:
|
|
277
|
+
return False
|
|
278
|
+
try:
|
|
279
|
+
float(ref[0])
|
|
280
|
+
float(ref[1])
|
|
281
|
+
except (TypeError, ValueError):
|
|
282
|
+
return False
|
|
283
|
+
return True
|
|
284
|
+
|
|
285
|
+
@classmethod
|
|
286
|
+
def from_path(cls, path: Path | str) -> RaceMap:
|
|
287
|
+
path = Path(path)
|
|
288
|
+
with path.open(encoding="utf-8") as f:
|
|
289
|
+
data = json.load(f)
|
|
290
|
+
left = np.array(data["LeftBound"])[:, :2]
|
|
291
|
+
right = np.array(data["RightBound"])[:, :2]
|
|
292
|
+
ref = data["ReferencePoint"]
|
|
293
|
+
ref_pt = (float(ref[0]), float(ref[1]))
|
|
294
|
+
return cls(source_path=str(path), left_bound=left, right_bound=right, _reference_point=ref_pt,)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@dataclass
|
|
298
|
+
class HDMap(Map):
|
|
299
|
+
"""Compact HD map representation for global planning."""
|
|
300
|
+
|
|
301
|
+
@dataclass
|
|
302
|
+
class Lane:
|
|
303
|
+
id: int
|
|
304
|
+
uid: str
|
|
305
|
+
lane_element: ET.Element
|
|
306
|
+
center_line: np.ndarray = field(default_factory=lambda: np.array([]))
|
|
307
|
+
left_d: list[float] = field(default_factory=list)
|
|
308
|
+
right_d: list[float] = field(default_factory=list)
|
|
309
|
+
road: Optional["HDMap.Road"] = None
|
|
310
|
+
pred_id: str = ""
|
|
311
|
+
succ_id: str = ""
|
|
312
|
+
pred_type: str = "lane"
|
|
313
|
+
succ_type: str = "lane"
|
|
314
|
+
side: str = "left"
|
|
315
|
+
type: str = "driving"
|
|
316
|
+
road_id: str = ""
|
|
317
|
+
width: float = 0.0
|
|
318
|
+
lane_section_idx: int = 0
|
|
319
|
+
predecessors: list["HDMap.Lane"] = field(default_factory=list)
|
|
320
|
+
successors: list["HDMap.Lane"] = field(default_factory=list)
|
|
321
|
+
neighbors: set["HDMap.Lane"] = field(default_factory=set)
|
|
322
|
+
drivable_neighbors: set["HDMap.Lane"] = field(default_factory=set)
|
|
323
|
+
|
|
324
|
+
def __hash__(self):
|
|
325
|
+
return hash(self.uid)
|
|
326
|
+
|
|
327
|
+
@dataclass
|
|
328
|
+
class Road:
|
|
329
|
+
"""Compact road representation for global planning."""
|
|
330
|
+
|
|
331
|
+
id: str
|
|
332
|
+
road_element: ET.Element
|
|
333
|
+
pred_id: str = ""
|
|
334
|
+
succ_id: str = ""
|
|
335
|
+
pred_type: str = "road"
|
|
336
|
+
succ_type: str = "road"
|
|
337
|
+
length: float = 0.0
|
|
338
|
+
junction_id: str = ""
|
|
339
|
+
center_line: np.ndarray = field(default_factory=lambda: np.array([]))
|
|
340
|
+
predecessors: list["HDMap.Road"] = field(default_factory=list)
|
|
341
|
+
successors: list["HDMap.Road"] = field(default_factory=list)
|
|
342
|
+
lane_sections: list[list["HDMap.Lane"]] = field(default_factory=list)
|
|
343
|
+
lane_section_s_vals: list[float] = field(default_factory=list)
|
|
344
|
+
reversed: bool = False
|
|
345
|
+
|
|
346
|
+
xodr_file_name: str = ""
|
|
347
|
+
sampling_resolution: float = 0.1
|
|
348
|
+
roads: list[Road] = field(default_factory=list)
|
|
349
|
+
lanes: list[Lane] = field(default_factory=list)
|
|
350
|
+
road_by_id: dict[str, Road] = field(default_factory=dict)
|
|
351
|
+
lane_by_uid: dict[str, Lane] = field(default_factory=dict)
|
|
352
|
+
junction_by_id: dict[str, list[Road]] = field(default_factory=dict)
|
|
353
|
+
road_network: nx.DiGraph = field(default_factory=nx.DiGraph)
|
|
354
|
+
lane_network: nx.DiGraph = field(default_factory=nx.DiGraph)
|
|
355
|
+
root: ET.Element | None = field(default=None, repr=False)
|
|
356
|
+
|
|
357
|
+
_point_to_road: dict[tuple[float, float], Road] = field(default_factory=dict, init=False, repr=False)
|
|
358
|
+
_point_to_drivable_lane: dict[tuple[float, float], Lane] = field(default_factory=dict, init=False, repr=False)
|
|
359
|
+
_road_kdtree: Optional[KDTree] = field(default=None, init=False, repr=False)
|
|
360
|
+
_lane_kdtree_drivable: Optional[KDTree] = field(default=None, init=False, repr=False)
|
|
361
|
+
_all_road_points: list[tuple[float, float]] = field(default_factory=list, init=False, repr=False)
|
|
362
|
+
_all_drivable_lane_points: list[tuple[float, float]] = field(default_factory=list, init=False, repr=False)
|
|
363
|
+
_reference_point: tuple[float, float] | None = field(default=None, init=False, repr=False)
|
|
364
|
+
|
|
365
|
+
@property
|
|
366
|
+
def source_path(self) -> str:
|
|
367
|
+
return self.xodr_file_name
|
|
368
|
+
|
|
369
|
+
@property
|
|
370
|
+
def reference_point(self) -> tuple[float, float] | None:
|
|
371
|
+
return self._reference_point
|
|
372
|
+
|
|
373
|
+
@staticmethod
|
|
374
|
+
def is_loadable(path: Path | str) -> bool:
|
|
375
|
+
path = Path(path)
|
|
376
|
+
return path.suffix.lower() == ".xodr" and path.is_file()
|
|
377
|
+
|
|
378
|
+
@classmethod
|
|
379
|
+
def from_path(cls, path: Path | str) -> HDMap:
|
|
380
|
+
return cls(xodr_file_name=str(Path(path).resolve()))
|
|
381
|
+
|
|
382
|
+
def __post_init__(self) -> None:
|
|
383
|
+
if not self.xodr_file_name:
|
|
384
|
+
log.error("No OpenDRIVE file specified.")
|
|
385
|
+
return
|
|
386
|
+
from avlite.c10_perception.c18_hdmap_parser import parse_opendrive
|
|
387
|
+
|
|
388
|
+
parse_opendrive(self)
|
|
389
|
+
|
|
390
|
+
def find_nearest_road(self, x: float, y: float) -> Road | None:
|
|
391
|
+
if self._road_kdtree is not None:
|
|
392
|
+
_, index = self._road_kdtree.query((x, y))
|
|
393
|
+
if index >= 0 and index < len(self._all_road_points):
|
|
394
|
+
px, py = self._all_road_points[index]
|
|
395
|
+
if (px, py) not in self._point_to_road:
|
|
396
|
+
log.error("Point not found in point_to_road mapping: (%s, %s)", px, py)
|
|
397
|
+
return self._point_to_road.get((px, py), None)
|
|
398
|
+
|
|
399
|
+
def find_nearest_lane(self, x: float, y: float) -> Lane | None:
|
|
400
|
+
if self._lane_kdtree_drivable is not None:
|
|
401
|
+
_, index = self._lane_kdtree_drivable.query((x, y))
|
|
402
|
+
if 0 <= index < len(self._all_drivable_lane_points):
|
|
403
|
+
lx, ly = self._all_drivable_lane_points[index]
|
|
404
|
+
if (lx, ly) not in self._point_to_drivable_lane:
|
|
405
|
+
log.error("Point not found in point_to_lane mapping: (%s, %s)", x, y)
|
|
406
|
+
return self._point_to_drivable_lane.get((lx, ly), None)
|
|
407
|
+
|
|
408
|
+
def find_nearest_lane_and_idx(self, x: float, y: float) -> tuple[Lane | None, int]:
|
|
409
|
+
lane = self.find_nearest_lane(x, y)
|
|
410
|
+
if lane is None or lane.center_line.size == 0:
|
|
411
|
+
return None, -1
|
|
412
|
+
dists = np.linalg.norm(lane.center_line - np.array([[x], [y]]), axis=0)
|
|
413
|
+
idx = int(np.argmin(dists))
|
|
414
|
+
return lane, idx
|
|
415
|
+
|
|
416
|
+
def can_laneA_access_laneB(self, lane_a: Lane, lane_b: Lane) -> bool:
|
|
417
|
+
check1 = lane_b in lane_a.neighbors
|
|
418
|
+
b_start_end = [lane_b.center_line[:, 0], lane_b.center_line[:, -1]]
|
|
419
|
+
a = lane_a.center_line[:, -1] if int(lane_a.id) < 0 else lane_a.center_line[:, 0]
|
|
420
|
+
dists = [(np.linalg.norm(a - b), j) for j, b in enumerate(b_start_end)]
|
|
421
|
+
min_dist, b_idx = min(dists, key=lambda item: item[0])
|
|
422
|
+
check2 = min_dist < 0.5
|
|
423
|
+
check3 = False
|
|
424
|
+
if check2:
|
|
425
|
+
if int(lane_a.id) < 0:
|
|
426
|
+
vec_a = lane_a.center_line[:, -3] - lane_a.center_line[:, -1]
|
|
427
|
+
else:
|
|
428
|
+
vec_a = lane_a.center_line[:, 2] - lane_a.center_line[:, 0]
|
|
429
|
+
if int(lane_b.id) < 0:
|
|
430
|
+
vec_b = (
|
|
431
|
+
lane_b.center_line[:, 0] - lane_b.center_line[:, 2]
|
|
432
|
+
if b_idx == 0
|
|
433
|
+
else lane_b.center_line[:, -1] - lane_b.center_line[:, -3]
|
|
434
|
+
)
|
|
435
|
+
else:
|
|
436
|
+
vec_b = (
|
|
437
|
+
lane_b.center_line[:, 1] - lane_b.center_line[:, 0]
|
|
438
|
+
if b_idx == 0
|
|
439
|
+
else lane_b.center_line[:, -1] - lane_b.center_line[:, -3]
|
|
440
|
+
)
|
|
441
|
+
norm_a = np.linalg.norm(vec_a)
|
|
442
|
+
norm_b = np.linalg.norm(vec_b)
|
|
443
|
+
if norm_a > 0 and norm_b > 0:
|
|
444
|
+
check3 = np.dot(vec_a / norm_a, vec_b / norm_b) > 0.9
|
|
445
|
+
return check1 and check2 and check3
|
|
446
|
+
|
|
447
|
+
def road_has_driving_lanes(self, road: Road) -> bool:
|
|
448
|
+
road_element = road.road_element
|
|
449
|
+
for section in road_element.findall(".//laneSection"):
|
|
450
|
+
for lane in section.findall(".//lane"):
|
|
451
|
+
if lane.get("type") == "driving":
|
|
452
|
+
return True
|
|
453
|
+
return False
|
|
454
|
+
|
|
455
|
+
def road_is_bidirectional(self, road: Road) -> bool:
|
|
456
|
+
road_element = road.road_element
|
|
457
|
+
right = False
|
|
458
|
+
left = False
|
|
459
|
+
for section in road_element.findall(".//laneSection"):
|
|
460
|
+
for lane in section.findall(".//lane"):
|
|
461
|
+
lane_type = lane.get("type")
|
|
462
|
+
lane_id = int(lane.get("id", "0"))
|
|
463
|
+
if lane_type == "driving" and lane_id < 0:
|
|
464
|
+
right = True
|
|
465
|
+
if lane_type == "driving" and lane_id > 0:
|
|
466
|
+
left = True
|
|
467
|
+
if right and left:
|
|
468
|
+
return True
|
|
469
|
+
return False
|
|
470
|
+
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from avlite.c10_perception.c11_perception_model import PerceptionModel
|
|
4
|
+
from avlite.c10_perception.c19_settings import PerceptionSettings, PerceptionSettingsSchema
|
|
5
|
+
from avlite.c50_common.c51_capabilities import WorldCapability, StackCapability
|
|
6
|
+
from avlite.c50_common.c52_sensor_datatypes import SensorFrame
|
|
7
|
+
|
|
8
|
+
log = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PerceptionStrategy(ABC):
|
|
12
|
+
"""
|
|
13
|
+
Abstract base class for perception strategies.
|
|
14
|
+
This class defines the interface for perception strategies, including methods for detection, tracking, and prediction
|
|
15
|
+
"""
|
|
16
|
+
registry = {}
|
|
17
|
+
def __init__(self, perception_model: PerceptionModel, setting: PerceptionSettingsSchema = PerceptionSettings):
|
|
18
|
+
self.perception_model = perception_model
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def world_requirements(self) -> set[WorldCapability]:
|
|
23
|
+
"""World (sensor) capabilities this strategy requires from the bridge."""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def stack_requirements(self) -> set[StackCapability]:
|
|
28
|
+
"""Upstream stack capabilities this strategy depends on (default: none)."""
|
|
29
|
+
return set()
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def stack_capabilities(self) -> set[StackCapability]:
|
|
34
|
+
"""Stack capabilities this strategy provides to downstream modules."""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def perceive(
|
|
39
|
+
self,
|
|
40
|
+
perception_model: PerceptionModel | None = None,
|
|
41
|
+
sensors: SensorFrame | None = None,
|
|
42
|
+
) -> PerceptionModel | None:
|
|
43
|
+
"""Main perception method that combines detection, tracking, and prediction."""
|
|
44
|
+
raise NotImplementedError("Perception method not implemented.")
|
|
45
|
+
|
|
46
|
+
def reset(self):
|
|
47
|
+
"""
|
|
48
|
+
Reset the perception strategy to its initial state.
|
|
49
|
+
"""
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
def __init_subclass__(cls, abstract=False, **kwargs):
|
|
53
|
+
super().__init_subclass__(**kwargs)
|
|
54
|
+
if not abstract:
|
|
55
|
+
PerceptionStrategy.registry[cls.__name__] = cls
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class DetectionStrategy(ABC):
|
|
59
|
+
"""
|
|
60
|
+
A simple perception strategy that only performs detection.
|
|
61
|
+
"""
|
|
62
|
+
registry = {}
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def world_requirements(self) -> set[WorldCapability]:
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def detect(self, perception_model: PerceptionModel, rgb_img=None, depth_img=None, lidar_data=None) -> PerceptionModel:
|
|
71
|
+
"""
|
|
72
|
+
Detect objects in the environment using the specified detection method.
|
|
73
|
+
"""
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
def __init_subclass__(cls, abstract=False, **kwargs):
|
|
77
|
+
super().__init_subclass__(**kwargs)
|
|
78
|
+
if not abstract:
|
|
79
|
+
DetectionStrategy.registry[cls.__name__] = cls
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TrackingStrategy(ABC):
|
|
83
|
+
registry = {}
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
@abstractmethod
|
|
87
|
+
def world_requirements(self) -> set[WorldCapability]:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
@abstractmethod
|
|
91
|
+
def track(self, perception_model: PerceptionModel) -> PerceptionModel:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
def __init_subclass__(cls, abstract=False, **kwargs):
|
|
95
|
+
super().__init_subclass__(**kwargs)
|
|
96
|
+
if not abstract:
|
|
97
|
+
TrackingStrategy.registry[cls.__name__] = cls
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class PredictionStrategy(ABC):
|
|
101
|
+
registry = {}
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
@abstractmethod
|
|
105
|
+
def world_requirements(self) -> set[WorldCapability]:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
@abstractmethod
|
|
109
|
+
def predict(self, perception_model: PerceptionModel) -> PerceptionModel | None:
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
def __init_subclass__(cls, abstract=False, **kwargs):
|
|
113
|
+
super().__init_subclass__(**kwargs)
|
|
114
|
+
if not abstract:
|
|
115
|
+
PredictionStrategy.registry[cls.__name__] = cls
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class PerceptionPipeline(PerceptionStrategy):
|
|
119
|
+
"""
|
|
120
|
+
Pipelined perception strategy: detect → track → predict.
|
|
121
|
+
Each stage is resolved by name from its registry at construction time.
|
|
122
|
+
Empty name means that stage is skipped (ground truth for detect/track; no prediction).
|
|
123
|
+
"""
|
|
124
|
+
def __init__(self, perception_model: PerceptionModel, setting: PerceptionSettingsSchema = PerceptionSettings):
|
|
125
|
+
super().__init__(perception_model, setting)
|
|
126
|
+
self._detector = self._resolve(DetectionStrategy.registry, setting.c12_detection_strategy)
|
|
127
|
+
self._tracker = self._resolve(TrackingStrategy.registry, setting.c12_tracking_strategy)
|
|
128
|
+
self._predictor = self._resolve(PredictionStrategy.registry, setting.c12_prediction_strategy)
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def _resolve(registry: dict, name: str):
|
|
132
|
+
if name and name in registry:
|
|
133
|
+
return registry[name]()
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def world_requirements(self) -> set[WorldCapability]:
|
|
138
|
+
reqs = set()
|
|
139
|
+
for child in (self._detector, self._tracker, self._predictor):
|
|
140
|
+
if child is not None:
|
|
141
|
+
reqs |= child.world_requirements
|
|
142
|
+
return reqs
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def stack_requirements(self) -> set[StackCapability]:
|
|
146
|
+
# Stages with no strategy fall back to ground truth from the world bridge
|
|
147
|
+
reqs = set()
|
|
148
|
+
if self._detector is None:
|
|
149
|
+
reqs.add(StackCapability.DETECTION)
|
|
150
|
+
if self._tracker is None:
|
|
151
|
+
reqs.add(StackCapability.TRACKING)
|
|
152
|
+
return reqs
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def stack_capabilities(self) -> set[StackCapability]:
|
|
156
|
+
return {StackCapability.DETECTION, StackCapability.TRACKING, StackCapability.PREDICTION}
|
|
157
|
+
|
|
158
|
+
def perceive(
|
|
159
|
+
self,
|
|
160
|
+
perception_model=None,
|
|
161
|
+
sensors: SensorFrame | None = None,
|
|
162
|
+
) -> PerceptionModel | None:
|
|
163
|
+
if perception_model is not None:
|
|
164
|
+
self.perception_model = perception_model
|
|
165
|
+
if self._detector is not None:
|
|
166
|
+
self.perception_model = self._detector.detect(
|
|
167
|
+
self.perception_model,
|
|
168
|
+
rgb_img=sensors.rgb if sensors else None,
|
|
169
|
+
depth_img=sensors.depth if sensors else None,
|
|
170
|
+
lidar_data=sensors.lidar if sensors else None,
|
|
171
|
+
)
|
|
172
|
+
if self._tracker is not None:
|
|
173
|
+
self.perception_model = self._tracker.track(self.perception_model)
|
|
174
|
+
if self._predictor is not None:
|
|
175
|
+
self.perception_model = self._predictor.predict(self.perception_model)
|
|
176
|
+
return self.perception_model
|