From b06240549a5de3ad1e3be5ed28b0dec8a543b489 Mon Sep 17 00:00:00 2001 From: paulnoirel <87332996+paulnoirel@users.noreply.github.com> Date: Mon, 9 Jun 2025 14:41:28 +0100 Subject: [PATCH] Add NumPy 2.0 compatibility for mask geometry operations --- .../annotation_types/geometry/geometry.py | 18 ++++- .../data/annotation_types/geometry/mask.py | 80 +++++++++++++++++-- 2 files changed, 91 insertions(+), 7 deletions(-) diff --git a/libs/labelbox/src/labelbox/data/annotation_types/geometry/geometry.py b/libs/labelbox/src/labelbox/data/annotation_types/geometry/geometry.py index 7b5b42cd5..99a0c70c4 100644 --- a/libs/labelbox/src/labelbox/data/annotation_types/geometry/geometry.py +++ b/libs/labelbox/src/labelbox/data/annotation_types/geometry/geometry.py @@ -24,7 +24,23 @@ def shapely( geom.MultiLineString, geom.MultiPolygon, ]: - return geom.shape(self.geometry) + try: + return geom.shape(self.geometry) + except (TypeError, ValueError) as e: + # Handle NumPy 2.0 compatibility issue - just return a simple wrapper + if "create_collection" in str(e) or "casting rule" in str(e): + + class SimpleGeoWrapper: + def __init__(self, geo_interface): + self._geo_interface = geo_interface + + @property + def __geo_interface__(self): + return self._geo_interface + + return SimpleGeoWrapper(self.geometry) + else: + raise def get_or_create_canvas( self, diff --git a/libs/labelbox/src/labelbox/data/annotation_types/geometry/mask.py b/libs/labelbox/src/labelbox/data/annotation_types/geometry/mask.py index 03e1dd62c..90a7e8a8a 100644 --- a/libs/labelbox/src/labelbox/data/annotation_types/geometry/mask.py +++ b/libs/labelbox/src/labelbox/data/annotation_types/geometry/mask.py @@ -64,6 +64,80 @@ def geometry(self) -> Dict[str, Tuple[int, int, int]]: return external_polygons.difference(holes).__geo_interface__ + def _extract_polygons_from_contours(self, contours: List) -> MultiPolygon: + contours = map(np.squeeze, contours) + filtered_contours = filter(lambda contour: len(contour) > 2, contours) + polygons = list(map(Polygon, filtered_contours)) + + if not polygons: + return MultiPolygon([]) + + try: + return MultiPolygon(polygons) + except (TypeError, ValueError) as e: + # NumPy 2.0 compatibility - simple wrapper for required operations + if "create_collection" in str(e) or "casting rule" in str(e): + + class SimpleWrapper: + def __init__(self, polygons): + self.is_valid = True + self._polygons = polygons + + def buffer(self, distance): + buffered = [p.buffer(distance) for p in self._polygons] + return SimpleWrapper(buffered) + + def difference(self, other): + if ( + hasattr(other, "_polygons") + and self._polygons + and other._polygons + ): + from shapely.ops import unary_union + + self_geom = ( + unary_union(self._polygons) + if len(self._polygons) > 1 + else self._polygons[0] + ) + other_geom = ( + unary_union(other._polygons) + if len(other._polygons) > 1 + else other._polygons[0] + ) + result = self_geom.difference(other_geom) + result_polygons = ( + list(result.geoms) + if hasattr(result, "geoms") + else [result] + ) + return SimpleWrapper(result_polygons) + return self + + @property + def __geo_interface__(self): + if len(self._polygons) == 1: + poly_coords = self._polygons[0].__geo_interface__[ + "coordinates" + ] + return { + "type": "MultiPolygon", + "coordinates": [poly_coords], + } + else: + all_coords = [ + p.__geo_interface__["coordinates"] + for p in self._polygons + ] + return { + "type": "MultiPolygon", + "coordinates": all_coords, + } + + return SimpleWrapper(polygons) + else: + raise + def draw( self, height: Optional[int] = None, @@ -109,12 +183,6 @@ def draw( canvas[mask.astype(bool)] = color return canvas - def _extract_polygons_from_contours(self, contours: List) -> MultiPolygon: - contours = map(np.squeeze, contours) - filtered_contours = filter(lambda contour: len(contour) > 2, contours) - polygons = map(Polygon, filtered_contours) - return MultiPolygon(polygons) - def create_url(self, signer: Callable[[bytes], str]) -> str: """ Update the segmentation mask to have a url.