Skip to content
Snippets Groups Projects
Commit 9942e931 authored by Christopher Randolph Rhodes's avatar Christopher Randolph Rhodes
Browse files

zmask docstring, test coverage of expand_box_by param

parent 2bd47356
No related branches found
No related tags found
No related merge requests found
...@@ -25,12 +25,13 @@ class TestZStackDerivedDataProducts(unittest.TestCase): ...@@ -25,12 +25,13 @@ class TestZStackDerivedDataProducts(unittest.TestCase):
self.obmap = InMemoryDataAccessor(self.pxmap.data > pipeline_params['threshold']) self.obmap = InMemoryDataAccessor(self.pxmap.data > pipeline_params['threshold'])
# write_accessor_data_to_file(output_path / 'obmap.tif', self.obmap) # write_accessor_data_to_file(output_path / 'obmap.tif', self.obmap)
def test_zmask_makes_correct_boxes(self, mask_type='boxes', filters=None): def test_zmask_makes_correct_boxes(self, mask_type='boxes', filters=None, expand_box_by=None):
zmask, meta = build_zmask_from_object_mask( zmask, meta = build_zmask_from_object_mask(
self.obmap.get_one_channel_data(0), self.obmap.get_one_channel_data(0),
self.stack.get_one_channel_data(0), self.stack.get_one_channel_data(0),
mask_type=mask_type, mask_type=mask_type,
filters=filters, filters=filters,
expand_box_by=expand_box_by,
) )
zmask_acc = InMemoryDataAccessor(zmask) zmask_acc = InMemoryDataAccessor(zmask)
self.assertTrue(zmask_acc.is_mask()) self.assertTrue(zmask_acc.is_mask())
...@@ -54,4 +55,7 @@ class TestZStackDerivedDataProducts(unittest.TestCase): ...@@ -54,4 +55,7 @@ class TestZStackDerivedDataProducts(unittest.TestCase):
return self.test_zmask_makes_correct_boxes(mask_type='contours') return self.test_zmask_makes_correct_boxes(mask_type='contours')
def test_zmask_makes_correct_boxes_with_filters(self): def test_zmask_makes_correct_boxes_with_filters(self):
return self.test_zmask_makes_correct_boxes(filters={'area': (1e3, 1e4)}) return self.test_zmask_makes_correct_boxes(filters={'area': (1e3, 1e4)})
\ No newline at end of file
def test_zmask_makes_correct_expanded_boxes(self):
return self.test_zmask_makes_correct_boxes(expand_box_by=(64, 2))
\ No newline at end of file
...@@ -8,27 +8,40 @@ from model_server.accessors import GenericImageDataAccessor ...@@ -8,27 +8,40 @@ from model_server.accessors import GenericImageDataAccessor
# build a single boolean 3d mask (objects v. bboxes) and return bounding boxes # build a single boolean 3d mask (objects v. bboxes) and return bounding boxes
def build_zmask_from_object_mask( def build_zmask_from_object_mask(
obmask: GenericImageDataAccessor, obmask: GenericImageDataAccessor,
stack: GenericImageDataAccessor, zstack: GenericImageDataAccessor,
filters=None, filters=None,
mask_type='contour', mask_type='contour',
expand_box_by=(0, 0) expand_box_by=(0, 0),
): ):
""" """
Given a 2D Given a 2D mask of objects, build a 3D mask, where each object's z-position is determined by the index of
filters: dict of (min, max) tuples maximum intensity in z. Return this zmask and a list of each object's meta information.
expand_box_by: (xy, z) pixelsf :param obmask: GenericImageDataAccessor monochrome 2D inary mask of objects
:param zstack: GenericImageDataAccessor monochrome zstack of same Y, X dimension as obmask
:param filters: dictionary of form {attribute: (min, max)}; valid attributes are 'area' and 'solidity'
:param mask_type: if 'boxes', zmask is True in each object's complete bounding box; otherwise 'contours'
:param expand_box_by: (xy, z) expands bounding box by (xy, z) pixels except where this hits a boundary
:return: tuple (zmask, meta)
np.ndarray zmask: boolean mask of same size as stack
meta: List containing one Dict per object, with keys:
info: object's properties from skimage.measure.regionprops_table, including bounding box (y0, y1, x0, x1)
slice: named slice (np.s_) of (optionally) expanded bounding box
relative_bounding_box: bounding box (y0, y1, x0, x1) in relative frame of (optionally) expanded bounding box
contour: object's contour returned by skimage.measure.find_contours
mask: mask of object in relative frame of (optionally) expanded bounding box
""" """
# validate inputs # validate inputs
assert stack.chroma == 1 assert zstack.chroma == 1
assert stack.shape_dict['Z'] > 1 assert zstack.nz > 1
assert mask_type in ('contours', 'boxes'), mask_type assert mask_type in ('contours', 'boxes'), mask_type
assert obmask.is_mask() assert obmask.is_mask()
assert obmask.chroma == 1 assert obmask.chroma == 1
assert obmask.shape_dict['Z'] == 1 assert obmask.nz == 1
lamap = label(obmask.data[:, :, 0, 0]) assert zstack.hw == obmask.hw
# build object query # assign object labels and build object query
lamap = label(obmask.data[:, :, 0, 0])
query_str = 'label > 0' # always true query_str = 'label > 0' # always true
if filters is not None: if filters is not None:
for k in filters.keys(): for k in filters.keys():
...@@ -38,7 +51,7 @@ def build_zmask_from_object_mask( ...@@ -38,7 +51,7 @@ def build_zmask_from_object_mask(
query_str = query_str + f' & {k} > {vmin} & {k} < {vmax}' query_str = query_str + f' & {k} > {vmin} & {k} < {vmax}'
# build dataframe of objects, assign z index to each object # build dataframe of objects, assign z index to each object
argmax = stack.data.argmax(axis=3, keepdims=True)[:, :, 0, 0] argmax = zstack.data.argmax(axis=3, keepdims=True)[:, :, 0, 0]
df = ( df = (
pd.DataFrame( pd.DataFrame(
regionprops_table( regionprops_table(
...@@ -63,11 +76,11 @@ def build_zmask_from_object_mask( ...@@ -63,11 +76,11 @@ def build_zmask_from_object_mask(
lut = np.zeros(lamap.max() + 1) - 1 lut = np.zeros(lamap.max() + 1) - 1
lut[df.label] = df.zi lut[df.label] = df.zi
# convert bounding boxes to slices # convert bounding boxes to numpy slice objects
ebxy, ebz = expand_box_by ebxy, ebz = expand_box_by
h, w, c, nz = stack.shape h, w, c, nz = zstack.shape
boxes = [] meta = []
for ob in df.itertuples(name='LabeledObject'): for ob in df.itertuples(name='LabeledObject'):
y0 = max(ob.y0 - ebxy, 0) y0 = max(ob.y0 - ebxy, 0)
y1 = min(ob.y1 + ebxy, h - 1) y1 = min(ob.y1 + ebxy, h - 1)
...@@ -84,7 +97,6 @@ def build_zmask_from_object_mask( ...@@ -84,7 +97,6 @@ def build_zmask_from_object_mask(
'x1': ob.x1 - x0, 'x1': ob.x1 - x0,
} }
# sl = np.s_[z0: z1 + 1, y0: y1, x0: x1]
sl = np.s_[y0: y1, x0: x1, 0, z0: z1 + 1] sl = np.s_[y0: y1, x0: x1, 0, z0: z1 + 1]
# compute contours # compute contours
...@@ -92,7 +104,7 @@ def build_zmask_from_object_mask( ...@@ -92,7 +104,7 @@ def build_zmask_from_object_mask(
contour = find_contours(obmask) contour = find_contours(obmask)
mask = obmask[ob.y0: ob.y1, ob.x0: ob.x1] mask = obmask[ob.y0: ob.y1, ob.x0: ob.x1]
boxes.append({ meta.append({
'info': ob, 'info': ob,
'slice': sl, 'slice': sl,
'relative_bounding_box': rbb, 'relative_bounding_box': rbb,
...@@ -101,7 +113,7 @@ def build_zmask_from_object_mask( ...@@ -101,7 +113,7 @@ def build_zmask_from_object_mask(
}) })
# build mask z-stack # build mask z-stack
zi_st = np.zeros(stack.shape, dtype='bool') zi_st = np.zeros(zstack.shape, dtype='bool')
if mask_type == 'contours': if mask_type == 'contours':
zi_map = (lut[lamap] + 1.0).astype('int') zi_map = (lut[lamap] + 1.0).astype('int')
idxs = np.array(zi_map) - 1 idxs = np.array(zi_map) - 1
...@@ -116,8 +128,8 @@ def build_zmask_from_object_mask( ...@@ -116,8 +128,8 @@ def build_zmask_from_object_mask(
zi_st[:, :, :, -1][lamap == 0] = 0 zi_st[:, :, :, -1][lamap == 0] = 0
elif mask_type == 'boxes': elif mask_type == 'boxes':
for bb in boxes: for bb in meta:
sl = bb['slice'] sl = bb['slice']
zi_st[sl] = 1 zi_st[sl] = 1
return zi_st, boxes return zi_st, meta
\ No newline at end of file \ No newline at end of file
...@@ -39,6 +39,22 @@ class GenericImageDataAccessor(ABC): ...@@ -39,6 +39,22 @@ class GenericImageDataAccessor(ABC):
c = int(channel) c = int(channel)
return InMemoryDataAccessor(self.data[:, :, c:(c+1), :]) return InMemoryDataAccessor(self.data[:, :, c:(c+1), :])
@property
def dtype(self):
return self.data.dtype
@property
def hw(self):
"""
Get data height and width as a tuple
:return: tuple of (Y, X) dimensions
"""
return self.shape_dict['Y'], self.shape_dict['X']
@property
def nz(self):
return self.shape_dict['Z']
@property @property
def data(self): def data(self):
""" """
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment