Skip to content

Commit 90063da

Browse files
authored
Merge pull request #30 from will-moore/handle_masks
Support opening of masks
2 parents e413cc9 + 9f7f878 commit 90063da

File tree

1 file changed

+48
-4
lines changed

1 file changed

+48
-4
lines changed

ome_zarr.py

+48-4
Original file line numberDiff line numberDiff line change
@@ -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

223269
class 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-
235280
class 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-
253297
def info(path):
254298
"""
255299
print information about the ome-zarr fileset

0 commit comments

Comments
 (0)