from shapely.geometry import LineString, box, Polygon, LinearRing from shapely.geometry.base import BaseGeometry from shapely import ops import numpy as np from scipy.spatial import distance from typing import List, Optional, Tuple from numpy.typing import NDArray def split_collections(geom: BaseGeometry) -> List[Optional[BaseGeometry]]: ''' Split Multi-geoms to list and check is valid or is empty. Args: geom (BaseGeometry): geoms to be split or validate. Returns: geometries (List): list of geometries. ''' assert geom.geom_type in ['MultiLineString', 'LineString', 'MultiPolygon', 'Polygon', 'GeometryCollection'], f"got geom type {geom.geom_type}" if 'Multi' in geom.geom_type: outs = [] for g in geom.geoms: if g.is_valid and not g.is_empty: outs.append(g) return outs else: if geom.is_valid and not geom.is_empty: return [geom,] else: return [] def get_drivable_area_contour(drivable_areas: List[Polygon], roi_size: Tuple) -> List[LineString]: ''' Extract drivable area contours to get list of boundaries. Args: drivable_areas (list): list of drivable areas. roi_size (tuple): bev range size Returns: boundaries (List): list of boundaries. ''' max_x = roi_size[0] / 2 max_y = roi_size[1] / 2 # a bit smaller than roi to avoid unexpected boundaries on edges local_patch = box(-max_x + 0.2, -max_y + 0.2, max_x - 0.2, max_y - 0.2) exteriors = [] interiors = [] for poly in drivable_areas: exteriors.append(poly.exterior) for inter in poly.interiors: interiors.append(inter) results = [] for ext in exteriors: # NOTE: we make sure all exteriors are clock-wise # such that each boundary's right-hand-side is drivable area # and left-hand-side is walk way if ext.is_ccw: ext = LinearRing(list(ext.coords)[::-1]) lines = ext.intersection(local_patch) if lines.geom_type == 'MultiLineString': lines = ops.linemerge(lines) assert lines.geom_type in ['MultiLineString', 'LineString'] results.extend(split_collections(lines)) for inter in interiors: # NOTE: we make sure all interiors are counter-clock-wise if not inter.is_ccw: inter = LinearRing(list(inter.coords)[::-1]) lines = inter.intersection(local_patch) if lines.geom_type == 'MultiLineString': lines = ops.linemerge(lines) assert lines.geom_type in ['MultiLineString', 'LineString'] results.extend(split_collections(lines)) return results def get_ped_crossing_contour(polygon: Polygon, local_patch: box) -> Optional[LineString]: ''' Extract ped crossing contours to get a closed polyline. Different from `get_drivable_area_contour`, this function ensures a closed polyline. Args: polygon (Polygon): ped crossing polygon to be extracted. local_patch (tuple): local patch params Returns: line (LineString): a closed line ''' ext = polygon.exterior if not ext.is_ccw: ext = LinearRing(list(ext.coords)[::-1]) lines = ext.intersection(local_patch) if lines.type != 'LineString': # remove points in intersection results lines = [l for l in lines.geoms if l.geom_type != 'Point'] lines = ops.linemerge(lines) # same instance but not connected. if lines.type != 'LineString': ls = [] for l in lines.geoms: ls.append(np.array(l.coords)) lines = np.concatenate(ls, axis=0) lines = LineString(lines) if not lines.is_empty: return lines return None