Repository: scikit-image/scikit-image

Issue:
'slic' crash with spacing
sigma division does not seems correct since sigma is a number and spacing is array_like

to Reproduce:

```
import numpy as np
from skimage import segmentation, io

image = io.imread('drosophila.png')

image = image[:,:,1]
image = image / float(np.max(image))
print image.shape
seg = segmentation.slic(image, n_segments=500, compactness=0.2, multichannel=False, spacing=(1.,1.)) 
```

![drosophila](https://cloud.githubusercontent.com/assets/6035284/9661268/3f3297ae-525a-11e5-8b02-09de65c6dba7.png)



Discussion:
@Borda, the error message is obscure, I'll grant, =), but the docstring does specify that spacing must be length 3!

We don't currently support spacing in 2D images because it's rare to have different voxel spacing in just 2D images. (I have never come across this.) Do you have a use-case for 2D spacing? If so, I can add support for that. If not, I'll add a `ValueError` that explains what went wrong better than the current message.

Thanks for the report!

Well, I need segment 3D image but I wanted to start with simpler case (2D) a and give a try to the parameters I will need for 3D such as spacing... btw, what is the explanation of these numbers, 1 mean that distance between neighboring positions is 1? Thanks

@Borda the spacing is meant to be relative. (5, 1, 1) means that the distance between adjacent voxels in the leading dimension (e.g. planes) is 5 times greater than the distance between voxels in the row / column dimensions. 

@jni How about a sentence in the user guide summarizing your comments here?  We can perhaps also show how to address @Borda's user case (perhaps it requires adding a single dimension to make a 2D slice 3D).

I'd argue that we should be as general as possible when we can be. 2D anisotropic is a strange use case, but I think it's entirely possible to relax the length-3 requirement in favor of an iterable of length `image.ndim`. In fact, I'd recommend any algorithm which accepts a `spacing` kwarg should behave in that manner.

I can provide even code for 3D with interactive visualization in iPython if you like:

```
import libtiff
from skimage import segmentation

im = libtiff.TiffFile('drosophila.tiff') .get_tiff_array() 
img = np.empty(im.shape)
for i in range(img.shape[0]) :
    img[i,:,:] = np.array(im[i])
img = img / float(np.max(img))

seg = segmentation.slic(img, n_segments=5000, compactness=0.2, multichannel=False, spacing=(5.,1.,1.))

def showImageSegment(z):
    plt.figure(figsize=(12,8))
    _= plt.subplot(1,2,1), plt.imshow(img[z], cmap=plt.cm.gray), plt.axis('off')
    _= plt.subplot(1,2,2), plt.imshow(seg[z]), plt.axis('off')
    display()

interact( showImageSegment, z=widgets.IntSliderWidget(min=0,max=im.shape[0]-1,step=1,value=im.shape[0]/2) )
```

@JDWarner I agree, but it's a bit finicky to deal with all the possible cases (multichannel = True/False, sigma being a scalar or an array-like). A quick glance at the code didn't suggest an immediate solution. PRs welcome, as always! ;)

Happy to keep this open as a reminder to do that?

/ping @jni @JDWarner 

Can we close this with an addition to the docstring, or is something more involved needed?

A case for 2-D anisotropic images is the case of spatio-temporal diagrams (used for example in chaos and non-linear systems).

any update here? :)
Stand by my earlier comment that we should generalize `spacing`, here and elsewhere, to match the spatial dimensionality of the image.

I won't be able to dive into the code this week as I am on 12 hour overnight call.
i gave the code a quick scan, i think the real challenge is coming up with tests. PRs would help :D.
I agree with @JDWarner, just haven't had the bandwidth to deal with this.

@Borda 👋 😊 maybe you'd like to tackle this? imho the right way to do it would be to normalise both spacing and sigma to be the same size array early on in the function.
@jni not sure what you meant by "tackle this"? :smiley: 

Resolving patch:
diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py
--- a/skimage/segmentation/slic_superpixels.py
+++ b/skimage/segmentation/slic_superpixels.py
@@ -1,4 +1,5 @@
 from collections.abc import Iterable
+from warnings import warn
 
 import numpy as np
 from numpy import random
@@ -134,17 +135,20 @@ def slic(image, n_segments=100, compactness=10., max_num_iter=10, sigma=0,
         refining around a chosen value.
     max_num_iter : int, optional
         Maximum number of iterations of k-means.
-    sigma : float or (3,) array-like of floats, optional
+    sigma : float or array-like of floats, optional
         Width of Gaussian smoothing kernel for pre-processing for each
         dimension of the image. The same sigma is applied to each dimension in
         case of a scalar value. Zero means no smoothing.
-        Note, that `sigma` is automatically scaled if it is scalar and a
-        manual voxel spacing is provided (see Notes section).
-    spacing : (3,) array-like of floats, optional
-        The voxel spacing along each image dimension. By default, `slic`
-        assumes uniform spacing (same voxel resolution along z, y and x).
-        This parameter controls the weights of the distances along z, y,
-        and x during k-means clustering.
+        Note that `sigma` is automatically scaled if it is scalar and
+        if a manual voxel spacing is provided (see Notes section). If
+        sigma is array-like, its size must match ``image``'s number
+        of spatial dimensions.
+    spacing : array-like of floats, optional
+        The voxel spacing along each spatial dimension. By default,
+        `slic` assumes uniform spacing (same voxel resolution along
+        each spatial dimension).
+        This parameter controls the weights of the distances along the
+        spatial dimensions during k-means clustering.
     multichannel : bool, optional
         Whether the last axis of the image is to be interpreted as multiple
         channels or another spatial dimension. This argument is deprecated:
@@ -243,7 +247,8 @@ def slic(image, n_segments=100, compactness=10., max_num_iter=10, sigma=0,
 
     image = img_as_float(image)
     float_dtype = utils._supported_float_type(image.dtype)
-    # copy=True so subsequent in-place operations do not modify the function input
+    # copy=True so subsequent in-place operations do not modify the
+    # function input
     image = image.astype(float_dtype, copy=True)
 
     # Rescale image to [0, 1] to make choice of compactness insensitive to
@@ -295,16 +300,48 @@ def slic(image, n_segments=100, compactness=10., max_num_iter=10, sigma=0,
 
     if spacing is None:
         spacing = np.ones(3, dtype=dtype)
-    elif isinstance(spacing, (list, tuple)):
+    elif isinstance(spacing, Iterable):
+        spacing = np.asarray(spacing, dtype=dtype)
+        if is_2d:
+            if spacing.size != 2:
+                if spacing.size == 3:
+                    warn("Input image is 2D: spacing number of "
+                         "elements must be 2. In the future, a ValueError "
+                         "will be raised.", FutureWarning, stacklevel=2)
+                else:
+                    raise ValueError(f"Input image is 2D, but spacing has "
+                                     f"{spacing.size} elements (expected 2).")
+            else:
+                spacing = np.insert(spacing, 0, 1)
+        elif spacing.size != 3:
+            raise ValueError(f"Input image is 3D, but spacing has "
+                             f"{spacing.size} elements (expected 3).")
         spacing = np.ascontiguousarray(spacing, dtype=dtype)
+    else:
+        raise TypeError("spacing must be None or iterable.")
 
-    if not isinstance(sigma, Iterable):
+    if np.isscalar(sigma):
         sigma = np.array([sigma, sigma, sigma], dtype=dtype)
-        sigma /= spacing.astype(dtype)
-    elif isinstance(sigma, (list, tuple)):
-        sigma = np.array(sigma, dtype=dtype)
+        sigma /= spacing
+    elif isinstance(sigma, Iterable):
+        sigma = np.asarray(sigma, dtype=dtype)
+        if is_2d:
+            if sigma.size != 2:
+                if spacing.size == 3:
+                    warn("Input image is 2D: sigma number of "
+                         "elements must be 2. In the future, a ValueError "
+                         "will be raised.", FutureWarning, stacklevel=2)
+                else:
+                    raise ValueError(f"Input image is 2D, but sigma has "
+                                     f"{sigma.size} elements (expected 2).")
+            else:
+                sigma = np.insert(sigma, 0, 0)
+        elif sigma.size != 3:
+            raise ValueError(f"Input image is 3D, but sigma has "
+                             f"{sigma.size} elements (expected 3).")
+
     if (sigma > 0).any():
-        # add zero smoothing for multichannel dimension
+        # add zero smoothing for channel dimension
         sigma = list(sigma) + [0]
         image = ndi.gaussian_filter(image, sigma)
 


Test patch:
diff --git a/skimage/segmentation/tests/test_slic.py b/skimage/segmentation/tests/test_slic.py
--- a/skimage/segmentation/tests/test_slic.py
+++ b/skimage/segmentation/tests/test_slic.py
@@ -170,8 +170,10 @@ def test_list_sigma():
     img += 0.1 * rnd.normal(size=img.shape)
     result_sigma = np.array([[0, 0, 0, 1, 1, 1],
                              [0, 0, 0, 1, 1, 1]], int)
-    seg_sigma = slic(img, n_segments=2, sigma=[1, 50, 1],
-                     channel_axis=None, start_label=0)
+    with expected_warnings(["Input image is 2D: sigma number of "
+                            "elements must be 2"]):
+        seg_sigma = slic(img, n_segments=2, sigma=[1, 50, 1],
+                         channel_axis=None, start_label=0)
     assert_equal(seg_sigma, result_sigma)
 
 
@@ -186,7 +188,7 @@ def test_spacing():
     img += 0.1 * rnd.normal(size=img.shape)
     seg_non_spaced = slic(img, n_segments=2, sigma=0, channel_axis=None,
                           compactness=1.0, start_label=0)
-    seg_spaced = slic(img, n_segments=2, sigma=0, spacing=[1, 500, 1],
+    seg_spaced = slic(img, n_segments=2, sigma=0, spacing=[500, 1],
                       compactness=1.0, channel_axis=None, start_label=0)
     assert_equal(seg_non_spaced, result_non_spaced)
     assert_equal(seg_spaced, result_spaced)
@@ -360,7 +362,7 @@ def test_list_sigma_mask():
     img += 0.1 * rnd.normal(size=img.shape)
     result_sigma = np.array([[0, 1, 1, 2, 2, 0],
                              [0, 1, 1, 2, 2, 0]], int)
-    seg_sigma = slic(img, n_segments=2, sigma=[1, 50, 1],
+    seg_sigma = slic(img, n_segments=2, sigma=[50, 1],
                      channel_axis=None, mask=msk)
     assert_equal(seg_sigma, result_sigma)
 
@@ -378,7 +380,7 @@ def test_spacing_mask():
     img += 0.1 * rnd.normal(size=img.shape)
     seg_non_spaced = slic(img, n_segments=2, sigma=0, channel_axis=None,
                           compactness=1.0, mask=msk)
-    seg_spaced = slic(img, n_segments=2, sigma=0, spacing=[1, 50, 1],
+    seg_spaced = slic(img, n_segments=2, sigma=0, spacing=[50, 1],
                       compactness=1.0, channel_axis=None, mask=msk)
     assert_equal(seg_non_spaced, result_non_spaced)
     assert_equal(seg_spaced, result_spaced)