Skip to content

Commit 2ee7bcf

Browse files
committed
Add intersection to argoverse 2 sensor map
1 parent e49f0d1 commit 2ee7bcf

File tree

6 files changed

+504
-77
lines changed

6 files changed

+504
-77
lines changed

d123/common/geometry/line/polylines.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import shapely.geometry as geom
1010
from scipy.interpolate import interp1d
1111

12-
from d123.common.geometry.base import Point2D, Point2DIndex, Point3D, StateSE2, StateSE2Index
12+
from d123.common.geometry.base import Point2D, Point2DIndex, Point3D, Point3DIndex, StateSE2, StateSE2Index
1313
from d123.common.geometry.constants import DEFAULT_Z
1414
from d123.common.geometry.line.helper import get_linestring_yaws, get_path_progress
1515
from d123.common.geometry.utils import normalize_angle
@@ -33,7 +33,15 @@ def from_linestring(cls, linestring: geom.LineString) -> Polyline2D:
3333

3434
@classmethod
3535
def from_array(cls, polyline_array: npt.NDArray[np.float32]) -> Polyline2D:
36-
raise NotImplementedError
36+
assert polyline_array.ndim == 2
37+
linestring: Optional[geom.LineString] = None
38+
if polyline_array.shape[-1] == len(Point2DIndex):
39+
linestring = geom_creation.linestrings(polyline_array)
40+
elif polyline_array.shape[-1] == len(Point3DIndex):
41+
linestring = geom_creation.linestrings(polyline_array[:, Point3DIndex.XY])
42+
else:
43+
raise ValueError("Array must have shape (N, 2) or (N, 3) for Point2D or Point3D respectively.")
44+
return Polyline2D(linestring)
3745

3846
@property
3947
def array(self) -> npt.NDArray[np.float64]:

d123/dataset/dataset_specific/av2/av2_map_conversion.py

Lines changed: 171 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
import numpy as np
66
import numpy.typing as npt
77
import pandas as pd
8+
import shapely
89
import shapely.geometry as geom
910
from flask import json
1011

1112
from d123.common.geometry.base import Point3DIndex
12-
from d123.common.geometry.line.polylines import Polyline3D
13+
from d123.common.geometry.line.polylines import Polyline2D, Polyline3D
14+
from d123.common.geometry.occupancy_map import OccupancyMap2D
1315
from d123.dataset.conversion.map.road_edge.road_edge_2d_utils import split_line_geometry_by_max_length
1416
from d123.dataset.conversion.map.road_edge.road_edge_3d_utils import (
1517
get_road_edges_3d_from_generic_drivable_area_df,
@@ -32,10 +34,7 @@ def _extract_polyline(data: List[Dict[str, float]], close: bool = False) -> Poly
3234
return Polyline3D.from_array(polyline)
3335

3436
map_folder = source_log_path / "map"
35-
36-
next(map_folder.glob("*.npy"))
3737
log_map_archive_path = next(map_folder.glob("log_map_archive_*.json"))
38-
next(map_folder.glob("*img_Sim2_city.json*"))
3938

4039
with open(log_map_archive_path, "r") as f:
4140
log_map_archive = json.load(f)
@@ -71,26 +70,29 @@ def _extract_polyline(data: List[Dict[str, float]], close: bool = False) -> Poly
7170
p3, p4 = np.array([[p["x"], p["y"], p["z"]] for p in crosswalk_dict["edge2"]], dtype=np.float64)
7271
crosswalk_dict["outline"] = Polyline3D.from_array(np.array([p1, p2, p4, p3, p1], dtype=np.float64))
7372

73+
lane_group_dict = _extract_lane_group_dict(log_map_archive["lane_segments"])
74+
intersection_dict = _extract_intersection_dict(log_map_archive["lane_segments"], lane_group_dict)
75+
7476
lane_df = get_lane_df(log_map_archive["lane_segments"])
75-
lane_group_df = get_lane_group_df(log_map_archive["lane_segments"])
76-
get_empty_gdf()
77+
lane_group_df = get_lane_group_df(lane_group_dict)
78+
intersection_df = get_intersections_df(intersection_dict)
7779
crosswalk_df = get_crosswalk_df(log_map_archive["pedestrian_crossings"])
78-
get_empty_gdf()
79-
get_empty_gdf()
80+
walkway_df = get_empty_gdf() # NOTE: AV2 does not provide walkways, so we create an empty DataFrame.
81+
carpark_df = get_empty_gdf() # NOTE: AV2 does not provide carparks, so we create an empty DataFrame.
8082
generic_drivable_df = get_generic_drivable_df(drivable_areas)
8183
road_edge_df = get_road_edge_df(generic_drivable_df)
82-
get_empty_gdf()
84+
# road_line_df = get_empty_gdf()
8385

8486
map_file_path.unlink(missing_ok=True)
8587
if not map_file_path.parent.exists():
8688
map_file_path.parent.mkdir(parents=True, exist_ok=True)
8789

8890
lane_df.to_file(map_file_path, layer=MapLayer.LANE.serialize(), driver="GPKG")
8991
lane_group_df.to_file(map_file_path, layer=MapLayer.LANE_GROUP.serialize(), driver="GPKG", mode="a")
90-
# intersection_df.to_file(map_file_path, layer=MapLayer.INTERSECTION.serialize(), driver="GPKG", mode="a")
92+
intersection_df.to_file(map_file_path, layer=MapLayer.INTERSECTION.serialize(), driver="GPKG", mode="a")
9193
crosswalk_df.to_file(map_file_path, layer=MapLayer.CROSSWALK.serialize(), driver="GPKG", mode="a")
92-
# walkway_df.to_file(map_file_path, layer=MapLayer.WALKWAY.serialize(), driver="GPKG", mode="a")
93-
# carpark_df.to_file(map_file_path, layer=MapLayer.CARPARK.serialize(), driver="GPKG", mode="a")
94+
walkway_df.to_file(map_file_path, layer=MapLayer.WALKWAY.serialize(), driver="GPKG", mode="a")
95+
carpark_df.to_file(map_file_path, layer=MapLayer.CARPARK.serialize(), driver="GPKG", mode="a")
9496
generic_drivable_df.to_file(map_file_path, layer=MapLayer.GENERIC_DRIVABLE.serialize(), driver="GPKG", mode="a")
9597
road_edge_df.to_file(map_file_path, layer=MapLayer.ROAD_EDGE.serialize(), driver="GPKG", mode="a")
9698
# road_line_df.to_file(map_file_path, layer=MapLayer.ROAD_LINE.serialize(), driver="GPKG", mode="a")
@@ -193,12 +195,9 @@ def _get_centerline_from_boundaries(
193195
return gdf
194196

195197

196-
def get_lane_group_df(lanes: Dict[int, Any]) -> gpd.GeoDataFrame:
197-
198-
lane_group_sets = find_lane_groups(lanes)
199-
lane_group_set_dict = {i: lane_group for i, lane_group in enumerate(lane_group_sets)}
198+
def get_lane_group_df(lane_group_dict: Dict[int, Any]) -> gpd.GeoDataFrame:
200199

201-
ids = list(lane_group_set_dict.keys())
200+
ids = list(lane_group_dict.keys())
202201
lane_ids = []
203202
intersection_ids = []
204203
predecessor_lane_group_ids = []
@@ -207,38 +206,21 @@ def get_lane_group_df(lanes: Dict[int, Any]) -> gpd.GeoDataFrame:
207206
right_boundaries = []
208207
geometries = []
209208

210-
def _get_lane_group_ids_of_lanes_ids(lane_ids: List[str]) -> List[int]:
211-
"""Helper to find lane group ids that contain any of the given lane ids."""
212-
lane_group_ids_ = []
213-
for lane_group_id_, lane_group_set_ in lane_group_set_dict.items():
214-
if any(str(lane_id) in lane_group_set_ for lane_id in lane_ids):
215-
lane_group_ids_.append(lane_group_id_)
216-
return list(set(lane_group_ids_))
217-
218-
for lane_group_id, lane_group_set in lane_group_set_dict.items():
219-
220-
lane_ids.append([int(lane_id) for lane_id in lane_group_set])
221-
intersection_ids.append(None) # NOTE: AV2 doesn't have explicit intersection objects.
222-
223-
successor_lanes = []
224-
predecessor_lanes = []
225-
for lane_id in lane_group_set:
226-
lane_dict = lanes[str(lane_id)]
227-
successor_lanes.extend(lane_dict["successors"])
228-
predecessor_lanes.extend(lane_dict["predecessors"])
209+
for lane_group_id, lane_group_values in lane_group_dict.items():
229210

230-
left_boundary = lanes[lane_group_set[0]]["left_lane_boundary"]
231-
right_boundary = lanes[lane_group_set[-1]]["right_lane_boundary"]
211+
lane_ids.append(lane_group_values["lane_ids"])
212+
intersection_ids.append(lane_group_values["intersection_id"])
232213

233-
predecessor_lane_group_ids.append(_get_lane_group_ids_of_lanes_ids(predecessor_lanes))
234-
successor_lane_group_ids.append(_get_lane_group_ids_of_lanes_ids(successor_lanes))
235-
left_boundaries.append(left_boundary.linestring)
236-
right_boundaries.append(right_boundary.linestring)
214+
predecessor_lane_group_ids.append(lane_group_values["predecessor_ids"])
215+
successor_lane_group_ids.append(lane_group_values["successor_ids"])
216+
left_boundaries.append(lane_group_values["left_boundary"].linestring)
217+
right_boundaries.append(lane_group_values["right_boundary"].linestring)
237218
geometry = geom.Polygon(
238219
np.vstack(
239220
[
240-
left_boundary.array[:, :2],
241-
right_boundary.array[:, :2][::-1],
221+
lane_group_values["left_boundary"].array[:, :2],
222+
lane_group_values["right_boundary"].array[:, :2][::-1],
223+
lane_group_values["left_boundary"].array[0, :2][None, ...],
242224
]
243225
)
244226
)
@@ -259,13 +241,19 @@ def _get_lane_group_ids_of_lanes_ids(lane_ids: List[str]) -> List[int]:
259241
return gdf
260242

261243

262-
def get_intersections_df() -> gpd.GeoDataFrame:
244+
def get_intersections_df(intersection_dict: Dict[int, Any]) -> gpd.GeoDataFrame:
263245
ids = []
264246
lane_group_ids = []
247+
outlines = []
265248
geometries = []
266249

267-
# NOTE: WOPD does not provide intersections, so we create an empty DataFrame.
268-
data = pd.DataFrame({"id": ids, "lane_group_ids": lane_group_ids})
250+
for intersection_id, intersection_values in intersection_dict.items():
251+
ids.append(intersection_id)
252+
lane_group_ids.append(intersection_values["lane_group_ids"])
253+
outlines.append(intersection_values["outline_3d"].linestring)
254+
geometries.append(geom.Polygon(intersection_values["outline_3d"].array[:, Point3DIndex.XY]))
255+
256+
data = pd.DataFrame({"id": ids, "lane_group_ids": lane_group_ids, "outline": outlines})
269257
gdf = gpd.GeoDataFrame(data, geometry=geometries)
270258
return gdf
271259

@@ -341,7 +329,56 @@ def get_road_line_df(
341329
return gdf
342330

343331

344-
def find_lane_groups(lanes) -> List[List[str]]:
332+
def _extract_lane_group_dict(lanes: Dict[int, Any]) -> gpd.GeoDataFrame:
333+
334+
lane_group_sets = _extract_lane_group(lanes)
335+
lane_group_set_dict = {i: lane_group for i, lane_group in enumerate(lane_group_sets)}
336+
337+
lane_group_dict: Dict[int, Dict[str, Any]] = {}
338+
339+
def _get_lane_group_ids_of_lanes_ids(lane_ids: List[str]) -> List[int]:
340+
"""Helper to find lane group ids that contain any of the given lane ids."""
341+
lane_group_ids_ = []
342+
for lane_group_id_, lane_group_set_ in lane_group_set_dict.items():
343+
if any(str(lane_id) in lane_group_set_ for lane_id in lane_ids):
344+
lane_group_ids_.append(lane_group_id_)
345+
return list(set(lane_group_ids_))
346+
347+
for lane_group_id, lane_group_set in lane_group_set_dict.items():
348+
349+
lane_group_dict[lane_group_id] = {}
350+
lane_group_dict[lane_group_id]["id"] = lane_group_id
351+
lane_group_dict[lane_group_id]["lane_ids"] = [int(lane_id) for lane_id in lane_group_set]
352+
353+
successor_lanes = []
354+
predecessor_lanes = []
355+
for lane_id in lane_group_set:
356+
lane_dict = lanes[str(lane_id)]
357+
successor_lanes.extend(lane_dict["successors"])
358+
predecessor_lanes.extend(lane_dict["predecessors"])
359+
360+
left_boundary = lanes[lane_group_set[0]]["left_lane_boundary"]
361+
right_boundary = lanes[lane_group_set[-1]]["right_lane_boundary"]
362+
363+
lane_group_dict[lane_group_id]["intersection_id"] = None
364+
lane_group_dict[lane_group_id]["predecessor_ids"] = _get_lane_group_ids_of_lanes_ids(predecessor_lanes)
365+
lane_group_dict[lane_group_id]["successor_ids"] = _get_lane_group_ids_of_lanes_ids(successor_lanes)
366+
lane_group_dict[lane_group_id]["left_boundary"] = left_boundary
367+
lane_group_dict[lane_group_id]["right_boundary"] = right_boundary
368+
outline_array = np.vstack(
369+
[
370+
left_boundary.array[:, :3],
371+
right_boundary.array[:, :3][::-1],
372+
left_boundary.array[0, :3][None, ...],
373+
]
374+
)
375+
376+
lane_group_dict[lane_group_id]["outline"] = Polyline3D.from_array(outline_array)
377+
378+
return lane_group_dict
379+
380+
381+
def _extract_lane_group(lanes) -> List[List[str]]:
345382

346383
visited = set()
347384
lane_groups = []
@@ -391,3 +428,89 @@ def _traverse_group(start_lane_id):
391428
lane_groups.append(group)
392429

393430
return lane_groups
431+
432+
433+
def _extract_intersection_dict(
434+
lanes: Dict[int, Any], lane_group_dict: Dict[int, Any], max_distance: float = 0.01
435+
) -> Dict[str, Any]:
436+
437+
def _interpolate_z_on_segment(point: shapely.Point, segment_coords: npt.NDArray[np.float64]) -> float:
438+
"""Interpolate Z coordinate along a 3D line segment."""
439+
p1, p2 = segment_coords[0], segment_coords[1]
440+
441+
# Project point onto segment
442+
segment_vec = p2[:2] - p1[:2]
443+
point_vec = np.array([point.x, point.y]) - p1[:2]
444+
445+
# Handle degenerate case
446+
segment_length_sq = np.dot(segment_vec, segment_vec)
447+
if segment_length_sq == 0:
448+
return p1[2]
449+
450+
# Calculate projection parameter
451+
t = np.dot(point_vec, segment_vec) / segment_length_sq
452+
t = np.clip(t, 0, 1) # Clamp to segment bounds
453+
454+
# Interpolate Z
455+
return p1[2] + t * (p2[2] - p1[2])
456+
457+
# 1. Collect all lane groups where at least one lane is marked as an intersection.
458+
lane_group_intersection_dict = {}
459+
for lane_group_id, lane_group in lane_group_dict.items():
460+
is_intersection_lanes = [lanes[str(lane_id)]["is_intersection"] for lane_id in lane_group["lane_ids"]]
461+
if any(is_intersection_lanes):
462+
lane_group_intersection_dict[lane_group_id] = lane_group
463+
464+
# 2. Merge polygons of lane groups that are marked as intersections.
465+
lane_group_intersection_geometry = {
466+
lane_group_id: shapely.Polygon(lane_group["outline"].array[:, Point3DIndex.XY])
467+
for lane_group_id, lane_group in lane_group_intersection_dict.items()
468+
}
469+
intersection_polygons = gpd.GeoSeries(lane_group_intersection_geometry).union_all()
470+
471+
# 3. Collect all intersection polygons and their lane group IDs.
472+
intersection_dict = {}
473+
for intersection_idx, intersection_polygon in enumerate(intersection_polygons.geoms):
474+
if intersection_polygon.is_empty:
475+
continue
476+
lane_group_ids = [
477+
lane_group_id
478+
for lane_group_id, lane_group_polygon in lane_group_intersection_geometry.items()
479+
if intersection_polygon.intersects(lane_group_polygon)
480+
]
481+
for lane_group_id in lane_group_ids:
482+
lane_group_dict[lane_group_id]["intersection_id"] = intersection_idx
483+
484+
intersection_dict[intersection_idx] = {
485+
"id": intersection_idx,
486+
"outline_2d": Polyline2D.from_array(np.array(list(intersection_polygon.exterior.coords), dtype=np.float64)),
487+
"lane_group_ids": lane_group_ids,
488+
}
489+
490+
# 4. Lift intersection outlines to 3D.
491+
boundary_segments = []
492+
for lane_group in lane_group_intersection_dict.values():
493+
coords = np.array(lane_group["outline"].linestring.coords, dtype=np.float64).reshape(-1, 1, 3)
494+
segment_coords_boundary = np.concatenate([coords[:-1], coords[1:]], axis=1)
495+
boundary_segments.append(segment_coords_boundary)
496+
497+
boundary_segments = np.concatenate(boundary_segments, axis=0)
498+
boundary_segment_linestrings = shapely.creation.linestrings(boundary_segments)
499+
occupancy_map = OccupancyMap2D(boundary_segment_linestrings)
500+
501+
for intersection_id, intersection_data in intersection_dict.items():
502+
points_2d = intersection_data["outline_2d"].array
503+
points_3d = np.zeros((len(points_2d), 3), dtype=np.float64)
504+
points_3d[:, :2] = points_2d
505+
506+
query_points = shapely.creation.points(points_2d)
507+
results = occupancy_map.query_nearest(query_points, max_distance=max_distance, exclusive=True)
508+
for query_idx, geometry_idx in zip(*results):
509+
query_point = query_points[query_idx]
510+
segment_coords = boundary_segments[geometry_idx]
511+
best_z = _interpolate_z_on_segment(query_point, segment_coords)
512+
points_3d[query_idx, 2] = best_z
513+
514+
intersection_dict[intersection_id]["outline_3d"] = Polyline3D.from_array(points_3d)
515+
516+
return intersection_dict

notebooks/av2/delete_me.ipynb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"split = \"train\"\n",
3434
"# split = \"val\"\n",
3535
"\n",
36-
"split_folder = Path(f\"/mnt/elements_0/argoverse/sensor_mini/{split}\")\n",
36+
"split_folder = Path(f\"/media/nvme1/argoverse/sensor_mini/{split}\")\n",
3737
"log_folder = sorted(list(split_folder.iterdir()))[0]\n",
3838
"\n",
3939
"log_folder.name"
@@ -458,7 +458,7 @@
458458
],
459459
"metadata": {
460460
"kernelspec": {
461-
"display_name": "d123_dev",
461+
"display_name": "d123",
462462
"language": "python",
463463
"name": "python3"
464464
},

0 commit comments

Comments
 (0)