diff --git a/extensions/chaeo/test_zstack.py b/extensions/chaeo/test_zstack.py index e423d0c5de352d7791c1774f5cf11e79a1b81731..ebe1172203187b345d9e4b16b35a5f932fd2d3ab 100644 --- a/extensions/chaeo/test_zstack.py +++ b/extensions/chaeo/test_zstack.py @@ -25,12 +25,13 @@ class TestZStackDerivedDataProducts(unittest.TestCase): self.obmap = InMemoryDataAccessor(self.pxmap.data > pipeline_params['threshold']) # 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( self.obmap.get_one_channel_data(0), self.stack.get_one_channel_data(0), mask_type=mask_type, filters=filters, + expand_box_by=expand_box_by, ) zmask_acc = InMemoryDataAccessor(zmask) self.assertTrue(zmask_acc.is_mask()) @@ -54,4 +55,7 @@ class TestZStackDerivedDataProducts(unittest.TestCase): return self.test_zmask_makes_correct_boxes(mask_type='contours') def test_zmask_makes_correct_boxes_with_filters(self): - return self.test_zmask_makes_correct_boxes(filters={'area': (1e3, 1e4)}) \ No newline at end of file + return self.test_zmask_makes_correct_boxes(filters={'area': (1e3, 1e4)}) + + 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 diff --git a/extensions/chaeo/zmask.py b/extensions/chaeo/zmask.py index 0141386b82319c8dfad81d5533b296f7ffa00da4..03188688db74cff9dc168745ac0dbcb2ca61ea42 100644 --- a/extensions/chaeo/zmask.py +++ b/extensions/chaeo/zmask.py @@ -8,27 +8,40 @@ from model_server.accessors import GenericImageDataAccessor # build a single boolean 3d mask (objects v. bboxes) and return bounding boxes def build_zmask_from_object_mask( obmask: GenericImageDataAccessor, - stack: GenericImageDataAccessor, + zstack: GenericImageDataAccessor, filters=None, mask_type='contour', - expand_box_by=(0, 0) + expand_box_by=(0, 0), ): """ - Given a 2D - filters: dict of (min, max) tuples - expand_box_by: (xy, z) pixelsf + Given a 2D mask of objects, build a 3D mask, where each object's z-position is determined by the index of + maximum intensity in z. Return this zmask and a list of each object's meta information. + :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 - assert stack.chroma == 1 - assert stack.shape_dict['Z'] > 1 + assert zstack.chroma == 1 + assert zstack.nz > 1 assert mask_type in ('contours', 'boxes'), mask_type assert obmask.is_mask() assert obmask.chroma == 1 - assert obmask.shape_dict['Z'] == 1 - lamap = label(obmask.data[:, :, 0, 0]) + assert obmask.nz == 1 + 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 if filters is not None: for k in filters.keys(): @@ -38,7 +51,7 @@ def build_zmask_from_object_mask( query_str = query_str + f' & {k} > {vmin} & {k} < {vmax}' # 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 = ( pd.DataFrame( regionprops_table( @@ -63,11 +76,11 @@ def build_zmask_from_object_mask( lut = np.zeros(lamap.max() + 1) - 1 lut[df.label] = df.zi - # convert bounding boxes to slices + # convert bounding boxes to numpy slice objects 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'): y0 = max(ob.y0 - ebxy, 0) y1 = min(ob.y1 + ebxy, h - 1) @@ -84,7 +97,6 @@ def build_zmask_from_object_mask( '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] # compute contours @@ -92,7 +104,7 @@ def build_zmask_from_object_mask( contour = find_contours(obmask) mask = obmask[ob.y0: ob.y1, ob.x0: ob.x1] - boxes.append({ + meta.append({ 'info': ob, 'slice': sl, 'relative_bounding_box': rbb, @@ -101,7 +113,7 @@ def build_zmask_from_object_mask( }) # build mask z-stack - zi_st = np.zeros(stack.shape, dtype='bool') + zi_st = np.zeros(zstack.shape, dtype='bool') if mask_type == 'contours': zi_map = (lut[lamap] + 1.0).astype('int') idxs = np.array(zi_map) - 1 @@ -116,8 +128,8 @@ def build_zmask_from_object_mask( zi_st[:, :, :, -1][lamap == 0] = 0 elif mask_type == 'boxes': - for bb in boxes: + for bb in meta: sl = bb['slice'] zi_st[sl] = 1 - return zi_st, boxes \ No newline at end of file + return zi_st, meta \ No newline at end of file diff --git a/model_server/accessors.py b/model_server/accessors.py index d9211c4876c21e24ac4330af4de5806193a215a5..7183036a63c653328315761e89c066a30fc64d64 100644 --- a/model_server/accessors.py +++ b/model_server/accessors.py @@ -39,6 +39,22 @@ class GenericImageDataAccessor(ABC): c = int(channel) 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 def data(self): """