Repository: scikit-image/scikit-image

Issue:
`0.19.0` `multichannel` change for `pyramid_expand` is not backwards compatible for single-channel images
## Description

<!--
(Note: for guidance on how to use `scikit-image`, please post instead on https://forum.image.sc/tag/scikit-image)
-->

With default values, the `0.19.0` version of `pyramid_expand` produces different results than the `0.18.3` version of `pyramid_expand`.

The root cause is due to a backwards-incompatible change to the default parameters of the function. (Looking at the `git blame`, I think this issue was introduced in https://github.com/scikit-image/scikit-image/pull/5285.)

## Way to reproduce
```python
# Place the full code we need to recreate your issue here
# upload all necessary images to github too!
```

Run this call in both `0.18.3` and `0.19.0`:

```python
import numpy as np
from skimage import transform

array_r = transform.pyramid_expand(np.ones([17, 13]))  # NB: This call doesn't use `multichannel`
```

Then check the value of `multichannel` inside `pyramid_expand`:

* For `0.18.3`, it will be `False` by default.
* For `0.19.0`, the default value should (in theory) still be `False` (I presume?):
  
  https://github.com/scikit-image/scikit-image/blob/5d594d70ffbcb04db108a8560fb684ba85884a3a/skimage/transform/pyramids.py#L35  

  However, this value gets overwritten by the following line:

  https://github.com/scikit-image/scikit-image/blob/5d594d70ffbcb04db108a8560fb684ba85884a3a/skimage/transform/pyramids.py#L84

  Since the default value of the new `channel_axis` parameter is `-1` (i.e. `not None`):

  https://github.com/scikit-image/scikit-image/blob/5d594d70ffbcb04db108a8560fb684ba85884a3a/skimage/transform/pyramids.py#L36

As a result, the resized image has its last axis treated as the channel axis (`[17, 13] -> [34, 13]`) rather than resizing that axis (`[17, 13] -> [34, 26]`).

## 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(f'scikit-image version: {skimage.__version__}')
import numpy; print(f'numpy version: {numpy.__version__}')
```

```python
# for 0.18.3 venv
3.7.11 (default, Jul 27 2021, 14:32:16) 
[GCC 7.5.0]
Linux-5.11.0-41-generic-x86_64-with-debian-bullseye-sid
scikit-image version: 0.18.3
numpy version: 1.18.5

# for 0.19.0 venv
3.7.11 (default, Jul 27 2021, 14:32:16) 
[GCC 7.5.0]
Linux-5.11.0-41-generic-x86_64-with-debian-bullseye-sid
scikit-image version: 0.19.0
numpy version: 1.18.5
```



Discussion:
Was this intentional? (i.e. is `multichannel=True` the new default behavior?)

And, if so, should I manually be setting `channel_axis=None` in my calls to this function? 

(My expectation was that a deprecation would preserve backwards compatibility for default functionality, at least during a deprecation period with a warning, but my understanding could be off here.)

Thanks much! :heart: 
Definitely not intentional @joshuacwnewton, thank you for the report! In the future, grayscale images will be the default for all skimage functions. This was a mistake and a PR to fix it would be most welcome! We'll backport the fix to release 0.19.1 as we don't make backwards-incompatible API changes without a deprecation period (except in exceptional circumstances such as broken/buggy functionality).

If you are too busy to submit a PR we will aim to get this fixed very shortly.
Thanks for reporting this @joshuacwnewton. Indeed, this was an unintentional change to the default behavior.
> In the future, grayscale images will be the default for all skimage functions.

Thanks for the quick reply! I appreciate the clarification here. :heart: 

> and a PR to fix it would be most welcome!

Will do! :)

Resolving patch:
diff --git a/skimage/transform/pyramids.py b/skimage/transform/pyramids.py
--- a/skimage/transform/pyramids.py
+++ b/skimage/transform/pyramids.py
@@ -104,7 +104,7 @@ def pyramid_reduce(image, downscale=2, sigma=None, order=1,
 @utils.deprecate_multichannel_kwarg(multichannel_position=6)
 def pyramid_expand(image, upscale=2, sigma=None, order=1,
                    mode='reflect', cval=0, multichannel=False,
-                   preserve_range=False, *, channel_axis=-1):
+                   preserve_range=False, *, channel_axis=None):
     """Upsample and then smooth image.
 
     Parameters


Test patch:
diff --git a/skimage/feature/tests/test_basic_features.py b/skimage/feature/tests/test_basic_features.py
--- a/skimage/feature/tests/test_basic_features.py
+++ b/skimage/feature/tests/test_basic_features.py
@@ -7,7 +7,23 @@
 
 @pytest.mark.parametrize('edges', (False, True))
 @pytest.mark.parametrize('texture', (False, True))
-def test_multiscale_basic_features(edges, texture):
+def test_multiscale_basic_features_gray(edges, texture):
+    img = np.zeros((20, 20))
+    img[:10] = 1
+    img += 0.05 * np.random.randn(*img.shape)
+    features = multiscale_basic_features(img, edges=edges, texture=texture)
+
+    n_sigmas = 6
+    intensity = True
+    assert features.shape[-1] == (
+        n_sigmas * (int(intensity) + int(edges) + 2 * int(texture))
+    )
+    assert features.shape[:-1] == img.shape[:]
+
+
+@pytest.mark.parametrize('edges', (False, True))
+@pytest.mark.parametrize('texture', (False, True))
+def test_multiscale_basic_features_rgb(edges, texture):
     img = np.zeros((20, 20, 3))
     img[:10] = 1
     img += 0.05 * np.random.randn(*img.shape)
diff --git a/skimage/transform/tests/test_pyramids.py b/skimage/transform/tests/test_pyramids.py
--- a/skimage/transform/tests/test_pyramids.py
+++ b/skimage/transform/tests/test_pyramids.py
@@ -77,8 +77,7 @@ def test_pyramid_expand_rgb_deprecated_multichannel():
 
 def test_pyramid_expand_gray():
     rows, cols = image_gray.shape
-    out = pyramids.pyramid_expand(image_gray, upscale=2,
-                                  channel_axis=None)
+    out = pyramids.pyramid_expand(image_gray, upscale=2)
     assert_array_equal(out.shape, (rows * 2, cols * 2))
 
 
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
@@ -179,8 +179,7 @@ def test_rescale():
     # same scale factor
     x = np.zeros((5, 5), dtype=np.double)
     x[1, 1] = 1
-    scaled = rescale(x, 2, order=0,
-                     channel_axis=None, anti_aliasing=False, mode='constant')
+    scaled = rescale(x, 2, order=0, anti_aliasing=False, mode='constant')
     ref = np.zeros((10, 10))
     ref[2:4, 2:4] = 1
     assert_array_almost_equal(scaled, ref)
@@ -189,8 +188,7 @@ def test_rescale():
     x = np.zeros((5, 5), dtype=np.double)
     x[1, 1] = 1
 
-    scaled = rescale(x, (2, 1), order=0,
-                     channel_axis=None, anti_aliasing=False, mode='constant')
+    scaled = rescale(x, (2, 1), order=0, anti_aliasing=False, mode='constant')
     ref = np.zeros((10, 5))
     ref[2:4, 1] = 1
     assert_array_almost_equal(scaled, ref)