diff --git a/model_server/base/process.py b/model_server/base/process.py index 3ee227d77b5355725ca130a4c4ea8567c3c9c3a9..992c9fb93140ab49859f0807b55a1f8e321cd80d 100644 --- a/model_server/base/process.py +++ b/model_server/base/process.py @@ -4,6 +4,7 @@ Image processing utility functions from math import ceil, floor import numpy as np +import skimage from skimage.exposure import rescale_intensity @@ -85,3 +86,43 @@ def make_rgb(nda): outdata = np.zeros((h, w, 3, nz), dtype=nda.dtype) outdata[:, :, 0:c, :] = nda[:, :, :, :] return outdata + + +def mask_largest_object( + img: np.ndarray, + max_allowed: int = 10, + verbose: bool = True +) -> np.ndarray: + """ + Where more than one connected component is found in an image, return the largest object by area + :param img: (np.ndarray) containing object labels or binary mask + :param max_allowed: raise an error if more than this number of objects is found + :param verbose: print a message each time more than one object is found + :return: np.ndarray of same size as img + """ + if is_mask(img): # assign object labels + ob_id = skimage.measure.label(img) + else: # assume img is contains object labels + ob_id = img + + num_obj = len(np.unique(ob_id)) - 1 + if num_obj > max_allowed: + raise TooManyObjectError(f'Found {num_obj} objects in frame') + if num_obj > 1: + if verbose: + print(f'Found {num_obj} nonzero unique values in object map; keeping the one with the largest area') + val, cts = np.unique(ob_id, return_counts=True) + mask = ob_id == val[1 + cts[1:].argmax()] + return mask * img + else: + return img + + +class Error(Exception): + pass + + +class TooManyObjectError(Exception): + pass + + diff --git a/model_server/base/roiset.py b/model_server/base/roiset.py index bf40227a0629c2d3747e1faf20360565fa664a63..4aecae346e71ef56dc805b0b2549df589038563a 100644 --- a/model_server/base/roiset.py +++ b/model_server/base/roiset.py @@ -18,7 +18,8 @@ from model_server.base.models import InstanceSegmentationModel from model_server.base.process import pad, rescale, resample_to_8bit, make_rgb from model_server.extensions.chaeo.annotators import draw_box_on_patch, draw_contours_on_patch, draw_boxes_on_3d_image from model_server.extensions.chaeo.accessors import write_patch_to_file, MonoPatchStack, Multichannel3dPatchStack -from model_server.extensions.chaeo.process import mask_largest_object +from base.process import mask_largest_object + class PatchParams(BaseModel): draw_bounding_box: bool = False diff --git a/model_server/extensions/chaeo/process.py b/model_server/extensions/chaeo/process.py deleted file mode 100644 index e41578eee35f7849e7d714326c59ca68e34042b0..0000000000000000000000000000000000000000 --- a/model_server/extensions/chaeo/process.py +++ /dev/null @@ -1,48 +0,0 @@ -import numpy as np -import skimage - -from model_server.base.process import is_mask - -def mask_largest_object( - img: np.ndarray, - max_allowed: int = 10, - verbose: bool = True -) -> np.ndarray: - """ - Where more than one connected component is found in an image, return the largest object by area - :param img: (np.ndarray) containing object labels or binary mask - :param max_allowed: raise an error if more than this number of objects is found - :param verbose: print a message each time more than one object is found - :return: np.ndarray of same size as img - """ - if is_mask(img): # assign object labels - ob_id = skimage.measure.label(img) - else: # assume img is contains object labels - ob_id = img - - # import skimage - # from pathlib import Path - # where = Path('c:/Users/rhodes/projects/proj0011-plankton-seg/tmp') - # skimage.io.imsave(where / 'raw.png', img) - num_obj = len(np.unique(ob_id)) - 1 - if num_obj > max_allowed: - raise TooManyObjectError(f'Found {num_obj} objects in frame') - if num_obj > 1: - if verbose: - print(f'Found {num_obj} nonzero unique values in object map; keeping the one with the largest area') - # pr = regionprops_table(ob_id, properties=['label', 'area']) - val, cts = np.unique(ob_id, return_counts=True) - mask = ob_id == val[1 + cts[1:].argmax()] - # idx_max_area = pr['area'].argmax() - # mask = ob_id == pr['label'][idx_max_area] - return mask * img - else: - return img - - -class Error(Exception): - pass - - -class TooManyObjectError(Exception): - pass diff --git a/model_server/extensions/chaeo/tests/test_process.py b/model_server/extensions/chaeo/tests/test_process.py deleted file mode 100644 index 79e6883c5f7099869b9b9901474c5c4cb833d158..0000000000000000000000000000000000000000 --- a/model_server/extensions/chaeo/tests/test_process.py +++ /dev/null @@ -1,32 +0,0 @@ -import unittest - -import numpy as np - -from model_server.extensions.chaeo.process import mask_largest_object - -class TestMaskLargestObject(unittest.TestCase): - def test_mask_largest_touching_object(self): - arr = np.zeros([5, 5], dtype='uint8') - arr[0:3, 0:3] = 2 - arr[3:, 2:] = 4 - masked = mask_largest_object(arr) - self.assertTrue(np.all(np.unique(masked) == [0, 2])) - self.assertTrue(np.all(masked[4:5, 0:2] == 0)) - self.assertTrue(np.all(masked[0:3, 3:5] == 0)) - - def test_no_change(self): - arr = np.zeros([5, 5], dtype='uint8') - arr[0:3, 0:3] = 2 - masked = mask_largest_object(arr) - self.assertTrue(np.all(masked == arr)) - - def test_mask_multiple_objects_in_binary_maks(self): - arr = np.zeros([5, 5], dtype='uint8') - arr[0:3, 0:3] = 255 - arr[4, 2:5] = 255 - masked = mask_largest_object(arr) - print(np.unique(masked)) - self.assertTrue(np.all(np.unique(masked) == [0, 255])) - self.assertTrue(np.all(masked[:, 3:5] == 0)) - self.assertTrue(np.all(masked[3:5, :] == 0)) - diff --git a/model_server/extensions/chaeo/workflows.py b/model_server/extensions/chaeo/workflows.py index b837877979017384effdb840c1f171d7e6d1d808..c651f95ecdf68f4b8f3aa431e6d4e512a2467aa4 100644 --- a/model_server/extensions/chaeo/workflows.py +++ b/model_server/extensions/chaeo/workflows.py @@ -9,7 +9,7 @@ from skimage.morphology import dilation from sklearn.model_selection import train_test_split from base.roiset import RoiSetMetaParams, RoiSetExportParams -from model_server.extensions.chaeo.process import mask_largest_object +from base.process import mask_largest_object from base.roiset import _get_label_ids, RoiSet from model_server.base.accessors import generate_file_accessor, InMemoryDataAccessor, write_accessor_data_to_file diff --git a/tests/test_process.py b/tests/test_process.py index 6908e9d7c1b779065d4de226b7a203979570fb9a..454151bc4f84156348624318a43837b5e9a370c8 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -2,6 +2,7 @@ import unittest import numpy as np +from base.process import mask_largest_object from model_server.base.process import pad class TestProcessingUtilityMethods(unittest.TestCase): @@ -27,4 +28,31 @@ class TestProcessingUtilityMethods(unittest.TestCase): nc = self.data4d.shape[2] nz = self.data4d.shape[3] padded = pad(self.data4d, 256) - self.assertEqual(padded.shape, (256, 256, nc, nz)) \ No newline at end of file + self.assertEqual(padded.shape, (256, 256, nc, nz)) + + +class TestMaskLargestObject(unittest.TestCase): + def test_mask_largest_touching_object(self): + arr = np.zeros([5, 5], dtype='uint8') + arr[0:3, 0:3] = 2 + arr[3:, 2:] = 4 + masked = mask_largest_object(arr) + self.assertTrue(np.all(np.unique(masked) == [0, 2])) + self.assertTrue(np.all(masked[4:5, 0:2] == 0)) + self.assertTrue(np.all(masked[0:3, 3:5] == 0)) + + def test_no_change(self): + arr = np.zeros([5, 5], dtype='uint8') + arr[0:3, 0:3] = 2 + masked = mask_largest_object(arr) + self.assertTrue(np.all(masked == arr)) + + def test_mask_multiple_objects_in_binary_maks(self): + arr = np.zeros([5, 5], dtype='uint8') + arr[0:3, 0:3] = 255 + arr[4, 2:5] = 255 + masked = mask_largest_object(arr) + print(np.unique(masked)) + self.assertTrue(np.all(np.unique(masked) == [0, 255])) + self.assertTrue(np.all(masked[:, 3:5] == 0)) + self.assertTrue(np.all(masked[3:5, :] == 0))