diff --git a/python/cucim/src/cucim/skimage/segmentation/__init__.py b/python/cucim/src/cucim/skimage/segmentation/__init__.py index fe217eebc..b5527b8a1 100644 --- a/python/cucim/src/cucim/skimage/segmentation/__init__.py +++ b/python/cucim/src/cucim/skimage/segmentation/__init__.py @@ -1,3 +1,4 @@ +from ._clear_border import clear_border from ._join import join_segmentations, relabel_sequential from .boundaries import find_boundaries, mark_boundaries from .morphsnakes import (checkerboard_level_set, disk_level_set, diff --git a/python/cucim/src/cucim/skimage/segmentation/_clear_border.py b/python/cucim/src/cucim/skimage/segmentation/_clear_border.py new file mode 100644 index 000000000..dbbdb8d00 --- /dev/null +++ b/python/cucim/src/cucim/skimage/segmentation/_clear_border.py @@ -0,0 +1,120 @@ +import cupy as cp + +from ..measure import label + +_clear_border_labels = cp.ElementwiseKernel( + in_params="raw X labels, raw X borders_indices, int32 nvals, Y bgval", + out_params="Y out", + operation=""" + for (int j=0; j>> import cupy as cp + >>> from cucim.skimage.segmentation import clear_border + >>> labels = cp.array([[0, 0, 0, 0, 0, 0, 0, 1, 0], + ... [1, 1, 0, 0, 1, 0, 0, 1, 0], + ... [1, 1, 0, 1, 0, 1, 0, 0, 0], + ... [0, 0, 0, 1, 1, 1, 1, 0, 0], + ... [0, 1, 1, 1, 1, 1, 1, 1, 0], + ... [0, 0, 0, 0, 0, 0, 0, 0, 0]]) + >>> clear_border(labels) + array([[0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0]]) + >>> mask = cp.array([[0, 0, 1, 1, 1, 1, 1, 1, 1], + ... [0, 0, 1, 1, 1, 1, 1, 1, 1], + ... [1, 1, 1, 1, 1, 1, 1, 1, 1], + ... [1, 1, 1, 1, 1, 1, 1, 1, 1], + ... [1, 1, 1, 1, 1, 1, 1, 1, 1], + ... [1, 1, 1, 1, 1, 1, 1, 1, 1]]).astype(bool) + >>> clear_border(labels, mask=mask) + array([[0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 1, 0, 0, 1, 0], + [0, 0, 0, 1, 0, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0]]) + + """ + if any((buffer_size >= s for s in labels.shape)) and mask is None: + # ignore buffer_size if mask + raise ValueError("buffer size may not be greater than labels size") + + if out is not None: + cp.copyto(out, labels, casting='no') + else: + out = labels.copy() + + if mask is not None: + err_msg = (f'labels and mask should have the same shape but ' + f'are {out.shape} and {mask.shape}') + if out.shape != mask.shape: + raise(ValueError, err_msg) + if mask.dtype != bool: + raise TypeError("mask should be of type bool.") + borders = ~mask + else: + # create borders with buffer_size + borders = cp.zeros_like(out, dtype=bool) + ext = buffer_size + 1 + slstart = slice(ext) + slend = slice(-ext, None) + slices = [slice(None) for _ in out.shape] + for d in range(out.ndim): + slices[d] = slstart + borders[tuple(slices)] = True + slices[d] = slend + borders[tuple(slices)] = True + slices[d] = slice(None) + + # Re-label, in case we are dealing with a binary out + # and to get consistent labeling + labels, number = label(out, background=0, return_num=True) + + # determine all objects that are connected to borders + borders_indices = cp.unique(labels[borders]) + + _clear_border_labels( + labels, borders_indices, borders_indices.size, bgval, out + ) + return out diff --git a/python/cucim/src/cucim/skimage/segmentation/tests/test_clear_border.py b/python/cucim/src/cucim/skimage/segmentation/tests/test_clear_border.py new file mode 100644 index 000000000..8c21d9e19 --- /dev/null +++ b/python/cucim/src/cucim/skimage/segmentation/tests/test_clear_border.py @@ -0,0 +1,233 @@ +import cupy as cp +from cupy.testing import assert_array_equal + +from cucim.skimage.segmentation import clear_border + + +def test_clear_border(): + image = cp.array( + [[0, 0, 0, 0, 0, 0, 0, 1, 0], + [1, 1, 0, 0, 1, 0, 0, 1, 0], + [1, 1, 0, 1, 0, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0]] + ) + + # test default case + result = clear_border(image.copy()) + ref = image.copy() + ref[1:3, 0:2] = 0 + ref[0:2, -2] = 0 + assert_array_equal(result, ref) + + # test buffer + result = clear_border(image.copy(), 1) + assert_array_equal(result, cp.zeros(result.shape)) + + # test background value + result = clear_border(image.copy(), buffer_size=1, bgval=2) + assert_array_equal(result, cp.full_like(image, 2)) + + # test mask + mask = cp.array([[0, 0, 1, 1, 1, 1, 1, 1, 1], + [0, 0, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1]]).astype(bool) + result = clear_border(image.copy(), mask=mask) + ref = image.copy() + ref[1:3, 0:2] = 0 + assert_array_equal(result, ref) + + +def test_clear_border_3d(): + image = cp.array( + [[[0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [1, 0, 0, 0]], + [[0, 0, 0, 0], + [0, 1, 1, 0], + [0, 0, 1, 0], + [0, 0, 0, 0]], + [[0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0]]] + ) + # test default case + result = clear_border(image.copy()) + ref = image.copy() + ref[0, 3, 0] = 0 + assert_array_equal(result, ref) + + # test buffer + result = clear_border(image.copy(), 1) + assert_array_equal(result, cp.zeros(result.shape)) + + # test background value + result = clear_border(image.copy(), buffer_size=1, bgval=2) + assert_array_equal(result, cp.full_like(image, 2)) + + # test floating-point background value + image_f32 = image.astype(cp.float32) + result = clear_border(image_f32, buffer_size=1, bgval=2.5) + assert_array_equal(result, cp.full_like(image_f32, 2.5)) + + +def test_clear_border_non_binary(): + image = cp.array([[1, 2, 3, 1, 2], + [3, 3, 5, 4, 2], + [3, 4, 5, 4, 2], + [3, 3, 2, 1, 2]]) + + result = clear_border(image) + expected = cp.array([[0, 0, 0, 0, 0], + [0, 0, 5, 4, 0], + [0, 4, 5, 4, 0], + [0, 0, 0, 0, 0]]) + + assert_array_equal(result, expected) + assert not cp.all(image == result) + + +def test_clear_border_non_binary_3d(): + image3d = cp.array( + [[[1, 2, 3, 1, 2], + [3, 3, 3, 4, 2], + [3, 4, 3, 4, 2], + [3, 3, 2, 1, 2]], + [[1, 2, 3, 1, 2], + [3, 3, 5, 4, 2], + [3, 4, 5, 4, 2], + [3, 3, 2, 1, 2]], + [[1, 2, 3, 1, 2], + [3, 3, 3, 4, 2], + [3, 4, 3, 4, 2], + [3, 3, 2, 1, 2]]] + ) + + result = clear_border(image3d) + expected = cp.array( + [[[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]], + [[0, 0, 0, 0, 0], + [0, 0, 5, 0, 0], + [0, 0, 5, 0, 0], + [0, 0, 0, 0, 0]], + [[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]]] + ) + + assert_array_equal(result, expected) + assert not cp.all(image3d == result) + + +def test_clear_border_non_binary_inplace(): + image = cp.array([[1, 2, 3, 1, 2], + [3, 3, 5, 4, 2], + [3, 4, 5, 4, 2], + [3, 3, 2, 1, 2]]) + result = clear_border(image, out=image) + expected = cp.array([[0, 0, 0, 0, 0], + [0, 0, 5, 4, 0], + [0, 4, 5, 4, 0], + [0, 0, 0, 0, 0]]) + + assert_array_equal(result, expected) + assert_array_equal(image, result) + + +def test_clear_border_non_binary_inplace_3d(): + image3d = cp.array( + [[[1, 2, 3, 1, 2], + [3, 3, 3, 4, 2], + [3, 4, 3, 4, 2], + [3, 3, 2, 1, 2]], + [[1, 2, 3, 1, 2], + [3, 3, 5, 4, 2], + [3, 4, 5, 4, 2], + [3, 3, 2, 1, 2]], + [[1, 2, 3, 1, 2], + [3, 3, 3, 4, 2], + [3, 4, 3, 4, 2], + [3, 3, 2, 1, 2]]] + ) + + result = clear_border(image3d, out=image3d) + expected = cp.array( + [[[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]], + [[0, 0, 0, 0, 0], + [0, 0, 5, 0, 0], + [0, 0, 5, 0, 0], + [0, 0, 0, 0, 0]], + [[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]]] + ) + + assert_array_equal(result, expected) + assert_array_equal(image3d, result) + + +def test_clear_border_non_binary_out(): + image = cp.array([[1, 2, 3, 1, 2], + [3, 3, 5, 4, 2], + [3, 4, 5, 4, 2], + [3, 3, 2, 1, 2]]) + out = cp.empty_like(image) + result = clear_border(image, out=out) + expected = cp.array([[0, 0, 0, 0, 0], + [0, 0, 5, 4, 0], + [0, 4, 5, 4, 0], + [0, 0, 0, 0, 0]]) + + assert_array_equal(result, expected) + assert_array_equal(out, result) + + +def test_clear_border_non_binary_out_3d(): + image3d = cp.array( + [[[1, 2, 3, 1, 2], + [3, 3, 3, 4, 2], + [3, 4, 3, 4, 2], + [3, 3, 2, 1, 2]], + [[1, 2, 3, 1, 2], + [3, 3, 5, 4, 2], + [3, 4, 5, 4, 2], + [3, 3, 2, 1, 2]], + [[1, 2, 3, 1, 2], + [3, 3, 3, 4, 2], + [3, 4, 3, 4, 2], + [3, 3, 2, 1, 2]]] + ) + out = cp.empty_like(image3d) + + result = clear_border(image3d, out=out) + expected = cp.array( + [[[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]], + [[0, 0, 0, 0, 0], + [0, 0, 5, 0, 0], + [0, 0, 5, 0, 0], + [0, 0, 0, 0, 0]], + [[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]]] + ) + + assert_array_equal(result, expected) + assert_array_equal(out, result)