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

Add roi utils for creating masks #18

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
6 changes: 5 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,11 @@ def read(fname):

setup(
version=version,
packages=['', 'omero.plugins'],
packages=[
'',
'omero.plugins',
'omero_metadata',
],
package_dir={"": "src"},
name='omero-metadata',
description="Metadata plugin for use in the OMERO CLI.",
Expand Down
12 changes: 12 additions & 0 deletions src/omero_metadata/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from rois import (
NoMaskFound,
InvalidBinaryImage,
mask_from_binary_image,
masks_from_label_image,
)
__all__ = (
'NoMaskFound',
'InvalidBinaryImage',
'mask_from_binary_image',
'masks_from_label_image',
)
118 changes: 118 additions & 0 deletions src/omero_metadata/rois.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
ROI utils

Copyright (C) 2019 University of Dundee. All rights reserved.
Use is subject to license terms supplied in LICENSE.txt
"""
import numpy as np
from omero.gateway import ColorHolder
from omero.model import MaskI
from omero.rtypes import (
rdouble,
rint,
rstring,
)


class NoMaskFound(ValueError):
"""
Exception thrown when no foreground pixels were found in a mask
"""
def __init__(self, msg='No mask found'):
super(Exception, self).__init__(msg)


class InvalidBinaryImage(ValueError):
"""
Exception thrown when invalid labels are found
"""
def __init__(self, msg='Invalid labels found'):
super(Exception, self).__init__(msg)


def mask_from_binary_image(
binim, rgba=None, z=None, c=None, t=None, text=None,
raise_on_no_mask=True):
"""
Create a mask shape from a binary image (background=0)
:param numpy.array binim: Binary 2D array, must contain values [0, 1] only
:param rgba int-4-tuple: Optional (red, green, blue, alpha) colour
:param z: Optional Z-index for the mask
:param c: Optional C-index for the mask
:param t: Optional T-index for the mask
:param text: Optional text for the mask
:param raise_on_no_mask: If True (default) throw an exception if no mask
found, otherwise return an empty Mask
Copy link
Member Author

Choose a reason for hiding this comment

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

Not sure what the default should be for raise_on_no_mask.

Copy link
Member

Choose a reason for hiding this comment

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

If I read correctly the code, no mask means no non-zero pixels found. Depending on the use case, I can certainly see two behaviors:

  • return an empty Mask
  • return None or throw an exception

The former call is more friendly to the caller of this API as it ensures that a mask is always returned. However from an IDR consumer perspective, I am not sure that there is value in storing an empty Mask rather than no mask. Given this API already has some logic to find bounding boxes, I think raising exception by default but having the possiblity to change the behavior is totally acceptable.

:return: An OMERO mask
:raises NoMaskFound: If no labels were found
:raises InvalidBinaryImage: If the maximum labels is greater than 1
"""
# Find bounding box to minimise size of mask
xmask = binim.sum(0).nonzero()[0]
ymask = binim.sum(1).nonzero()[0]
if any(xmask) and any(ymask):
x0 = min(xmask)
w = max(xmask) - x0 + 1
y0 = min(ymask)
h = max(ymask) - y0 + 1
submask = binim[y0:(y0 + h), x0:(x0 + w)]
if not np.array_equal(np.unique(submask), [0, 1]):
raise InvalidBinaryImage()
else:
if raise_on_no_mask:
raise NoMaskFound()
x0 = 0
w = 0
y0 = 0
h = 0
submask = []

mask = MaskI()
# BUG in older versions of Numpy:
# https://github.com/numpy/numpy/issues/5377
# Need to convert to an int array
# mask.setBytes(np.packbits(submask))
mask.setBytes(np.packbits(np.asarray(submask, dtype=int)))
mask.setWidth(rdouble(w))
mask.setHeight(rdouble(h))
mask.setX(rdouble(x0))
mask.setY(rdouble(y0))

if rgba is not None:
ch = ColorHolder.fromRGBA(*rgba)
mask.setFillColor(rint(ch.getInt()))
if z is not None:
mask.setTheZ(rint(z))
if c is not None:
mask.setTheC(rint(c))
if t is not None:
mask.setTheT(rint(t))
if text is not None:
mask.setTextValue(rstring(text))

return mask


def masks_from_label_image(
labelim, rgba=None, z=None, c=None, t=None, text=None,
raise_on_no_mask=True):
"""
Create mask shapes from a label image (background=0)
:param numpy.array labelim: 2D label array
:param rgba int-4-tuple: Optional (red, green, blue, alpha) colour
:param z: Optional Z-index for the mask
:param c: Optional C-index for the mask
:param t: Optional T-index for the mask
:param text: Optional text for the mask
:param raise_on_no_mask: If True (default) throw an exception if no mask
found, otherwise return an empty Mask
:return: A list of OMERO masks in label order ([] if no labels found)
"""
masks = []
for i in xrange(1, labelim.max() + 1):
mask = mask_from_binary_image(labelim == i, rgba, z, c, t, text,
raise_on_no_mask)
masks.append(mask)
return masks
135 changes: 135 additions & 0 deletions test/unit/test_masks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Test of ROI mask utils

Copyright (C) 2019 University of Dundee. All rights reserved.
Use is subject to license terms supplied in LICENSE.txt
"""
from omero.rtypes import unwrap
import numpy as np
import pytest

from omero_metadata import (
mask_from_binary_image,
masks_from_label_image,
NoMaskFound,
)


@pytest.fixture
def binary_image():
return np.array([
[0, 0, 0, 0],
[0, 1, 1, 0],
[0, 1, 0, 0],
[0, 0, 0, 0],
])


@pytest.fixture
def label_image():
return np.array([
[0, 0, 0, 2],
[0, 1, 1, 0],
[0, 1, 2, 0],
[0, 0, 0, 0],
])


class TestMaskUtils(object):

@pytest.mark.parametrize('args', [
{},
{'rgba': (255, 128, 64, 128), 'z': 1, 'c': 2, 't': 3, 'text': 'test'}
])
def test_mask_from_binary_image(self, binary_image, args):
mask = mask_from_binary_image(binary_image, **args)

assert unwrap(mask.getWidth()) == 2
assert unwrap(mask.getHeight()) == 2
assert unwrap(mask.getX()) == 1
assert unwrap(mask.getY()) == 1

assert np.array_equal(mask.getBytes(), np.array([224], dtype=np.uint8))

if args:
assert unwrap(mask.getTheZ()) == 1
assert unwrap(mask.getTheC()) == 2
assert unwrap(mask.getTheT()) == 3
assert unwrap(mask.getTextValue()) == 'test'
else:
assert unwrap(mask.getTheZ()) is None
assert unwrap(mask.getTheC()) is None
assert unwrap(mask.getTheT()) is None
assert unwrap(mask.getTextValue()) is None

@pytest.mark.parametrize('args', [
{},
{'rgba': (255, 128, 64, 128), 'z': 1, 'c': 2, 't': 3, 'text': 'test'}
])
def test_masks_from_label_image(self, label_image, args):
masks = masks_from_label_image(label_image, **args)
expected = (
# w, h, x, y, bytes
(2, 2, 1, 1, np.array([224], dtype=np.uint8)),
(2, 3, 2, 0, np.array([72], dtype=np.uint8)),
)

assert len(masks) == 2

for i, mask in enumerate(masks):
assert unwrap(mask.getWidth()) == expected[i][0]
assert unwrap(mask.getHeight()) == expected[i][1]
assert unwrap(mask.getX()) == expected[i][2]
assert unwrap(mask.getY()) == expected[i][3]

assert np.array_equal(mask.getBytes(), expected[i][4])

if args:
assert unwrap(mask.getTheZ()) == 1
assert unwrap(mask.getTheC()) == 2
assert unwrap(mask.getTheT()) == 3
assert unwrap(mask.getTextValue()) == 'test'
else:
assert unwrap(mask.getTheZ()) is None
assert unwrap(mask.getTheC()) is None
assert unwrap(mask.getTheT()) is None
assert unwrap(mask.getTextValue()) is None

@pytest.mark.parametrize('args', [
{},
{'rgba': (255, 128, 64, 128), 'z': 1, 'c': 2, 't': 3, 'text': 'test'}
])
@pytest.mark.parametrize('raise_on_no_mask', [
True,
False,
])
def test_empty_mask_from_binary_image(self, args, raise_on_no_mask):
empty_binary_image = np.array([[0]])
if raise_on_no_mask:
with pytest.raises(NoMaskFound):
mask = mask_from_binary_image(
empty_binary_image, raise_on_no_mask=raise_on_no_mask,
**args)
else:
mask = mask_from_binary_image(
empty_binary_image, raise_on_no_mask=raise_on_no_mask,
**args)
assert unwrap(mask.getWidth()) == 0
assert unwrap(mask.getHeight()) == 0
assert unwrap(mask.getX()) == 0
assert unwrap(mask.getY()) == 0
assert np.array_equal(mask.getBytes(), [])

if args:
assert unwrap(mask.getTheZ()) == 1
assert unwrap(mask.getTheC()) == 2
assert unwrap(mask.getTheT()) == 3
assert unwrap(mask.getTextValue()) == 'test'
else:
assert unwrap(mask.getTheZ()) is None
assert unwrap(mask.getTheC()) is None
assert unwrap(mask.getTheT()) is None
assert unwrap(mask.getTextValue()) is None