Skip to content

Commit 059a9b3

Browse files
committed
Merge branch '3-identify-map-segments-intersection-roundabout-in-lanes-e-g-wod-motion' into 'main'
Resolve "Identify Map Segments (Intersection, Roundabout, ...) in Lanes (e.g. wod-motion)" Closes #3 See merge request fb-fi/data/omega-prime!6
2 parents 141af9f + f07d3b3 commit 059a9b3

16 files changed

+2994
-1784
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ omegadoc
99
*.html
1010
*.parquet
1111
*.xodr
12-
*.png
12+
*.png
13+
01_tracks.mcap
14+
testfile_wod.mcap

hdds-test.mcap

403 KB
Binary file not shown.

intersection.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import numpy as np
2+
import omega_prime
3+
from pathlib import Path
4+
from omega_prime.intersection_helper import *
5+
import sys
6+
7+
# # Read all mcap files in the directory:
8+
# folder_path = Path("/scenario-center-playground/scenarios/")
9+
10+
# for file in folder_path.glob("*.mcap"):
11+
# print(f"Processing file: {file}")
12+
# output_path = Path("/scenario-center-playground/scenarios/") / file.stem
13+
# output_path.mkdir(parents=True, exist_ok=True)
14+
# r = omega_prime.Recording.from_file(filepath=file)
15+
# locator = omega_prime.Locator.from_maposi(r.map)
16+
# mapsegment = omega_prime.mapsemgents.Mapsegmentation(r.map, locator)
17+
# mapsegment.init_intersections()
18+
# mapsegment.plot(output_path)
19+
# mapsegment.plot_intersections(output_path)
20+
print(sys.getrecursionlimit())
21+
22+
file = Path(
23+
"/scenario-center-playground/data/training_20s/training_20s.tfrecord-00003-of-01000/training_20s.tfrecord-00003-of-01000_4a63430816f60102.mcap"
24+
)
25+
output_path = Path("/scenario-center-playground/scenarios/") / file.stem
26+
output_path.mkdir(parents=True, exist_ok=True)
27+
r = omega_prime.Recording.from_file(filepath=file, compute_polygons=True, split_lanes=True, split_lanes_lenght=10)
28+
r.create_mapsegments()
29+
mapsegment = r.mapsegment
30+
locator = omega_prime.Locator.from_map(r.map)
31+
32+
33+
id = r.host_vehicle_idx
34+
# Assuming r.moving_objects is a list of objects with x and y attributes
35+
x_values = r.moving_objects[id].x.to_numpy() # Convert Polars Series to NumPy array
36+
y_values = r.moving_objects[id].y.to_numpy() # Convert Polars Series to NumPy array
37+
frame = r.moving_objects[id]._df["frame"].to_numpy() # Convert Polars Series to NumPy array
38+
39+
# Combine x and y into a single NumPy array of shape (n, 2)
40+
positions = np.column_stack((frame, x_values, y_values))
41+
42+
xy = np.column_stack((x_values, y_values))
43+
sts = locator.xys2sts(xy)
44+
45+
segments = mapsegment.trajectory_segment_detection(positions)
46+
mapsegment.plot(output_plot=output_path, trajectory=positions)
47+
mapsegment.plot_intersections(output_path)
48+
49+
for i, segment in enumerate(segments):
50+
if segment[1] is not None:
51+
print(f"Segment {i}: {segment[1].idx} with the type {segment[1].type}")
52+
else:
53+
print(f"Segment {i}: Trajectory could not be assigned to a segment.")

omega_prime/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@
66
from .locator import LaneRelation, Locator
77
from .map import Lane, LaneBoundary, Map, MapOsi
88
from .recording import MovingObject, Recording
9+
from .mapsemgents import Mapsegmentation
10+
11+
912
from importlib.metadata import version
1013

1114
__version__ = version("omega_prime")
1215

16+
1317
__all__ = [
1418
"Recording",
1519
"MovingObject",
@@ -22,4 +26,5 @@
2226
"LaneRelation",
2327
"converters",
2428
"metrics",
29+
"Mapsegmentation",
2530
]

omega_prime/intersection_helper.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import networkx as nx
2+
import numpy as np
3+
import matplotlib.pyplot as plt
4+
import shapely
5+
import shapely.ops
6+
7+
8+
def find_misclassified_intersection_lanes(lane_dict, all_lanes):
9+
"""
10+
Finds lanes classified as `on_intersection` but have no intersections in the lane_dict.
11+
12+
Args:
13+
lane_dict (dict): A dictionary where keys are lane indices and values are arrays of intersecting lane indices.
14+
all_lanes (list): A list of all lane objects, each with an `idx` and `on_intersection` attribute.
15+
16+
Returns:
17+
np.ndarray: An array of lane indices that are misclassified as `on_intersection`.
18+
"""
19+
misclassified_lanes = []
20+
for lane in all_lanes:
21+
if lane.on_intersection and (lane.idx not in lane_dict or len(lane_dict[lane.idx]) == 0):
22+
misclassified_lanes.append(lane.idx)
23+
return np.array(misclassified_lanes)
24+
25+
26+
def add_lanexy_to_graph(G, lanes):
27+
"""
28+
Adds lane coordinates to the graph as node attributes.
29+
30+
Args:
31+
G (networkx.Graph): The graph to which lane coordinates will be added.
32+
locator (omega_prime.Locator): The locator object containing lane information.
33+
34+
Returns:
35+
networkx.Graph: The updated graph with lane coordinates as node attributes.
36+
"""
37+
for lane in lanes.values():
38+
if lane.idx.lane_id in G.nodes:
39+
G.nodes[lane.idx.lane_id]["x"] = shapely.centroid(lane.centerline).x
40+
G.nodes[lane.idx.lane_id]["y"] = shapely.centroid(lane.centerline).y
41+
return G
42+
43+
44+
def plot_graph(G, Path):
45+
"""
46+
Plots the graph with lane coordinates.
47+
48+
Args:
49+
G (networkx.Graph): The graph to be plotted.
50+
Path (str or Path): The file path to save the plot.
51+
"""
52+
pos = {node: (data["x"], data["y"]) for node, data in G.nodes(data=True)}
53+
nx.draw(G, pos, with_labels=True, node_size=10, font_size=5)
54+
plt.title("Intersection Graph")
55+
plt.xlabel("X Coordinate") # Add label for the x-axis
56+
plt.ylabel("Y Coordinate") # Add label for the y-axis
57+
plt.grid(True) # Optional: Add a grid for better visualization
58+
plt.savefig(Path)
59+
plt.close() # Close the plot to free memory
60+
61+
62+
def is_matrix_symmetrical(successors_dict, predecessors_dict):
63+
# Check successors against predecessors
64+
for lane_idx, successors in successors_dict.items():
65+
for successor in successors:
66+
if lane_idx not in predecessors_dict.get(successor, []):
67+
print(f"Lane {lane_idx} is not in the predecessors of its successor {successor}")
68+
print(f"Successors: {successors_dict[lane_idx]}")
69+
print(f"Predecessors: {predecessors_dict.get(successor, [])}")
70+
return False
71+
72+
# Check predecessors against successors
73+
for lane_idx, predecessors in predecessors_dict.items():
74+
for predecessor in predecessors:
75+
if lane_idx not in successors_dict.get(predecessor, []):
76+
print(f"Lane {lane_idx} is not in the successors of its predecessor {predecessor}")
77+
print(f"Predecessors: {predecessors_dict[lane_idx]}")
78+
print(f"Successors: {successors_dict.get(predecessor, [])}")
79+
return False
80+
81+
return True

omega_prime/locator.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ def extend_linestring(cls, l: shapely.LineString, simplify: bool = True, l_appen
3838
if isinstance(l, shapely.Point):
3939
cl = np.asarray(l.coords)[0]
4040
return shapely.LineString(np.stack([cl - np.array([0, 1]), cl, cl + np.array([0, 1])]))
41+
if l.length == 0:
42+
return l
4143

4244
startvec = np.asarray(l.interpolate(0, normalized=True).coords) - np.asarray(
4345
l.interpolate(0 + cls.epsi, normalized=True).coords
@@ -179,7 +181,7 @@ class LaneRelation(StrEnum):
179181
neighbour_left = "left neighbour"
180182

181183

182-
@dataclass
184+
@dataclass(repr=False)
183185
class Locator:
184186
all_lanes: Any # array of all lanes
185187
external2internal_laneid: dict[Any, int] = field(init=False)
@@ -196,6 +198,7 @@ def from_map(cls, map):
196198
return cls(all_lanes=all_lanes)
197199

198200
def __post_init__(self):
201+
# Create mapping with lane_id as key
199202
self.external2internal_laneid = {l.idx: i for i, l in enumerate(self.all_lanes)}
200203
self.internal2external_laneid = [l.idx for l in self.all_lanes]
201204

@@ -290,7 +293,7 @@ def _xys2sts(self, xys, polygons=None):
290293
# Returns the indxes of all centerlines that are in range
291294
nearby_idx = self.query_centerlines(poly, range_percentage=0.1)
292295
# Connect the no_assosciation_idxs with the intersection_lane_ids
293-
no_asscociation_idxs = np.append(no_asscociation_idxs, [no_associations[idx]] * len(nearby_idx))
296+
no_asscociation_idxs = np.append(no_asscociation_idxs, [idx] * len(nearby_idx))
294297
intersection_lane_ids = np.append(intersection_lane_ids, nearby_idx)
295298

296299
# Need a convertion from float values to int values. This is because the shapely STRtree query_nearest returns float values
@@ -356,6 +359,7 @@ def _get_routing_graph(self):
356359
external2internal_laneid = self.external2internal_laneid
357360
g = nx.DiGraph()
358361
for lid, lane in enumerate(all_lanes):
362+
g.add_node(lid, lane=lane)
359363
for external_pid in lane.predecessor_ids:
360364
try:
361365
g.add_edge(lid, external2internal_laneid[external_pid], label=LaneRelation.predecessor)
@@ -366,6 +370,8 @@ def _get_routing_graph(self):
366370
g.add_edge(lid, external2internal_laneid[external_sid], label=LaneRelation.successor)
367371
except KeyError:
368372
pass
373+
if lane.right_boundary is None or lane.left_boundary is None:
374+
continue
369375
right_neigbours = [
370376
int(i) for i in str_tree.query(lane.right_boundary.polyline, predicate="covered_by") if int(i) != lid
371377
]
@@ -440,3 +446,7 @@ def get_single_lane_association(
440446

441447
def __repr__(self):
442448
return f"Locator({len(self.all_lanes)} lanes)<{id(self)}>"
449+
450+
def update_lane_ids_dict(self):
451+
self.external2internal_laneid = {l.idx: i for i, l in enumerate(self.all_lanes)}
452+
self.internal2external_laneid = [l.idx for l in self.all_lanes]

omega_prime/map.py

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from dataclasses import dataclass, field
22
from typing import Any
3-
3+
from collections import namedtuple
44
import betterosi
55
import numpy as np
66
import shapely
@@ -11,6 +11,9 @@
1111
import polars_st as st
1212

1313

14+
OsiLaneId = namedtuple("OsiLaneId", ["road_id", "lane_id"])
15+
16+
1417
@dataclass
1518
class ProjectionOffset:
1619
x: float
@@ -133,6 +136,10 @@ def end_points(self):
133136
@dataclass(repr=False)
134137
class LaneOsiCenterline(LaneBase):
135138
_osi: betterosi.Lane
139+
left_boundary = None
140+
right_boundary = None
141+
on_intersection: bool = field(init=False, default=None)
142+
is_approaching: bool = field(init=False, default=None)
136143

137144
@staticmethod
138145
def _get_centerline(lane: betterosi.Lane):
@@ -143,14 +150,21 @@ def _get_centerline(lane: betterosi.Lane):
143150

144151
@classmethod
145152
def create(cls, lane: betterosi.Lane):
153+
successor_ids = [
154+
p.successor_lane_id.value for p in lane.classification.lane_pairing if p.successor_lane_id is not None
155+
]
156+
predecessor_ids = [
157+
p.antecessor_lane_id.value for p in lane.classification.lane_pairing if p.antecessor_lane_id is not None
158+
]
159+
lid = lane.id.value
146160
return cls(
147161
_osi=lane,
148-
idx=lane.id.value,
162+
idx=OsiLaneId(road_id=lid, lane_id=lid),
149163
centerline=cls._get_centerline(lane),
150164
type=betterosi.LaneClassificationType(lane.classification.type),
151165
subtype=betterosi.LaneClassificationSubtype(lane.classification.subtype),
152-
successor_ids=[],
153-
predecessor_ids=[],
166+
successor_ids=np.array(list(set(successor_ids))),
167+
predecessor_ids=np.array(list(set(predecessor_ids))),
154168
)
155169

156170

@@ -162,9 +176,10 @@ class LaneOsi(Lane, LaneOsiCenterline):
162176

163177
@classmethod
164178
def create(cls, lane: betterosi.Lane):
179+
lid = int(lane.id.value)
165180
return cls(
166181
_osi=lane,
167-
idx=int(lane.id.value),
182+
idx=OsiLaneId(road_id=lid, lane_id=lid),
168183
centerline=cls._get_centerline(lane),
169184
type=betterosi.LaneClassificationType(lane.classification.type),
170185
subtype=betterosi.LaneClassificationSubtype(lane.classification.subtype),
@@ -296,7 +311,10 @@ def create(cls, gt: betterosi.GroundTruth):
296311
return cls(
297312
_osi=gt,
298313
lane_boundaries={b.id.value: LaneBoundaryOsi.create(b) for b in gt.lane_boundary},
299-
lanes={l.id.value: LaneOsi.create(l) for l in gt.lane if len(l.classification.right_lane_boundary_id) > 0},
314+
lanes={
315+
l.idx: l
316+
for l in [LaneOsi.create(l) for l in gt.lane if len(l.classification.right_lane_boundary_id) > 0]
317+
},
300318
)
301319

302320
def __post_init__(self):
@@ -305,7 +323,10 @@ def __post_init__(self):
305323
def setup_lanes_and_boundaries(self):
306324
for b in self.lane_boundaries.values():
307325
b._map = self
326+
map_osi_id2idx = {l._osi.id.value: l.idx for l in self.lanes.values()}
308327
for l in self.lanes.values():
328+
l.successor_ids = [map_osi_id2idx[i] for i in l.successor_ids]
329+
l.predecessor_ids = [map_osi_id2idx[i] for i in l.predecessor_ids]
309330
l._map = self
310331
l.set_boundaries()
311332
l.set_polygon()
@@ -322,10 +343,28 @@ def create(cls, gt: betterosi.GroundTruth):
322343
raise RuntimeError("No Map")
323344
return cls(
324345
_osi=gt,
325-
lanes={l.id.value: LaneOsiCenterline.create(l) for l in gt.lane},
346+
lanes={l.idx: l for l in [LaneOsiCenterline.create(l) for l in gt.lane]},
326347
lane_boundaries={},
327348
)
328349

329350
def setup_lanes_and_boundaries(self):
351+
map_osi_id2idx = {l._osi.id.value: l.idx for l in self.lanes.values()}
352+
for l in self.lanes.values():
353+
l.successor_ids = [map_osi_id2idx[int(i)] for i in l.successor_ids if int(i) in map_osi_id2idx]
354+
l.predecessor_ids = [map_osi_id2idx[int(i)] for i in l.predecessor_ids if int(i) in map_osi_id2idx]
330355
for l in self.lanes.values():
331356
l._map = self
357+
358+
# Sometimes a presuccessor lane is not set as a successor lane in the other lane, therefore we need to check where this is the case and add it
359+
for l in self.lanes.values():
360+
for p in l.predecessor_ids:
361+
if p not in self.lanes:
362+
continue
363+
if l.idx not in self.lanes[p].successor_ids:
364+
self.lanes[p].successor_ids.append(l.idx)
365+
for l in self.lanes.values():
366+
for s in l.successor_ids:
367+
if s not in self.lanes:
368+
continue
369+
if l.idx not in self.lanes[s].predecessor_ids:
370+
self.lanes[s].predecessor_ids.append(l.idx)

0 commit comments

Comments
 (0)