Repository: scikit-image/scikit-image

Issue:
Should rgb2gray accept gray images?
https://github.com/scikit-image/scikit-image/blob/4686b7da06593ff002986de6a378cba44f768148/skimage/color/colorconv.py#L800

See issue: #3185

it is pretty difficult to correctly guess if an ND image is grey. I think ND color conversion would be super useful for skimage, so that is why I'm asking for input for how to deal with this.

I could do something like:

```python
if rgb.shape[-1] not in [3, 4]:
```
or 
```python
if rgb.shape[-1] > 4:
```
but that would break things for tiny little images ;)

maybe this kind of automagic behaviour is better suited for an other wrapper function if we want to keep it?


Discussion:
As a followup question, should `gray2rgb` attempt to guess if an image is RGB?

Also, the logic is a little wonky. it uses squeeze, though, not entirely correctly.

https://github.com/scikit-image/scikit-image/blob/4686b7da06593ff002986de6a378cba44f768148/skimage/color/colorconv.py#L843
Could I add a `multichannel` parameter to turn off all this automagic logic?
@hmaarrfk yes, our new mission statement will include something to the effect of "we don't like magic", but there is some legacy code that violates that, and guessing RGB is one of these. So, we are in favour of removing this magic, but we have to be careful with backwards compatibility and not breaking people's code.

The concern for tiny axes is very real and iirc someone *was* bitten by this very problem a while back.

Something like a keyword argument would be useful, but we have to be careful with it: 

- `multichannel=None` -> do some magic
- `multichannel=True` -> always assume RGB (raise ValueError if shape incorrect)
- `multichannel=False` -> ???? 

So, it's a bit wonky for this function. But something along these lines is desirable, yes.
I kinda get stuck thinking that these functions should not take a `guess_channels` (instead of `multichannel`) parameter later in the project life cycle.

  - `guess_channels=True` old behaviour where it tries to guess in certain places.
  - `guess_channels=False` new behaviour, where if it works with `numpy`, then it is probably the right thing?

  - `guess_channels=None` is always default but changes behaviour depending on the version number

  - 0.15: `None` behaves like `True` to retain the old behaviour. Raise a warning if `None` or `True` are set. 
  - 0.16: `None` behaves like `False`. 
      - `guess_channels=True` is an invalid. Raise an **error** if it is set to `True`. 
      - Raise a **warning** if `guess_channels` is explicitly set to `False`
  - 0.17: `guess_channels` is an invalid parameter. Let python deal with the warnings/errors.
This looks fine, except that the behavior and 0.15 and 0.16 should be the same when the argument is not specified.
@stefanv, do you think the `guess_channels` parameter should be kept at infinitum? If not, I would like to give the chance for people that set it to `False` to not have their code break between a version upgrade.
As such, I think there should be a change in the behaviour of `None` during one version bump.

|Version | Parameter | `None` | `False` | `True` |
|----------|----------------|---------|----------|----------|
| 0.15 | `guess_channels` | Like `True` | No guessing | Guessing + Warning | 
| 0.16 | `guess_channels` | Like `True` | No guessing + warning | Error | 
| 0.17 | `guess_channels` | Like `False` | No guessing + warning | Error |
| 0.18 | No Parameter | |    |   |
Hrm, actually, why introduce a parameter that you mean to deprecate anyway.  How about:

0.15 — keep guessing, but add multichannel=None, warn if multichannel is not specified and data looks to be multichannel
0.16 — keep guessing, but error if multichannel is not specified and data looks to be multichannel
0.17 — stop guessing, rely on multichannel
My original thoughts were:
For me, the logic for guessing the multichannel will end up being annoying to maintain and test.

Thinking long term, it doesn't make sense to ask future developers to ensure that it continues to hit corner cases.

For example:
```python
def gray2rgb(image, alpha=None):
    is_rgb = False
    is_alpha = False
    dims = np.squeeze(image).ndim

    if dims == 3:
        if image.shape[2] == 3:
            is_rgb = True
        elif image.shape[2] == 4:
            is_alpha = True
            is_rgb = True
```

looks at the shape, and looked at dimensions. It doesn't check that `image.dims == np.sqeeze(image).dims` and then indexes the `2` address assuming that it is the color axis while it could have been squeezed out.

Glad to fix it, but I have a feeling that things like this are going to keep popping out.
For `rgb2gray()`, the current documentation defines a `ValueError`:
https://github.com/scikit-image/scikit-image/blob/b46a4961157510eb395028cd9b6cf8739cedb7c2/skimage/color/colorconv.py#L764-L768

But, the following snippet does not produce an error:
```python
from skimage.color import rgb2gray
import numpy as np

# Random grayscale image
x = (np.random.rand(50, 50) * 255).astype("uint8")
y = rgb2gray(x)
```

Then, should we change the documentation to reflect that, indeed, 2-D images will also not produce an error?
@parekhnish thanks for chiming in! I guess it’s still unclear what the best way forward is based on the above discussion, but fixing the documentation to be correct is certainly desirable in the short term! Would you like to submit a pull request? We would be happy to guide you through the process if you aren’t familiar with it yet. 

Thanks!
@jni yes, I can do that. Do you have any pull-request guidelines (commit message format, tests to be done before committing and creating the pull request, etc.)?
Yes, you can read our contributing guidelines here:

https://scikit-image.org/docs/dev/contribute.html

The are a bit on the long side, but I hope quite clear. Since this is a documentation fix, I don't think you will need to do any testing. Our continuous integration will catch any ReST formatting errors if they creep in. =)
The problem is even worst then it looks: `rgb2gray` ignores all image's channels with indices greater then 3 and doesn't raise any error if the number of channels is not 3 or 4 (despite what the documentation says:
https://github.com/scikit-image/scikit-image/blob/b46a4961157510eb395028cd9b6cf8739cedb7c2/skimage/color/colorconv.py#L764-L768)
```python
from skimage.color import rgb2gray
import numpy as np
x = (np.random.rand(50, 50, 5) * 255).astype("uint8")
y = rgb2gray(x)  # Supposed to raise an error be because  x.shape[-1] is not 3 or 4
```
The problem comes from this line: https://github.com/scikit-image/scikit-image/blob/b46a4961157510eb395028cd9b6cf8739cedb7c2/skimage/color/colorconv.py#L794

My solution to this issue is may be a little bit radical: I consider that

- `multichannel` is implicitly `True` for all the functions but `gray2rgb`,
- `rgb2gray` does not accept gray level images,
- `rgb2gray` does not accept RGBA images

I implemented It in #4418. It allows to easily add nD support using `Ellipsis` and alleviates all the "guessing"...
Oops, this comment is better suited for #3185

Resolving patch:
diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py
--- a/skimage/color/colorconv.py
+++ b/skimage/color/colorconv.py
@@ -790,6 +790,12 @@ def rgb2gray(rgb):
     """
 
     if rgb.ndim == 2:
+        warn('The behavior of rgb2gray will change in scikit-image 0.19. '
+             'Currently, rgb2gray allows 2D grayscale image to be passed '
+             'as inputs and leaves them unmodified as outputs. '
+             'Starting from version 0.19, 2D arrays will '
+             'be treated as 1D images with 3 channels.',
+             FutureWarning, stacklevel=2)
         return np.ascontiguousarray(rgb)
 
     if rgb.shape[-1] > 3:


Test patch:
diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py
--- a/skimage/color/tests/test_colorconv.py
+++ b/skimage/color/tests/test_colorconv.py
@@ -299,7 +299,8 @@ def test_rgb2gray_alpha(self):
             assert rgb2gray(x).ndim == 2
 
     def test_rgb2gray_on_gray(self):
-        rgb2gray(np.random.rand(5, 5))
+        with expected_warnings(['The behavior of rgb2gray will change']):
+            rgb2gray(np.random.rand(5, 5))
 
     def test_rgb2gray_dtype(self):
         img = np.random.rand(10, 10, 3).astype('float64')