@@ -37,6 +37,7 @@ class Mask(Geometry):
37
37
38
38
@property
39
39
def geometry (self ) -> Dict [str , Tuple [int , int , int ]]:
40
+ # Extract mask contours and build geometry
40
41
mask = self .draw (color = 1 )
41
42
contours , hierarchy = cv2 .findContours (
42
43
image = mask , mode = cv2 .RETR_TREE , method = cv2 .CHAIN_APPROX_NONE
@@ -62,7 +63,74 @@ def geometry(self) -> Dict[str, Tuple[int, int, int]]:
62
63
if not holes .is_valid :
63
64
holes = holes .buffer (0 )
64
65
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
66
134
67
135
def draw (
68
136
self ,
0 commit comments