From 568ac4d23655e86de16a86ef652f7c223f9be706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20P=C3=A9rez-Garc=C3=ADa?= Date: Wed, 20 Apr 2022 13:44:00 +0000 Subject: [PATCH 1/7] Add failing tests --- hi-ml/testhiml/testhiml/test_box_utils.py | 59 ++++++++++++++++++++--- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/hi-ml/testhiml/testhiml/test_box_utils.py b/hi-ml/testhiml/testhiml/test_box_utils.py index da27474bf..784e811fb 100644 --- a/hi-ml/testhiml/testhiml/test_box_utils.py +++ b/hi-ml/testhiml/testhiml/test_box_utils.py @@ -8,27 +8,72 @@ from health_ml.utils.box_utils import Box, get_bounding_box -def test_get_bounding_box() -> None: +def test_no_zeros() -> None: length_x = 3 length_y = 4 # If no elements are zero, the bounding box will have the same shape as the original - mask = np.random.randint(1, 10, size=(length_x, length_y)) + mask = np.random.randint(1, 10, size=(length_y, length_x)) bbox = get_bounding_box(mask) assert isinstance(bbox, Box) assert bbox.w == length_x assert bbox.h == length_y + +def test_bounding_box_3d() -> None: # passing a 3D array should cause an error to be raised - length_z = 5 - mask_3d = np.random.randint(0, 10, size=(length_x, length_y, length_z)) + mask_3d = np.random.randint(0, 10, size=(1, 2, 3)) with pytest.raises(TypeError): get_bounding_box(mask_3d) + +def test_identity_matrix() -> None: # passing an identity matrix will return a bounding box with the same shape as the original, # and xmin and ymin will both be zero - mask_eye = np.eye(length_x) + length = 5 + mask_eye = np.eye(length) bbox_eye = get_bounding_box(mask_eye) assert isinstance(bbox_eye, Box) - assert bbox_eye.w == length_x - assert bbox_eye.h == length_x + assert bbox_eye.w == length + assert bbox_eye.h == length assert bbox_eye.x == bbox_eye.y == 0 + + +def test_all_zeros() -> None: + mask = np.zeros((2, 3)) + with pytest.raises(RuntimeError): + get_bounding_box(mask) + + +def test_small_rectangle() -> None: + mask = np.zeros((5, 5), int) + row = 0 + height = 1 + width = 2 + col = 3 + mask[row:row + height, col:col + width] = 1 + # array([[0, 0, 0, 1, 1], + # [0, 0, 0, 0, 0], + # [0, 0, 0, 0, 0], + # [0, 0, 0, 0, 0], + # [0, 0, 0, 0, 0]]) + bbox = get_bounding_box(mask) + assert bbox.x == col + assert bbox.y == row + assert bbox.w == width + assert bbox.h == height + + +def test_tiny_mask() -> None: + mask = np.array(1).reshape(1, 1) + bbox = get_bounding_box(mask) + assert bbox.x == bbox.y == 0 + assert bbox.w == bbox.h == 1 + + +def test_tiny_box() -> None: + mask = np.array(( + (0, 0), + (0, 1), + )) + bbox = get_bounding_box(mask) + assert bbox.x == bbox.y == bbox.w == bbox.h == 1 \ No newline at end of file From 58b8f3f395e43193a630f5eff62ced9d928e6d0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20P=C3=A9rez-Garc=C3=ADa?= Date: Wed, 20 Apr 2022 14:04:05 +0000 Subject: [PATCH 2/7] Add line at the end of the file --- hi-ml/testhiml/testhiml/test_box_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hi-ml/testhiml/testhiml/test_box_utils.py b/hi-ml/testhiml/testhiml/test_box_utils.py index 784e811fb..8986d2685 100644 --- a/hi-ml/testhiml/testhiml/test_box_utils.py +++ b/hi-ml/testhiml/testhiml/test_box_utils.py @@ -76,4 +76,4 @@ def test_tiny_box() -> None: (0, 1), )) bbox = get_bounding_box(mask) - assert bbox.x == bbox.y == bbox.w == bbox.h == 1 \ No newline at end of file + assert bbox.x == bbox.y == bbox.w == bbox.h == 1 From e43646b4ef336bad4a6125a9953b72d7bdce4282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20P=C3=A9rez-Garc=C3=ADa?= Date: Wed, 20 Apr 2022 14:16:55 +0000 Subject: [PATCH 3/7] Fix get_bounding_box --- hi-ml/src/health_ml/utils/box_utils.py | 27 +++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/hi-ml/src/health_ml/utils/box_utils.py b/hi-ml/src/health_ml/utils/box_utils.py index 5beb39fb1..786341851 100644 --- a/hi-ml/src/health_ml/utils/box_utils.py +++ b/hi-ml/src/health_ml/utils/box_utils.py @@ -2,6 +2,7 @@ from typing import Optional, Sequence, Tuple import numpy as np +from scipy import ndimage @dataclass(frozen=True) @@ -123,14 +124,22 @@ def get_bounding_box(mask: np.ndarray) -> Box: :param mask: A 2D array with 0 (or `False`) as background and >0 (or `True`) as foreground. :return: The smallest box covering all non-zero elements of `mask`. + :raises TypeError: When the input mask has more than two dimensions. + :raises RuntimeError: When all elements in the mask are zero. """ if mask.ndim != 2: - raise TypeError(f"Expected a 2D array but got {mask.ndim} dimensions") - - xs = np.sum(mask, 1).nonzero()[0] - ys = np.sum(mask, 0).nonzero()[0] - x_min, x_max = xs.min(), xs.max() - y_min, y_max = ys.min(), ys.max() - width = x_max - x_min + 1 - height = y_max - y_min + 1 - return Box(x_min, y_min, width, height) + raise TypeError(f"Expected a 2D array but got an array with shape {mask.shape}") + + slices = ndimage.find_objects(mask > 0) + if not slices: + raise RuntimeError("The input mask is empty") + assert len(slices) == 1 + + slice_y, slice_x = slices[0] + box = Box( + x=slice_x.start, + y=slice_y.start, + w=slice_x.stop - slice_x.start, + h=slice_y.stop - slice_y.start, + ) + return box From 0dfbda9ac9af1e24d52d09952961a4addc555a53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20P=C3=A9rez-Garc=C3=ADa?= Date: Wed, 20 Apr 2022 17:14:25 +0000 Subject: [PATCH 4/7] Add test for multiple connected components --- hi-ml/testhiml/testhiml/test_box_utils.py | 28 ++++++++++++++--------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/hi-ml/testhiml/testhiml/test_box_utils.py b/hi-ml/testhiml/testhiml/test_box_utils.py index 8986d2685..2f3391d0f 100644 --- a/hi-ml/testhiml/testhiml/test_box_utils.py +++ b/hi-ml/testhiml/testhiml/test_box_utils.py @@ -15,8 +15,8 @@ def test_no_zeros() -> None: mask = np.random.randint(1, 10, size=(length_y, length_x)) bbox = get_bounding_box(mask) assert isinstance(bbox, Box) - assert bbox.w == length_x - assert bbox.h == length_y + expected = Box(x=0, y=0, w=length_x, h=length_y) + assert bbox == expected def test_bounding_box_3d() -> None: @@ -31,11 +31,9 @@ def test_identity_matrix() -> None: # and xmin and ymin will both be zero length = 5 mask_eye = np.eye(length) - bbox_eye = get_bounding_box(mask_eye) - assert isinstance(bbox_eye, Box) - assert bbox_eye.w == length - assert bbox_eye.h == length - assert bbox_eye.x == bbox_eye.y == 0 + bbox = get_bounding_box(mask_eye) + expected = Box(x=0, y=0, w=length, h=length) + assert bbox == expected def test_all_zeros() -> None: @@ -57,10 +55,8 @@ def test_small_rectangle() -> None: # [0, 0, 0, 0, 0], # [0, 0, 0, 0, 0]]) bbox = get_bounding_box(mask) - assert bbox.x == col - assert bbox.y == row - assert bbox.w == width - assert bbox.h == height + expected = Box(x=col, y=row, w=width, h=height) + assert bbox == expected def test_tiny_mask() -> None: @@ -77,3 +73,13 @@ def test_tiny_box() -> None: )) bbox = get_bounding_box(mask) assert bbox.x == bbox.y == bbox.w == bbox.h == 1 + + +def test_multiple_components() -> None: + length = 3 + mask = np.zeros((length, length), int) + mask[0, 0] = 1 + mask[length - 1, length - 1] = 1 + bbox = get_bounding_box(mask) + expected = Box(x=0, y=0, w=length, h=length) + assert bbox == expected \ No newline at end of file From 77b7b622dc7deb8d898df807a96ac436fef29ef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20P=C3=A9rez-Garc=C3=ADa?= Date: Wed, 20 Apr 2022 17:15:20 +0000 Subject: [PATCH 5/7] Use factory staticmethod to instantiate box --- hi-ml/src/health_ml/utils/box_utils.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/hi-ml/src/health_ml/utils/box_utils.py b/hi-ml/src/health_ml/utils/box_utils.py index 786341851..a54fe1e1b 100644 --- a/hi-ml/src/health_ml/utils/box_utils.py +++ b/hi-ml/src/health_ml/utils/box_utils.py @@ -135,11 +135,4 @@ def get_bounding_box(mask: np.ndarray) -> Box: raise RuntimeError("The input mask is empty") assert len(slices) == 1 - slice_y, slice_x = slices[0] - box = Box( - x=slice_x.start, - y=slice_y.start, - w=slice_x.stop - slice_x.start, - h=slice_y.stop - slice_y.start, - ) - return box + return Box.from_slices(slices[0]) From b15ce44ed34486bda7e93e97bc71f48daa845594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20P=C3=A9rez-Garc=C3=ADa?= Date: Wed, 20 Apr 2022 18:49:49 +0000 Subject: [PATCH 6/7] Add line at the end of the file again --- hi-ml/testhiml/testhiml/test_box_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hi-ml/testhiml/testhiml/test_box_utils.py b/hi-ml/testhiml/testhiml/test_box_utils.py index 2f3391d0f..5974e7338 100644 --- a/hi-ml/testhiml/testhiml/test_box_utils.py +++ b/hi-ml/testhiml/testhiml/test_box_utils.py @@ -82,4 +82,4 @@ def test_multiple_components() -> None: mask[length - 1, length - 1] = 1 bbox = get_bounding_box(mask) expected = Box(x=0, y=0, w=length, h=length) - assert bbox == expected \ No newline at end of file + assert bbox == expected From f06bb4e9cc0d43e8dc285a5d1e20e120dab09bd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20P=C3=A9rez-Garc=C3=ADa?= Date: Thu, 21 Apr 2022 09:06:46 +0000 Subject: [PATCH 7/7] Add Microsoft copyright notice --- hi-ml/src/health_ml/utils/box_utils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/hi-ml/src/health_ml/utils/box_utils.py b/hi-ml/src/health_ml/utils/box_utils.py index a54fe1e1b..1e398a55e 100644 --- a/hi-ml/src/health_ml/utils/box_utils.py +++ b/hi-ml/src/health_ml/utils/box_utils.py @@ -1,3 +1,8 @@ +# ------------------------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +# ------------------------------------------------------------------------------------------ + from dataclasses import dataclass from typing import Optional, Sequence, Tuple