4
4
import numpy as np
5
5
from pydantic import field_validator
6
6
from shapely .geometry import MultiPolygon , Polygon
7
+ from shapely import geometry as geom
7
8
8
9
from ..data import MaskData
9
10
from .geometry import Geometry
10
11
11
12
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
+
12
30
class Mask (Geometry ):
13
31
"""Mask used to represent a single class in a larger segmentation mask
14
32
@@ -35,8 +53,71 @@ class Mask(Geometry):
35
53
mask : MaskData
36
54
color : Union [Tuple [int , int , int ], int ]
37
55
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
+
38
118
@property
39
119
def geometry (self ) -> Dict [str , Tuple [int , int , int ]]:
120
+ # Extract mask contours and build geometry
40
121
mask = self .draw (color = 1 )
41
122
contours , hierarchy = cv2 .findContours (
42
123
image = mask , mode = cv2 .RETR_TREE , method = cv2 .CHAIN_APPROX_NONE
@@ -62,7 +143,10 @@ def geometry(self) -> Dict[str, Tuple[int, int, int]]:
62
143
if not holes .is_valid :
63
144
holes = holes .buffer (0 )
64
145
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 )
66
150
67
151
def draw (
68
152
self ,
0 commit comments