@@ -102,6 +102,20 @@ def is_zarr(self):
102102 def is_ome_zarr (self ):
103103 return self .zgroup and "multiscales" in self .root_attrs
104104
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+
105119 def get_json (self , subpath ):
106120 raise NotImplementedError ("unknown" )
107121
@@ -110,6 +124,10 @@ def get_reader_function(self):
110124 raise Exception (f"not a zarr: { self } " )
111125 return self .reader_function
112126
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+
113131 def reader_function (self , path : PathLike ) -> List [LayerData ]:
114132 """Take a path or list of paths and return a list of LayerData tuples."""
115133
@@ -118,12 +136,22 @@ def reader_function(self, path: PathLike) -> List[LayerData]:
118136 # TODO: safe to ignore this path?
119137
120138 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
122147
123148 elif self .zarray :
124149 data = da .from_zarr (f"{ self .zarr_path } " )
125150 return [(data ,)]
126151
152+ elif self .is_ome_mask ():
153+ return self .load_ome_masks ()
154+
127155 def load_omero_metadata (self , assert_channel_count = None ):
128156 """Load OMERO metadata as json and convert for napari"""
129157 metadata = {}
@@ -191,7 +219,6 @@ def load_omero_metadata(self, assert_channel_count=None):
191219
192220 return metadata
193221
194-
195222 def load_ome_zarr (self ):
196223
197224 resolutions = ["0" ] # TODO: could be first alphanumeric dataset on err
@@ -219,6 +246,25 @@ def load_ome_zarr(self):
219246 return (pyramid , {'channel_axis' : 1 , ** metadata })
220247
221248
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+
222268
223269class LocalZarr (BaseZarr ):
224270
@@ -231,7 +277,6 @@ def get_json(self, subpath):
231277 with open (filename ) as f :
232278 return json .loads (f .read ())
233279
234-
235280class RemoteZarr (BaseZarr ):
236281
237282 def get_json (self , subpath ):
@@ -249,7 +294,6 @@ def get_json(self, subpath):
249294 LOGGER .error (f"({ rsp .status_code } ): { rsp .text } " )
250295 return {}
251296
252-
253297def info (path ):
254298 """
255299 print information about the ome-zarr fileset
0 commit comments