-
Notifications
You must be signed in to change notification settings - Fork 13
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
Closed
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
: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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
.There was a problem hiding this comment.
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:
None
or throw an exceptionThe 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.