Repository: scikit-image/scikit-image

Issue:
profile_line gives wrong output for a binary mask
## Description

As of now (skimage 0.12.3), profile_line sometimes returns wrong output for binary masks (np.array(dtype=bool)). See below for an example. If the image is cast to an uint8 first, it returns the expected result.

A possible patch might be even just specifying the allowed dtypes in the docstring.
## Way to reproduce

``` python
from numpy import meshgrid, pi, cos, sin, all, uint8
from skimage.measure import profile_line

shape = (200, 200)
center_x, center_y = (140, 150)
radius = 20
x, y = meshgrid(range(shape[1]), range(shape[0]))
mask = (y - center_y)**2 + (x - center_x)**2 < radius**2
src = (center_y, center_x)
phi = 4*pi/9.
dy = 31 * cos(phi)
dx = 31 * sin(phi)
dst = (center_y + dy, center_x + dx)

profile_uint8 = profile_line(mask.astype(uint8), src, dst)
print profile_uint8
assert all(profile_uint8[:radius] == 1)

profile_bool = profile_line(mask, src, dst)
print profile_bool
assert all(profile_bool[:radius] == 1) # Fails

assert all(profile_bool == profile_uint8) # Fails
```

The output is (there is a zero in the middle of the profile_line output):

```
[ 1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.
  1.  1.  1.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
[ 1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  0. (ZERO HERE?!)  1.  1.
  1.  1.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]

---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-24-1a94903a5586> in <module>()
     19 profile_bool = profile_line(mask, src, dst)
     20 print profile_bool
---> 21 assert all(profile_bool[:radius] == 1) # Fails
     22 
     23 assert all(profile_bool == profile_uint8) # Fails

AssertionError: 
```



Discussion:
This is an issue with the `ndi.map_coordinates` call, and also dependent on the order of the spline used. 
If `order` is set to `0`, the test passes. 

Perhaps we should cast `bool` inputs to `uint8` for the `map_coordinates` call and then cast back for the return?

@jmetz that's a nice workaround, but I don't understand why the interpolation works better in that case...?

@jni - I'm not sure about that either :/ poked around here : https://github.com/scipy/scipy/blob/master/scipy/ndimage/src/ni_interpolation.c#L358 as this is where I think the magic is happening, but can't figure out why `bool` should behave differently to `uint8`. 

@jmetz I believe you have stumbled upon a bug in ndimage.  If you are still interested, can you create a small test case, that illustrates something like:

```
In [59]: ndi.map_coordinates(mask.astype(np.uint8), perp_lines, order=1, cval=0)[:, 0]
Out[59]: 
array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0], dtype=uint8)

In [60]: ndi.map_coordinates(mask, perp_lines, order=1, cval=0)[:, 0].astype(np.uint8)
Out[60]: 
array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0], dtype=uint8)
```

And then file it with scipy?

If you don't have time to do that, please let me know.
Hey @stefanv - sure no problem, just filed the issue with Scipy. 

Please comment and improve the issue as needed : https://github.com/scipy/scipy/issues/7051
Using the `order=0` for the interpolation fixes the issue.
```python
>>> profile_bool = profile_line(mask, src, dst, order=0)
>>> print(profile_bool)
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
```
It seems reasonable when a boolean array is provided (what means interpolating between `True` and `False`?).
Should we set the order to 0 when input dtype is bool?
May I close or do you still consider this as a bug?

Resolving patch:
diff --git a/skimage/measure/profile.py b/skimage/measure/profile.py
--- a/skimage/measure/profile.py
+++ b/skimage/measure/profile.py
@@ -1,9 +1,10 @@
+from warnings import warn
 import numpy as np
 from scipy import ndimage as ndi
 
 
 def profile_line(image, src, dst, linewidth=1,
-                 order=1, mode='constant', cval=0.0,
+                 order=None, mode='constant', cval=0.0,
                  *, reduce_func=np.mean):
     """Return the intensity profile of an image measured along a scan line.
 
@@ -21,8 +22,9 @@ def profile_line(image, src, dst, linewidth=1,
     linewidth : int, optional
         Width of the scan, perpendicular to the line
     order : int in {0, 1, 2, 3, 4, 5}, optional
-        The order of the spline interpolation to compute image values at
-        non-integer coordinates. 0 means nearest-neighbor interpolation.
+        The order of the spline interpolation, default is 0 if
+        image.dtype is bool and 1 otherwise. The order has to be in
+        the range 0-5. See `skimage.transform.warp` for detail.
     mode : {'constant', 'nearest', 'reflect', 'mirror', 'wrap'}, optional
         How to compute any values falling outside of the image.
     cval : float, optional
@@ -86,14 +88,26 @@ def profile_line(image, src, dst, linewidth=1,
            [1.        , 1.        , 0.        ],
            [1.41421356, 1.41421356, 0.        ]])
     """
+    if order is None:
+        order = 0 if image.dtype == bool else 1
+
+    if image.dtype == bool and order != 0:
+        warn("Input image dtype is bool. Interpolation is not defined "
+             "with bool data type. Please set order to 0 or explicitely "
+             "cast input image to another data type. Starting from version "
+             "0.19 a ValueError will be raised instead of this warning.",
+             FutureWarning, stacklevel=2)
+
     perp_lines = _line_profile_coordinates(src, dst, linewidth=linewidth)
     if image.ndim == 3:
         pixels = [ndi.map_coordinates(image[..., i], perp_lines,
-                                      order=order, mode=mode, cval=cval)
-                  for i in range(image.shape[2])]
+                                      prefilter=order > 1,
+                                      order=order, mode=mode,
+                                      cval=cval) for i in
+                  range(image.shape[2])]
         pixels = np.transpose(np.asarray(pixels), (1, 2, 0))
     else:
-        pixels = ndi.map_coordinates(image, perp_lines,
+        pixels = ndi.map_coordinates(image, perp_lines, prefilter=order > 1,
                                      order=order, mode=mode, cval=cval)
     # The outputted array with reduce_func=None gives an array where the
     # row values (axis=1) are flipped. Here, we make this consistent.


Test patch:
diff --git a/skimage/measure/tests/test_profile.py b/skimage/measure/tests/test_profile.py
--- a/skimage/measure/tests/test_profile.py
+++ b/skimage/measure/tests/test_profile.py
@@ -1,7 +1,7 @@
 import numpy as np
-from skimage.measure import profile_line
 
-from skimage._shared.testing import assert_equal, assert_almost_equal
+from ..._shared.testing import assert_equal, assert_almost_equal
+from ..profile import profile_line
 
 
 image = np.arange(100).reshape((10, 10)).astype(np.float)
@@ -178,3 +178,25 @@ def test_reduce_func_sumofsqrt_linewidth_3():
                         reduce_func=lambda x: np.sum(x**0.5))
     expected_prof = np.array([2.89083412, 3.45787824, 2.11623746, 0.77459667])
     assert_almost_equal(prof, expected_prof)
+
+
+def test_bool_array_input():
+
+    shape = (200, 200)
+    center_x, center_y = (140, 150)
+    radius = 20
+    x, y = np.meshgrid(range(shape[1]), range(shape[0]))
+    mask = (y - center_y) ** 2 + (x - center_x) ** 2 < radius ** 2
+    src = (center_y, center_x)
+    phi = 4 * np.pi / 9.
+    dy = 31 * np.cos(phi)
+    dx = 31 * np.sin(phi)
+    dst = (center_y + dy, center_x + dx)
+
+    profile_u8 = profile_line(mask.astype(np.uint8), src, dst)
+    assert all(profile_u8[:radius] == 1)
+
+    profile_b = profile_line(mask, src, dst)
+    assert all(profile_b[:radius] == 1)
+
+    assert all(profile_b == profile_u8)