@@ -102,6 +102,20 @@ def is_zarr(self):
102
102
def is_ome_zarr (self ):
103
103
return self .zgroup and "multiscales" in self .root_attrs
104
104
105
+ def has_ome_masks (self ):
106
+ "Does the zarr Image also include /masks sub-dir"
107
+ return self .get_json ('masks/.zgroup' )
108
+
109
+ def is_ome_mask (self ):
110
+ return self .zarr_path .endswith ('masks/' ) and self .get_json ('.zgroup' )
111
+
112
+ def get_mask_names (self ):
113
+ """
114
+ Called if is_ome_mask is true
115
+ """
116
+ # If this is a mask, the names are in root .zattrs
117
+ return self .root_attrs .get ('masks' , [])
118
+
105
119
def get_json (self , subpath ):
106
120
raise NotImplementedError ("unknown" )
107
121
@@ -110,6 +124,10 @@ def get_reader_function(self):
110
124
raise Exception (f"not a zarr: { self } " )
111
125
return self .reader_function
112
126
127
+ def to_rgba (self , v ):
128
+ """Get rgba (0-1) e.g. (1, 0.5, 0, 1) from integer"""
129
+ return [x / 255 for x in v .to_bytes (4 , signed = True , byteorder = 'big' )]
130
+
113
131
def reader_function (self , path : PathLike ) -> List [LayerData ]:
114
132
"""Take a path or list of paths and return a list of LayerData tuples."""
115
133
@@ -118,12 +136,22 @@ def reader_function(self, path: PathLike) -> List[LayerData]:
118
136
# TODO: safe to ignore this path?
119
137
120
138
if self .is_ome_zarr ():
121
- return [self .load_ome_zarr ()]
139
+ layers = [self .load_ome_zarr ()]
140
+ # If the Image contains masks...
141
+ if self .has_ome_masks ():
142
+ mask_path = os .path .join (self .zarr_path , 'masks' )
143
+ # Create a new OME Zarr Reader to load masks
144
+ masks = self .__class__ (mask_path ).reader_function (None )
145
+ layers .extend (masks )
146
+ return layers
122
147
123
148
elif self .zarray :
124
149
data = da .from_zarr (f"{ self .zarr_path } " )
125
150
return [(data ,)]
126
151
152
+ elif self .is_ome_mask ():
153
+ return self .load_ome_masks ()
154
+
127
155
def load_omero_metadata (self , assert_channel_count = None ):
128
156
"""Load OMERO metadata as json and convert for napari"""
129
157
metadata = {}
@@ -191,7 +219,6 @@ def load_omero_metadata(self, assert_channel_count=None):
191
219
192
220
return metadata
193
221
194
-
195
222
def load_ome_zarr (self ):
196
223
197
224
resolutions = ["0" ] # TODO: could be first alphanumeric dataset on err
@@ -219,6 +246,25 @@ def load_ome_zarr(self):
219
246
return (pyramid , {'channel_axis' : 1 , ** metadata })
220
247
221
248
249
+ def load_ome_masks (self ):
250
+ # look for masks in this dir...
251
+ mask_names = self .get_mask_names ()
252
+ masks = []
253
+ for name in mask_names :
254
+ mask_path = os .path .join (self .zarr_path , name )
255
+ mask_attrs = self .get_json (f'{ name } /.zattrs' )
256
+ colors = {}
257
+ if 'color' in mask_attrs :
258
+ color_dict = mask_attrs .get ('color' )
259
+ colors = {int (k ):self .to_rgba (v ) for (k , v ) in color_dict .items ()}
260
+ data = da .from_zarr (mask_path )
261
+ # Split masks into separate channels, 1 per layer
262
+ for n in range (data .shape [1 ]):
263
+ masks .append ((data [:,n ,:,:,:],
264
+ {'name' : name , 'color' : colors },
265
+ 'labels' ))
266
+ return masks
267
+
222
268
223
269
class LocalZarr (BaseZarr ):
224
270
@@ -231,7 +277,6 @@ def get_json(self, subpath):
231
277
with open (filename ) as f :
232
278
return json .loads (f .read ())
233
279
234
-
235
280
class RemoteZarr (BaseZarr ):
236
281
237
282
def get_json (self , subpath ):
@@ -249,7 +294,6 @@ def get_json(self, subpath):
249
294
LOGGER .error (f"({ rsp .status_code } ): { rsp .text } " )
250
295
return {}
251
296
252
-
253
297
def info (path ):
254
298
"""
255
299
print information about the ome-zarr fileset
0 commit comments