From 0bd1d4490ee63baa5f65d2d181e6bd98b158edcc Mon Sep 17 00:00:00 2001 From: Michal Romaniuk Date: Wed, 27 Nov 2013 17:07:48 +0000 Subject: [PATCH 1/5] Added SLIC-zero to SLIC and changed SLIC implementation slightly --- skimage/segmentation/_slic.pyx | 95 ++++++++++++++++++++---- skimage/segmentation/slic_superpixels.py | 20 +++-- 2 files changed, 95 insertions(+), 20 deletions(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index 64d09520..2c4d3199 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -3,6 +3,7 @@ #cython: nonecheck=False #cython: wraparound=False from libc.float cimport DBL_MAX +from cpython cimport bool import numpy as np cimport numpy as cnp @@ -11,8 +12,11 @@ from skimage.util import regular_grid def _slic_cython(double[:, :, :, ::1] image_zyx, double[:, ::1] segments, + float step, Py_ssize_t max_iter, - double[::1] spacing): + double[::1] spacing, + bool slic_zero, + ): """Helper function for SLIC segmentation. Parameters @@ -21,12 +25,17 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, The input image. segments : 2D array of double, shape (N, 3 + C) The initial centroids obtained by SLIC as [Z, Y, X, C...]. + step : double + The size of the step between two seeds in voxels. max_iter : int The maximum number of k-means iterations. spacing : 1D array of double, shape (3,) The voxel spacing along each image dimension. This parameter controls the weights of the distances along z, y, and x during - k-means clustering. + k-means clustering. + slic_zero : bool + True to run SLIC-ZERO, False to run original SLIC. + Returns ------- @@ -85,6 +94,14 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, sy = spacing[1] sx = spacing[2] + # The colors are scaled before being passed to _slic_cython so + # max_color_sq can be initialised as all ones + cdef double[::1] max_dist_color = np.ones(n_segments, dtype=np.double) + cdef double dist_color + + # The reference implementation calls this invxywt + cdef double zyx_wt = float(1) / (step ** 2) + for i in range(max_iter): change = 0 distance[:, :, :] = DBL_MAX @@ -105,19 +122,42 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, x_min = max(cx - 2 * step_x, 0) x_max = min(cx + 2 * step_x + 1, width) - for z in range(z_min, z_max): - dz = (sz * (cz - z)) ** 2 - for y in range(y_min, y_max): - dy = (sy * (cy - y)) ** 2 - for x in range(x_min, x_max): - dist_center = dz + dy + (sx * (cx - x)) ** 2 - for c in range(3, n_features): - dist_center += (image_zyx[z, y, x, c - 3] - - segments[k, c]) ** 2 - if distance[z, y, x] > dist_center: - nearest_segments[z, y, x] = k - distance[z, y, x] = dist_center - change = 1 + # The loop is duplicated to avoid looking up slic_zero in every + # iteration but perhaps it's better to improve readability at + # the cost of performance. + if slic_zero: + for z in range(z_min, z_max): + dz = (sz * (cz - z)) ** 2 + for y in range(y_min, y_max): + dy = (sy * (cy - y)) ** 2 + for x in range(x_min, x_max): + dist_center = (dz + dy + (sx * (cx - x)) ** 2) * zyx_wt + dist_color = 0 + for c in range(3, n_features): + dist_color += (image_zyx[z, y, x, c - 3] + - segments[k, c]) ** 2 + + dist_center += dist_color / max_dist_color[k] + + if distance[z, y, x] > dist_center: + nearest_segments[z, y, x] = k + distance[z, y, x] = dist_center + change = 1 + else: + for z in range(z_min, z_max): + dz = (sz * (cz - z)) ** 2 + for y in range(y_min, y_max): + dy = (sy * (cy - y)) ** 2 + for x in range(x_min, x_max): + dist_center = (dz + dy + (sx * (cx - x)) ** 2) * zyx_wt + for c in range(3, n_features): + dist_center += (image_zyx[z, y, x, c - 3] + - segments[k, c]) ** 2 + + if distance[z, y, x] > dist_center: + nearest_segments[z, y, x] = k + distance[z, y, x] = dist_center + change = 1 # stop if no pixel changed its segment if change == 0: @@ -144,6 +184,31 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, for c in range(n_features): segments[k, c] /= n_segment_elems[k] + # If in SLICO mode, update the color distance maxima + if slic_zero: + for z in range(depth): + for y in range(height): + for x in range(width): + + k = nearest_segments[z, y, x] + dist_color = 0 + + for c in range(3, n_features): + dist_color += (image_zyx[z, y, x, c - 3] + - segments[k, c]) ** 2 + + # The reference implementation seems to only change + # the color if it increases from previous iteration + if max_dist_color[k] < dist_color: + max_dist_color[k] = dist_color + + ## DEBUG + print('Iter %d' % (i,)) + print(str(np.asarray(max_dist_color))) + + print('Image: ') + print(str(np.asarray(image_zyx))) + return np.asarray(nearest_segments) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index 9be5aa62..3c447ff8 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -25,7 +25,8 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, compactness : float, optional Balances color-space proximity and image-space proximity. Higher values give more weight to image-space. As `compactness` tends to - infinity, superpixel shapes become square/cubic. + infinity, superpixel shapes become square/cubic. In SLICO mode, this + is the initial compactness. max_iter : int, optional Maximum number of iterations of k-means. sigma : float or (3,) array-like of floats, optional @@ -46,6 +47,8 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, Whether the input should be converted to Lab colorspace prior to segmentation. For this purpose, the input is assumed to be RGB. Highly recommended. + slic_zero: bool, optional + True to run SLIC-zero, False to run original SLIC. ratio : float, optional Synonym for `compactness`. This keyword is deprecated. enforce_connectivity: bool, optional (default False) @@ -171,10 +174,17 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, # we do the scaling of ratio in the same way as in the SLIC paper # so the values have the same meaning - ratio = float(max((step_z, step_y, step_x))) / compactness - image = np.ascontiguousarray(image * ratio) + step = float(max((step_z, step_y, step_x))) + ratio = float(1) / compactness + + if slic_zero: + image = np.ascontiguousarray(image * ratio) + else: + image = np.ascontiguousarray(image * ratio) - labels = _slic_cython(image, segments, max_iter, spacing) + # _slic_cython expects the image in zyx format... but isn't image in xyz + # format??? + labels = _slic_cython(image, segments, step, max_iter, spacing, slic_zero) if enforce_connectivity: segment_size = depth * height * width / n_segments @@ -188,4 +198,4 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, if is_2d: labels = labels[0] - return labels + return labels \ No newline at end of file From b4ac25200b67a457b02ea84f3a14cb245f314ab2 Mon Sep 17 00:00:00 2001 From: Michal Romaniuk Date: Wed, 27 Nov 2013 20:38:23 +0000 Subject: [PATCH 2/5] Bug fix: removed temporary debug code --- skimage/segmentation/_slic.pyx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index 2c4d3199..eb9742b1 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -34,7 +34,7 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, controls the weights of the distances along z, y, and x during k-means clustering. slic_zero : bool - True to run SLIC-ZERO, False to run original SLIC. + True to run SLIC-zero, False to run original SLIC. Returns @@ -202,13 +202,6 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, if max_dist_color[k] < dist_color: max_dist_color[k] = dist_color - ## DEBUG - print('Iter %d' % (i,)) - print(str(np.asarray(max_dist_color))) - - print('Image: ') - print(str(np.asarray(image_zyx))) - return np.asarray(nearest_segments) From 131dea07a0157421854083af4bdb629edc5c280d Mon Sep 17 00:00:00 2001 From: Michal Romaniuk Date: Tue, 21 Jan 2014 19:25:21 +0000 Subject: [PATCH 3/5] Merged the separate loops for SLIC-zero and SLIC into one, and some minor improvements based on feedback on Github. --- skimage/segmentation/_slic.pyx | 55 ++++++++---------------- skimage/segmentation/slic_superpixels.py | 17 ++++---- skimage/segmentation/tests/test_slic.py | 1 + 3 files changed, 28 insertions(+), 45 deletions(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index eb9742b1..ea5d9a30 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -10,12 +10,13 @@ cimport numpy as cnp from skimage.util import regular_grid + def _slic_cython(double[:, :, :, ::1] image_zyx, double[:, ::1] segments, float step, Py_ssize_t max_iter, double[::1] spacing, - bool slic_zero, + bint slic_zero, ): """Helper function for SLIC segmentation. @@ -36,7 +37,6 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, slic_zero : bool True to run SLIC-zero, False to run original SLIC. - Returns ------- nearest_segments : 3D array of int, shape (Z, Y, X) @@ -122,42 +122,25 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, x_min = max(cx - 2 * step_x, 0) x_max = min(cx + 2 * step_x + 1, width) - # The loop is duplicated to avoid looking up slic_zero in every - # iteration but perhaps it's better to improve readability at - # the cost of performance. - if slic_zero: - for z in range(z_min, z_max): - dz = (sz * (cz - z)) ** 2 - for y in range(y_min, y_max): - dy = (sy * (cy - y)) ** 2 - for x in range(x_min, x_max): - dist_center = (dz + dy + (sx * (cx - x)) ** 2) * zyx_wt - dist_color = 0 - for c in range(3, n_features): - dist_color += (image_zyx[z, y, x, c - 3] - - segments[k, c]) ** 2 - + for z in range(z_min, z_max): + dz = (sz * (cz - z)) ** 2 + for y in range(y_min, y_max): + dy = (sy * (cy - y)) ** 2 + for x in range(x_min, x_max): + dist_center = (dz + dy + (sx * (cx - x)) ** 2) * zyx_wt + dist_color = 0 + for c in range(3, n_features): + dist_color += (image_zyx[z, y, x, c - 3] + - segments[k, c]) ** 2 + if slic_zero: dist_center += dist_color / max_dist_color[k] + else: + dist_center += dist_color - if distance[z, y, x] > dist_center: - nearest_segments[z, y, x] = k - distance[z, y, x] = dist_center - change = 1 - else: - for z in range(z_min, z_max): - dz = (sz * (cz - z)) ** 2 - for y in range(y_min, y_max): - dy = (sy * (cy - y)) ** 2 - for x in range(x_min, x_max): - dist_center = (dz + dy + (sx * (cx - x)) ** 2) * zyx_wt - for c in range(3, n_features): - dist_center += (image_zyx[z, y, x, c - 3] - - segments[k, c]) ** 2 - - if distance[z, y, x] > dist_center: - nearest_segments[z, y, x] = k - distance[z, y, x] = dist_center - change = 1 + if distance[z, y, x] > dist_center: + nearest_segments[z, y, x] = k + distance[z, y, x] = dist_center + change = 1 # stop if no pixel changed its segment if change == 0: diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index 3c447ff8..b0175b71 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -12,7 +12,8 @@ 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, - enforce_connectivity=False, min_size_factor=0.5, max_size_factor=3): + enforce_connectivity=False, min_size_factor=0.5, max_size_factor=3, + slic_zero=False): """Segments image using k-means clustering in Color-(x,y,z) space. Parameters @@ -47,8 +48,6 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, Whether the input should be converted to Lab colorspace prior to segmentation. For this purpose, the input is assumed to be RGB. Highly recommended. - slic_zero: bool, optional - True to run SLIC-zero, False to run original SLIC. ratio : float, optional Synonym for `compactness`. This keyword is deprecated. enforce_connectivity: bool, optional (default False) @@ -59,6 +58,8 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, max_size_factor: float, optional Proportion of the maximum connected segment size. A value of 3 works in most of the cases. + slic_zero: bool, optional + True to run SLIC-zero, False to run original SLIC. Returns ------- labels : 2D or 3D array @@ -169,21 +170,19 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, segments_y[..., np.newaxis], segments_x[..., np.newaxis], segments_color - ], axis=-1).reshape(-1, 3 + image.shape[3]) + ], axis=-1).reshape(-1, 3 + image.shape[3]) segments = np.ascontiguousarray(segments) # we do the scaling of ratio in the same way as in the SLIC paper # so the values have the same meaning step = float(max((step_z, step_y, step_x))) - ratio = float(1) / compactness - + ratio = 1.0 / compactness + if slic_zero: image = np.ascontiguousarray(image * ratio) else: image = np.ascontiguousarray(image * ratio) - # _slic_cython expects the image in zyx format... but isn't image in xyz - # format??? labels = _slic_cython(image, segments, step, max_iter, spacing, slic_zero) if enforce_connectivity: @@ -198,4 +197,4 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, if is_2d: labels = labels[0] - return labels \ No newline at end of file + return labels diff --git a/skimage/segmentation/tests/test_slic.py b/skimage/segmentation/tests/test_slic.py index 15f03437..a0ea39f1 100644 --- a/skimage/segmentation/tests/test_slic.py +++ b/skimage/segmentation/tests/test_slic.py @@ -147,6 +147,7 @@ def test_enforce_connectivity(): assert_equal(segments_connected, result_connected) assert_equal(segments_disconnected, result_disconnected) + if __name__ == '__main__': from numpy import testing From 0c4adddf818a4d1f3d35975b5d52be2a6fc87d1c Mon Sep 17 00:00:00 2001 From: Michal Romaniuk Date: Wed, 22 Jan 2014 19:25:18 +0000 Subject: [PATCH 4/5] Some changes based on feedback from discussion on Github. --- skimage/segmentation/_slic.pyx | 6 +++--- skimage/segmentation/slic_superpixels.py | 12 +++++------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index ea5d9a30..51505cae 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -16,7 +16,7 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, float step, Py_ssize_t max_iter, double[::1] spacing, - bint slic_zero, + bint slic_zero ): """Helper function for SLIC segmentation. @@ -177,8 +177,8 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, dist_color = 0 for c in range(3, n_features): - dist_color += (image_zyx[z, y, x, c - 3] - - segments[k, c]) ** 2 + dist_color += (image_zyx[z, y, x, c - 3] - + segments[k, c]) ** 2 # The reference implementation seems to only change # the color if it increases from previous iteration diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index b0175b71..11373381 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -59,7 +59,8 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, Proportion of the maximum connected segment size. A value of 3 works in most of the cases. slic_zero: bool, optional - True to run SLIC-zero, False to run original SLIC. + Run SLIC-zero, the zero-parameter mode of SLIC + Returns ------- labels : 2D or 3D array @@ -169,8 +170,8 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, segments = np.concatenate([segments_z[..., np.newaxis], segments_y[..., np.newaxis], segments_x[..., np.newaxis], - segments_color - ], axis=-1).reshape(-1, 3 + image.shape[3]) + segments_color], + axis=-1).reshape(-1, 3 + image.shape[3]) segments = np.ascontiguousarray(segments) # we do the scaling of ratio in the same way as in the SLIC paper @@ -178,10 +179,7 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, step = float(max((step_z, step_y, step_x))) ratio = 1.0 / compactness - if slic_zero: - image = np.ascontiguousarray(image * ratio) - else: - image = np.ascontiguousarray(image * ratio) + image = np.ascontiguousarray(image * ratio) labels = _slic_cython(image, segments, step, max_iter, spacing, slic_zero) From ee3ca829c6e9ec55a444d2c15ab538c548312976 Mon Sep 17 00:00:00 2001 From: Michal Romaniuk Date: Sun, 26 Jan 2014 18:01:59 +0000 Subject: [PATCH 5/5] Added a test for slic_zero and some other changes based on feedback from Github. --- skimage/segmentation/_slic.pyx | 9 ++++----- skimage/segmentation/tests/test_slic.py | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index 51505cae..f7caee03 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -16,8 +16,7 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, float step, Py_ssize_t max_iter, double[::1] spacing, - bint slic_zero - ): + bint slic_zero): """Helper function for SLIC segmentation. Parameters @@ -99,8 +98,8 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, cdef double[::1] max_dist_color = np.ones(n_segments, dtype=np.double) cdef double dist_color - # The reference implementation calls this invxywt - cdef double zyx_wt = float(1) / (step ** 2) + # The reference implementation (Achanta et al.) calls this invxywt + cdef double spatial_weight = float(1) / (step ** 2) for i in range(max_iter): change = 0 @@ -127,7 +126,7 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, for y in range(y_min, y_max): dy = (sy * (cy - y)) ** 2 for x in range(x_min, x_max): - dist_center = (dz + dy + (sx * (cx - x)) ** 2) * zyx_wt + dist_center = (dz + dy + (sx * (cx - x)) ** 2) * spatial_weight dist_color = 0 for c in range(3, n_features): dist_color += (image_zyx[z, y, x, c - 3] diff --git a/skimage/segmentation/tests/test_slic.py b/skimage/segmentation/tests/test_slic.py index a0ea39f1..7dda66d2 100644 --- a/skimage/segmentation/tests/test_slic.py +++ b/skimage/segmentation/tests/test_slic.py @@ -148,6 +148,29 @@ def test_enforce_connectivity(): assert_equal(segments_disconnected, result_disconnected) +def test_slic_zero(): + # Same as test_color_2d but with slic_zero=True + rnd = np.random.RandomState(0) + img = np.zeros((20, 21, 3)) + img[:10, :10, 0] = 1 + img[10:, :10, 1] = 1 + img[10:, 10:, 2] = 1 + img += 0.01 * rnd.normal(size=img.shape) + img[img > 1] = 1 + img[img < 0] = 0 + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + seg = slic(img, n_segments=4, sigma=0, slic_zero=True) + + # we expect 4 segments + assert_equal(len(np.unique(seg)), 4) + assert_equal(seg.shape, img.shape[:-1]) + assert_equal(seg[:10, :10], 0) + assert_equal(seg[10:, :10], 2) + assert_equal(seg[:10, 10:], 1) + assert_equal(seg[10:, 10:], 3) + + if __name__ == '__main__': from numpy import testing