Repository: scikit-image/scikit-image

Issue:
Rescaling boolean arrays yields plausible result or array of zeros depending on the scale factor
## Description
Hello!
I am using `skimage.transform.rescale` to down-scale boolean arrays that have a single connected component.
Depending on the scale factor selected, the output yields a result that seems correct or an array full of zeros.

If I convert the boolean array to float, the result is different than when I pass the array with values as bool or int. 
Setting 'anti-aliasing' to False yields the same result for int, bool or float values in the array.

Furthermore, the expression given in the `rescale` function documentation to compute the "anti_aliasing_sigma" is different than the actual expression used inside `skimage.transform.resize` (which is called in `skimage.transform.rescale`):
- `skimage.transform.rescale`: "By default, this value is chosen as (1 - s) / 2 where s is the down-scaling factor."
- `skimage.transform.resize`: "By default, this value is chosen as (s - 1) / 2 where s is the down-scaling factor, where s > 1."

## Way to reproduce
```python
import numpy as np
from skimage.transform import rescale

input_matrix_shape = (1000, 1200, 400)
high_resolution_matrix = np.zeros(input_matrix_shape)
high_resolution_matrix[200:800, 300:900, 100:300] = 1 # Fill matrix with some ones
high_resolution_matrix = high_resolution_matrix.astype(np.bool_)
scale_factor_list = [0.625, 0.305]

for current_scale_factor in scale_factor_list:
    rescaled_matrix = rescale(high_resolution_matrix, current_scale_factor, order = 0, anti_aliasing=True, multichannel=False, preserve_range=True)
    print('Scale factor: {}'.format(current_scale_factor))
    print('\tResult shape: {}'.format(rescaled_matrix.shape))
    print('\tTotal non-zero values: {}'.format(np.sum(rescaled_matrix > 0)))
    print()
```


## Version information
```python
# Paste the output of the following python commands
from __future__ import print_function
import sys; print(sys.version)
import platform; print(platform.platform())
import skimage; print("scikit-image version: {}".format(skimage.__version__))
import numpy; print("numpy version: {}".format(numpy.__version__))
```

```python
3.7.5 (default, Oct 17 2019, 12:21:00) 
[GCC 8.3.1 20190223 (Red Hat 8.3.1-2)]
Linux-5.2.17-100.fc29.x86_64-x86_64-with-fedora-29-Twenty_Nine
scikit-image version: 0.16.2
numpy version: 1.17.3
```




Discussion:
Confirmed! I can reproduce it :sob:  `resize` is also affected...
Thank you @franciscomcm for the report and sorry for answering so late :pray: 
OK, deactivating anti-aliasing fixes the issue!
I think that it makes sens when a bool array is provided as input to set `order=0` and `anti-aliasing=False` by default and to raise a warning if the user provides different values. I will open a PR for this.

Resolving patch:
diff --git a/skimage/_shared/utils.py b/skimage/_shared/utils.py
--- a/skimage/_shared/utils.py
+++ b/skimage/_shared/utils.py
@@ -286,3 +286,41 @@ def convert_to_float(image, preserve_range):
     else:
         image = img_as_float(image)
     return image
+
+
+def _validate_interpolation_order(image_dtype, order):
+    """Validate and return spline interpolation's order.
+
+    Parameters
+    ----------
+    image_dtype : dtype
+        Image dtype.
+    order : int, optional
+        The order of the spline interpolation. The order has to be in
+        the range 0-5. See `skimage.transform.warp` for detail.
+
+    Returns
+    -------
+    order : int
+        if input order is None, returns 0 if image_dtype is bool and 1
+        otherwise. Otherwise, image_dtype is checked and input order
+        is validated accordingly (order > 0 is not supported for bool
+        image dtype)
+
+    """
+
+    if order is None:
+        return 0 if image_dtype == bool else 1
+
+    if order < 0 or order > 5:
+        raise ValueError("Spline interpolation order has to be in the "
+                         "range 0-5.")
+
+    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)
+
+    return order
diff --git a/skimage/measure/profile.py b/skimage/measure/profile.py
--- a/skimage/measure/profile.py
+++ b/skimage/measure/profile.py
@@ -2,6 +2,8 @@
 import numpy as np
 from scipy import ndimage as ndi
 
+from .._shared.utils import _validate_interpolation_order
+
 
 def profile_line(image, src, dst, linewidth=1,
                  order=None, mode=None, cval=0.0,
@@ -89,15 +91,9 @@ 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)
+
+    order = _validate_interpolation_order(image.dtype, order)
+
     if mode is None:
         warn("Default out of bounds interpolation mode 'constant' is "
              "deprecated. In version 0.19 it will be set to 'reflect'. "
diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py
--- a/skimage/transform/_warps.py
+++ b/skimage/transform/_warps.py
@@ -7,7 +7,7 @@
 from ..measure import block_reduce
 
 from .._shared.utils import (get_bound_method_class, safe_as_int, warn,
-                             convert_to_float)
+                             convert_to_float, _validate_interpolation_order)
 
 HOMOGRAPHY_TRANSFORMS = (
     SimilarityTransform,
@@ -16,8 +16,8 @@
 )
 
 
-def resize(image, output_shape, order=1, mode='reflect', cval=0, clip=True,
-           preserve_range=False, anti_aliasing=True, anti_aliasing_sigma=None):
+def resize(image, output_shape, order=None, mode='reflect', cval=0, clip=True,
+           preserve_range=False, anti_aliasing=None, anti_aliasing_sigma=None):
     """Resize image to match a certain size.
 
     Performs interpolation to up-size or down-size N-dimensional images. Note
@@ -43,8 +43,9 @@ def resize(image, output_shape, order=1, mode='reflect', cval=0, clip=True,
     Other parameters
     ----------------
     order : int, optional
-        The order of the spline interpolation, default is 1. The order has to
-        be in the range 0-5. See `skimage.transform.warp` for detail.
+        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', 'edge', 'symmetric', 'reflect', 'wrap'}, optional
         Points outside the boundaries of the input are filled according
         to the given mode.  Modes match the behaviour of `numpy.pad`.
@@ -60,9 +61,10 @@ def resize(image, output_shape, order=1, mode='reflect', cval=0, clip=True,
         image is converted according to the conventions of `img_as_float`.
         Also see https://scikit-image.org/docs/dev/user_guide/data_types.html
     anti_aliasing : bool, optional
-        Whether to apply a Gaussian filter to smooth the image prior to
-        down-scaling. It is crucial to filter when down-sampling the image to
-        avoid aliasing artifacts.
+        Whether to apply a Gaussian filter to smooth the image prior
+        to down-scaling. It is crucial to filter when down-sampling
+        the image to avoid aliasing artifacts. If input image data
+        type is bool, no anti-aliasing is applied.
     anti_aliasing_sigma : {float, tuple of floats}, optional
         Standard deviation for Gaussian filtering to avoid aliasing artifacts.
         By default, this value is chosen as (s - 1) / 2 where s is the
@@ -100,6 +102,16 @@ def resize(image, output_shape, order=1, mode='reflect', cval=0, clip=True,
         raise ValueError("len(output_shape) cannot be smaller than the image "
                          "dimensions")
 
+    if anti_aliasing is None:
+        anti_aliasing = not image.dtype == bool
+
+    if image.dtype == bool and anti_aliasing:
+        warn("Input image dtype is bool. Gaussian convolution is not defined "
+             "with bool data type. Please set anti_aliasing to False 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)
+
     factors = (np.asarray(input_shape, dtype=float) /
                np.asarray(output_shape, dtype=float))
 
@@ -166,6 +178,8 @@ def resize(image, output_shape, order=1, mode='reflect', cval=0, clip=True,
                    preserve_range=preserve_range)
 
     else:  # n-dimensional interpolation
+        order = _validate_interpolation_order(image.dtype, order)
+
         coord_arrays = [factors[i] * (np.arange(d) + 0.5) - 0.5
                         for i, d in enumerate(output_shape)]
 
@@ -184,9 +198,9 @@ def resize(image, output_shape, order=1, mode='reflect', cval=0, clip=True,
     return out
 
 
-def rescale(image, scale, order=1, mode='reflect', cval=0, clip=True,
+def rescale(image, scale, order=None, mode='reflect', cval=0, clip=True,
             preserve_range=False, multichannel=False,
-            anti_aliasing=True, anti_aliasing_sigma=None):
+            anti_aliasing=None, anti_aliasing_sigma=None):
     """Scale image by a certain factor.
 
     Performs interpolation to up-scale or down-scale N-dimensional images.
@@ -210,8 +224,9 @@ def rescale(image, scale, order=1, mode='reflect', cval=0, clip=True,
     Other parameters
     ----------------
     order : int, optional
-        The order of the spline interpolation, default is 1. The order has to
-        be in the range 0-5. See `skimage.transform.warp` for detail.
+        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', 'edge', 'symmetric', 'reflect', 'wrap'}, optional
         Points outside the boundaries of the input are filled according
         to the given mode.  Modes match the behaviour of `numpy.pad`.
@@ -231,9 +246,10 @@ def rescale(image, scale, order=1, mode='reflect', cval=0, clip=True,
         Whether the last axis of the image is to be interpreted as multiple
         channels or another spatial dimension.
     anti_aliasing : bool, optional
-        Whether to apply a Gaussian filter to smooth the image prior to
-        down-scaling. It is crucial to filter when down-sampling the image to
-        avoid aliasing artifacts.
+        Whether to apply a Gaussian filter to smooth the image prior
+        to down-scaling. It is crucial to filter when down-sampling
+        the image to avoid aliasing artifacts. If input image data
+        type is bool, no anti-aliasing is applied.
     anti_aliasing_sigma : {float, tuple of floats}, optional
         Standard deviation for Gaussian filtering to avoid aliasing artifacts.
         By default, this value is chosen as (s - 1) / 2 where s is the
@@ -277,8 +293,8 @@ def rescale(image, scale, order=1, mode='reflect', cval=0, clip=True,
                   anti_aliasing_sigma=anti_aliasing_sigma)
 
 
-def rotate(image, angle, resize=False, center=None, order=1, mode='constant',
-           cval=0, clip=True, preserve_range=False):
+def rotate(image, angle, resize=False, center=None, order=None,
+           mode='constant', cval=0, clip=True, preserve_range=False):
     """Rotate image by a certain angle around its center.
 
     Parameters
@@ -305,8 +321,9 @@ def rotate(image, angle, resize=False, center=None, order=1, mode='constant',
     Other parameters
     ----------------
     order : int, optional
-        The order of the spline interpolation, default is 1. The order has to
-        be in the range 0-5. See `skimage.transform.warp` for detail.
+        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', 'edge', 'symmetric', 'reflect', 'wrap'}, optional
         Points outside the boundaries of the input are filled according
         to the given mode.  Modes match the behaviour of `numpy.pad`.
@@ -454,7 +471,7 @@ def _swirl_mapping(xy, center, rotation, strength, radius):
 
 
 def swirl(image, center=None, strength=1, radius=100, rotation=0,
-          output_shape=None, order=1, mode='reflect', cval=0, clip=True,
+          output_shape=None, order=None, mode='reflect', cval=0, clip=True,
           preserve_range=False):
     """Perform a swirl transformation.
 
@@ -483,8 +500,9 @@ def swirl(image, center=None, strength=1, radius=100, rotation=0,
         Shape of the output image generated. By default the shape of the input
         image is preserved.
     order : int, optional
-        The order of the spline interpolation, default is 1. The order has to
-        be in the range 0-5. See `skimage.transform.warp` for detail.
+        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', 'edge', 'symmetric', 'reflect', 'wrap'}, optional
         Points outside the boundaries of the input are filled according
         to the given mode, with 'constant' used as the default. Modes match
@@ -663,7 +681,7 @@ def _clip_warp_output(input_image, output_image, order, mode, cval, clip):
             output_image[cval_mask] = cval
 
 
-def warp(image, inverse_map, map_args={}, output_shape=None, order=1,
+def warp(image, inverse_map, map_args={}, output_shape=None, order=None,
          mode='constant', cval=0., clip=True, preserve_range=False):
     """Warp an image according to a given coordinate transformation.
 
@@ -717,6 +735,8 @@ def warp(image, inverse_map, map_args={}, output_shape=None, order=1,
          - 3: Bi-cubic
          - 4: Bi-quartic
          - 5: Bi-quintic
+
+         Default is 0 if image.dtype is bool and 1 otherwise.
     mode : {'constant', 'edge', 'symmetric', 'reflect', 'wrap'}, optional
         Points outside the boundaries of the input are filled according
         to the given mode.  Modes match the behaviour of `numpy.pad`.
@@ -804,7 +824,10 @@ def warp(image, inverse_map, map_args={}, output_shape=None, order=1,
     """
 
     if image.size == 0:
-        raise ValueError("Cannot warp empty image with dimensions", image.shape)
+        raise ValueError("Cannot warp empty image with dimensions",
+                         image.shape)
+
+    order = _validate_interpolation_order(image.dtype, order)
 
     image = convert_to_float(image, preserve_range)
 


Test patch:
diff --git a/skimage/_shared/tests/test_utils.py b/skimage/_shared/tests/test_utils.py
--- a/skimage/_shared/tests/test_utils.py
+++ b/skimage/_shared/tests/test_utils.py
@@ -1,9 +1,11 @@
 import sys
-from skimage._shared.utils import check_nD, deprecate_kwarg
-import numpy.testing as npt
+import pytest
 import numpy as np
+import numpy.testing as npt
+from skimage._shared.utils import (check_nD, deprecate_kwarg,
+                                   _validate_interpolation_order)
 from skimage._shared import testing
-import pytest
+from skimage._shared._warnings import expected_warnings
 
 
 def test_deprecated_kwarg():
@@ -59,5 +61,26 @@ def test_check_nD():
         check_nD(x, 2)
 
 
+@pytest.mark.parametrize('dtype', [bool, int, np.uint8, np.uint16,
+                                   float, np.float32, np.float64])
+@pytest.mark.parametrize('order', [None, -1, 0, 1, 2, 3, 4, 5, 6])
+def test_validate_interpolation_order(dtype, order):
+    if order is None:
+        # Default order
+        assert (_validate_interpolation_order(dtype, None) == 0
+                if dtype == bool else 1)
+    elif order < 0 or order > 5:
+        # Order not in valid range
+        with testing.raises(ValueError):
+            _validate_interpolation_order(dtype, order)
+    elif dtype == bool and order != 0:
+        # Deprecated order for bool array
+        with expected_warnings(["Input image dtype is bool"]):
+            assert _validate_interpolation_order(bool, order) == order
+    else:
+        # Valid use case
+        assert _validate_interpolation_order(dtype, order) == order
+
+
 if __name__ == "__main__":
     npt.run_module_suite()
diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py
--- a/skimage/transform/tests/test_warps.py
+++ b/skimage/transform/tests/test_warps.py
@@ -1,23 +1,26 @@
 import numpy as np
 from scipy.ndimage import map_coordinates
 
-from skimage.transform._warps import (_stackcopy, _linear_polar_mapping,
-                                      _log_polar_mapping)
-from skimage.transform import (warp, warp_coords, rotate, resize, rescale,
-                               AffineTransform,
-                               ProjectiveTransform,
-                               SimilarityTransform,
-                               downscale_local_mean,
-                               warp_polar)
-from skimage import transform as tf, data, img_as_float
-from skimage.color import rgb2gray
-from skimage.draw import circle_perimeter_aa
-from skimage.feature import peak_local_max
+from skimage.data import checkerboard, astronaut
+from skimage.util.dtype import img_as_float
+from skimage.color.colorconv import rgb2gray
+from skimage.draw.draw import circle_perimeter_aa
+from skimage.feature.peak import peak_local_max
 from skimage._shared import testing
 from skimage._shared.testing import (assert_almost_equal, assert_equal,
                                      test_parallel)
 from skimage._shared._warnings import expected_warnings
 
+from skimage.transform._warps import (_stackcopy,
+                                      _linear_polar_mapping,
+                                      _log_polar_mapping, warp,
+                                      warp_coords, rotate, resize,
+                                      rescale, warp_polar, swirl,
+                                      downscale_local_mean)
+from skimage.transform._geometric import (AffineTransform,
+                                          ProjectiveTransform,
+                                          SimilarityTransform)
+
 
 np.random.seed(0)
 
@@ -353,21 +356,21 @@ def test_resize_dtype():
 
 
 def test_swirl():
-    image = img_as_float(data.checkerboard())
+    image = img_as_float(checkerboard())
 
     swirl_params = {'radius': 80, 'rotation': 0, 'order': 2, 'mode': 'reflect'}
 
     with expected_warnings(['Bi-quadratic.*bug']):
-        swirled = tf.swirl(image, strength=10, **swirl_params)
-        unswirled = tf.swirl(swirled, strength=-10, **swirl_params)
+        swirled = swirl(image, strength=10, **swirl_params)
+        unswirled = swirl(swirled, strength=-10, **swirl_params)
 
     assert np.mean(np.abs(image - unswirled)) < 0.01
 
     swirl_params.pop('mode')
 
     with expected_warnings(['Bi-quadratic.*bug']):
-        swirled = tf.swirl(image, strength=10, **swirl_params)
-        unswirled = tf.swirl(swirled, strength=-10, **swirl_params)
+        swirled = swirl(image, strength=10, **swirl_params)
+        unswirled = swirl(swirled, strength=-10, **swirl_params)
 
     assert np.mean(np.abs(image[1:-1, 1:-1] - unswirled[1:-1, 1:-1])) < 0.01
 
@@ -380,7 +383,7 @@ def test_const_cval_out_of_range():
 
 
 def test_warp_identity():
-    img = img_as_float(rgb2gray(data.astronaut()))
+    img = img_as_float(rgb2gray(astronaut()))
     assert len(img.shape) == 2
     assert np.allclose(img, warp(img, AffineTransform(rotation=0)))
     assert not np.allclose(img, warp(img, AffineTransform(rotation=0.1)))
@@ -394,7 +397,7 @@ def test_warp_identity():
 
 
 def test_warp_coords_example():
-    image = data.astronaut().astype(np.float32)
+    image = astronaut().astype(np.float32)
     assert 3 == image.shape[2]
     tform = SimilarityTransform(translation=(0, -10))
     coords = warp_coords(tform, (30, 30, 3))
@@ -633,3 +636,44 @@ def test_invalid_dimensions_polar():
         warp_polar(np.zeros((10, 10)), (5, 5), multichannel=True)
     with testing.raises(ValueError):
         warp_polar(np.zeros((10, 10, 10, 3)), (5, 5), multichannel=True)
+
+
+def test_bool_img_rescale():
+    img = np.ones((12, 18), dtype=bool)
+    img[2:-2, 4:-4] = False
+    res = rescale(img, 0.5)
+
+    expected = np.ones((6, 9))
+    expected[1:-1, 2:-2] = False
+
+    assert_equal(res, expected)
+
+
+def test_bool_img_resize():
+    img = np.ones((12, 18), dtype=bool)
+    img[2:-2, 4:-4] = False
+    res = resize(img, (6, 9))
+
+    expected = np.ones((6, 9))
+    expected[1:-1, 2:-2] = False
+
+    assert_equal(res, expected)
+
+
+def test_boll_array_warnings():
+    img = np.zeros((10, 10), dtype=bool)
+
+    with expected_warnings(['Input image dtype is bool']):
+        rescale(img, 0.5, anti_aliasing=True)
+
+    with expected_warnings(['Input image dtype is bool']):
+        resize(img, (5, 5), anti_aliasing=True)
+
+    with expected_warnings(['Input image dtype is bool']):
+        rescale(img, 0.5, order=1)
+
+    with expected_warnings(['Input image dtype is bool']):
+        resize(img, (5, 5), order=1)
+
+    with expected_warnings(['Input image dtype is bool']):
+        warp(img, np.eye(3), order=1)