From 8877bf8beeb76105779a1087fb9c13d4fe4d3ff3 Mon Sep 17 00:00:00 2001 From: William Moore Date: Fri, 3 Jul 2020 16:50:14 +0100 Subject: [PATCH 1/5] Support opening of local masks --- ome_zarr.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/ome_zarr.py b/ome_zarr.py index a18307b1..4def04b8 100644 --- a/ome_zarr.py +++ b/ome_zarr.py @@ -102,6 +102,9 @@ def is_zarr(self): def is_ome_zarr(self): return self.zgroup and "multiscales" in self.root_attrs + def is_ome_mask(self): + return self.zarr_path.endswith('masks/') and self.get_json('.zgroup') + def get_json(self, subpath): raise NotImplementedError("unknown") @@ -110,6 +113,10 @@ def get_reader_function(self): raise Exception(f"not a zarr: {self}") return self.reader_function + def to_rgba(self, v): + """Get rgba (0-1) e.g. (1, 0.5, 0, 1) from integer""" + return [x/255 for x in v.to_bytes(4, signed=True, byteorder='big')] + def reader_function(self, path: PathLike) -> List[LayerData]: """Take a path or list of paths and return a list of LayerData tuples.""" @@ -124,6 +131,9 @@ def reader_function(self, path: PathLike) -> List[LayerData]: data = da.from_zarr(f"{self.zarr_path}") return [(data,)] + elif self.is_ome_mask(): + return self.load_ome_masks() + def load_omero_metadata(self, assert_channel_count=None): """Load OMERO metadata as json and convert for napari""" metadata = {} @@ -219,6 +229,24 @@ def load_ome_zarr(self): return (pyramid, {'channel_axis': 1, **metadata}) + def load_ome_masks(self): + # look for masks in this dir... + mask_names = self.get_mask_names() + masks = [] + for name in mask_names: + mask_path = os.path.join(self.zarr_path, name) + mask_attrs = self.get_json(f'{name}/.zattrs') + colors = {} + if 'color' in mask_attrs: + color_dict = mask_attrs.get('color') + colors = {int(k):self.to_rgba(v) for (k, v) in color_dict.items()} + data = da.from_zarr(mask_path) + # mask data is 5D (t, c, z, y, x) but each layer in napari is 4D (no C) + # NB: Assume we want 'first Channel' + data = data[:,0,:,:,:] + masks.append((data, {'name': name, 'color': colors}, 'labels')) + return masks + class LocalZarr(BaseZarr): @@ -231,6 +259,10 @@ def get_json(self, subpath): with open(filename) as f: return json.loads(f.read()) + def get_mask_names(self): + dirnames = os.listdir(self.zarr_path) + dirnames = [name for name in dirnames if os.path.isdir(os.path.join(self.zarr_path, name))] + return dirnames class RemoteZarr(BaseZarr): @@ -249,6 +281,10 @@ def get_json(self, subpath): LOGGER.error(f"({rsp.status_code}): {rsp.text}") return {} + def get_mask_names(self): + # TODO: find mask dirs remotely + return [] + def info(path): """ From 61f3a8f2d1458147d9c0d888d98025048e6d200f Mon Sep 17 00:00:00 2001 From: William Moore Date: Mon, 6 Jul 2020 15:52:43 +0100 Subject: [PATCH 2/5] Read zarr.1/masks when opening image in napari --- ome_zarr.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/ome_zarr.py b/ome_zarr.py index 4def04b8..e9de55fb 100644 --- a/ome_zarr.py +++ b/ome_zarr.py @@ -125,7 +125,14 @@ def reader_function(self, path: PathLike) -> List[LayerData]: # TODO: safe to ignore this path? if self.is_ome_zarr(): - return [self.load_ome_zarr()] + layers = [self.load_ome_zarr()] + # If the Image contains masks... + if self.has_ome_masks(): + mask_path = os.path.join(self.zarr_path, 'masks') + # Create a new OME Zarr Reader to load masks + masks = self.__class__(mask_path).reader_function(None) + layers.extend(masks) + return layers elif self.zarray: data = da.from_zarr(f"{self.zarr_path}") @@ -259,9 +266,15 @@ def get_json(self, subpath): with open(filename) as f: return json.loads(f.read()) + def has_ome_masks(self): + "Does the zarr Image also include /masks sub-dir" + mask_dir = os.path.join(self.zarr_path, 'masks') + return os.path.exists(mask_dir) and os.path.isdir(mask_dir) and self.get_json('masks/.zgroup') + def get_mask_names(self): dirnames = os.listdir(self.zarr_path) - dirnames = [name for name in dirnames if os.path.isdir(os.path.join(self.zarr_path, name))] + dirnames = [name for name in dirnames if os.path.isdir( + os.path.join(self.zarr_path, name))] return dirnames class RemoteZarr(BaseZarr): @@ -285,6 +298,9 @@ def get_mask_names(self): # TODO: find mask dirs remotely return [] + def has_ome_masks(self): + # TODO: check for /masks/ + return False def info(path): """ From e59d92890cd132fa376c3ce62c8433d1b2aebbd4 Mon Sep 17 00:00:00 2001 From: William Moore Date: Mon, 6 Jul 2020 16:52:35 +0100 Subject: [PATCH 3/5] Implement RemoteZarr mask methods --- ome_zarr.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ome_zarr.py b/ome_zarr.py index e9de55fb..7f07d164 100644 --- a/ome_zarr.py +++ b/ome_zarr.py @@ -295,12 +295,17 @@ def get_json(self, subpath): return {} def get_mask_names(self): - # TODO: find mask dirs remotely + # If this is a mask, the names are in root .zattrs + masks = self.root_attrs.get('masks') + if masks is not None: + return masks return [] def has_ome_masks(self): - # TODO: check for /masks/ - return False + # check for /masks/.zattrs with 'masks' key + mask_attrs = self.get_json('masks/.zattrs') + masks = mask_attrs.get('masks') + return masks is not None and len(masks) > 0 def info(path): """ From ec13afdab525664c395c9d244e141ccb71278242 Mon Sep 17 00:00:00 2001 From: jmoore Date: Tue, 7 Jul 2020 10:11:13 +0200 Subject: [PATCH 4/5] masks: unify remote and local handling --- ome_zarr.py | 36 +++++++++++------------------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/ome_zarr.py b/ome_zarr.py index 7f07d164..22a7dd03 100644 --- a/ome_zarr.py +++ b/ome_zarr.py @@ -102,9 +102,20 @@ def is_zarr(self): def is_ome_zarr(self): return self.zgroup and "multiscales" in self.root_attrs + def has_ome_masks(self): + "Does the zarr Image also include /masks sub-dir" + return self.get_json('masks/.zgroup') + def is_ome_mask(self): return self.zarr_path.endswith('masks/') and self.get_json('.zgroup') + def get_mask_names(self): + """ + Called if is_ome_mask is true + """ + # If this is a mask, the names are in root .zattrs + return self.root_attrs.get('masks', []) + def get_json(self, subpath): raise NotImplementedError("unknown") @@ -208,7 +219,6 @@ def load_omero_metadata(self, assert_channel_count=None): return metadata - def load_ome_zarr(self): resolutions = ["0"] # TODO: could be first alphanumeric dataset on err @@ -266,17 +276,6 @@ def get_json(self, subpath): with open(filename) as f: return json.loads(f.read()) - def has_ome_masks(self): - "Does the zarr Image also include /masks sub-dir" - mask_dir = os.path.join(self.zarr_path, 'masks') - return os.path.exists(mask_dir) and os.path.isdir(mask_dir) and self.get_json('masks/.zgroup') - - def get_mask_names(self): - dirnames = os.listdir(self.zarr_path) - dirnames = [name for name in dirnames if os.path.isdir( - os.path.join(self.zarr_path, name))] - return dirnames - class RemoteZarr(BaseZarr): def get_json(self, subpath): @@ -294,19 +293,6 @@ def get_json(self, subpath): LOGGER.error(f"({rsp.status_code}): {rsp.text}") return {} - def get_mask_names(self): - # If this is a mask, the names are in root .zattrs - masks = self.root_attrs.get('masks') - if masks is not None: - return masks - return [] - - def has_ome_masks(self): - # check for /masks/.zattrs with 'masks' key - mask_attrs = self.get_json('masks/.zattrs') - masks = mask_attrs.get('masks') - return masks is not None and len(masks) > 0 - def info(path): """ print information about the ome-zarr fileset From 9f7f878b6a555d217c42b54825ac5c85646549f4 Mon Sep 17 00:00:00 2001 From: William Moore Date: Tue, 7 Jul 2020 16:00:54 +0100 Subject: [PATCH 5/5] Don't just pick first masks Channel. Use all --- ome_zarr.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ome_zarr.py b/ome_zarr.py index 22a7dd03..5af84c08 100644 --- a/ome_zarr.py +++ b/ome_zarr.py @@ -258,10 +258,11 @@ def load_ome_masks(self): color_dict = mask_attrs.get('color') colors = {int(k):self.to_rgba(v) for (k, v) in color_dict.items()} data = da.from_zarr(mask_path) - # mask data is 5D (t, c, z, y, x) but each layer in napari is 4D (no C) - # NB: Assume we want 'first Channel' - data = data[:,0,:,:,:] - masks.append((data, {'name': name, 'color': colors}, 'labels')) + # Split masks into separate channels, 1 per layer + for n in range(data.shape[1]): + masks.append((data[:,n,:,:,:], + {'name': name, 'color': colors}, + 'labels')) return masks