Skip to content

Commit fe4520c

Browse files
committed
Add coordinate format normalization for Mask geometry
1 parent 50e045f commit fe4520c

File tree

1 file changed

+85
-1
lines changed
  • libs/labelbox/src/labelbox/data/annotation_types/geometry

1 file changed

+85
-1
lines changed

libs/labelbox/src/labelbox/data/annotation_types/geometry/mask.py

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,29 @@
44
import numpy as np
55
from pydantic import field_validator
66
from shapely.geometry import MultiPolygon, Polygon
7+
from shapely import geometry as geom
78

89
from ..data import MaskData
910
from .geometry import Geometry
1011

1112

13+
class NormalizedShapelyWrapper:
14+
"""Wrapper for shapely objects that normalizes coordinate format in __geo_interface__"""
15+
16+
def __init__(self, shapely_obj, normalizer_func):
17+
self._shapely_obj = shapely_obj
18+
self._normalizer_func = normalizer_func
19+
20+
def __getattr__(self, name):
21+
"""Delegate all attributes to the wrapped shapely object"""
22+
return getattr(self._shapely_obj, name)
23+
24+
@property
25+
def __geo_interface__(self):
26+
"""Return normalized coordinates"""
27+
return self._normalizer_func(self._shapely_obj.__geo_interface__)
28+
29+
1230
class Mask(Geometry):
1331
"""Mask used to represent a single class in a larger segmentation mask
1432
@@ -35,8 +53,71 @@ class Mask(Geometry):
3553
mask: MaskData
3654
color: Union[Tuple[int, int, int], int]
3755

56+
def _normalize_coordinates(self, geometry_dict: Dict) -> Dict:
57+
"""
58+
Normalize coordinate format to match expected GeoJSON structure.
59+
60+
Ensures that coordinate pairs are tuples (x, y) and coordinate rings are tuples,
61+
but preserves the outer list structure to match GeoJSON specification.
62+
63+
Args:
64+
geometry_dict: GeoJSON-style geometry dictionary
65+
66+
Returns:
67+
Geometry dictionary with coordinates normalized to expected format
68+
"""
69+
70+
def normalize_coord_sequence(coords, level=0):
71+
"""Recursively normalize coordinate sequences"""
72+
if isinstance(coords, (list, tuple)):
73+
if len(coords) == 2 and isinstance(coords[0], (int, float)):
74+
# This is a coordinate pair [x, y] or (x, y) - convert to tuple
75+
return (float(coords[0]), float(coords[1]))
76+
else:
77+
# This is a sequence of coordinates or nested sequences
78+
# For MultiPolygon: preserve outermost list, convert inner structures to tuples
79+
if level == 0:
80+
# Keep outermost as list for GeoJSON compatibility
81+
return [
82+
normalize_coord_sequence(item, level + 1)
83+
for item in coords
84+
]
85+
else:
86+
# Convert inner coordinate rings to tuples
87+
return tuple(
88+
normalize_coord_sequence(item, level + 1)
89+
for item in coords
90+
)
91+
return coords
92+
93+
if "coordinates" in geometry_dict:
94+
geometry_dict = geometry_dict.copy()
95+
geometry_dict["coordinates"] = normalize_coord_sequence(
96+
geometry_dict["coordinates"]
97+
)
98+
99+
return geometry_dict
100+
101+
@property
102+
def shapely(
103+
self,
104+
) -> Union[
105+
geom.Point,
106+
geom.LineString,
107+
geom.Polygon,
108+
geom.MultiPoint,
109+
geom.MultiLineString,
110+
geom.MultiPolygon,
111+
]:
112+
"""Override shapely property to ensure normalized coordinates via wrapper"""
113+
original_shapely = geom.shape(self.geometry)
114+
return NormalizedShapelyWrapper(
115+
original_shapely, self._normalize_coordinates
116+
)
117+
38118
@property
39119
def geometry(self) -> Dict[str, Tuple[int, int, int]]:
120+
# Extract mask contours and build geometry
40121
mask = self.draw(color=1)
41122
contours, hierarchy = cv2.findContours(
42123
image=mask, mode=cv2.RETR_TREE, method=cv2.CHAIN_APPROX_NONE
@@ -62,7 +143,10 @@ def geometry(self) -> Dict[str, Tuple[int, int, int]]:
62143
if not holes.is_valid:
63144
holes = holes.buffer(0)
64145

65-
return external_polygons.difference(holes).__geo_interface__
146+
# Get geometry from shapely and normalize coordinates for consistency
147+
# This ensures customers always get list format regardless of shapely version
148+
geometry_dict = external_polygons.difference(holes).__geo_interface__
149+
return self._normalize_coordinates(geometry_dict)
66150

67151
def draw(
68152
self,

0 commit comments

Comments
 (0)