diff --git a/TODO.txt b/TODO.txt index 4fe8e90d..bbf248be 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,7 +1,8 @@ Version 0.11 ------------ * Remove deprecated `reverse_map` parameter of `skimage.transform.warp` - +* Change depecrated `enforce_connectivity=False`on skimage.segmentation.slic + and set it to True as default Version 0.10 ------------ diff --git a/skimage/_build.py b/skimage/_build.py index 862117ed..b2ea94fc 100644 --- a/skimage/_build.py +++ b/skimage/_build.py @@ -68,4 +68,4 @@ def _changed(filename): with open(filename_cache, 'wb') as cf: cf.write(md5_new.encode('utf-8')) - return md5_cached != md5_new + return md5_cached != md5_new.encode('utf-8') diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index c3d95ee0..64d09520 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -9,7 +9,6 @@ cimport numpy as cnp from skimage.util import regular_grid - def _slic_cython(double[:, :, :, ::1] image_zyx, double[:, ::1] segments, Py_ssize_t max_iter, @@ -146,3 +145,104 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, segments[k, c] /= n_segment_elems[k] return np.asarray(nearest_segments) + + +def _enforce_label_connectivity_cython(Py_ssize_t[:, :, ::1] segments, + Py_ssize_t n_segments, + Py_ssize_t min_size, + Py_ssize_t max_size): + """ Helper function to remove small disconnected regions from the labels + + Parameters + ---------- + segments : 3D array of int, shape (Z, Y, X) + The label field/superpixels found by SLIC. + n_segments: int + Number of specified segments + min_size: int + Minimum size of the segment + max_size: int + Maximum size of the segment. This is done for performance reasons, + to pre-allocate a sufficiently large array for the breadth first search + Returns + ------- + connected_segments : 3D array of int, shape (Z, Y, X) + A label field with connected labels starting at label=1 + """ + + # get image dimensions + cdef Py_ssize_t depth, height, width + depth = segments.shape[0] + height = segments.shape[1] + width = segments.shape[2] + + # neighborhood arrays + cdef Py_ssize_t[::1] ddx = np.array((1, -1, 0, 0, 0, 0)) + cdef Py_ssize_t[::1] ddy = np.array((0, 0, 1, -1, 0, 0)) + cdef Py_ssize_t[::1] ddz = np.array((0, 0, 0, 0, 1, -1)) + + # new object with connected segments initialized to -1 + cdef Py_ssize_t[:, :, ::1] connected_segments \ + = -1 * np.ones_like(segments, dtype=np.intp) + + cdef Py_ssize_t current_new_label = 0 + cdef Py_ssize_t label = 0 + + # variables for the breadth first search + cdef Py_ssize_t current_segment_size = 1 + cdef Py_ssize_t bfs_visited = 0 + cdef Py_ssize_t adjacent + + cdef Py_ssize_t zz, yy, xx + + cdef Py_ssize_t[:, ::1] coord_list = np.zeros((max_size, 3), dtype=np.intp) + + # loop through all image + for z in range(depth): + for y in range(height): + for x in range(width): + if connected_segments[z, y, x] >= 0: + continue + # find the component size + adjacent = 0 + label = segments[z, y, x] + connected_segments[z, y, x] = current_new_label + current_segment_size = 1 + bfs_visited = 0 + coord_list[bfs_visited, 0] = z + coord_list[bfs_visited, 1] = y + coord_list[bfs_visited, 2] = x + + #perform a breadth first search to find + # the size of the connected component + while bfs_visited != current_segment_size: + for i in range(6): + zz = coord_list[bfs_visited, 0] + ddz[i] + yy = coord_list[bfs_visited, 1] + ddy[i] + xx = coord_list[bfs_visited, 2] + ddx[i] + if (0 <= xx < width and + 0 <= yy < height and + 0 <= zz < depth): + if (segments[zz, yy, xx] == label and + connected_segments[zz, yy, xx] == -1): + connected_segments[zz, yy, xx] = \ + current_new_label + coord_list[current_segment_size, 0] = zz + coord_list[current_segment_size, 1] = yy + coord_list[current_segment_size, 2] = xx + current_segment_size += 1 + elif (connected_segments[zz, yy, xx] >= 0 and + connected_segments[zz, yy, xx] != current_new_label): + adjacent = connected_segments[zz, yy, xx] + bfs_visited += 1 + + # change to an adjacent one, like in the original paper + if current_segment_size < min_size: + for i in range(current_segment_size): + connected_segments[coord_list[i, 0], + coord_list[i, 1], + coord_list[i, 2]] = adjacent + else: + current_new_label += 1 + + return np.asarray(connected_segments) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index 21df45fd..9be5aa62 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -6,12 +6,13 @@ from scipy import ndimage import warnings from skimage.util import img_as_float, regular_grid -from skimage.segmentation._slic import _slic_cython +from skimage.segmentation._slic import _slic_cython, _enforce_label_connectivity_cython from skimage.color import rgb2lab def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, - spacing=None, multichannel=True, convert2lab=True, ratio=None): + spacing=None, multichannel=True, convert2lab=True, ratio=None, + enforce_connectivity=False, min_size_factor=0.5, max_size_factor=3): """Segments image using k-means clustering in Color-(x,y,z) space. Parameters @@ -47,7 +48,14 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, recommended. ratio : float, optional Synonym for `compactness`. This keyword is deprecated. - + enforce_connectivity: bool, optional (default False) + Whether the generated segments are connected or not + min_size_factor: float, optional + Proportion of the minimum segment size to be removed with respect + to the supposed segment size ```depth*width*height/n_segments``` + max_size_factor: float, optional + Proportion of the maximum connected segment size. A value of 3 works + in most of the cases. Returns ------- labels : 2D or 3D array @@ -104,6 +112,11 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, 'instead.') compactness = ratio + if enforce_connectivity is None: + warnings.warn('Deprecation: enforce_connectivity will default to' + ' True in future versions.') + enforce_connectivity = False + image = img_as_float(image) is_2d = False if image.ndim == 2: @@ -163,6 +176,15 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, labels = _slic_cython(image, segments, max_iter, spacing) + if enforce_connectivity: + segment_size = depth * height * width / n_segments + min_size = int(min_size_factor * segment_size) + max_size = int(max_size_factor * segment_size) + labels = _enforce_label_connectivity_cython(labels, + n_segments, + min_size, + max_size) + if is_2d: labels = labels[0] diff --git a/skimage/segmentation/tests/test_slic.py b/skimage/segmentation/tests/test_slic.py index 15d1fe6d..15f03437 100644 --- a/skimage/segmentation/tests/test_slic.py +++ b/skimage/segmentation/tests/test_slic.py @@ -76,7 +76,7 @@ def test_gray_3d(): midpoint = dim_size // 2 slices.append((slice(None, midpoint), slice(midpoint, None))) slices = list(it.product(*slices)) - shades = np.arange(0, 1.000001, 1.0/7) + shades = np.arange(0, 1.000001, 1.0 / 7) for s, sh in zip(slices, shades): img[s] = sh img += 0.001 * rnd.normal(size=img.shape) @@ -120,11 +120,34 @@ def test_spacing(): def test_invalid_lab_conversion(): img = np.array([[1, 1, 1, 0, 0], - [1, 1, 0, 0, 0]], np.float) + [1, 1, 0, 0, 0]], np.float) + 1 assert_raises(ValueError, slic, img, multichannel=True, convert2lab=True) +def test_enforce_connectivity(): + img = np.array([[0, 0, 0, 1, 1, 1], + [1, 0, 0, 1, 1, 0], + [0, 0, 0, 1, 1, 0]], np.float) + + segments_connected = slic(img, 2, compactness=0.0001, + enforce_connectivity=True, + convert2lab=False) + segments_disconnected = slic(img, 2, compactness=0.0001, + enforce_connectivity=False, + convert2lab=False) + + result_connected = np.array([[0, 0, 0, 1, 1, 1], + [0, 0, 0, 1, 1, 1], + [0, 0, 0, 1, 1, 1]], np.float) + + result_disconnected = np.array([[0, 0, 0, 1, 1, 1], + [1, 0, 0, 1, 1, 0], + [0, 0, 0, 1, 1, 0]], np.float) + + assert_equal(segments_connected, result_connected) + assert_equal(segments_disconnected, result_disconnected) if __name__ == '__main__': from numpy import testing + testing.run_module_suite()