Skip to content

Commit a56501f

Browse files
authored
Add coordinate format normalization for Mask geometry (#1985)
2 parents cd7af4d + 5761802 commit a56501f

File tree

1 file changed

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

1 file changed

+69
-1
lines changed

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

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class Mask(Geometry):
3737

3838
@property
3939
def geometry(self) -> Dict[str, Tuple[int, int, int]]:
40+
# Extract mask contours and build geometry
4041
mask = self.draw(color=1)
4142
contours, hierarchy = cv2.findContours(
4243
image=mask, mode=cv2.RETR_TREE, method=cv2.CHAIN_APPROX_NONE
@@ -62,7 +63,74 @@ def geometry(self) -> Dict[str, Tuple[int, int, int]]:
6263
if not holes.is_valid:
6364
holes = holes.buffer(0)
6465

65-
return external_polygons.difference(holes).__geo_interface__
66+
# Get geometry result
67+
result_geometry = external_polygons.difference(holes)
68+
69+
# Ensure consistent MultiPolygon format across shapely versions
70+
if (
71+
hasattr(result_geometry, "geom_type")
72+
and result_geometry.geom_type == "Polygon"
73+
):
74+
result_geometry = MultiPolygon([result_geometry])
75+
76+
# Get the geo interface and ensure consistent coordinate format
77+
geometry_dict = result_geometry.__geo_interface__
78+
79+
# Normalize coordinates to ensure deterministic output across platforms
80+
if "coordinates" in geometry_dict:
81+
geometry_dict = self._normalize_polygon_coordinates(geometry_dict)
82+
83+
return geometry_dict
84+
85+
def _normalize_polygon_coordinates(self, geometry_dict):
86+
"""Ensure consistent polygon coordinate format across platforms and shapely versions"""
87+
88+
def clean_ring(ring):
89+
"""Normalize ring coordinates to ensure consistent output across shapely versions"""
90+
if not ring or len(ring) < 3:
91+
return ring
92+
93+
# Convert to tuples
94+
coords = [tuple(float(x) for x in coord) for coord in ring]
95+
96+
# Remove the closing duplicate (last coordinate that equals first)
97+
if len(coords) > 1 and coords[0] == coords[-1]:
98+
coords = coords[:-1]
99+
100+
# Remove any other consecutive duplicates
101+
cleaned = []
102+
for coord in coords:
103+
if not cleaned or cleaned[-1] != coord:
104+
cleaned.append(coord)
105+
106+
# For shapely 2.1.1 compatibility: ensure we start with the minimum coordinate
107+
# to get consistent ring orientation and starting point
108+
if len(cleaned) >= 3:
109+
min_idx = min(range(len(cleaned)), key=lambda i: cleaned[i])
110+
cleaned = cleaned[min_idx:] + cleaned[:min_idx]
111+
112+
# Close the ring properly
113+
if len(cleaned) >= 3:
114+
cleaned.append(cleaned[0])
115+
116+
return cleaned
117+
118+
result = geometry_dict.copy()
119+
if geometry_dict["type"] == "MultiPolygon":
120+
normalized_coords = []
121+
for polygon in geometry_dict["coordinates"]:
122+
normalized_polygon = []
123+
for ring in polygon:
124+
cleaned_ring = clean_ring(ring)
125+
if (
126+
len(cleaned_ring) >= 4
127+
): # Minimum for a valid closed ring
128+
normalized_polygon.append(tuple(cleaned_ring))
129+
if normalized_polygon:
130+
normalized_coords.append(tuple(normalized_polygon))
131+
result["coordinates"] = normalized_coords
132+
133+
return result
66134

67135
def draw(
68136
self,

0 commit comments

Comments
 (0)