Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support opening of masks #30

Merged
merged 6 commits into from
Jul 10, 2020
Merged
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 47 additions & 4 deletions ome_zarr.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +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")

Expand All @@ -110,6 +124,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."""

Expand All @@ -118,12 +136,22 @@ 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}")
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 = {}
Expand Down Expand Up @@ -191,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
Expand Down Expand Up @@ -219,6 +246,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,:,:,:]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be handled on the napari side, instead of in this library?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah interesting. There does seem to be a limitation here in napari if you have multiple (image) channels along with a mask for each one. In the near(ish) future, we'll have layer "groups" that would let you associate each labels layer with a given image layer... but for now, you basically have no choice but to have nC x 2 unassociated layers in napari if you have a mask for every image channel.

So for here, doing data[:, 0, :, :, :] will make sure you only have a single labels layer, but will obviously omit some data. So you could return a list of [(data[:,n,:,:,:],meta,'labels') for n in range(nChannels)] if you want to (and, until we provide layer groups, give them names that indicate which image layer they go with?)

lastly, singleton dimensions are handled well in napari, so if nC == 1, you wouldn't need to squeeze it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without that line, the masks shape is 5D: (40, 1, 31, 256, 256) whereas the Image channels have 4D shape (40, 31, 256, 256), so you get a mismatch of dimensions (masks and Image have independent T sliders):

Screenshot 2020-07-07 at 15 50 13

In 9f7f878 I've split mask channels into layers, as suggested by @tlambert03 - Thanks.

masks.append((data, {'name': name, 'color': colors}, 'labels'))
return masks


class LocalZarr(BaseZarr):

Expand All @@ -231,7 +276,6 @@ def get_json(self, subpath):
with open(filename) as f:
return json.loads(f.read())


class RemoteZarr(BaseZarr):

def get_json(self, subpath):
Expand All @@ -249,7 +293,6 @@ def get_json(self, subpath):
LOGGER.error(f"({rsp.status_code}): {rsp.text}")
return {}


def info(path):
"""
print information about the ome-zarr fileset
Expand Down