From 3d03bd63e06977144aab7c6e0baa50cb0935820d Mon Sep 17 00:00:00 2001 From: vnath Date: Thu, 12 May 2022 20:30:10 -0500 Subject: [PATCH 01/40] Try 1 Signed-off-by: vnath --- monai/apps/nuclick/__init__.py | 0 monai/apps/nuclick/transforms.py | 162 +++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 monai/apps/nuclick/__init__.py create mode 100644 monai/apps/nuclick/transforms.py diff --git a/monai/apps/nuclick/__init__.py b/monai/apps/nuclick/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/monai/apps/nuclick/transforms.py b/monai/apps/nuclick/transforms.py new file mode 100644 index 0000000000..dc6f0ad6d5 --- /dev/null +++ b/monai/apps/nuclick/transforms.py @@ -0,0 +1,162 @@ +import os +import random +import math +import cv2 +import skimage +import numpy as np + +from monai.config import KeysCollection +from monai.transforms import ( + MapTransform, + RandomizableTransform, + Transform +) + +class FlattenLabeld(MapTransform): + def __call__(self, data): + d = dict(data) + for key in self.keys: + _, labels, _, _ = cv2.connectedComponentsWithStats(d[key], 4, cv2.CV_32S) + d[key] = labels.astype(np.uint8) + return d + + +class ExtractPatchd(MapTransform): + def __init__(self, keys: KeysCollection, centroid_key="centroid", patch_size=128): + super().__init__(keys) + self.centroid_key = centroid_key + self.patch_size = patch_size + + def __call__(self, data): + d = dict(data) + + centroid = d[self.centroid_key] # create mask based on centroid (select nuclei based on centroid) + roi_size = (self.patch_size, self.patch_size) + + for key in self.keys: + img = d[key] + x_start, x_end, y_start, y_end = self.bbox(self.patch_size, centroid, img.shape[-2:]) + cropped = img[:, x_start:x_end, y_start:y_end] + d[key] = self.pad_to_shape(cropped, roi_size) + return d + + @staticmethod + def bbox(patch_size, centroid, size): + x, y = centroid + m, n = size + + x_start = int(max(x - patch_size / 2, 0)) + y_start = int(max(y - patch_size / 2, 0)) + x_end = x_start + patch_size + y_end = y_start + patch_size + if x_end > m: + x_end = m + x_start = m - patch_size + if y_end > n: + y_end = n + y_start = n - patch_size + return x_start, x_end, y_start, y_end + + @staticmethod + def pad_to_shape(img, shape): + img_shape = img.shape[-2:] + s_diff = np.array(shape) - np.array(img_shape) + diff = [(0, 0), (0, s_diff[0]), (0, s_diff[1])] + return np.pad( + img, + diff, + mode="constant", + constant_values=0, + ) + + +class SplitLabeld(Transform): + def __init__(self, label="label", others="others", mask_value="mask_value", min_area=5): + self.label = label + self.others = others + self.mask_value = mask_value + self.min_area = min_area + + def __call__(self, data): + d = dict(data) + label = d[self.label] + mask_value = d[self.mask_value] + + mask = np.uint8(label == mask_value) + others = (1 - mask) * label + others = self._mask_relabeling(others[0], min_area=self.min_area)[np.newaxis] + + d[self.label] = mask + d[self.others] = others + return d + + @staticmethod + def _mask_relabeling(mask, min_area=5): + res = np.zeros_like(mask) + for l in np.unique(mask): + if l == 0: + continue + + m = skimage.measure.label(mask == l, connectivity=1) + for stat in skimage.measure.regionprops(m): + if stat.area > min_area: + res[stat.coords[:, 0], stat.coords[:, 1]] = l + return res + + +class AddPointGuidanceSignald(RandomizableTransform): + def __init__(self, image="image", label="label", others="others", drop_rate=0.5, jitter_range=3): + super().__init__() + + self.image = image + self.label = label + self.others = others + self.drop_rate = drop_rate + self.jitter_range = jitter_range + + def __call__(self, data): + d = dict(data) + + image = d[self.image] + mask = d[self.label] + others = d[self.others] + + inc_sig = self.inclusion_map(mask[0]) + exc_sig = self.exclusion_map(others[0], drop_rate=self.drop_rate, jitter_range=self.jitter_range) + + image = np.concatenate((image, inc_sig[np.newaxis, ...], exc_sig[np.newaxis, ...]), axis=0) + d[self.image] = image + return d + + @staticmethod + def inclusion_map(mask): + point_mask = np.zeros_like(mask) + indices = np.argwhere(mask > 0) + if len(indices) > 0: + idx = np.random.randint(0, len(indices)) + point_mask[indices[idx, 0], indices[idx, 1]] = 1 + + return point_mask + + @staticmethod + def exclusion_map(others, jitter_range=3, drop_rate=0.5): + point_mask = np.zeros_like(others) + if drop_rate == 1.0: + return point_mask + + max_x = point_mask.shape[0] - 1 + max_y = point_mask.shape[1] - 1 + stats = skimage.measure.regionprops(others) + for stat in stats: + x, y = stat.centroid + if np.random.choice([True, False], p=[drop_rate, 1 - drop_rate]): + continue + + # random jitter + x = int(math.floor(x)) + random.randint(a=-jitter_range, b=jitter_range) + y = int(math.floor(y)) + random.randint(a=-jitter_range, b=jitter_range) + x = min(max(0, x), max_x) + y = min(max(0, y), max_y) + point_mask[x, y] = 1 + + return point_mask \ No newline at end of file From d8c2c56d46595e65a9ca11450c6d1eef5407b61c Mon Sep 17 00:00:00 2001 From: vnath Date: Mon, 16 May 2022 09:35:04 -0500 Subject: [PATCH 02/40] dataset prep addition Signed-off-by: vnath --- monai/apps/nuclick/dataset_prep.py | 57 ++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 monai/apps/nuclick/dataset_prep.py diff --git a/monai/apps/nuclick/dataset_prep.py b/monai/apps/nuclick/dataset_prep.py new file mode 100644 index 0000000000..6a8213b4d6 --- /dev/null +++ b/monai/apps/nuclick/dataset_prep.py @@ -0,0 +1,57 @@ +import os +import tqdm +import numpy as np + +def split_pannuke_dataset(image, label, output_dir, groups): + groups = groups if groups else dict() + groups = [groups] if isinstance(groups, str) else groups + if not isinstance(groups, dict): + groups = {v: k + 1 for k, v in enumerate(groups)} + + label_channels = { + 0: "Neoplastic cells", + 1: "Inflammatory", + 2: "Connective/Soft tissue cells", + 3: "Dead Cells", + 4: "Epithelial", + } + + print(f"++ Using Groups: {groups}") + print(f"++ Using Label Channels: {label_channels}") + #logger.info(f"++ Using Groups: {groups}") + #logger.info(f"++ Using Label Channels: {label_channels}") + + images = np.load(image) + labels = np.load(label) + print(f"Image Shape: {images.shape}") + print(f"Labels Shape: {labels.shape}") + #logger.info(f"Image Shape: {images.shape}") + #logger.info(f"Labels Shape: {labels.shape}") + + images_dir = output_dir + labels_dir = os.path.join(output_dir, "labels", "final") + os.makedirs(images_dir, exist_ok=True) + os.makedirs(labels_dir, exist_ok=True) + + dataset_json = [] + for i in tqdm(range(images.shape[0])): + name = f"img_{str(i).zfill(4)}.npy" + image_file = os.path.join(images_dir, name) + label_file = os.path.join(labels_dir, name) + + image_np = images[i] + mask = labels[i] + label_np = np.zeros(shape=mask.shape[:2]) + + for idx, name in label_channels.items(): + if idx < mask.shape[2]: + m = mask[:, :, idx] + if np.count_nonzero(m): + m[m > 0] = groups.get(name, 1) + label_np = np.where(m > 0, m, label_np) + + np.save(image_file, image_np) + np.save(label_file, label_np) + dataset_json.append({"image": image_file, "label": label_file}) + + return dataset_json \ No newline at end of file From 3d22109689312a7ef5086d3725902fce61914a97 Mon Sep 17 00:00:00 2001 From: vnath Date: Wed, 18 May 2022 16:15:31 -0500 Subject: [PATCH 03/40] Refactoring of transforms as there were loose hanging functions Signed-off-by: vnath --- monai/apps/nuclick/transforms.py | 56 +++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/monai/apps/nuclick/transforms.py b/monai/apps/nuclick/transforms.py index dc6f0ad6d5..335969dd17 100644 --- a/monai/apps/nuclick/transforms.py +++ b/monai/apps/nuclick/transforms.py @@ -1,4 +1,3 @@ -import os import random import math import cv2 @@ -12,6 +11,8 @@ Transform ) +from skimage.morphology import remove_small_objects + class FlattenLabeld(MapTransform): def __call__(self, data): d = dict(data) @@ -103,6 +104,59 @@ def _mask_relabeling(mask, min_area=5): res[stat.coords[:, 0], stat.coords[:, 1]] = l return res +class FilterImaged(MapTransform): + def __init__(self, keys: KeysCollection, min_size=500): + super().__init__(keys) + self.min_size = min_size + + def __call__(self, data): + d = dict(data) + for key in self.keys: + img = d[key] + d[key] = self.filter(img) + return d + + def filter(self, rgb): + mask_not_green = self.filter_green_channel(rgb) + mask_not_gray = self.filter_grays(rgb) + mask_gray_green = mask_not_gray & mask_not_green + mask = ( + self.filter_remove_small_objects(mask_gray_green, min_size=self.min_size) if self.min_size else mask_gray_green + ) + + return rgb * np.dstack([mask, mask, mask]) + + def filter_green_channel(self, img_np, green_thresh=200, avoid_overmask=True, overmask_thresh=90, output_type="bool"): + g = img_np[:, :, 1] + gr_ch_mask = (g < green_thresh) & (g > 0) + mask_percentage = self.mask_percent(gr_ch_mask) + if (mask_percentage >= overmask_thresh) and (green_thresh < 255) and (avoid_overmask is True): + new_green_thresh = math.ceil((255 - green_thresh) / 2 + green_thresh) + gr_ch_mask = self.filter_green_channel(img_np, new_green_thresh, avoid_overmask, overmask_thresh, output_type) + return gr_ch_mask + + def filter_grays(self, rgb, tolerance=15): + rg_diff = abs(rgb[:, :, 0] - rgb[:, :, 1]) <= tolerance + rb_diff = abs(rgb[:, :, 0] - rgb[:, :, 2]) <= tolerance + gb_diff = abs(rgb[:, :, 1] - rgb[:, :, 2]) <= tolerance + return ~(rg_diff & rb_diff & gb_diff) + + def mask_percent(self, img_np): + if (len(img_np.shape) == 3) and (img_np.shape[2] == 3): + np_sum = img_np[:, :, 0] + img_np[:, :, 1] + img_np[:, :, 2] + mask_percentage = 100 - np.count_nonzero(np_sum) / np_sum.size * 100 + else: + mask_percentage = 100 - np.count_nonzero(img_np) / img_np.size * 100 + return mask_percentage + + def filter_remove_small_objects(self, img_np, min_size=3000, avoid_overmask=True, overmask_thresh=95): + rem_sm = remove_small_objects(img_np.astype(bool), min_size=min_size) + mask_percentage = self.mask_percent(rem_sm) + if (mask_percentage >= overmask_thresh) and (min_size >= 1) and (avoid_overmask is True): + new_min_size = round(min_size / 2) + rem_sm = self.filter_remove_small_objects(img_np, new_min_size, avoid_overmask, overmask_thresh) + return rem_sm + class AddPointGuidanceSignald(RandomizableTransform): def __init__(self, image="image", label="label", others="others", drop_rate=0.5, jitter_range=3): From 04e8782b2f1f9a9c72e96c1c0089103e340a07d8 Mon Sep 17 00:00:00 2001 From: vnath Date: Thu, 19 May 2022 19:55:36 -0500 Subject: [PATCH 04/40] Minor changes to transforms Signed-off-by: vnath --- monai/apps/nuclick/transforms.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/monai/apps/nuclick/transforms.py b/monai/apps/nuclick/transforms.py index 335969dd17..767d6d64a8 100644 --- a/monai/apps/nuclick/transforms.py +++ b/monai/apps/nuclick/transforms.py @@ -21,7 +21,6 @@ def __call__(self, data): d[key] = labels.astype(np.uint8) return d - class ExtractPatchd(MapTransform): def __init__(self, keys: KeysCollection, centroid_key="centroid", patch_size=128): super().__init__(keys) @@ -105,7 +104,7 @@ def _mask_relabeling(mask, min_area=5): return res class FilterImaged(MapTransform): - def __init__(self, keys: KeysCollection, min_size=500): + def __init__(self, keys: KeysCollection, min_size: int = 500): super().__init__(keys) self.min_size = min_size From bbadb7092c3e815ea6f112a42eeae628af9340fa Mon Sep 17 00:00:00 2001 From: vnath Date: Thu, 19 May 2022 20:02:00 -0500 Subject: [PATCH 05/40] Added test cases for all transforms Signed-off-by: vnath --- tests/test_nuclick_transforms.py | 187 +++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 tests/test_nuclick_transforms.py diff --git a/tests/test_nuclick_transforms.py b/tests/test_nuclick_transforms.py new file mode 100644 index 0000000000..1135da3c0d --- /dev/null +++ b/tests/test_nuclick_transforms.py @@ -0,0 +1,187 @@ +# Copyright (c) MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +import numpy as np +from parameterized import parameterized + +from monai.apps.nuclick.transforms import ( +FlattenLabeld, +FilterImaged, +ExtractPatchd, +SplitLabeld, +AddPointGuidanceSignald +) + +# Data Definitions +RGB_IMAGE_1 = np.array([ + [[0, 0, 0], + [0, 1, 0], + [0, 0, 1]], + [[2, 0, 2], + [0, 1, 0], + [1, 0, 1]], + [[3, 0, 2], + [0, 1, 0], + [1, 3, 1]] + ]) + +LABEL_1 = np.array([[1, 1, 1, 0, 0, 0, 0], + [1, 1, 1, 0, 0, 0, 0], + [1, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 0, 1, 1, 1], + [1, 1, 1, 0, 1, 1, 1], + [1, 1, 1, 0, 1, 1, 1]], dtype=np.uint8) + +LABEL_2 = np.array([[0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0]], dtype=np.uint8) + +LABEL_3 = np.array([[[1, 1, 1, 1], + [2, 2, 2, 2], + [3, 3, 3, 3], + [4, 4, 4, 4]]], dtype=np.uint8) + +IL_IMAGE_1 = np.array( + [ + [[0, 0, 0, 0, 0], + [0, 1, 0, 0, 0], + [0, 0, 1, 1, 1], + [0, 0, 1, 1, 1], + [0, 0, 1, 1, 1]], + [[0, 0, 0, 0, 0], + [0, 1, 0, 0, 0], + [0, 0, 1, 1, 1], + [0, 0, 1, 1, 1], + [0, 0, 1, 1, 1]], + [[0, 0, 0, 0, 0], + [0, 1, 0, 0, 0], + [0, 0, 1, 1, 1], + [0, 0, 1, 1, 1], + [0, 0, 1, 1, 1]], + ] + ) + +IL_LABEL_1 = np.array( + [[[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]]], dtype=np.uint8 + ) + +IL_OTHERS_1 = np.array( + [[[1, 1, 1, 1, 1], + [2, 0, 0, 0, 2], + [3, 0, 0, 0, 3], + [4, 0, 0, 0, 4], + [5, 5, 5, 5, 5]]], dtype=np.uint8 + ) + +IL_IMAGE_2 = np.array( + [ + [[0, 0, 0], + [0, 1, 0], + [0, 0, 1]], + [[0, 0, 0], + [0, 1, 0], + [0, 0, 1]], + [[0, 0, 0], + [0, 1, 0], + [0, 0, 1]] + ] + ) + +IL_LABEL_2 = np.array( + [[[0, 0, 0], + [0, 1, 0], + [0, 0, 0]]], dtype=np.uint8 + ) + +DATA_FILTER_1 = {"image": RGB_IMAGE_1} + +DATA_FLATTEN_1 = {"label": LABEL_1} +DATA_FLATTEN_2 = {"label": LABEL_2} + +DATA_EXTRACT_1 = {"image": IL_IMAGE_1, "label": IL_LABEL_1, "centroid": (2, 2)} +DATA_EXTRACT_2 = {"image": IL_IMAGE_2, "label": IL_LABEL_2, "centroid": (1, 1)} + +DATA_SPLIT_1 = {"label": LABEL_3, "mask_value": 1} + +DATA_GUIDANCE_1 = {"image": IL_IMAGE_1, "label": IL_LABEL_1, "others":IL_OTHERS_1, "centroid": (2, 2)} + +# Result Definitions +EXTRACT_RESULT_TC1 = np.array([[[0, 0, 0], + [0, 0, 0], + [0, 0, 1]]], dtype=np.uint8) + +SPLIT_RESULT_TC1 = np.array([[[1, 1, 1, 1], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0]]], dtype=np.uint8) + +# Test Case Definitions +FILTER_IMAGE_TEST_CASE_1 = [{"keys": "image", "min_size": 1}, DATA_FILTER_1, [3, 3, 3]] + +FLATTEN_LABEL_TEST_CASE_1 = [{"keys": "label"}, DATA_FLATTEN_1, [0, 1, 2, 3]] +FLATTEN_LABEL_TEST_CASE_2 = [{"keys": "label"}, DATA_FLATTEN_2, [0]] + +EXTRACT_TEST_CASE_1 = [{"keys": ["image", "label"], "patch_size": 3}, DATA_EXTRACT_1, [1, 3, 3]] +EXTRACT_TEST_CASE_2 = [{"keys": ["image", "label"], "patch_size": 5}, DATA_EXTRACT_1, [1, 5, 5]] +EXTRACT_TEST_CASE_3 = [{"keys": ["image", "label"], "patch_size": 1}, DATA_EXTRACT_2, [1, 1, 1]] + +EXTRACT_RESULT_TEST_CASE_1 = [{"keys": ["image", "label"], "patch_size": 3}, DATA_EXTRACT_1, EXTRACT_RESULT_TC1] + +SPLIT_TEST_CASE_1 = [{"label": "label", "mask_value": "mask_value", "min_area": 1}, DATA_SPLIT_1, SPLIT_RESULT_TC1] + +GUIDANCE_TEST_CASE_1 = [{"image": "image", "label": "label", "others": "others"}, DATA_GUIDANCE_1, [5, 5, 5]] + +# Test Case Classes +class TestFilterImaged(unittest.TestCase): + @parameterized.expand([FILTER_IMAGE_TEST_CASE_1]) + def test_correct_shape(self, arguments, input_data, expected_shape): + result = FilterImaged(**arguments)(input_data) + np.testing.assert_equal(result["image"].shape, expected_shape) + +class TestFlattenLabeld(unittest.TestCase): + @parameterized.expand([FLATTEN_LABEL_TEST_CASE_1, FLATTEN_LABEL_TEST_CASE_2]) + def test_correct_num_labels(self, arguments, input_data, expected_result): + result = FlattenLabeld(**arguments)(input_data) + np.testing.assert_equal(np.unique(result["label"]), expected_result) + +class TestExtractPatchd(unittest.TestCase): + @parameterized.expand([EXTRACT_TEST_CASE_1, EXTRACT_TEST_CASE_2, EXTRACT_TEST_CASE_3]) + def test_correct_patch_size(self, arguments, input_data, expected_shape): + result = ExtractPatchd(**arguments)(input_data) + np.testing.assert_equal(result["label"].shape, expected_shape) + + @parameterized.expand([EXTRACT_RESULT_TEST_CASE_1]) + def test_correct_results(self, arguments, input_data, expected_result): + result = ExtractPatchd(**arguments)(input_data) + np.testing.assert_equal(result["label"], expected_result) + +class TestSplitLabelsd(unittest.TestCase): + @parameterized.expand([SPLIT_TEST_CASE_1]) + def test_correct_results(self, arguments, input_data, expected_result): + result = SplitLabeld(**arguments)(input_data) + np.testing.assert_equal(result["label"], expected_result) + +class TestGuidanceSignal(unittest.TestCase): + @parameterized.expand([GUIDANCE_TEST_CASE_1]) + def test_correct_shape(self, arguments, input_data, expected_shape): + result = AddPointGuidanceSignald(**arguments)(input_data) + np.testing.assert_equal(result["image"].shape, expected_shape) + +if __name__ == "__main__": + unittest.main() From 0e7199dfec0f8dcc0c36fbbc3037875e7059d116 Mon Sep 17 00:00:00 2001 From: vnath Date: Thu, 19 May 2022 20:04:06 -0500 Subject: [PATCH 06/40] Removed dataset prep, it will be a part of tutorial, added opencv to dev requirements Signed-off-by: vnath --- monai/apps/nuclick/dataset_prep.py | 57 ------------------------------ requirements-dev.txt | 1 + 2 files changed, 1 insertion(+), 57 deletions(-) delete mode 100644 monai/apps/nuclick/dataset_prep.py diff --git a/monai/apps/nuclick/dataset_prep.py b/monai/apps/nuclick/dataset_prep.py deleted file mode 100644 index 6a8213b4d6..0000000000 --- a/monai/apps/nuclick/dataset_prep.py +++ /dev/null @@ -1,57 +0,0 @@ -import os -import tqdm -import numpy as np - -def split_pannuke_dataset(image, label, output_dir, groups): - groups = groups if groups else dict() - groups = [groups] if isinstance(groups, str) else groups - if not isinstance(groups, dict): - groups = {v: k + 1 for k, v in enumerate(groups)} - - label_channels = { - 0: "Neoplastic cells", - 1: "Inflammatory", - 2: "Connective/Soft tissue cells", - 3: "Dead Cells", - 4: "Epithelial", - } - - print(f"++ Using Groups: {groups}") - print(f"++ Using Label Channels: {label_channels}") - #logger.info(f"++ Using Groups: {groups}") - #logger.info(f"++ Using Label Channels: {label_channels}") - - images = np.load(image) - labels = np.load(label) - print(f"Image Shape: {images.shape}") - print(f"Labels Shape: {labels.shape}") - #logger.info(f"Image Shape: {images.shape}") - #logger.info(f"Labels Shape: {labels.shape}") - - images_dir = output_dir - labels_dir = os.path.join(output_dir, "labels", "final") - os.makedirs(images_dir, exist_ok=True) - os.makedirs(labels_dir, exist_ok=True) - - dataset_json = [] - for i in tqdm(range(images.shape[0])): - name = f"img_{str(i).zfill(4)}.npy" - image_file = os.path.join(images_dir, name) - label_file = os.path.join(labels_dir, name) - - image_np = images[i] - mask = labels[i] - label_np = np.zeros(shape=mask.shape[:2]) - - for idx, name in label_channels.items(): - if idx < mask.shape[2]: - m = mask[:, :, idx] - if np.count_nonzero(m): - m[m > 0] = groups.get(name, 1) - label_np = np.where(m > 0, m, label_np) - - np.save(image_file, image_np) - np.save(label_file, label_np) - dataset_json.append({"image": image_file, "label": label_file}) - - return dataset_json \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index 651a99eba9..4709b5df92 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,6 +8,7 @@ nibabel pillow!=8.3.0 # https://github.com/python-pillow/Pillow/issues/5571 tensorboard scikit-image>=0.14.2 +opencv-python==4.5.5.64 tqdm>=4.47.0 lmdb flake8>=3.8.1 From 1e601940e6ca8f70176bf4f59a74aeef4bce2705 Mon Sep 17 00:00:00 2001 From: vnath Date: Thu, 19 May 2022 20:07:25 -0500 Subject: [PATCH 07/40] Added Init for NuClick Signed-off-by: vnath --- monai/apps/nuclick/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/monai/apps/nuclick/__init__.py b/monai/apps/nuclick/__init__.py index e69de29bb2..1e97f89407 100644 --- a/monai/apps/nuclick/__init__.py +++ b/monai/apps/nuclick/__init__.py @@ -0,0 +1,10 @@ +# Copyright (c) MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. From bdd1697ba5accf8bb12fea7652d594bfd1ab9336 Mon Sep 17 00:00:00 2001 From: vnath Date: Thu, 19 May 2022 20:15:29 -0500 Subject: [PATCH 08/40] Code formatting changes Signed-off-by: vnath --- monai/apps/nuclick/transforms.py | 46 +++++++---- tests/test_nuclick_transforms.py | 137 +++++++++++-------------------- 2 files changed, 75 insertions(+), 108 deletions(-) diff --git a/monai/apps/nuclick/transforms.py b/monai/apps/nuclick/transforms.py index 767d6d64a8..fc07dc1257 100644 --- a/monai/apps/nuclick/transforms.py +++ b/monai/apps/nuclick/transforms.py @@ -1,17 +1,24 @@ -import random +# Copyright (c) MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import math +import random + import cv2 -import skimage import numpy as np +import skimage +from skimage.morphology import remove_small_objects from monai.config import KeysCollection -from monai.transforms import ( - MapTransform, - RandomizableTransform, - Transform -) +from monai.transforms import MapTransform, RandomizableTransform, Transform -from skimage.morphology import remove_small_objects class FlattenLabeld(MapTransform): def __call__(self, data): @@ -21,6 +28,7 @@ def __call__(self, data): d[key] = labels.astype(np.uint8) return d + class ExtractPatchd(MapTransform): def __init__(self, keys: KeysCollection, centroid_key="centroid", patch_size=128): super().__init__(keys) @@ -62,12 +70,7 @@ def pad_to_shape(img, shape): img_shape = img.shape[-2:] s_diff = np.array(shape) - np.array(img_shape) diff = [(0, 0), (0, s_diff[0]), (0, s_diff[1])] - return np.pad( - img, - diff, - mode="constant", - constant_values=0, - ) + return np.pad(img, diff, mode="constant", constant_values=0) class SplitLabeld(Transform): @@ -103,6 +106,7 @@ def _mask_relabeling(mask, min_area=5): res[stat.coords[:, 0], stat.coords[:, 1]] = l return res + class FilterImaged(MapTransform): def __init__(self, keys: KeysCollection, min_size: int = 500): super().__init__(keys) @@ -120,18 +124,24 @@ def filter(self, rgb): mask_not_gray = self.filter_grays(rgb) mask_gray_green = mask_not_gray & mask_not_green mask = ( - self.filter_remove_small_objects(mask_gray_green, min_size=self.min_size) if self.min_size else mask_gray_green + self.filter_remove_small_objects(mask_gray_green, min_size=self.min_size) + if self.min_size + else mask_gray_green ) return rgb * np.dstack([mask, mask, mask]) - def filter_green_channel(self, img_np, green_thresh=200, avoid_overmask=True, overmask_thresh=90, output_type="bool"): + def filter_green_channel( + self, img_np, green_thresh=200, avoid_overmask=True, overmask_thresh=90, output_type="bool" + ): g = img_np[:, :, 1] gr_ch_mask = (g < green_thresh) & (g > 0) mask_percentage = self.mask_percent(gr_ch_mask) if (mask_percentage >= overmask_thresh) and (green_thresh < 255) and (avoid_overmask is True): new_green_thresh = math.ceil((255 - green_thresh) / 2 + green_thresh) - gr_ch_mask = self.filter_green_channel(img_np, new_green_thresh, avoid_overmask, overmask_thresh, output_type) + gr_ch_mask = self.filter_green_channel( + img_np, new_green_thresh, avoid_overmask, overmask_thresh, output_type + ) return gr_ch_mask def filter_grays(self, rgb, tolerance=15): @@ -212,4 +222,4 @@ def exclusion_map(others, jitter_range=3, drop_rate=0.5): y = min(max(0, y), max_y) point_mask[x, y] = 1 - return point_mask \ No newline at end of file + return point_mask diff --git a/tests/test_nuclick_transforms.py b/tests/test_nuclick_transforms.py index 1135da3c0d..2f33700e60 100644 --- a/tests/test_nuclick_transforms.py +++ b/tests/test_nuclick_transforms.py @@ -15,99 +15,56 @@ from parameterized import parameterized from monai.apps.nuclick.transforms import ( -FlattenLabeld, -FilterImaged, -ExtractPatchd, -SplitLabeld, -AddPointGuidanceSignald + AddPointGuidanceSignald, + ExtractPatchd, + FilterImaged, + FlattenLabeld, + SplitLabeld, ) # Data Definitions -RGB_IMAGE_1 = np.array([ - [[0, 0, 0], - [0, 1, 0], - [0, 0, 1]], - [[2, 0, 2], - [0, 1, 0], - [1, 0, 1]], - [[3, 0, 2], - [0, 1, 0], - [1, 3, 1]] - ]) - -LABEL_1 = np.array([[1, 1, 1, 0, 0, 0, 0], - [1, 1, 1, 0, 0, 0, 0], - [1, 1, 1, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0], - [1, 1, 1, 0, 1, 1, 1], - [1, 1, 1, 0, 1, 1, 1], - [1, 1, 1, 0, 1, 1, 1]], dtype=np.uint8) - -LABEL_2 = np.array([[0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0]], dtype=np.uint8) - -LABEL_3 = np.array([[[1, 1, 1, 1], - [2, 2, 2, 2], - [3, 3, 3, 3], - [4, 4, 4, 4]]], dtype=np.uint8) +RGB_IMAGE_1 = np.array( + [[[0, 0, 0], [0, 1, 0], [0, 0, 1]], [[2, 0, 2], [0, 1, 0], [1, 0, 1]], [[3, 0, 2], [0, 1, 0], [1, 3, 1]]] +) + +LABEL_1 = np.array( + [ + [1, 1, 1, 0, 0, 0, 0], + [1, 1, 1, 0, 0, 0, 0], + [1, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 0, 1, 1, 1], + [1, 1, 1, 0, 1, 1, 1], + [1, 1, 1, 0, 1, 1, 1], + ], + dtype=np.uint8, +) + +LABEL_2 = np.array([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], dtype=np.uint8) + +LABEL_3 = np.array([[[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3], [4, 4, 4, 4]]], dtype=np.uint8) IL_IMAGE_1 = np.array( - [ - [[0, 0, 0, 0, 0], - [0, 1, 0, 0, 0], - [0, 0, 1, 1, 1], - [0, 0, 1, 1, 1], - [0, 0, 1, 1, 1]], - [[0, 0, 0, 0, 0], - [0, 1, 0, 0, 0], - [0, 0, 1, 1, 1], - [0, 0, 1, 1, 1], - [0, 0, 1, 1, 1]], - [[0, 0, 0, 0, 0], - [0, 1, 0, 0, 0], - [0, 0, 1, 1, 1], - [0, 0, 1, 1, 1], - [0, 0, 1, 1, 1]], - ] - ) + [ + [[0, 0, 0, 0, 0], [0, 1, 0, 0, 0], [0, 0, 1, 1, 1], [0, 0, 1, 1, 1], [0, 0, 1, 1, 1]], + [[0, 0, 0, 0, 0], [0, 1, 0, 0, 0], [0, 0, 1, 1, 1], [0, 0, 1, 1, 1], [0, 0, 1, 1, 1]], + [[0, 0, 0, 0, 0], [0, 1, 0, 0, 0], [0, 0, 1, 1, 1], [0, 0, 1, 1, 1], [0, 0, 1, 1, 1]], + ] +) IL_LABEL_1 = np.array( - [[[0, 0, 0, 0, 0], - [0, 0, 0, 0, 0], - [0, 0, 1, 0, 0], - [0, 0, 0, 0, 0], - [0, 0, 0, 0, 0]]], dtype=np.uint8 - ) + [[[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 1, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]], dtype=np.uint8 +) IL_OTHERS_1 = np.array( - [[[1, 1, 1, 1, 1], - [2, 0, 0, 0, 2], - [3, 0, 0, 0, 3], - [4, 0, 0, 0, 4], - [5, 5, 5, 5, 5]]], dtype=np.uint8 - ) + [[[1, 1, 1, 1, 1], [2, 0, 0, 0, 2], [3, 0, 0, 0, 3], [4, 0, 0, 0, 4], [5, 5, 5, 5, 5]]], dtype=np.uint8 +) IL_IMAGE_2 = np.array( - [ - [[0, 0, 0], - [0, 1, 0], - [0, 0, 1]], - [[0, 0, 0], - [0, 1, 0], - [0, 0, 1]], - [[0, 0, 0], - [0, 1, 0], - [0, 0, 1]] - ] - ) - -IL_LABEL_2 = np.array( - [[[0, 0, 0], - [0, 1, 0], - [0, 0, 0]]], dtype=np.uint8 - ) + [[[0, 0, 0], [0, 1, 0], [0, 0, 1]], [[0, 0, 0], [0, 1, 0], [0, 0, 1]], [[0, 0, 0], [0, 1, 0], [0, 0, 1]]] +) + +IL_LABEL_2 = np.array([[[0, 0, 0], [0, 1, 0], [0, 0, 0]]], dtype=np.uint8) DATA_FILTER_1 = {"image": RGB_IMAGE_1} @@ -119,17 +76,12 @@ DATA_SPLIT_1 = {"label": LABEL_3, "mask_value": 1} -DATA_GUIDANCE_1 = {"image": IL_IMAGE_1, "label": IL_LABEL_1, "others":IL_OTHERS_1, "centroid": (2, 2)} +DATA_GUIDANCE_1 = {"image": IL_IMAGE_1, "label": IL_LABEL_1, "others": IL_OTHERS_1, "centroid": (2, 2)} # Result Definitions -EXTRACT_RESULT_TC1 = np.array([[[0, 0, 0], - [0, 0, 0], - [0, 0, 1]]], dtype=np.uint8) +EXTRACT_RESULT_TC1 = np.array([[[0, 0, 0], [0, 0, 0], [0, 0, 1]]], dtype=np.uint8) -SPLIT_RESULT_TC1 = np.array([[[1, 1, 1, 1], - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0]]], dtype=np.uint8) +SPLIT_RESULT_TC1 = np.array([[[1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]], dtype=np.uint8) # Test Case Definitions FILTER_IMAGE_TEST_CASE_1 = [{"keys": "image", "min_size": 1}, DATA_FILTER_1, [3, 3, 3]] @@ -154,12 +106,14 @@ def test_correct_shape(self, arguments, input_data, expected_shape): result = FilterImaged(**arguments)(input_data) np.testing.assert_equal(result["image"].shape, expected_shape) + class TestFlattenLabeld(unittest.TestCase): @parameterized.expand([FLATTEN_LABEL_TEST_CASE_1, FLATTEN_LABEL_TEST_CASE_2]) def test_correct_num_labels(self, arguments, input_data, expected_result): result = FlattenLabeld(**arguments)(input_data) np.testing.assert_equal(np.unique(result["label"]), expected_result) + class TestExtractPatchd(unittest.TestCase): @parameterized.expand([EXTRACT_TEST_CASE_1, EXTRACT_TEST_CASE_2, EXTRACT_TEST_CASE_3]) def test_correct_patch_size(self, arguments, input_data, expected_shape): @@ -171,17 +125,20 @@ def test_correct_results(self, arguments, input_data, expected_result): result = ExtractPatchd(**arguments)(input_data) np.testing.assert_equal(result["label"], expected_result) + class TestSplitLabelsd(unittest.TestCase): @parameterized.expand([SPLIT_TEST_CASE_1]) def test_correct_results(self, arguments, input_data, expected_result): result = SplitLabeld(**arguments)(input_data) np.testing.assert_equal(result["label"], expected_result) + class TestGuidanceSignal(unittest.TestCase): @parameterized.expand([GUIDANCE_TEST_CASE_1]) def test_correct_shape(self, arguments, input_data, expected_shape): result = AddPointGuidanceSignald(**arguments)(input_data) np.testing.assert_equal(result["image"].shape, expected_shape) + if __name__ == "__main__": unittest.main() From 4c8d809302d66eac20a07b4904cb12f450b2983a Mon Sep 17 00:00:00 2001 From: vnath Date: Thu, 19 May 2022 21:27:46 -0500 Subject: [PATCH 09/40] Linting & Formatting Signed-off-by: vnath --- monai/apps/nuclick/transforms.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/monai/apps/nuclick/transforms.py b/monai/apps/nuclick/transforms.py index fc07dc1257..82ff507db4 100644 --- a/monai/apps/nuclick/transforms.py +++ b/monai/apps/nuclick/transforms.py @@ -21,6 +21,11 @@ class FlattenLabeld(MapTransform): + """ + FlattenLabeld creates labels per closed object contour (defined by a connectivity). For e.g if there are + 12 small regions of 1's it will delineate them into 12 different label classes + """ + def __call__(self, data): d = dict(data) for key in self.keys: @@ -30,6 +35,10 @@ def __call__(self, data): class ExtractPatchd(MapTransform): + """ + Extracts a patch from the given image and label, however it is based on the centroid location + """ + def __init__(self, keys: KeysCollection, centroid_key="centroid", patch_size=128): super().__init__(keys) self.centroid_key = centroid_key @@ -74,6 +83,11 @@ def pad_to_shape(img, shape): class SplitLabeld(Transform): + """ + Extracts a single label from all the given classes, the single label is defined by mask_value, the remaining + labels are kept in others + """ + def __init__(self, label="label", others="others", mask_value="mask_value", min_area=5): self.label = label self.others = others @@ -93,8 +107,7 @@ def __call__(self, data): d[self.others] = others return d - @staticmethod - def _mask_relabeling(mask, min_area=5): + def _mask_relabeling(self, mask, min_area=5): res = np.zeros_like(mask) for l in np.unique(mask): if l == 0: @@ -108,6 +121,10 @@ def _mask_relabeling(mask, min_area=5): class FilterImaged(MapTransform): + """ + Filters Green and Gray channel of the image + """ + def __init__(self, keys: KeysCollection, min_size: int = 500): super().__init__(keys) self.min_size = min_size @@ -168,6 +185,10 @@ def filter_remove_small_objects(self, img_np, min_size=3000, avoid_overmask=True class AddPointGuidanceSignald(RandomizableTransform): + """ + Add Guidance Signal + """ + def __init__(self, image="image", label="label", others="others", drop_rate=0.5, jitter_range=3): super().__init__() From 95a56f60c5020d6b96a1165b188dd51328554e4f Mon Sep 17 00:00:00 2001 From: vnath Date: Fri, 20 May 2022 12:34:43 -0500 Subject: [PATCH 10/40] Fixed Flake 8 & opencv addition to requirement & env files Signed-off-by: vnath --- docs/source/installation.md | 2 +- environment-dev.yml | 1 + monai/apps/nuclick/transforms.py | 14 ++++---------- monai/config/deviceconfig.py | 1 + requirements-dev.txt | 2 +- setup.cfg | 1 + tests/test_nuclick_transforms.py | 2 ++ 7 files changed, 11 insertions(+), 12 deletions(-) diff --git a/docs/source/installation.md b/docs/source/installation.md index 12bf544cba..26df31bc56 100644 --- a/docs/source/installation.md +++ b/docs/source/installation.md @@ -190,7 +190,7 @@ Since MONAI v0.2.0, the extras syntax such as `pip install 'monai[nibabel]'` is - The options are ``` -[nibabel, skimage, pillow, tensorboard, gdown, ignite, torchvision, itk, tqdm, lmdb, psutil, cucim, openslide, pandas, einops, transformers, mlflow, matplotlib, tensorboardX, tifffile, imagecodecs, pyyaml, fire, jsonschema] +[nibabel, skimage, pillow, tensorboard, gdown, ignite, torchvision, itk, tqdm, lmdb, psutil, cucim, openslide, pandas, opencv-python, einops, transformers, mlflow, matplotlib, tensorboardX, tifffile, imagecodecs, pyyaml, fire, jsonschema] ``` which correspond to `nibabel`, `scikit-image`, `pillow`, `tensorboard`, `gdown`, `pytorch-ignite`, `torchvision`, `itk`, `tqdm`, `lmdb`, `psutil`, `cucim`, `openslide-python`, `pandas`, `einops`, `transformers`, `mlflow`, `matplotlib`, `tensorboardX`, `tifffile`, `imagecodecs`, `pyyaml`, `fire`, `jsonschema`, respectively. diff --git a/environment-dev.yml b/environment-dev.yml index a361262930..931485874f 100644 --- a/environment-dev.yml +++ b/environment-dev.yml @@ -37,6 +37,7 @@ dependencies: - sphinx-autodoc-typehints==1.11.1 - sphinx_rtd_theme==0.5.2 - pandas + - opencv-python - requests - einops - transformers diff --git a/monai/apps/nuclick/transforms.py b/monai/apps/nuclick/transforms.py index 82ff507db4..18d5cda050 100644 --- a/monai/apps/nuclick/transforms.py +++ b/monai/apps/nuclick/transforms.py @@ -57,8 +57,7 @@ def __call__(self, data): d[key] = self.pad_to_shape(cropped, roi_size) return d - @staticmethod - def bbox(patch_size, centroid, size): + def bbox(self, patch_size, centroid, size): x, y = centroid m, n = size @@ -74,8 +73,7 @@ def bbox(patch_size, centroid, size): y_start = n - patch_size return x_start, x_end, y_start, y_end - @staticmethod - def pad_to_shape(img, shape): + def pad_to_shape(self, img, shape): img_shape = img.shape[-2:] s_diff = np.array(shape) - np.array(img_shape) diff = [(0, 0), (0, s_diff[0]), (0, s_diff[1])] @@ -98,11 +96,9 @@ def __call__(self, data): d = dict(data) label = d[self.label] mask_value = d[self.mask_value] - mask = np.uint8(label == mask_value) others = (1 - mask) * label others = self._mask_relabeling(others[0], min_area=self.min_area)[np.newaxis] - d[self.label] = mask d[self.others] = others return d @@ -212,8 +208,7 @@ def __call__(self, data): d[self.image] = image return d - @staticmethod - def inclusion_map(mask): + def inclusion_map(self, mask): point_mask = np.zeros_like(mask) indices = np.argwhere(mask > 0) if len(indices) > 0: @@ -222,8 +217,7 @@ def inclusion_map(mask): return point_mask - @staticmethod - def exclusion_map(others, jitter_range=3, drop_rate=0.5): + def exclusion_map(self, others, jitter_range=3, drop_rate=0.5): point_mask = np.zeros_like(others) if drop_rate == 1.0: return point_mask diff --git a/monai/config/deviceconfig.py b/monai/config/deviceconfig.py index fd7ca572e6..c6628056e7 100644 --- a/monai/config/deviceconfig.py +++ b/monai/config/deviceconfig.py @@ -72,6 +72,7 @@ def get_optional_config_values(): output["lmdb"] = get_package_version("lmdb") output["psutil"] = psutil_version output["pandas"] = get_package_version("pandas") + output["opencv-python"] = get_package_version("opencv-python") output["einops"] = get_package_version("einops") output["transformers"] = get_package_version("transformers") output["mlflow"] = get_package_version("mlflow") diff --git a/requirements-dev.txt b/requirements-dev.txt index 4709b5df92..778f4e0cb6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,6 @@ nibabel pillow!=8.3.0 # https://github.com/python-pillow/Pillow/issues/5571 tensorboard scikit-image>=0.14.2 -opencv-python==4.5.5.64 tqdm>=4.47.0 lmdb flake8>=3.8.1 @@ -38,6 +37,7 @@ openslide-python==1.1.2 imagecodecs; platform_system == "Linux" tifffile; platform_system == "Linux" pandas +opencv-python requests einops transformers diff --git a/setup.cfg b/setup.cfg index 12f974ca6d..8d1594075b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,6 +45,7 @@ all = tifffile imagecodecs pandas + opencv-python einops transformers mlflow diff --git a/tests/test_nuclick_transforms.py b/tests/test_nuclick_transforms.py index 2f33700e60..5276bcb390 100644 --- a/tests/test_nuclick_transforms.py +++ b/tests/test_nuclick_transforms.py @@ -100,6 +100,8 @@ GUIDANCE_TEST_CASE_1 = [{"image": "image", "label": "label", "others": "others"}, DATA_GUIDANCE_1, [5, 5, 5]] # Test Case Classes + + class TestFilterImaged(unittest.TestCase): @parameterized.expand([FILTER_IMAGE_TEST_CASE_1]) def test_correct_shape(self, arguments, input_data, expected_shape): From 3925e8d1233a13f8b82bbfdf24cebce6df362aa9 Mon Sep 17 00:00:00 2001 From: vnath Date: Fri, 20 May 2022 13:30:39 -0500 Subject: [PATCH 11/40] Fixed Flake 8 & opencv addition to requirement & env files Signed-off-by: vnath --- docs/source/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/installation.md b/docs/source/installation.md index 26df31bc56..12bf544cba 100644 --- a/docs/source/installation.md +++ b/docs/source/installation.md @@ -190,7 +190,7 @@ Since MONAI v0.2.0, the extras syntax such as `pip install 'monai[nibabel]'` is - The options are ``` -[nibabel, skimage, pillow, tensorboard, gdown, ignite, torchvision, itk, tqdm, lmdb, psutil, cucim, openslide, pandas, opencv-python, einops, transformers, mlflow, matplotlib, tensorboardX, tifffile, imagecodecs, pyyaml, fire, jsonschema] +[nibabel, skimage, pillow, tensorboard, gdown, ignite, torchvision, itk, tqdm, lmdb, psutil, cucim, openslide, pandas, einops, transformers, mlflow, matplotlib, tensorboardX, tifffile, imagecodecs, pyyaml, fire, jsonschema] ``` which correspond to `nibabel`, `scikit-image`, `pillow`, `tensorboard`, `gdown`, `pytorch-ignite`, `torchvision`, `itk`, `tqdm`, `lmdb`, `psutil`, `cucim`, `openslide-python`, `pandas`, `einops`, `transformers`, `mlflow`, `matplotlib`, `tensorboardX`, `tifffile`, `imagecodecs`, `pyyaml`, `fire`, `jsonschema`, respectively. From a951b428f20a4325c7a63663fed41cc2871f54e5 Mon Sep 17 00:00:00 2001 From: vnath Date: Fri, 20 May 2022 13:49:18 -0500 Subject: [PATCH 12/40] Fixed Flake 8 & opencv addition to requirement & env files Signed-off-by: vnath --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 14eb2b30e9..efed07fd38 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ torch>=1.7 numpy>=1.17 +opencv-python From e8e47fd5ace9d92897aa6a6671a5704d1b74bd12 Mon Sep 17 00:00:00 2001 From: vnath Date: Fri, 20 May 2022 13:52:26 -0500 Subject: [PATCH 13/40] Fixed Flake 8 & opencv addition to requirement & env files Signed-off-by: vnath --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index efed07fd38..6b5bf4eb3f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ torch>=1.7 numpy>=1.17 +scikit-image>=0.14.2 opencv-python From 3c2e9285a70dd8e5f7689b6e4cfc87b62f6692b9 Mon Sep 17 00:00:00 2001 From: vnath Date: Fri, 20 May 2022 17:27:08 -0500 Subject: [PATCH 14/40] Minor in line docs need to be updated Signed-off-by: vnath --- monai/apps/nuclick/transforms.py | 70 ++++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/monai/apps/nuclick/transforms.py b/monai/apps/nuclick/transforms.py index 18d5cda050..2bdd7d098b 100644 --- a/monai/apps/nuclick/transforms.py +++ b/monai/apps/nuclick/transforms.py @@ -26,6 +26,12 @@ class FlattenLabeld(MapTransform): 12 small regions of 1's it will delineate them into 12 different label classes """ + def __init__( + self, + keys: KeysCollection, + ): + super().__init__(keys) + def __call__(self, data): d = dict(data) for key in self.keys: @@ -36,10 +42,23 @@ def __call__(self, data): class ExtractPatchd(MapTransform): """ - Extracts a patch from the given image and label, however it is based on the centroid location + Extracts a patch from the given image and label, however it is based on the centroid location. + The centroid location is a 2D coordinate (H, W). The extracted patch is extracted around the centroid, + if the centroid is towards the edge, the centroid will not be the center of the image as the patch will be + extracted from the edges onwards + + Args: + keys: image, label + centroid_key: key where the centroid values are stored + patch_size: size of the extracted patch """ - def __init__(self, keys: KeysCollection, centroid_key="centroid", patch_size=128): + def __init__( + self, + keys: KeysCollection, + centroid_key: str = "centroid", + patch_size: int = 128 + ): super().__init__(keys) self.centroid_key = centroid_key self.patch_size = patch_size @@ -84,9 +103,22 @@ class SplitLabeld(Transform): """ Extracts a single label from all the given classes, the single label is defined by mask_value, the remaining labels are kept in others + + Args: + label: label source + others: other labels storage key + mask_value: the mask_value that will be kept for binarization of the label + min_area: The smallest allowable object size. """ - def __init__(self, label="label", others="others", mask_value="mask_value", min_area=5): + def __init__( + self, + label: str = "label", + others: str = "others", + mask_value: str = "mask_value", + min_area: int = 5 + ): + self.label = label self.others = others self.mask_value = mask_value @@ -118,10 +150,20 @@ def _mask_relabeling(self, mask, min_area=5): class FilterImaged(MapTransform): """ - Filters Green and Gray channel of the image + Filters Green and Gray channel of the image using an allowable object size, this pre-processing transform + is specific towards NuClick training process. More details can be referred in this paper Koohbanani, + Navid Alemi, et al. "NuClick: a deep learning framework for interactive segmentation of microscopic images." + Medical Image Analysis 65 (2020): 101771. + + Args: + min_size: The smallest allowable object size """ - def __init__(self, keys: KeysCollection, min_size: int = 500): + def __init__( + self, + keys: KeysCollection, + min_size: int = 500 + ): super().__init__(keys) self.min_size = min_size @@ -182,10 +224,24 @@ def filter_remove_small_objects(self, img_np, min_size=3000, avoid_overmask=True class AddPointGuidanceSignald(RandomizableTransform): """ - Add Guidance Signal + Adds Guidance Signal to the input image + + Args: + image: source image + label: source label + others: source others (other labels from the binary mask which are not being used for training) + drop_rate: + jitter_range: noise added to the points in the point mask for exclusion mask """ - def __init__(self, image="image", label="label", others="others", drop_rate=0.5, jitter_range=3): + def __init__( + self, + image: str = "image", + label: str = "label", + others: str = "others", + drop_rate: float = 0.5, + jitter_range: int = 3 + ): super().__init__() self.image = image From 2f49e1ab005ef5a0fd52ad55b768f19098e3cf23 Mon Sep 17 00:00:00 2001 From: vnath Date: Fri, 20 May 2022 18:10:18 -0500 Subject: [PATCH 15/40] Minor in line docs need to be updated Signed-off-by: vnath --- monai/apps/nuclick/transforms.py | 38 +++++++++----------------------- 1 file changed, 10 insertions(+), 28 deletions(-) diff --git a/monai/apps/nuclick/transforms.py b/monai/apps/nuclick/transforms.py index 2bdd7d098b..5c7cf2c592 100644 --- a/monai/apps/nuclick/transforms.py +++ b/monai/apps/nuclick/transforms.py @@ -26,10 +26,7 @@ class FlattenLabeld(MapTransform): 12 small regions of 1's it will delineate them into 12 different label classes """ - def __init__( - self, - keys: KeysCollection, - ): + def __init__(self, keys: KeysCollection): super().__init__(keys) def __call__(self, data): @@ -53,12 +50,7 @@ class ExtractPatchd(MapTransform): patch_size: size of the extracted patch """ - def __init__( - self, - keys: KeysCollection, - centroid_key: str = "centroid", - patch_size: int = 128 - ): + def __init__(self, keys: KeysCollection, centroid_key: str = "centroid", patch_size: int = 128): super().__init__(keys) self.centroid_key = centroid_key self.patch_size = patch_size @@ -111,13 +103,7 @@ class SplitLabeld(Transform): min_area: The smallest allowable object size. """ - def __init__( - self, - label: str = "label", - others: str = "others", - mask_value: str = "mask_value", - min_area: int = 5 - ): + def __init__(self, label: str = "label", others: str = "others", mask_value: str = "mask_value", min_area: int = 5): self.label = label self.others = others @@ -159,11 +145,7 @@ class FilterImaged(MapTransform): min_size: The smallest allowable object size """ - def __init__( - self, - keys: KeysCollection, - min_size: int = 500 - ): + def __init__(self, keys: KeysCollection, min_size: int = 500): super().__init__(keys) self.min_size = min_size @@ -235,12 +217,12 @@ class AddPointGuidanceSignald(RandomizableTransform): """ def __init__( - self, - image: str = "image", - label: str = "label", - others: str = "others", - drop_rate: float = 0.5, - jitter_range: int = 3 + self, + image: str = "image", + label: str = "label", + others: str = "others", + drop_rate: float = 0.5, + jitter_range: int = 3, ): super().__init__() From 865ffe2d15b9f0d0ec21a0007449db8efd9f2452 Mon Sep 17 00:00:00 2001 From: vnath Date: Mon, 23 May 2022 20:35:42 -0500 Subject: [PATCH 16/40] Adding Two Transforms & Test Cases Signed-off-by: vnath --- monai/apps/nuclick/transforms.py | 197 ++++++++++++++++++++++++++++++- tests/test_nuclick_transforms.py | 188 ++++++++++++++++++++++------- 2 files changed, 339 insertions(+), 46 deletions(-) diff --git a/monai/apps/nuclick/transforms.py b/monai/apps/nuclick/transforms.py index 5c7cf2c592..52dc3da2fb 100644 --- a/monai/apps/nuclick/transforms.py +++ b/monai/apps/nuclick/transforms.py @@ -14,7 +14,7 @@ import cv2 import numpy as np import skimage -from skimage.morphology import remove_small_objects +from skimage.morphology import disk, reconstruction, remove_small_holes, remove_small_objects from monai.config import KeysCollection from monai.transforms import MapTransform, RandomizableTransform, Transform @@ -276,3 +276,198 @@ def exclusion_map(self, others, jitter_range=3, drop_rate=0.5): point_mask[x, y] = 1 return point_mask + +class AddClickSignalsd(Transform): + + def __init__(self, image, foreground="foreground", bb_size=128): + self.image = image + self.foreground = foreground + self.bb_size = bb_size + + def __call__(self, data): + d = dict(data) + + location = d.get("location", (0, 0)) + tx, ty = location[0], location[1] + pos = d.get(self.foreground) + pos = (np.array(pos) - (tx, ty)).astype(int).tolist() if pos else [] + + cx = [xy[0] for xy in pos] + cy = [xy[1] for xy in pos] + + img = d[self.image].astype(np.uint8) + img_width = img.shape[-1] + img_height = img.shape[-2] + + click_map, bounding_boxes = self.get_clickmap_boundingbox( + cx=cx, + cy=cy, + m=img_height, + n=img_width, + bb=self.bb_size + ) + + patches, nuc_points, other_points = self.get_patches_and_signals( + img=img, + click_map=click_map, + bounding_boxes=bounding_boxes, + cx=cx, + cy=cy, + m=img_height, + n=img_width, + bb=self.bb_size + ) + patches = patches / 255 + + d["bounding_boxes"] = bounding_boxes + d["img_width"] = img_width + d["img_height"] = img_height + d["nuc_points"] = nuc_points + + d[self.image] = np.concatenate((patches, nuc_points, other_points), axis=1, dtype=np.float32) + return d + + + def get_clickmap_boundingbox(self, cx, cy, m, n, bb=128): + click_map = np.zeros((m, n), dtype=np.uint8) + + # Removing points out of image dimension (these points may have been clicked unwanted) + x_del_indices = {i for i in range(len(cx)) if cx[i] >= n or cx[i] < 0} + y_del_indices = {i for i in range(len(cy)) if cy[i] >= m or cy[i] < 0} + del_indices = list(x_del_indices.union(y_del_indices)) + cx = np.delete(cx, del_indices) + cy = np.delete(cy, del_indices) + + click_map[cy, cx] = 1 + bounding_boxes = [] + for i in range(len(cx)): + x_start = cx[i] - bb // 2 + y_start = cy[i] - bb // 2 + if x_start < 0: + x_start = 0 + if y_start < 0: + y_start = 0 + x_end = x_start + bb - 1 + y_end = y_start + bb - 1 + if x_end > n - 1: + x_end = n - 1 + x_start = x_end - bb + 1 + if y_end > m - 1: + y_end = m - 1 + y_start = y_end - bb + 1 + bounding_boxes.append([x_start, y_start, x_end, y_end]) + return click_map, bounding_boxes + + + def get_patches_and_signals(self, img, click_map, bounding_boxes, cx, cy, m, n, bb=128): + # total = number of clicks + total = len(bounding_boxes) + img = np.array([img]) # img.shape=(1,3,m,n) + click_map = np.array([click_map]) # clickmap.shape=(1,m,n) + click_map = click_map[:, np.newaxis, ...] # clickmap.shape=(1,1,m,n) + + patches = np.ndarray((total, 3, bb, bb), dtype=np.uint8) + nuc_points = np.ndarray((total, 1, bb, bb), dtype=np.uint8) + other_points = np.ndarray((total, 1, bb, bb), dtype=np.uint8) + + # Removing points out of image dimension (these points may have been clicked unwanted) + x_del_indices = {i for i in range(len(cx)) if cx[i] >= n or cx[i] < 0} + y_del_indices = {i for i in range(len(cy)) if cy[i] >= m or cy[i] < 0} + del_indices = list(x_del_indices.union(y_del_indices)) + cx = np.delete(cx, del_indices) + cy = np.delete(cy, del_indices) + + for i in range(len(bounding_boxes)): + bounding_box = bounding_boxes[i] + x_start = bounding_box[0] + y_start = bounding_box[1] + x_end = bounding_box[2] + y_end = bounding_box[3] + + patches[i] = img[0, :, y_start : y_end + 1, x_start : x_end + 1] + + this_click_map = np.zeros((1, 1, m, n), dtype=np.uint8) + this_click_map[0, 0, cy[i], cx[i]] = 1 + + others_click_map = np.uint8((click_map - this_click_map) > 0) + + nuc_points[i] = this_click_map[0, :, y_start : y_end + 1, x_start : x_end + 1] + other_points[i] = others_click_map[0, :, y_start : y_end + 1, x_start : x_end + 1] + + # patches: (total, 3, m, n) + # nuc_points: (total, 1, m, n) + # other_points: (total, 1, m, n) + return patches, nuc_points, other_points + + +class PostFilterLabeld(MapTransform): + def __init__( + self, + keys: KeysCollection, + nuc_points="nuc_points", + bounding_boxes="bounding_boxes", + img_height="img_height", + img_width="img_width", + thresh=0.33, + min_size=10, + min_hole=30, + do_reconstruction=False, + ): + super().__init__(keys) + self.nuc_points = nuc_points + self.bounding_boxes = bounding_boxes + self.img_height = img_height + self.img_width = img_width + + self.thresh = thresh + self.min_size = min_size + self.min_hole = min_hole + self.do_reconstruction = do_reconstruction + + def __call__(self, data): + d = dict(data) + + nuc_points = d[self.nuc_points] + bounding_boxes = d[self.bounding_boxes] + img_height = d[self.img_height] + img_width = d[self.img_width] + + for key in self.keys: + label = d[key].astype(np.uint8) + masks = self.post_processing( + label, + thresh=self.thresh, + min_size=self.min_size, + min_hole=self.min_hole, + do_reconstruction=self.do_reconstruction, + nuc_points=nuc_points, + ) + + d[key] = self.gen_instance_map(masks, bounding_boxes, img_height, img_width).astype(np.uint8) + return d + + def post_processing(self, preds, thresh=0.33, min_size=10, min_hole=30, do_reconstruction=False, nuc_points=None): + masks = preds > thresh + masks = remove_small_objects(masks, min_size=min_size) + masks = remove_small_holes(masks, area_threshold=min_hole) + if do_reconstruction: + for i in range(len(masks)): + this_mask = masks[i] + this_marker = nuc_points[i, 0, :, :] > 0 + + try: + this_mask = reconstruction(this_marker, this_mask, footprint=disk(1)) + masks[i] = np.array([this_mask]) + except: + print("Nuclei reconstruction error #" + str(i)) + return masks # masks(no.patches, 128, 128) + + def gen_instance_map(self, masks, bounding_boxes, m, n, flatten=True): + instance_map = np.zeros((m, n), dtype=np.uint16) + for i in range(len(masks)): + this_bb = bounding_boxes[i] + this_mask_pos = np.argwhere(masks[i] > 0) + this_mask_pos[:, 0] = this_mask_pos[:, 0] + this_bb[1] + this_mask_pos[:, 1] = this_mask_pos[:, 1] + this_bb[0] + instance_map[this_mask_pos[:, 0], this_mask_pos[:, 1]] = 1 if flatten else i + 1 + return instance_map diff --git a/tests/test_nuclick_transforms.py b/tests/test_nuclick_transforms.py index 5276bcb390..e303d6319e 100644 --- a/tests/test_nuclick_transforms.py +++ b/tests/test_nuclick_transforms.py @@ -15,56 +15,130 @@ from parameterized import parameterized from monai.apps.nuclick.transforms import ( - AddPointGuidanceSignald, - ExtractPatchd, - FilterImaged, - FlattenLabeld, - SplitLabeld, +FlattenLabeld, +FilterImaged, +ExtractPatchd, +SplitLabeld, +AddPointGuidanceSignald, +AddClickSignalsd, +PostFilterLabeld ) # Data Definitions -RGB_IMAGE_1 = np.array( - [[[0, 0, 0], [0, 1, 0], [0, 0, 1]], [[2, 0, 2], [0, 1, 0], [1, 0, 1]], [[3, 0, 2], [0, 1, 0], [1, 3, 1]]] -) - -LABEL_1 = np.array( - [ - [1, 1, 1, 0, 0, 0, 0], - [1, 1, 1, 0, 0, 0, 0], - [1, 1, 1, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0], - [1, 1, 1, 0, 1, 1, 1], - [1, 1, 1, 0, 1, 1, 1], - [1, 1, 1, 0, 1, 1, 1], - ], - dtype=np.uint8, -) - -LABEL_2 = np.array([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], dtype=np.uint8) - -LABEL_3 = np.array([[[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3], [4, 4, 4, 4]]], dtype=np.uint8) +RGB_IMAGE_1 = np.array([ + [[0, 0, 0], + [0, 1, 0], + [0, 0, 1]], + [[2, 0, 2], + [0, 1, 0], + [1, 0, 1]], + [[3, 0, 2], + [0, 1, 0], + [1, 3, 1]] + ]) + +LABEL_1 = np.array([[1, 1, 1, 0, 0, 0, 0], + [1, 1, 1, 0, 0, 0, 0], + [1, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 0, 1, 1, 1], + [1, 1, 1, 0, 1, 1, 1], + [1, 1, 1, 0, 1, 1, 1]], dtype=np.uint8) + +LABEL_2 = np.array([[0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0]], dtype=np.uint8) + +LABEL_3 = np.array([[[1, 1, 1, 1], + [2, 2, 2, 2], + [3, 3, 3, 3], + [4, 4, 4, 4]]], dtype=np.uint8) IL_IMAGE_1 = np.array( - [ - [[0, 0, 0, 0, 0], [0, 1, 0, 0, 0], [0, 0, 1, 1, 1], [0, 0, 1, 1, 1], [0, 0, 1, 1, 1]], - [[0, 0, 0, 0, 0], [0, 1, 0, 0, 0], [0, 0, 1, 1, 1], [0, 0, 1, 1, 1], [0, 0, 1, 1, 1]], - [[0, 0, 0, 0, 0], [0, 1, 0, 0, 0], [0, 0, 1, 1, 1], [0, 0, 1, 1, 1], [0, 0, 1, 1, 1]], - ] -) + [ + [[0, 0, 0, 0, 0], + [0, 1, 0, 0, 0], + [0, 0, 1, 1, 1], + [0, 0, 1, 1, 1], + [0, 0, 1, 1, 1]], + [[0, 0, 0, 0, 0], + [0, 1, 0, 0, 0], + [0, 0, 1, 1, 1], + [0, 0, 1, 1, 1], + [0, 0, 1, 1, 1]], + [[0, 0, 0, 0, 0], + [0, 1, 0, 0, 0], + [0, 0, 1, 1, 1], + [0, 0, 1, 1, 1], + [0, 0, 1, 1, 1]], + ] + ) + +IL_FG_IMAGE_1 = np.array( + [[0, 0, 0, 0, 0], + [0, 1, 0, 0, 0], + [0, 0, 1, 1, 1], + [0, 0, 1, 1, 1], + [0, 0, 1, 1, 1]] + ) IL_LABEL_1 = np.array( - [[[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 1, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]], dtype=np.uint8 -) + [[[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]]], dtype=np.uint8 + ) IL_OTHERS_1 = np.array( - [[[1, 1, 1, 1, 1], [2, 0, 0, 0, 2], [3, 0, 0, 0, 3], [4, 0, 0, 0, 4], [5, 5, 5, 5, 5]]], dtype=np.uint8 -) + [[[1, 1, 1, 1, 1], + [2, 0, 0, 0, 2], + [3, 0, 0, 0, 3], + [4, 0, 0, 0, 4], + [5, 5, 5, 5, 5]]], dtype=np.uint8 + ) IL_IMAGE_2 = np.array( - [[[0, 0, 0], [0, 1, 0], [0, 0, 1]], [[0, 0, 0], [0, 1, 0], [0, 0, 1]], [[0, 0, 0], [0, 1, 0], [0, 0, 1]]] -) - -IL_LABEL_2 = np.array([[[0, 0, 0], [0, 1, 0], [0, 0, 0]]], dtype=np.uint8) + [ + [[0, 0, 0], + [0, 1, 0], + [0, 0, 1]], + [[0, 0, 0], + [0, 1, 0], + [0, 0, 1]], + [[0, 0, 0], + [0, 1, 0], + [0, 0, 1]] + ] + ) + +IL_LABEL_2 = np.array( + [[[0, 0, 0], + [0, 1, 0], + [0, 0, 0]]], dtype=np.uint8 + ) + +PRED_1 = np.array( + [[[1, 1, 1, 1, 1], + [2, 0, 0, 0, 2], + [3, 0, 0, 0, 3], + [4, 0, 0, 0, 4], + [5, 5, 5, 5, 5]]], dtype=np.float + ) + +NUC_POINTS_1 = np.array( + [[[[0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 0]]], + [[[0, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0]]]], + dtype=np.float + ) +BB_1 = np.array([[1, 1, 3, 3], [0, 0, 2, 2]], dtype=np.uint8) DATA_FILTER_1 = {"image": RGB_IMAGE_1} @@ -78,10 +152,23 @@ DATA_GUIDANCE_1 = {"image": IL_IMAGE_1, "label": IL_LABEL_1, "others": IL_OTHERS_1, "centroid": (2, 2)} +DATA_CLICK_1 = {"image": IL_IMAGE_1, "foreground": [[2, 2], [1, 1]]} + +DATA_LABEL_FILTER_1 = {"pred": PRED_1, + "nuc_points": NUC_POINTS_1, + "bounding_boxes": BB_1, + "img_height": 6, + "img_width": 6} + # Result Definitions -EXTRACT_RESULT_TC1 = np.array([[[0, 0, 0], [0, 0, 0], [0, 0, 1]]], dtype=np.uint8) +EXTRACT_RESULT_TC1 = np.array([[[0, 0, 0], + [0, 0, 0], + [0, 0, 1]]], dtype=np.uint8) -SPLIT_RESULT_TC1 = np.array([[[1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]], dtype=np.uint8) +SPLIT_RESULT_TC1 = np.array([[[1, 1, 1, 1], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0]]], dtype=np.uint8) # Test Case Definitions FILTER_IMAGE_TEST_CASE_1 = [{"keys": "image", "min_size": 1}, DATA_FILTER_1, [3, 3, 3]] @@ -99,6 +186,10 @@ GUIDANCE_TEST_CASE_1 = [{"image": "image", "label": "label", "others": "others"}, DATA_GUIDANCE_1, [5, 5, 5]] +CLICK_TEST_CASE_1 = [{"image": "image", "foreground": "foreground", "bb_size": 4}, DATA_CLICK_1, [2, 5, 4, 4]] + +LABEL_FILTER_TEST_CASE_1 = [{"keys": ["pred"]}, DATA_LABEL_FILTER_1, [6, 6]] + # Test Case Classes @@ -108,14 +199,12 @@ def test_correct_shape(self, arguments, input_data, expected_shape): result = FilterImaged(**arguments)(input_data) np.testing.assert_equal(result["image"].shape, expected_shape) - class TestFlattenLabeld(unittest.TestCase): @parameterized.expand([FLATTEN_LABEL_TEST_CASE_1, FLATTEN_LABEL_TEST_CASE_2]) def test_correct_num_labels(self, arguments, input_data, expected_result): result = FlattenLabeld(**arguments)(input_data) np.testing.assert_equal(np.unique(result["label"]), expected_result) - class TestExtractPatchd(unittest.TestCase): @parameterized.expand([EXTRACT_TEST_CASE_1, EXTRACT_TEST_CASE_2, EXTRACT_TEST_CASE_3]) def test_correct_patch_size(self, arguments, input_data, expected_shape): @@ -127,20 +216,29 @@ def test_correct_results(self, arguments, input_data, expected_result): result = ExtractPatchd(**arguments)(input_data) np.testing.assert_equal(result["label"], expected_result) - class TestSplitLabelsd(unittest.TestCase): @parameterized.expand([SPLIT_TEST_CASE_1]) def test_correct_results(self, arguments, input_data, expected_result): result = SplitLabeld(**arguments)(input_data) np.testing.assert_equal(result["label"], expected_result) - class TestGuidanceSignal(unittest.TestCase): @parameterized.expand([GUIDANCE_TEST_CASE_1]) def test_correct_shape(self, arguments, input_data, expected_shape): result = AddPointGuidanceSignald(**arguments)(input_data) np.testing.assert_equal(result["image"].shape, expected_shape) +class TestClickSignal(unittest.TestCase): + @parameterized.expand([CLICK_TEST_CASE_1]) + def test_correct_shape(self, arguments, input_data, expected_shape): + result = AddClickSignalsd(**arguments)(input_data) + np.testing.assert_equal(result["image"].shape, expected_shape) + +class TestPostFilterLabel(unittest.TestCase): + @parameterized.expand([LABEL_FILTER_TEST_CASE_1]) + def test_correct_shape(self, arguments, input_data, expected_shape): + result = PostFilterLabeld(**arguments)(input_data) + np.testing.assert_equal(result["pred"].shape, expected_shape) if __name__ == "__main__": unittest.main() From ae5209f7f9a75abd2fc64d43b2e2e494c0b1f3d1 Mon Sep 17 00:00:00 2001 From: vnath Date: Mon, 23 May 2022 21:23:12 -0500 Subject: [PATCH 17/40] Formatted the PR Signed-off-by: vnath --- environment-dev.yml | 2 +- monai/apps/nuclick/transforms.py | 14 +-- monai/config/deviceconfig.py | 2 +- requirements-dev.txt | 2 +- requirements.txt | 2 +- setup.cfg | 2 +- tests/test_nuclick_transforms.py | 187 +++++++++++-------------------- 7 files changed, 76 insertions(+), 135 deletions(-) diff --git a/environment-dev.yml b/environment-dev.yml index 931485874f..f473f4addb 100644 --- a/environment-dev.yml +++ b/environment-dev.yml @@ -37,7 +37,7 @@ dependencies: - sphinx-autodoc-typehints==1.11.1 - sphinx_rtd_theme==0.5.2 - pandas - - opencv-python + - opencv-python-headless - requests - einops - transformers diff --git a/monai/apps/nuclick/transforms.py b/monai/apps/nuclick/transforms.py index 52dc3da2fb..c6c6f4f086 100644 --- a/monai/apps/nuclick/transforms.py +++ b/monai/apps/nuclick/transforms.py @@ -277,8 +277,8 @@ def exclusion_map(self, others, jitter_range=3, drop_rate=0.5): return point_mask -class AddClickSignalsd(Transform): +class AddClickSignalsd(Transform): def __init__(self, image, foreground="foreground", bb_size=128): self.image = image self.foreground = foreground @@ -300,11 +300,7 @@ def __call__(self, data): img_height = img.shape[-2] click_map, bounding_boxes = self.get_clickmap_boundingbox( - cx=cx, - cy=cy, - m=img_height, - n=img_width, - bb=self.bb_size + cx=cx, cy=cy, m=img_height, n=img_width, bb=self.bb_size ) patches, nuc_points, other_points = self.get_patches_and_signals( @@ -315,7 +311,7 @@ def __call__(self, data): cy=cy, m=img_height, n=img_width, - bb=self.bb_size + bb=self.bb_size, ) patches = patches / 255 @@ -327,7 +323,6 @@ def __call__(self, data): d[self.image] = np.concatenate((patches, nuc_points, other_points), axis=1, dtype=np.float32) return d - def get_clickmap_boundingbox(self, cx, cy, m, n, bb=128): click_map = np.zeros((m, n), dtype=np.uint8) @@ -358,7 +353,6 @@ def get_clickmap_boundingbox(self, cx, cy, m, n, bb=128): bounding_boxes.append([x_start, y_start, x_end, y_end]) return click_map, bounding_boxes - def get_patches_and_signals(self, img, click_map, bounding_boxes, cx, cy, m, n, bb=128): # total = number of clicks total = len(bounding_boxes) @@ -458,7 +452,7 @@ def post_processing(self, preds, thresh=0.33, min_size=10, min_hole=30, do_recon try: this_mask = reconstruction(this_marker, this_mask, footprint=disk(1)) masks[i] = np.array([this_mask]) - except: + except BaseException: print("Nuclei reconstruction error #" + str(i)) return masks # masks(no.patches, 128, 128) diff --git a/monai/config/deviceconfig.py b/monai/config/deviceconfig.py index c6628056e7..3559c284e4 100644 --- a/monai/config/deviceconfig.py +++ b/monai/config/deviceconfig.py @@ -72,7 +72,7 @@ def get_optional_config_values(): output["lmdb"] = get_package_version("lmdb") output["psutil"] = psutil_version output["pandas"] = get_package_version("pandas") - output["opencv-python"] = get_package_version("opencv-python") + output["opencv-python-headless"] = get_package_version("opencv-python-headless") output["einops"] = get_package_version("einops") output["transformers"] = get_package_version("transformers") output["mlflow"] = get_package_version("mlflow") diff --git a/requirements-dev.txt b/requirements-dev.txt index 778f4e0cb6..7bf2acd383 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -37,7 +37,7 @@ openslide-python==1.1.2 imagecodecs; platform_system == "Linux" tifffile; platform_system == "Linux" pandas -opencv-python +opencv-python-headless requests einops transformers diff --git a/requirements.txt b/requirements.txt index 6b5bf4eb3f..cdf79bcc62 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ torch>=1.7 numpy>=1.17 scikit-image>=0.14.2 -opencv-python +opencv-python-headless diff --git a/setup.cfg b/setup.cfg index 8d1594075b..158a7371a0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,7 +45,7 @@ all = tifffile imagecodecs pandas - opencv-python + opencv-python-headless einops transformers mlflow diff --git a/tests/test_nuclick_transforms.py b/tests/test_nuclick_transforms.py index e303d6319e..353e9943b3 100644 --- a/tests/test_nuclick_transforms.py +++ b/tests/test_nuclick_transforms.py @@ -15,129 +15,72 @@ from parameterized import parameterized from monai.apps.nuclick.transforms import ( -FlattenLabeld, -FilterImaged, -ExtractPatchd, -SplitLabeld, -AddPointGuidanceSignald, -AddClickSignalsd, -PostFilterLabeld + AddClickSignalsd, + AddPointGuidanceSignald, + ExtractPatchd, + FilterImaged, + FlattenLabeld, + PostFilterLabeld, + SplitLabeld, ) # Data Definitions -RGB_IMAGE_1 = np.array([ - [[0, 0, 0], - [0, 1, 0], - [0, 0, 1]], - [[2, 0, 2], - [0, 1, 0], - [1, 0, 1]], - [[3, 0, 2], - [0, 1, 0], - [1, 3, 1]] - ]) - -LABEL_1 = np.array([[1, 1, 1, 0, 0, 0, 0], - [1, 1, 1, 0, 0, 0, 0], - [1, 1, 1, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0], - [1, 1, 1, 0, 1, 1, 1], - [1, 1, 1, 0, 1, 1, 1], - [1, 1, 1, 0, 1, 1, 1]], dtype=np.uint8) - -LABEL_2 = np.array([[0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0]], dtype=np.uint8) - -LABEL_3 = np.array([[[1, 1, 1, 1], - [2, 2, 2, 2], - [3, 3, 3, 3], - [4, 4, 4, 4]]], dtype=np.uint8) +RGB_IMAGE_1 = np.array( + [[[0, 0, 0], [0, 1, 0], [0, 0, 1]], [[2, 0, 2], [0, 1, 0], [1, 0, 1]], [[3, 0, 2], [0, 1, 0], [1, 3, 1]]] +) + +LABEL_1 = np.array( + [ + [1, 1, 1, 0, 0, 0, 0], + [1, 1, 1, 0, 0, 0, 0], + [1, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 0, 1, 1, 1], + [1, 1, 1, 0, 1, 1, 1], + [1, 1, 1, 0, 1, 1, 1], + ], + dtype=np.uint8, +) + +LABEL_2 = np.array([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], dtype=np.uint8) + +LABEL_3 = np.array([[[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3], [4, 4, 4, 4]]], dtype=np.uint8) IL_IMAGE_1 = np.array( - [ - [[0, 0, 0, 0, 0], - [0, 1, 0, 0, 0], - [0, 0, 1, 1, 1], - [0, 0, 1, 1, 1], - [0, 0, 1, 1, 1]], - [[0, 0, 0, 0, 0], - [0, 1, 0, 0, 0], - [0, 0, 1, 1, 1], - [0, 0, 1, 1, 1], - [0, 0, 1, 1, 1]], - [[0, 0, 0, 0, 0], - [0, 1, 0, 0, 0], - [0, 0, 1, 1, 1], - [0, 0, 1, 1, 1], - [0, 0, 1, 1, 1]], - ] - ) - -IL_FG_IMAGE_1 = np.array( - [[0, 0, 0, 0, 0], - [0, 1, 0, 0, 0], - [0, 0, 1, 1, 1], - [0, 0, 1, 1, 1], - [0, 0, 1, 1, 1]] - ) + [ + [[0, 0, 0, 0, 0], [0, 1, 0, 0, 0], [0, 0, 1, 1, 1], [0, 0, 1, 1, 1], [0, 0, 1, 1, 1]], + [[0, 0, 0, 0, 0], [0, 1, 0, 0, 0], [0, 0, 1, 1, 1], [0, 0, 1, 1, 1], [0, 0, 1, 1, 1]], + [[0, 0, 0, 0, 0], [0, 1, 0, 0, 0], [0, 0, 1, 1, 1], [0, 0, 1, 1, 1], [0, 0, 1, 1, 1]], + ] +) + +IL_FG_IMAGE_1 = np.array([[0, 0, 0, 0, 0], [0, 1, 0, 0, 0], [0, 0, 1, 1, 1], [0, 0, 1, 1, 1], [0, 0, 1, 1, 1]]) IL_LABEL_1 = np.array( - [[[0, 0, 0, 0, 0], - [0, 0, 0, 0, 0], - [0, 0, 1, 0, 0], - [0, 0, 0, 0, 0], - [0, 0, 0, 0, 0]]], dtype=np.uint8 - ) + [[[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 1, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]], dtype=np.uint8 +) IL_OTHERS_1 = np.array( - [[[1, 1, 1, 1, 1], - [2, 0, 0, 0, 2], - [3, 0, 0, 0, 3], - [4, 0, 0, 0, 4], - [5, 5, 5, 5, 5]]], dtype=np.uint8 - ) + [[[1, 1, 1, 1, 1], [2, 0, 0, 0, 2], [3, 0, 0, 0, 3], [4, 0, 0, 0, 4], [5, 5, 5, 5, 5]]], dtype=np.uint8 +) IL_IMAGE_2 = np.array( - [ - [[0, 0, 0], - [0, 1, 0], - [0, 0, 1]], - [[0, 0, 0], - [0, 1, 0], - [0, 0, 1]], - [[0, 0, 0], - [0, 1, 0], - [0, 0, 1]] - ] - ) - -IL_LABEL_2 = np.array( - [[[0, 0, 0], - [0, 1, 0], - [0, 0, 0]]], dtype=np.uint8 - ) + [[[0, 0, 0], [0, 1, 0], [0, 0, 1]], [[0, 0, 0], [0, 1, 0], [0, 0, 1]], [[0, 0, 0], [0, 1, 0], [0, 0, 1]]] +) + +IL_LABEL_2 = np.array([[[0, 0, 0], [0, 1, 0], [0, 0, 0]]], dtype=np.uint8) PRED_1 = np.array( - [[[1, 1, 1, 1, 1], - [2, 0, 0, 0, 2], - [3, 0, 0, 0, 3], - [4, 0, 0, 0, 4], - [5, 5, 5, 5, 5]]], dtype=np.float - ) + [[[1, 1, 1, 1, 1], [2, 0, 0, 0, 2], [3, 0, 0, 0, 3], [4, 0, 0, 0, 4], [5, 5, 5, 5, 5]]], dtype=np.float32 +) NUC_POINTS_1 = np.array( - [[[[0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 1, 0], - [0, 0, 0, 0]]], - [[[0, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0]]]], - dtype=np.float - ) + [ + [[[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, 0]]], + [[[0, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]], + ], + dtype=np.float32, +) BB_1 = np.array([[1, 1, 3, 3], [0, 0, 2, 2]], dtype=np.uint8) DATA_FILTER_1 = {"image": RGB_IMAGE_1} @@ -154,21 +97,18 @@ DATA_CLICK_1 = {"image": IL_IMAGE_1, "foreground": [[2, 2], [1, 1]]} -DATA_LABEL_FILTER_1 = {"pred": PRED_1, - "nuc_points": NUC_POINTS_1, - "bounding_boxes": BB_1, - "img_height": 6, - "img_width": 6} +DATA_LABEL_FILTER_1 = { + "pred": PRED_1, + "nuc_points": NUC_POINTS_1, + "bounding_boxes": BB_1, + "img_height": 6, + "img_width": 6, +} # Result Definitions -EXTRACT_RESULT_TC1 = np.array([[[0, 0, 0], - [0, 0, 0], - [0, 0, 1]]], dtype=np.uint8) +EXTRACT_RESULT_TC1 = np.array([[[0, 0, 0], [0, 0, 0], [0, 0, 1]]], dtype=np.uint8) -SPLIT_RESULT_TC1 = np.array([[[1, 1, 1, 1], - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0]]], dtype=np.uint8) +SPLIT_RESULT_TC1 = np.array([[[1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]], dtype=np.uint8) # Test Case Definitions FILTER_IMAGE_TEST_CASE_1 = [{"keys": "image", "min_size": 1}, DATA_FILTER_1, [3, 3, 3]] @@ -199,12 +139,14 @@ def test_correct_shape(self, arguments, input_data, expected_shape): result = FilterImaged(**arguments)(input_data) np.testing.assert_equal(result["image"].shape, expected_shape) + class TestFlattenLabeld(unittest.TestCase): @parameterized.expand([FLATTEN_LABEL_TEST_CASE_1, FLATTEN_LABEL_TEST_CASE_2]) def test_correct_num_labels(self, arguments, input_data, expected_result): result = FlattenLabeld(**arguments)(input_data) np.testing.assert_equal(np.unique(result["label"]), expected_result) + class TestExtractPatchd(unittest.TestCase): @parameterized.expand([EXTRACT_TEST_CASE_1, EXTRACT_TEST_CASE_2, EXTRACT_TEST_CASE_3]) def test_correct_patch_size(self, arguments, input_data, expected_shape): @@ -216,29 +158,34 @@ def test_correct_results(self, arguments, input_data, expected_result): result = ExtractPatchd(**arguments)(input_data) np.testing.assert_equal(result["label"], expected_result) + class TestSplitLabelsd(unittest.TestCase): @parameterized.expand([SPLIT_TEST_CASE_1]) def test_correct_results(self, arguments, input_data, expected_result): result = SplitLabeld(**arguments)(input_data) np.testing.assert_equal(result["label"], expected_result) + class TestGuidanceSignal(unittest.TestCase): @parameterized.expand([GUIDANCE_TEST_CASE_1]) def test_correct_shape(self, arguments, input_data, expected_shape): result = AddPointGuidanceSignald(**arguments)(input_data) np.testing.assert_equal(result["image"].shape, expected_shape) + class TestClickSignal(unittest.TestCase): @parameterized.expand([CLICK_TEST_CASE_1]) def test_correct_shape(self, arguments, input_data, expected_shape): result = AddClickSignalsd(**arguments)(input_data) np.testing.assert_equal(result["image"].shape, expected_shape) + class TestPostFilterLabel(unittest.TestCase): @parameterized.expand([LABEL_FILTER_TEST_CASE_1]) def test_correct_shape(self, arguments, input_data, expected_shape): result = PostFilterLabeld(**arguments)(input_data) np.testing.assert_equal(result["pred"].shape, expected_shape) + if __name__ == "__main__": unittest.main() From e3e3b6cd73e4f583fb61c9d946d6069e1ea36665 Mon Sep 17 00:00:00 2001 From: vnath Date: Mon, 23 May 2022 22:04:48 -0500 Subject: [PATCH 18/40] opencv changes Signed-off-by: vnath --- environment-dev.yml | 1 + monai/apps/nuclick/transforms.py | 3 ++- monai/config/deviceconfig.py | 2 +- requirements-dev.txt | 1 + requirements.txt | 1 + setup.cfg | 1 + 6 files changed, 7 insertions(+), 2 deletions(-) diff --git a/environment-dev.yml b/environment-dev.yml index f473f4addb..af86cbf02d 100644 --- a/environment-dev.yml +++ b/environment-dev.yml @@ -38,6 +38,7 @@ dependencies: - sphinx_rtd_theme==0.5.2 - pandas - opencv-python-headless + - opencv-python - requests - einops - transformers diff --git a/monai/apps/nuclick/transforms.py b/monai/apps/nuclick/transforms.py index c6c6f4f086..9d48da1527 100644 --- a/monai/apps/nuclick/transforms.py +++ b/monai/apps/nuclick/transforms.py @@ -11,14 +11,15 @@ import math import random -import cv2 import numpy as np import skimage from skimage.morphology import disk, reconstruction, remove_small_holes, remove_small_objects from monai.config import KeysCollection from monai.transforms import MapTransform, RandomizableTransform, Transform +from monai.utils import optional_import +cv2, _ = optional_import("cv2") class FlattenLabeld(MapTransform): """ diff --git a/monai/config/deviceconfig.py b/monai/config/deviceconfig.py index 3559c284e4..051cdf45db 100644 --- a/monai/config/deviceconfig.py +++ b/monai/config/deviceconfig.py @@ -72,7 +72,7 @@ def get_optional_config_values(): output["lmdb"] = get_package_version("lmdb") output["psutil"] = psutil_version output["pandas"] = get_package_version("pandas") - output["opencv-python-headless"] = get_package_version("opencv-python-headless") + output["cv2"] = get_package_version("cv2") output["einops"] = get_package_version("einops") output["transformers"] = get_package_version("transformers") output["mlflow"] = get_package_version("mlflow") diff --git a/requirements-dev.txt b/requirements-dev.txt index 7bf2acd383..a660c48168 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -38,6 +38,7 @@ imagecodecs; platform_system == "Linux" tifffile; platform_system == "Linux" pandas opencv-python-headless +opencv-python requests einops transformers diff --git a/requirements.txt b/requirements.txt index cdf79bcc62..13bb9b1be8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ torch>=1.7 numpy>=1.17 scikit-image>=0.14.2 opencv-python-headless +opencv-python diff --git a/setup.cfg b/setup.cfg index 158a7371a0..ad79d49daf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,6 +46,7 @@ all = imagecodecs pandas opencv-python-headless + opencv-python einops transformers mlflow From e627725156d793068a89eb7e18c500c9b2ee0763 Mon Sep 17 00:00:00 2001 From: vnath Date: Mon, 23 May 2022 22:18:45 -0500 Subject: [PATCH 19/40] opencv changes Signed-off-by: vnath --- monai/apps/nuclick/transforms.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/monai/apps/nuclick/transforms.py b/monai/apps/nuclick/transforms.py index 9d48da1527..c2fd0bc7ec 100644 --- a/monai/apps/nuclick/transforms.py +++ b/monai/apps/nuclick/transforms.py @@ -12,7 +12,6 @@ import random import numpy as np -import skimage from skimage.morphology import disk, reconstruction, remove_small_holes, remove_small_objects from monai.config import KeysCollection @@ -20,6 +19,11 @@ from monai.utils import optional_import cv2, _ = optional_import("cv2") +measure, _ = optional_import("skimage.measure") +#disk, _ = optional_import("skimage.morphology.disk") +#reconstruction, _ = optional_import("skimage.morphology.reconstruction") +#remove_small_holes, _ = optional_import("skimage.morphology.remove_small_holes") +#remove_small_objects, _ = optional_import("skimage.morphology.remove_small_objects") class FlattenLabeld(MapTransform): """ @@ -128,8 +132,8 @@ def _mask_relabeling(self, mask, min_area=5): if l == 0: continue - m = skimage.measure.label(mask == l, connectivity=1) - for stat in skimage.measure.regionprops(m): + m = measure.label(mask == l, connectivity=1) + for stat in measure.regionprops(m): if stat.area > min_area: res[stat.coords[:, 0], stat.coords[:, 1]] = l return res @@ -263,7 +267,7 @@ def exclusion_map(self, others, jitter_range=3, drop_rate=0.5): max_x = point_mask.shape[0] - 1 max_y = point_mask.shape[1] - 1 - stats = skimage.measure.regionprops(others) + stats = measure.regionprops(others) for stat in stats: x, y = stat.centroid if np.random.choice([True, False], p=[drop_rate, 1 - drop_rate]): From 0e41da544928e504dd3646b51869ff26809260e4 Mon Sep 17 00:00:00 2001 From: vnath Date: Mon, 23 May 2022 22:26:20 -0500 Subject: [PATCH 20/40] More optional import based changes Signed-off-by: vnath --- monai/apps/nuclick/transforms.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/monai/apps/nuclick/transforms.py b/monai/apps/nuclick/transforms.py index c2fd0bc7ec..4bd230944b 100644 --- a/monai/apps/nuclick/transforms.py +++ b/monai/apps/nuclick/transforms.py @@ -12,7 +12,6 @@ import random import numpy as np -from skimage.morphology import disk, reconstruction, remove_small_holes, remove_small_objects from monai.config import KeysCollection from monai.transforms import MapTransform, RandomizableTransform, Transform @@ -20,10 +19,8 @@ cv2, _ = optional_import("cv2") measure, _ = optional_import("skimage.measure") -#disk, _ = optional_import("skimage.morphology.disk") -#reconstruction, _ = optional_import("skimage.morphology.reconstruction") -#remove_small_holes, _ = optional_import("skimage.morphology.remove_small_holes") -#remove_small_objects, _ = optional_import("skimage.morphology.remove_small_objects") +morphology, _ = optional_import("skimage.morphology") + class FlattenLabeld(MapTransform): """ @@ -201,7 +198,7 @@ def mask_percent(self, img_np): return mask_percentage def filter_remove_small_objects(self, img_np, min_size=3000, avoid_overmask=True, overmask_thresh=95): - rem_sm = remove_small_objects(img_np.astype(bool), min_size=min_size) + rem_sm = morphology.remove_small_objects(img_np.astype(bool), min_size=min_size) mask_percentage = self.mask_percent(rem_sm) if (mask_percentage >= overmask_thresh) and (min_size >= 1) and (avoid_overmask is True): new_min_size = round(min_size / 2) @@ -447,15 +444,15 @@ def __call__(self, data): def post_processing(self, preds, thresh=0.33, min_size=10, min_hole=30, do_reconstruction=False, nuc_points=None): masks = preds > thresh - masks = remove_small_objects(masks, min_size=min_size) - masks = remove_small_holes(masks, area_threshold=min_hole) + masks = morphology.remove_small_objects(masks, min_size=min_size) + masks = morphology.remove_small_holes(masks, area_threshold=min_hole) if do_reconstruction: for i in range(len(masks)): this_mask = masks[i] this_marker = nuc_points[i, 0, :, :] > 0 try: - this_mask = reconstruction(this_marker, this_mask, footprint=disk(1)) + this_mask = morphology.reconstruction(this_marker, this_mask, footprint=morphology.disk(1)) masks[i] = np.array([this_mask]) except BaseException: print("Nuclei reconstruction error #" + str(i)) From 9ecdadc6e3d49e3dc3018b1c2da76b98becf5c16 Mon Sep 17 00:00:00 2001 From: vnath Date: Tue, 24 May 2022 12:50:41 -0500 Subject: [PATCH 21/40] Removed opencv-python-headles as that does not workout Signed-off-by: vnath --- environment-dev.yml | 1 - requirements-dev.txt | 1 - requirements.txt | 3 --- setup.cfg | 1 - 4 files changed, 6 deletions(-) diff --git a/environment-dev.yml b/environment-dev.yml index af86cbf02d..931485874f 100644 --- a/environment-dev.yml +++ b/environment-dev.yml @@ -37,7 +37,6 @@ dependencies: - sphinx-autodoc-typehints==1.11.1 - sphinx_rtd_theme==0.5.2 - pandas - - opencv-python-headless - opencv-python - requests - einops diff --git a/requirements-dev.txt b/requirements-dev.txt index a660c48168..778f4e0cb6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -37,7 +37,6 @@ openslide-python==1.1.2 imagecodecs; platform_system == "Linux" tifffile; platform_system == "Linux" pandas -opencv-python-headless opencv-python requests einops diff --git a/requirements.txt b/requirements.txt index 13bb9b1be8..14eb2b30e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,2 @@ torch>=1.7 numpy>=1.17 -scikit-image>=0.14.2 -opencv-python-headless -opencv-python diff --git a/setup.cfg b/setup.cfg index ad79d49daf..8d1594075b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,7 +45,6 @@ all = tifffile imagecodecs pandas - opencv-python-headless opencv-python einops transformers From b713fe54391f9b1ec40ddbc11dc25f968c62e567 Mon Sep 17 00:00:00 2001 From: vnath Date: Tue, 24 May 2022 13:15:12 -0500 Subject: [PATCH 22/40] adding sk image back to requirement.txt till we figure out an alternative Signed-off-by: vnath --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 14eb2b30e9..49b1030306 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ torch>=1.7 numpy>=1.17 +scikit-image>=0.14.2 From 7db648d45b4ed09b31ea5ad9fd3d0082896dec62 Mon Sep 17 00:00:00 2001 From: vnath Date: Tue, 24 May 2022 13:21:14 -0500 Subject: [PATCH 23/40] Replaced MapTransform instead of Transform for SplitLabeld Signed-off-by: vnath --- monai/apps/nuclick/transforms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/apps/nuclick/transforms.py b/monai/apps/nuclick/transforms.py index 4bd230944b..5bae3cb358 100644 --- a/monai/apps/nuclick/transforms.py +++ b/monai/apps/nuclick/transforms.py @@ -93,7 +93,7 @@ def pad_to_shape(self, img, shape): return np.pad(img, diff, mode="constant", constant_values=0) -class SplitLabeld(Transform): +class SplitLabeld(MapTransform): """ Extracts a single label from all the given classes, the single label is defined by mask_value, the remaining labels are kept in others From 225b6b5426c9fa6201c731efd28640267bf46c76 Mon Sep 17 00:00:00 2001 From: vnath Date: Tue, 24 May 2022 13:36:03 -0500 Subject: [PATCH 24/40] Added skimage and cv2 requirements.txt as tests fail otherwise Signed-off-by: vnath --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 49b1030306..6b5bf4eb3f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ torch>=1.7 numpy>=1.17 scikit-image>=0.14.2 +opencv-python From 419fc3382d84d1f8ed5c79ed086aa63049c1cb97 Mon Sep 17 00:00:00 2001 From: vnath Date: Tue, 24 May 2022 13:51:59 -0500 Subject: [PATCH 25/40] Added MapTransform Inheritance to AddClickSignalsd Signed-off-by: vnath --- monai/apps/nuclick/transforms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/apps/nuclick/transforms.py b/monai/apps/nuclick/transforms.py index 5bae3cb358..d709fbf40c 100644 --- a/monai/apps/nuclick/transforms.py +++ b/monai/apps/nuclick/transforms.py @@ -280,7 +280,7 @@ def exclusion_map(self, others, jitter_range=3, drop_rate=0.5): return point_mask -class AddClickSignalsd(Transform): +class AddClickSignalsd(MapTransform): def __init__(self, image, foreground="foreground", bb_size=128): self.image = image self.foreground = foreground From 12fac7c40e9376790116c3a20e5f6317abd22fb2 Mon Sep 17 00:00:00 2001 From: vnath Date: Tue, 24 May 2022 17:26:48 -0500 Subject: [PATCH 26/40] Spatial Pad Incorporated to ExtractPatchd Signed-off-by: vnath --- monai/apps/nuclick/transforms.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/monai/apps/nuclick/transforms.py b/monai/apps/nuclick/transforms.py index d709fbf40c..a476687f49 100644 --- a/monai/apps/nuclick/transforms.py +++ b/monai/apps/nuclick/transforms.py @@ -14,7 +14,7 @@ import numpy as np from monai.config import KeysCollection -from monai.transforms import MapTransform, RandomizableTransform, Transform +from monai.transforms import MapTransform, RandomizableTransform, Transform, SpatialPad from monai.utils import optional_import cv2, _ = optional_import("cv2") @@ -67,7 +67,7 @@ def __call__(self, data): img = d[key] x_start, x_end, y_start, y_end = self.bbox(self.patch_size, centroid, img.shape[-2:]) cropped = img[:, x_start:x_end, y_start:y_end] - d[key] = self.pad_to_shape(cropped, roi_size) + d[key] = SpatialPad(spatial_size=roi_size)(cropped) return d def bbox(self, patch_size, centroid, size): @@ -86,12 +86,6 @@ def bbox(self, patch_size, centroid, size): y_start = n - patch_size return x_start, x_end, y_start, y_end - def pad_to_shape(self, img, shape): - img_shape = img.shape[-2:] - s_diff = np.array(shape) - np.array(img_shape) - diff = [(0, 0), (0, s_diff[0]), (0, s_diff[1])] - return np.pad(img, diff, mode="constant", constant_values=0) - class SplitLabeld(MapTransform): """ @@ -281,7 +275,17 @@ def exclusion_map(self, others, jitter_range=3, drop_rate=0.5): class AddClickSignalsd(MapTransform): - def __init__(self, image, foreground="foreground", bb_size=128): + """ + Adds Guidance Signal to the input image + + Args: + image: source image + label: source label + others: source others (other labels from the binary mask which are not being used for training) + drop_rate: + jitter_range: noise added to the points in the point mask for exclusion mask + """ + def __init__(self, image: str = "image", foreground: str = "foreground", bb_size: int = 128): self.image = image self.foreground = foreground self.bb_size = bb_size From c92d24788bed073adb5ebc958930deafe78c7f92 Mon Sep 17 00:00:00 2001 From: vnath Date: Tue, 24 May 2022 19:30:01 -0500 Subject: [PATCH 27/40] Remvoed comments and added doc-strings Signed-off-by: vnath --- monai/apps/nuclick/transforms.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/monai/apps/nuclick/transforms.py b/monai/apps/nuclick/transforms.py index a476687f49..2aaaf2690c 100644 --- a/monai/apps/nuclick/transforms.py +++ b/monai/apps/nuclick/transforms.py @@ -276,14 +276,12 @@ def exclusion_map(self, others, jitter_range=3, drop_rate=0.5): class AddClickSignalsd(MapTransform): """ - Adds Guidance Signal to the input image + Adds Click Signal to the input image Args: image: source image - label: source label - others: source others (other labels from the binary mask which are not being used for training) - drop_rate: - jitter_range: noise added to the points in the point mask for exclusion mask + foreground: 2D click indices as list + bb_size: single integer size, defines a bounding box like (bb_size, bb_size) """ def __init__(self, image: str = "image", foreground: str = "foreground", bb_size: int = 128): self.image = image @@ -332,7 +330,6 @@ def __call__(self, data): def get_clickmap_boundingbox(self, cx, cy, m, n, bb=128): click_map = np.zeros((m, n), dtype=np.uint8) - # Removing points out of image dimension (these points may have been clicked unwanted) x_del_indices = {i for i in range(len(cx)) if cx[i] >= n or cx[i] < 0} y_del_indices = {i for i in range(len(cy)) if cy[i] >= m or cy[i] < 0} del_indices = list(x_del_indices.union(y_del_indices)) @@ -360,17 +357,16 @@ def get_clickmap_boundingbox(self, cx, cy, m, n, bb=128): return click_map, bounding_boxes def get_patches_and_signals(self, img, click_map, bounding_boxes, cx, cy, m, n, bb=128): - # total = number of clicks + total = len(bounding_boxes) - img = np.array([img]) # img.shape=(1,3,m,n) - click_map = np.array([click_map]) # clickmap.shape=(1,m,n) - click_map = click_map[:, np.newaxis, ...] # clickmap.shape=(1,1,m,n) + img = np.array([img]) + click_map = np.array([click_map]) + click_map = click_map[:, np.newaxis, ...] patches = np.ndarray((total, 3, bb, bb), dtype=np.uint8) nuc_points = np.ndarray((total, 1, bb, bb), dtype=np.uint8) other_points = np.ndarray((total, 1, bb, bb), dtype=np.uint8) - # Removing points out of image dimension (these points may have been clicked unwanted) x_del_indices = {i for i in range(len(cx)) if cx[i] >= n or cx[i] < 0} y_del_indices = {i for i in range(len(cy)) if cy[i] >= m or cy[i] < 0} del_indices = list(x_del_indices.union(y_del_indices)) @@ -394,13 +390,19 @@ def get_patches_and_signals(self, img, click_map, bounding_boxes, cx, cy, m, n, nuc_points[i] = this_click_map[0, :, y_start : y_end + 1, x_start : x_end + 1] other_points[i] = others_click_map[0, :, y_start : y_end + 1, x_start : x_end + 1] - # patches: (total, 3, m, n) - # nuc_points: (total, 1, m, n) - # other_points: (total, 1, m, n) return patches, nuc_points, other_points class PostFilterLabeld(MapTransform): + """ + Performs Filtering of Labels on the predicted probability map + + Args: + thresh: probability threshold for classifying a pixel as a mask + min_size: min_size objects that will be removed from the image, refer skimage remove_small_objects + min_hole: min_hole that will be removed from the image, refer skimage remove_small_holes + do_reconstruction: Boolean Flag, Perform a morphological reconstruction of an image, refer skimage + """ def __init__( self, keys: KeysCollection, @@ -460,7 +462,7 @@ def post_processing(self, preds, thresh=0.33, min_size=10, min_hole=30, do_recon masks[i] = np.array([this_mask]) except BaseException: print("Nuclei reconstruction error #" + str(i)) - return masks # masks(no.patches, 128, 128) + return masks def gen_instance_map(self, masks, bounding_boxes, m, n, flatten=True): instance_map = np.zeros((m, n), dtype=np.uint16) From bed18293b4bfda501908f414b8f74fcffaff6b35 Mon Sep 17 00:00:00 2001 From: vnath Date: Tue, 24 May 2022 20:39:12 -0500 Subject: [PATCH 28/40] codeformat Signed-off-by: vnath --- monai/apps/nuclick/transforms.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monai/apps/nuclick/transforms.py b/monai/apps/nuclick/transforms.py index 2aaaf2690c..19f53563ff 100644 --- a/monai/apps/nuclick/transforms.py +++ b/monai/apps/nuclick/transforms.py @@ -14,7 +14,7 @@ import numpy as np from monai.config import KeysCollection -from monai.transforms import MapTransform, RandomizableTransform, Transform, SpatialPad +from monai.transforms import MapTransform, RandomizableTransform, SpatialPad from monai.utils import optional_import cv2, _ = optional_import("cv2") @@ -283,6 +283,7 @@ class AddClickSignalsd(MapTransform): foreground: 2D click indices as list bb_size: single integer size, defines a bounding box like (bb_size, bb_size) """ + def __init__(self, image: str = "image", foreground: str = "foreground", bb_size: int = 128): self.image = image self.foreground = foreground @@ -403,6 +404,7 @@ class PostFilterLabeld(MapTransform): min_hole: min_hole that will be removed from the image, refer skimage remove_small_holes do_reconstruction: Boolean Flag, Perform a morphological reconstruction of an image, refer skimage """ + def __init__( self, keys: KeysCollection, From 62697d91174b5f41728a3be95a9bdb4894ea7e9f Mon Sep 17 00:00:00 2001 From: vnath Date: Tue, 24 May 2022 22:30:54 -0500 Subject: [PATCH 29/40] Adding NuClick Test to Min tests and removing opencv & scikit from requirements.txt Signed-off-by: vnath --- requirements.txt | 4 +--- tests/min_tests.py | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 6b5bf4eb3f..8cfe809bb4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,2 @@ torch>=1.7 -numpy>=1.17 -scikit-image>=0.14.2 -opencv-python +numpy>=1.17 \ No newline at end of file diff --git a/tests/min_tests.py b/tests/min_tests.py index 6549fdcd4b..bbd7efc1d1 100644 --- a/tests/min_tests.py +++ b/tests/min_tests.py @@ -114,6 +114,7 @@ def run_testsuit(): "test_nifti_header_revise", "test_nifti_rw", "test_nifti_saver", + "test_nuclick_transforms", "test_occlusion_sensitivity", "test_orientation", "test_orientationd", From 1b220522f76e093a5fdc19d8241349de7a3b5ffa Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 25 May 2022 04:11:16 +0000 Subject: [PATCH 30/40] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8cfe809bb4..14eb2b30e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ torch>=1.7 -numpy>=1.17 \ No newline at end of file +numpy>=1.17 From 73f0f8a96321f78cf6420182e7f86b5967137ae4 Mon Sep 17 00:00:00 2001 From: vnath Date: Wed, 25 May 2022 14:28:39 -0500 Subject: [PATCH 31/40] Removed cv2 dependency after modification of FlattenLabeld Transform to use skimage functionality instead Signed-off-by: vnath --- environment-dev.yml | 1 - monai/apps/nuclick/transforms.py | 3 +-- monai/config/deviceconfig.py | 1 - requirements-dev.txt | 1 - setup.cfg | 1 - 5 files changed, 1 insertion(+), 6 deletions(-) diff --git a/environment-dev.yml b/environment-dev.yml index e9dfb13386..9eef775b78 100644 --- a/environment-dev.yml +++ b/environment-dev.yml @@ -37,7 +37,6 @@ dependencies: - sphinx-autodoc-typehints==1.11.1 - sphinx_rtd_theme==0.5.2 - pandas - - opencv-python - requests - einops - transformers diff --git a/monai/apps/nuclick/transforms.py b/monai/apps/nuclick/transforms.py index 19f53563ff..9f33047b12 100644 --- a/monai/apps/nuclick/transforms.py +++ b/monai/apps/nuclick/transforms.py @@ -17,7 +17,6 @@ from monai.transforms import MapTransform, RandomizableTransform, SpatialPad from monai.utils import optional_import -cv2, _ = optional_import("cv2") measure, _ = optional_import("skimage.measure") morphology, _ = optional_import("skimage.morphology") @@ -34,7 +33,7 @@ def __init__(self, keys: KeysCollection): def __call__(self, data): d = dict(data) for key in self.keys: - _, labels, _, _ = cv2.connectedComponentsWithStats(d[key], 4, cv2.CV_32S) + labels = measure.label(d[key], connectivity=1).astype(np.int32) d[key] = labels.astype(np.uint8) return d diff --git a/monai/config/deviceconfig.py b/monai/config/deviceconfig.py index 88299fbfd3..8d6383ed97 100644 --- a/monai/config/deviceconfig.py +++ b/monai/config/deviceconfig.py @@ -72,7 +72,6 @@ def get_optional_config_values(): output["lmdb"] = get_package_version("lmdb") output["psutil"] = psutil_version output["pandas"] = get_package_version("pandas") - output["cv2"] = get_package_version("cv2") output["einops"] = get_package_version("einops") output["transformers"] = get_package_version("transformers") output["mlflow"] = get_package_version("mlflow") diff --git a/requirements-dev.txt b/requirements-dev.txt index 30623ba74e..ac8b3730d8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -37,7 +37,6 @@ openslide-python==1.1.2 imagecodecs; platform_system == "Linux" tifffile; platform_system == "Linux" pandas -opencv-python requests einops transformers diff --git a/setup.cfg b/setup.cfg index 18123f733b..914e404b2d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,7 +45,6 @@ all = tifffile imagecodecs pandas - opencv-python einops transformers mlflow From c67f531d4630829102bace376d0a390556c7d75f Mon Sep 17 00:00:00 2001 From: vnath Date: Wed, 25 May 2022 15:14:35 -0500 Subject: [PATCH 32/40] Minor addition to doc string Signed-off-by: vnath --- monai/apps/nuclick/transforms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monai/apps/nuclick/transforms.py b/monai/apps/nuclick/transforms.py index 9f33047b12..4893e8ad89 100644 --- a/monai/apps/nuclick/transforms.py +++ b/monai/apps/nuclick/transforms.py @@ -204,8 +204,8 @@ class AddPointGuidanceSignald(RandomizableTransform): Adds Guidance Signal to the input image Args: - image: source image - label: source label + image: key of source image + label: key of source label others: source others (other labels from the binary mask which are not being used for training) drop_rate: jitter_range: noise added to the points in the point mask for exclusion mask From ad27c24cce77287ec9d31cd0b42beed5567d68a7 Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Wed, 25 May 2022 23:07:45 +0100 Subject: [PATCH 33/40] update based on comments Signed-off-by: Wenqi Li --- monai/apps/nuclick/transforms.py | 91 +++++++++++++++++++++----------- 1 file changed, 61 insertions(+), 30 deletions(-) diff --git a/monai/apps/nuclick/transforms.py b/monai/apps/nuclick/transforms.py index 4893e8ad89..85fdb9795f 100644 --- a/monai/apps/nuclick/transforms.py +++ b/monai/apps/nuclick/transforms.py @@ -8,19 +8,42 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + import math import random +from enum import Enum +from typing import Union import numpy as np from monai.config import KeysCollection -from monai.transforms import MapTransform, RandomizableTransform, SpatialPad +from monai.transforms import MapTransform, Randomizable, SpatialPad from monai.utils import optional_import measure, _ = optional_import("skimage.measure") morphology, _ = optional_import("skimage.morphology") +class NuclickKeys(Enum): + """ + Keys for nuclick transforms. + """ + + IMAGE = "image" + LABEL = "label" + OTHERS = "others" # key of other labels from the binary mask which are not being used for training + FOREGROUND = "foreground" + + CENTROID = "centroid" # key where the centroid values are stored + MASK_VALUE = "mask_value" + LOCATION = "location" + + NUC_POINTS = "nuc_points" + BOUNDING_BOXES = "bounding_boxes" + IMG_HEIGHT = "img_height" + IMG_WIDTH = "img_width" + + class FlattenLabeld(MapTransform): """ FlattenLabeld creates labels per closed object contour (defined by a connectivity). For e.g if there are @@ -33,8 +56,7 @@ def __init__(self, keys: KeysCollection): def __call__(self, data): d = dict(data) for key in self.keys: - labels = measure.label(d[key], connectivity=1).astype(np.int32) - d[key] = labels.astype(np.uint8) + d[key] = measure.label(d[key], connectivity=1).astype(np.uint8) return d @@ -47,11 +69,11 @@ class ExtractPatchd(MapTransform): Args: keys: image, label - centroid_key: key where the centroid values are stored + centroid_key: key where the centroid values are stored, defaults to ``"centroid"`` patch_size: size of the extracted patch """ - def __init__(self, keys: KeysCollection, centroid_key: str = "centroid", patch_size: int = 128): + def __init__(self, keys: KeysCollection, centroid_key: str = NuclickKeys.CENTROID.value, patch_size: int = 128): super().__init__(keys) self.centroid_key = centroid_key self.patch_size = patch_size @@ -92,13 +114,19 @@ class SplitLabeld(MapTransform): labels are kept in others Args: - label: label source - others: other labels storage key - mask_value: the mask_value that will be kept for binarization of the label + label: label source, defaults to ``"label"`` + others: other labels storage key, defaults to ``"others"`` + mask_value: the mask_value that will be kept for binarization of the label, defaults to ``"mask_value"`` min_area: The smallest allowable object size. """ - def __init__(self, label: str = "label", others: str = "others", mask_value: str = "mask_value", min_area: int = 5): + def __init__( + self, + label: str = NuclickKeys.LABEL.value, + others: str = NuclickKeys.OTHERS.value, + mask_value: str = NuclickKeys.MASK_VALUE.value, + min_area: int = 5, + ): self.label = label self.others = others @@ -199,27 +227,28 @@ def filter_remove_small_objects(self, img_np, min_size=3000, avoid_overmask=True return rem_sm -class AddPointGuidanceSignald(RandomizableTransform): +class AddPointGuidanceSignald(Randomizable, MapTransform): """ Adds Guidance Signal to the input image Args: - image: key of source image - label: key of source label + image: key of source image, defaults to ``"image"`` + label: key of source label, defaults to ``"label"`` others: source others (other labels from the binary mask which are not being used for training) - drop_rate: - jitter_range: noise added to the points in the point mask for exclusion mask + defaults to ``"others"`` + drop_rate: probability of dropping the signal, defaults to ``0.5`` + jitter_range: noise added to the points in the point mask for exclusion mask, defaults to ``3`` """ def __init__( self, - image: str = "image", - label: str = "label", - others: str = "others", + image: str = NuclickKeys.IMAGE.value, + label: str = NuclickKeys.LABEL.value, + others: str = NuclickKeys.OTHERS.value, drop_rate: float = 0.5, jitter_range: int = 3, ): - super().__init__() + MapTransform.__init__(self, image) self.image = image self.label = label @@ -278,12 +307,14 @@ class AddClickSignalsd(MapTransform): Adds Click Signal to the input image Args: - image: source image - foreground: 2D click indices as list + image: source image, defaults to ``"image"`` + foreground: 2D click indices as list, defaults to ``"foreground"`` bb_size: single integer size, defines a bounding box like (bb_size, bb_size) """ - def __init__(self, image: str = "image", foreground: str = "foreground", bb_size: int = 128): + def __init__( + self, image: str = NuclickKeys.IMAGE.value, foreground: str = NuclickKeys.FOREGROUND.value, bb_size: int = 128 + ): self.image = image self.foreground = foreground self.bb_size = bb_size @@ -291,7 +322,7 @@ def __init__(self, image: str = "image", foreground: str = "foreground", bb_size def __call__(self, data): d = dict(data) - location = d.get("location", (0, 0)) + location = d.get(NuclickKeys.LOCATION.value, (0, 0)) tx, ty = location[0], location[1] pos = d.get(self.foreground) pos = (np.array(pos) - (tx, ty)).astype(int).tolist() if pos else [] @@ -319,10 +350,10 @@ def __call__(self, data): ) patches = patches / 255 - d["bounding_boxes"] = bounding_boxes - d["img_width"] = img_width - d["img_height"] = img_height - d["nuc_points"] = nuc_points + d[NuclickKeys.BOUNDING_BOXES.value] = bounding_boxes + d[NuclickKeys.IMG_WIDTH.value] = img_width + d[NuclickKeys.IMG_HEIGHT.value] = img_height + d[NuclickKeys.NUC_POINTS.value] = nuc_points d[self.image] = np.concatenate((patches, nuc_points, other_points), axis=1, dtype=np.float32) return d @@ -407,10 +438,10 @@ class PostFilterLabeld(MapTransform): def __init__( self, keys: KeysCollection, - nuc_points="nuc_points", - bounding_boxes="bounding_boxes", - img_height="img_height", - img_width="img_width", + nuc_points: str = NuclickKeys.NUC_POINTS.value, + bounding_boxes: str = NuclickKeys.BOUNDING_BOXES.value, + img_height: str = NuclickKeys.IMG_HEIGHT.value, + img_width: str = NuclickKeys.IMG_WIDTH.value, thresh=0.33, min_size=10, min_hole=30, From cecc7ec054c9075b51f9a623d80838ac6ddde551 Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Wed, 25 May 2022 23:13:19 +0100 Subject: [PATCH 34/40] adds unit test case Signed-off-by: Wenqi Li --- tests/test_nuclick_transforms.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/test_nuclick_transforms.py b/tests/test_nuclick_transforms.py index 353e9943b3..11edf3f791 100644 --- a/tests/test_nuclick_transforms.py +++ b/tests/test_nuclick_transforms.py @@ -42,6 +42,19 @@ dtype=np.uint8, ) +LABEL_1_1 = np.array( + [ + [1, 1, 1, 0, 0, 1, 1], + [1, 1, 1, 0, 0, 1, 1], + [1, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 0, 2, 2, 2], + [1, 1, 1, 0, 2, 2, 2], + [1, 1, 1, 0, 2, 2, 2], + ], + dtype=np.uint8, +) + LABEL_2 = np.array([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], dtype=np.uint8) LABEL_3 = np.array([[[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3], [4, 4, 4, 4]]], dtype=np.uint8) @@ -115,6 +128,7 @@ FLATTEN_LABEL_TEST_CASE_1 = [{"keys": "label"}, DATA_FLATTEN_1, [0, 1, 2, 3]] FLATTEN_LABEL_TEST_CASE_2 = [{"keys": "label"}, DATA_FLATTEN_2, [0]] +FLATTEN_LABEL_TEST_CASE_3 = [{"keys": "label"}, {"label": LABEL_1_1}, [0, 1, 2, 3, 4]] EXTRACT_TEST_CASE_1 = [{"keys": ["image", "label"], "patch_size": 3}, DATA_EXTRACT_1, [1, 3, 3]] EXTRACT_TEST_CASE_2 = [{"keys": ["image", "label"], "patch_size": 5}, DATA_EXTRACT_1, [1, 5, 5]] @@ -141,7 +155,7 @@ def test_correct_shape(self, arguments, input_data, expected_shape): class TestFlattenLabeld(unittest.TestCase): - @parameterized.expand([FLATTEN_LABEL_TEST_CASE_1, FLATTEN_LABEL_TEST_CASE_2]) + @parameterized.expand([FLATTEN_LABEL_TEST_CASE_1, FLATTEN_LABEL_TEST_CASE_2, FLATTEN_LABEL_TEST_CASE_3]) def test_correct_num_labels(self, arguments, input_data, expected_result): result = FlattenLabeld(**arguments)(input_data) np.testing.assert_equal(np.unique(result["label"]), expected_result) From 998e6551ef46a2d2555a3df26769626291022faf Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Wed, 25 May 2022 23:29:54 +0100 Subject: [PATCH 35/40] remove unused import Signed-off-by: Wenqi Li --- .pre-commit-config.yaml | 4 ++++ monai/apps/nuclick/transforms.py | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ede0366829..2da8ddff75 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -56,6 +56,10 @@ repos: monai/__init__.py| docs/source/conf.py )$ + - repo: https://github.com/hadialqattan/pycln + rev: v1.3.3 + hooks: + - id: pycln #- repo: https://github.com/PyCQA/isort # rev: 5.9.3 diff --git a/monai/apps/nuclick/transforms.py b/monai/apps/nuclick/transforms.py index 85fdb9795f..d54094b390 100644 --- a/monai/apps/nuclick/transforms.py +++ b/monai/apps/nuclick/transforms.py @@ -12,7 +12,6 @@ import math import random from enum import Enum -from typing import Union import numpy as np From 78fe16e993ccd3de7a8cc6b3e07e6f93879cea9c Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Thu, 26 May 2022 15:53:18 +0100 Subject: [PATCH 36/40] fixes unit test Signed-off-by: Wenqi Li --- monai/apps/nuclick/transforms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/apps/nuclick/transforms.py b/monai/apps/nuclick/transforms.py index d54094b390..6ad126ebb9 100644 --- a/monai/apps/nuclick/transforms.py +++ b/monai/apps/nuclick/transforms.py @@ -354,7 +354,7 @@ def __call__(self, data): d[NuclickKeys.IMG_HEIGHT.value] = img_height d[NuclickKeys.NUC_POINTS.value] = nuc_points - d[self.image] = np.concatenate((patches, nuc_points, other_points), axis=1, dtype=np.float32) + d[self.image] = np.concatenate((patches, nuc_points, other_points), axis=1).astype(dtype=np.float32) return d def get_clickmap_boundingbox(self, cx, cy, m, n, bb=128): From 5e68d807028225a5f764b9eebab6801a0a8b7dcd Mon Sep 17 00:00:00 2001 From: vnath Date: Thu, 26 May 2022 16:39:22 -0500 Subject: [PATCH 37/40] Adding changes as docs and init arguments, 2 additional test cases Signed-off-by: vnath --- monai/apps/nuclick/transforms.py | 40 ++++++++++++++++++++++---------- tests/test_nuclick_transforms.py | 11 +++++++-- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/monai/apps/nuclick/transforms.py b/monai/apps/nuclick/transforms.py index 6ad126ebb9..ca8aacfe40 100644 --- a/monai/apps/nuclick/transforms.py +++ b/monai/apps/nuclick/transforms.py @@ -12,6 +12,7 @@ import math import random from enum import Enum +from typing import Tuple, Union import numpy as np @@ -47,15 +48,20 @@ class FlattenLabeld(MapTransform): """ FlattenLabeld creates labels per closed object contour (defined by a connectivity). For e.g if there are 12 small regions of 1's it will delineate them into 12 different label classes + + Args: + connectivity: Max no. of orthogonal hops to consider a pixel/voxel as a neighbor. Refer skimage.measure.label + allow_missing_keys: don't raise exception if key is missing. """ - def __init__(self, keys: KeysCollection): - super().__init__(keys) + def __init__(self, keys: KeysCollection, connectivity: int = 1, allow_missing_keys: bool = False): + super().__init__(keys, allow_missing_keys) + self.connectivity = connectivity def __call__(self, data): d = dict(data) for key in self.keys: - d[key] = measure.label(d[key], connectivity=1).astype(np.uint8) + d[key] = measure.label(d[key], connectivity=self.connectivity).astype(np.uint8) return d @@ -70,10 +76,17 @@ class ExtractPatchd(MapTransform): keys: image, label centroid_key: key where the centroid values are stored, defaults to ``"centroid"`` patch_size: size of the extracted patch + allow_missing_keys: don't raise exception if key is missing. """ - def __init__(self, keys: KeysCollection, centroid_key: str = NuclickKeys.CENTROID.value, patch_size: int = 128): - super().__init__(keys) + def __init__( + self, + keys: KeysCollection, + centroid_key: str = NuclickKeys.CENTROID.value, + patch_size: Union[Tuple[int, int], int] = 128, + allow_missing_keys: bool = False, + ): + super().__init__(keys, allow_missing_keys) self.centroid_key = centroid_key self.patch_size = patch_size @@ -165,10 +178,11 @@ class FilterImaged(MapTransform): Args: min_size: The smallest allowable object size + allow_missing_keys: don't raise exception if key is missing. """ - def __init__(self, keys: KeysCollection, min_size: int = 500): - super().__init__(keys) + def __init__(self, keys: KeysCollection, min_size: int = 500, allow_missing_keys: bool = False): + super().__init__(keys, allow_missing_keys) self.min_size = min_size def __call__(self, data): @@ -432,6 +446,7 @@ class PostFilterLabeld(MapTransform): min_size: min_size objects that will be removed from the image, refer skimage remove_small_objects min_hole: min_hole that will be removed from the image, refer skimage remove_small_holes do_reconstruction: Boolean Flag, Perform a morphological reconstruction of an image, refer skimage + allow_missing_keys: don't raise exception if key is missing. """ def __init__( @@ -441,12 +456,13 @@ def __init__( bounding_boxes: str = NuclickKeys.BOUNDING_BOXES.value, img_height: str = NuclickKeys.IMG_HEIGHT.value, img_width: str = NuclickKeys.IMG_WIDTH.value, - thresh=0.33, - min_size=10, - min_hole=30, - do_reconstruction=False, + thresh: float = 0.33, + min_size: int = 10, + min_hole: int = 30, + do_reconstruction: bool = False, + allow_missing_keys: bool = False, ): - super().__init__(keys) + super().__init__(keys, allow_missing_keys) self.nuc_points = nuc_points self.bounding_boxes = bounding_boxes self.img_height = img_height diff --git a/tests/test_nuclick_transforms.py b/tests/test_nuclick_transforms.py index 11edf3f791..c1f723dcae 100644 --- a/tests/test_nuclick_transforms.py +++ b/tests/test_nuclick_transforms.py @@ -59,6 +59,8 @@ LABEL_3 = np.array([[[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3], [4, 4, 4, 4]]], dtype=np.uint8) +LABEL_4 = np.array([[[4, 4, 4, 4], [4, 4, 4, 4], [4, 4, 4, 4], [4, 4, 4, 4]]], dtype=np.uint8) + IL_IMAGE_1 = np.array( [ [[0, 0, 0, 0, 0], [0, 1, 0, 0, 0], [0, 0, 1, 1, 1], [0, 0, 1, 1, 1], [0, 0, 1, 1, 1]], @@ -105,6 +107,7 @@ DATA_EXTRACT_2 = {"image": IL_IMAGE_2, "label": IL_LABEL_2, "centroid": (1, 1)} DATA_SPLIT_1 = {"label": LABEL_3, "mask_value": 1} +DATA_SPLIT_2 = {"label": LABEL_4, "mask_value": 4} DATA_GUIDANCE_1 = {"image": IL_IMAGE_1, "label": IL_LABEL_1, "others": IL_OTHERS_1, "centroid": (2, 2)} @@ -120,8 +123,10 @@ # Result Definitions EXTRACT_RESULT_TC1 = np.array([[[0, 0, 0], [0, 0, 0], [0, 0, 1]]], dtype=np.uint8) +EXTRACT_RESULT_TC2 = np.array([[[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]], dtype=np.uint8) SPLIT_RESULT_TC1 = np.array([[[1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]], dtype=np.uint8) +SPLIT_RESULT_TC2 = np.array([[[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]], dtype=np.uint8) # Test Case Definitions FILTER_IMAGE_TEST_CASE_1 = [{"keys": "image", "min_size": 1}, DATA_FILTER_1, [3, 3, 3]] @@ -135,8 +140,10 @@ EXTRACT_TEST_CASE_3 = [{"keys": ["image", "label"], "patch_size": 1}, DATA_EXTRACT_2, [1, 1, 1]] EXTRACT_RESULT_TEST_CASE_1 = [{"keys": ["image", "label"], "patch_size": 3}, DATA_EXTRACT_1, EXTRACT_RESULT_TC1] +EXTRACT_RESULT_TEST_CASE_2 = [{"keys": ["image", "label"], "patch_size": 4}, DATA_EXTRACT_2, EXTRACT_RESULT_TC2] SPLIT_TEST_CASE_1 = [{"label": "label", "mask_value": "mask_value", "min_area": 1}, DATA_SPLIT_1, SPLIT_RESULT_TC1] +SPLIT_TEST_CASE_2 = [{"label": "label", "mask_value": "mask_value", "min_area": 3}, DATA_SPLIT_2, SPLIT_RESULT_TC2] GUIDANCE_TEST_CASE_1 = [{"image": "image", "label": "label", "others": "others"}, DATA_GUIDANCE_1, [5, 5, 5]] @@ -167,14 +174,14 @@ def test_correct_patch_size(self, arguments, input_data, expected_shape): result = ExtractPatchd(**arguments)(input_data) np.testing.assert_equal(result["label"].shape, expected_shape) - @parameterized.expand([EXTRACT_RESULT_TEST_CASE_1]) + @parameterized.expand([EXTRACT_RESULT_TEST_CASE_1, EXTRACT_RESULT_TEST_CASE_2]) def test_correct_results(self, arguments, input_data, expected_result): result = ExtractPatchd(**arguments)(input_data) np.testing.assert_equal(result["label"], expected_result) class TestSplitLabelsd(unittest.TestCase): - @parameterized.expand([SPLIT_TEST_CASE_1]) + @parameterized.expand([SPLIT_TEST_CASE_1, SPLIT_TEST_CASE_2]) def test_correct_results(self, arguments, input_data, expected_result): result = SplitLabeld(**arguments)(input_data) np.testing.assert_equal(result["label"], expected_result) From fc5180c10743517da03a288738a248fe4f9c1a2a Mon Sep 17 00:00:00 2001 From: vnath Date: Fri, 27 May 2022 15:34:37 -0500 Subject: [PATCH 38/40] Minor changes Signed-off-by: vnath --- monai/apps/nuclick/transforms.py | 16 +++++++++++++--- tests/test_nuclick_transforms.py | 4 ++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/monai/apps/nuclick/transforms.py b/monai/apps/nuclick/transforms.py index ca8aacfe40..a7cf44346f 100644 --- a/monai/apps/nuclick/transforms.py +++ b/monai/apps/nuclick/transforms.py @@ -126,7 +126,7 @@ class SplitLabeld(MapTransform): labels are kept in others Args: - label: label source, defaults to ``"label"`` + label: key of the label source others: other labels storage key, defaults to ``"others"`` mask_value: the mask_value that will be kept for binarization of the label, defaults to ``"mask_value"`` min_area: The smallest allowable object size. @@ -134,19 +134,29 @@ class SplitLabeld(MapTransform): def __init__( self, - label: str = NuclickKeys.LABEL.value, + keys: KeysCollection, + # label: str = NuclickKeys.LABEL.value, others: str = NuclickKeys.OTHERS.value, mask_value: str = NuclickKeys.MASK_VALUE.value, min_area: int = 5, ): - self.label = label + # self.label = label + super().__init__(keys, allow_missing_keys=False) self.others = others self.mask_value = mask_value self.min_area = min_area def __call__(self, data): d = dict(data) + + if len(self.keys) > 1: + print("Only 'label' key is supported, more than 1 key was found") + return None + + for key in self.keys: + self.label = key + label = d[self.label] mask_value = d[self.mask_value] mask = np.uint8(label == mask_value) diff --git a/tests/test_nuclick_transforms.py b/tests/test_nuclick_transforms.py index c1f723dcae..f9559a1c35 100644 --- a/tests/test_nuclick_transforms.py +++ b/tests/test_nuclick_transforms.py @@ -142,8 +142,8 @@ EXTRACT_RESULT_TEST_CASE_1 = [{"keys": ["image", "label"], "patch_size": 3}, DATA_EXTRACT_1, EXTRACT_RESULT_TC1] EXTRACT_RESULT_TEST_CASE_2 = [{"keys": ["image", "label"], "patch_size": 4}, DATA_EXTRACT_2, EXTRACT_RESULT_TC2] -SPLIT_TEST_CASE_1 = [{"label": "label", "mask_value": "mask_value", "min_area": 1}, DATA_SPLIT_1, SPLIT_RESULT_TC1] -SPLIT_TEST_CASE_2 = [{"label": "label", "mask_value": "mask_value", "min_area": 3}, DATA_SPLIT_2, SPLIT_RESULT_TC2] +SPLIT_TEST_CASE_1 = [{"keys": ["label"], "mask_value": "mask_value", "min_area": 1}, DATA_SPLIT_1, SPLIT_RESULT_TC1] +SPLIT_TEST_CASE_2 = [{"keys": ["label"], "mask_value": "mask_value", "min_area": 3}, DATA_SPLIT_2, SPLIT_RESULT_TC2] GUIDANCE_TEST_CASE_1 = [{"image": "image", "label": "label", "others": "others"}, DATA_GUIDANCE_1, [5, 5, 5]] From 5b127252c076c8f6bc7cf20571cf13312c1e0e46 Mon Sep 17 00:00:00 2001 From: vnath Date: Mon, 30 May 2022 16:59:55 -0500 Subject: [PATCH 39/40] kwargs added to ExtractPatchd Signed-off-by: vnath --- monai/apps/nuclick/transforms.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/monai/apps/nuclick/transforms.py b/monai/apps/nuclick/transforms.py index a7cf44346f..7c0197f86d 100644 --- a/monai/apps/nuclick/transforms.py +++ b/monai/apps/nuclick/transforms.py @@ -12,7 +12,7 @@ import math import random from enum import Enum -from typing import Tuple, Union +from typing import Any, Tuple, Union import numpy as np @@ -85,10 +85,12 @@ def __init__( centroid_key: str = NuclickKeys.CENTROID.value, patch_size: Union[Tuple[int, int], int] = 128, allow_missing_keys: bool = False, + **kwargs: Any, ): super().__init__(keys, allow_missing_keys) self.centroid_key = centroid_key self.patch_size = patch_size + self.kwargs = kwargs def __call__(self, data): d = dict(data) @@ -100,7 +102,7 @@ def __call__(self, data): img = d[key] x_start, x_end, y_start, y_end = self.bbox(self.patch_size, centroid, img.shape[-2:]) cropped = img[:, x_start:x_end, y_start:y_end] - d[key] = SpatialPad(spatial_size=roi_size)(cropped) + d[key] = SpatialPad(spatial_size=roi_size, **self.kwargs)(cropped) return d def bbox(self, patch_size, centroid, size): From dd188577f3a48f0e12c19e71688cf08eb2e98e6c Mon Sep 17 00:00:00 2001 From: vnath Date: Tue, 31 May 2022 11:17:54 -0500 Subject: [PATCH 40/40] Added a unit test for kwargs of SpatialPad in ExtractPatchd and a doc-string Signed-off-by: vnath --- monai/apps/nuclick/transforms.py | 1 + tests/test_nuclick_transforms.py | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/monai/apps/nuclick/transforms.py b/monai/apps/nuclick/transforms.py index 7c0197f86d..d6be1a84fa 100644 --- a/monai/apps/nuclick/transforms.py +++ b/monai/apps/nuclick/transforms.py @@ -77,6 +77,7 @@ class ExtractPatchd(MapTransform): centroid_key: key where the centroid values are stored, defaults to ``"centroid"`` patch_size: size of the extracted patch allow_missing_keys: don't raise exception if key is missing. + pad_kwargs: other arguments for the SpatialPad transform """ def __init__( diff --git a/tests/test_nuclick_transforms.py b/tests/test_nuclick_transforms.py index f9559a1c35..58876c3c36 100644 --- a/tests/test_nuclick_transforms.py +++ b/tests/test_nuclick_transforms.py @@ -142,6 +142,12 @@ EXTRACT_RESULT_TEST_CASE_1 = [{"keys": ["image", "label"], "patch_size": 3}, DATA_EXTRACT_1, EXTRACT_RESULT_TC1] EXTRACT_RESULT_TEST_CASE_2 = [{"keys": ["image", "label"], "patch_size": 4}, DATA_EXTRACT_2, EXTRACT_RESULT_TC2] +EXTRACT_KW_TEST_CASE_1 = [ + {"keys": ["image", "label"], "patch_size": 3, "mode": "constant"}, + DATA_EXTRACT_1, + EXTRACT_RESULT_TC1, +] + SPLIT_TEST_CASE_1 = [{"keys": ["label"], "mask_value": "mask_value", "min_area": 1}, DATA_SPLIT_1, SPLIT_RESULT_TC1] SPLIT_TEST_CASE_2 = [{"keys": ["label"], "mask_value": "mask_value", "min_area": 3}, DATA_SPLIT_2, SPLIT_RESULT_TC2] @@ -174,7 +180,7 @@ def test_correct_patch_size(self, arguments, input_data, expected_shape): result = ExtractPatchd(**arguments)(input_data) np.testing.assert_equal(result["label"].shape, expected_shape) - @parameterized.expand([EXTRACT_RESULT_TEST_CASE_1, EXTRACT_RESULT_TEST_CASE_2]) + @parameterized.expand([EXTRACT_RESULT_TEST_CASE_1, EXTRACT_RESULT_TEST_CASE_2, EXTRACT_KW_TEST_CASE_1]) def test_correct_results(self, arguments, input_data, expected_result): result = ExtractPatchd(**arguments)(input_data) np.testing.assert_equal(result["label"], expected_result)