@@ -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