55import numpy as np
66import numpy .typing as npt
77import pandas as pd
8+ import shapely
89import shapely .geometry as geom
910from flask import json
1011
1112from 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
1315from d123 .dataset .conversion .map .road_edge .road_edge_2d_utils import split_line_geometry_by_max_length
1416from 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
0 commit comments