Repository: napari/napari

Issue:
Napari crashed when trying to open image with value range outside -1 to 1
## 🐛 Bug

Napari crashes when I try to open an image:

```
image type: float64
image shape: (81, 480, 480, 4)
image min: -825.485107421875
image max: 401603.375

```
if I use:

`napari.view(array)`

it gives the following error:

```
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-42-5ae500208e78> in <module>
----> 1 napari.view(array)

/Applications/anaconda3/envs/chloe/lib/python3.7/site-packages/napari/view_function.py in view(title, metadata, multichannel, contrast_limits, *images, **named_images)
     51             metadata=metadata,
     52             multichannel=multichannel,
---> 53             contrast_limits=contrast_limits,
     54         )
     55     for name, image in named_images.items():

/Applications/anaconda3/envs/chloe/lib/python3.7/site-packages/napari/components/viewer_model.py in add_image(self, data, multichannel, colormap, contrast_limits, interpolation, rendering, name, metadata, scale, translate, opacity, blending, visible)
    411             opacity=opacity,
    412             blending=blending,
--> 413             visible=visible,
    414         )
    415         self.add_layer(layer)

/Applications/anaconda3/envs/chloe/lib/python3.7/site-packages/napari/layers/image/image.py in __init__(self, data, multichannel, colormap, contrast_limits, interpolation, rendering, name, metadata, scale, translate, opacity, blending, visible)
    174 
    175         # Trigger generation of view slice and thumbnail
--> 176         self._update_dims()
    177 
    178     @property

/Applications/anaconda3/envs/chloe/lib/python3.7/site-packages/napari/layers/base/base.py in _update_dims(self)
    313             self.dims.set_range(i, r)
    314 
--> 315         self._set_view_slice()
    316         self._update_coordinates()
    317 

/Applications/anaconda3/envs/chloe/lib/python3.7/site-packages/napari/layers/image/image.py in _set_view_slice(self)
    333 
    334         self._data_thumbnail = self._data_view
--> 335         self._update_thumbnail()
    336         self._update_coordinates()
    337         self.events.set_data()

/Applications/anaconda3/envs/chloe/lib/python3.7/site-packages/napari/layers/image/image.py in _update_thumbnail(self)
    392             colormapped = colormapped.reshape(downsampled.shape + (4,))
    393             colormapped[..., 3] *= self.opacity
--> 394         self.thumbnail = colormapped
    395 
    396     def get_value(self):

/Applications/anaconda3/envs/chloe/lib/python3.7/site-packages/napari/layers/base/base.py in thumbnail(self, thumbnail)
    353             with warnings.catch_warnings():
    354                 warnings.simplefilter("ignore")
--> 355                 thumbnail = img_as_ubyte(thumbnail)
    356         self._thumbnail = thumbnail
    357         self.events.thumbnail()

/Applications/anaconda3/envs/chloe/lib/python3.7/site-packages/skimage/util/dtype.py in img_as_ubyte(image, force_copy)
    484 
    485     """
--> 486     return convert(image, np.uint8, force_copy)
    487 
    488 

/Applications/anaconda3/envs/chloe/lib/python3.7/site-packages/skimage/util/dtype.py in convert(image, dtype, force_copy, uniform)
    253 
    254         if np.min(image) < -1.0 or np.max(image) > 1.0:
--> 255             raise ValueError("Images of type float must be between -1 and 1.")
    256         # floating point -> integer
    257         prec_loss()

ValueError: Images of type float must be between -1 and 1.
```

While it will work if I do:

`napari.view(
        ch0=image[:,:,:,0],
        ch1=image[:,:,:,1],
        ch2=image[:,:,:,2],
        ch3=image[:,:,:,3])`


## To Reproduce

```
array = np.arange(-50000.0, 50000.0).reshape(10,50,50,4);
napari.view(array);
napari.view(
        ch0=array[:,:,:,0],
        ch1=array[:,:,:,1],
        ch2=array[:,:,:,2],
        ch3=array[:,:,:,3])
```

## Environment

 - napari Version (e.g., 1.0):0.1.5
 - OS (e.g., Linux): osx
 - Python version: 3.7
 - Any other relevant information: n/a

## Additional context

<!-- Add any other context about the problem here. -->



Discussion:
Thanks for catching this, I'll work on a fix today.
So the reason this fails in the first case and not the second is that in the first case because the last dimension is either `3` or `4` we have a special case where we consider it as an rgb or rgba image and so we don't do any rescaling which causes some downstream code to crash. In the second case because the channels are added separately we treat them independently and rescale them.

We've recently proposed removing this special handling for rgb / rgba images - see here #481 which would also remove this problem. I can also think about alternate approaches, but may question for @bioimage-analysis is do you want this data rendered as a `RGBA` image where the last channel is an alpha value, or do you have 4 color channels which you might want to assign 4 different colormaps too?

Resolving patch:
diff --git a/napari/layers/image/image.py b/napari/layers/image/image.py
--- a/napari/layers/image/image.py
+++ b/napari/layers/image/image.py
@@ -327,9 +327,12 @@ def _set_view_slice(self):
         else:
             order = self.dims.displayed_order
 
-        self._data_view = np.asarray(self.data[self.dims.indices]).transpose(
-            order
-        )
+        image = np.asarray(self.data[self.dims.indices]).transpose(order)
+
+        if self.multichannel and image.dtype.kind == 'f':
+            self._data_view = np.clip(image, 0, 1)
+        else:
+            self._data_view = image
 
         self._data_thumbnail = self._data_view
         self._update_thumbnail()
diff --git a/napari/layers/pyramid/pyramid.py b/napari/layers/pyramid/pyramid.py
--- a/napari/layers/pyramid/pyramid.py
+++ b/napari/layers/pyramid/pyramid.py
@@ -183,9 +183,13 @@ def _set_view_slice(self):
         downsampled = np.round(downsampled.astype(float)).astype(int)
         downsampled = np.clip(downsampled, 0, self.level_shapes[-1, nd] - 1)
         indices[nd] = downsampled
-        self._data_thumbnail = np.asarray(
-            self.data[-1][tuple(indices)]
-        ).transpose(order)
+
+        image = np.asarray(self.data[-1][tuple(indices)]).transpose(order)
+
+        if self.multichannel and image.dtype.kind == 'f':
+            self._data_thumbnail = np.clip(image, 0, 1)
+        else:
+            self._data_thumbnail = image
 
         # Slice currently viewed level
         indices = np.array(self.dims.indices)
@@ -213,9 +217,12 @@ def _set_view_slice(self):
         else:
             self.translate = [0] * self.ndim
 
-        self._data_view = np.asarray(
-            self.data[level][tuple(indices)]
-        ).transpose(order)
+        image = np.asarray(self.data[level][tuple(indices)]).transpose(order)
+
+        if self.multichannel and image.dtype.kind == 'f':
+            self._data_view = np.clip(image, 0, 1)
+        else:
+            self._data_view = image
 
         self._update_thumbnail()
         self._update_coordinates()


Test patch:
diff --git a/napari/layers/image/tests/test_image.py b/napari/layers/image/tests/test_image.py
--- a/napari/layers/image/tests/test_image.py
+++ b/napari/layers/image/tests/test_image.py
@@ -20,6 +20,31 @@ def test_random_image():
     assert layer._data_view.shape == shape[-2:]
 
 
+def test_negative_image():
+    """Test instantiating Image layer with negative data."""
+    shape = (10, 15)
+    np.random.seed(0)
+    # Data between -1.0 and 1.0
+    data = 2 * np.random.random(shape) - 1.0
+    layer = Image(data)
+    assert np.all(layer.data == data)
+    assert layer.ndim == len(shape)
+    assert layer.shape == shape
+    assert layer.dims.range == [(0, m, 1) for m in shape]
+    assert layer.multichannel == False
+    assert layer._data_view.shape == shape[-2:]
+
+    # Data between -10 and 10
+    data = 20 * np.random.random(shape) - 10
+    layer = Image(data)
+    assert np.all(layer.data == data)
+    assert layer.ndim == len(shape)
+    assert layer.shape == shape
+    assert layer.dims.range == [(0, m, 1) for m in shape]
+    assert layer.multichannel == False
+    assert layer._data_view.shape == shape[-2:]
+
+
 def test_all_zeros_image():
     """Test instantiating Image layer with all zeros data."""
     shape = (10, 15)
@@ -135,6 +160,29 @@ def test_rgba_image():
     assert layer._data_view.shape == shape[-3:]
 
 
+def test_negative_rgba_image():
+    """Test instantiating Image layer with negative RGBA data."""
+    shape = (10, 15, 4)
+    np.random.seed(0)
+    # Data between -1.0 and 1.0
+    data = 2 * np.random.random(shape) - 1
+    layer = Image(data)
+    assert np.all(layer.data == data)
+    assert layer.ndim == len(shape) - 1
+    assert layer.shape == shape[:-1]
+    assert layer.multichannel == True
+    assert layer._data_view.shape == shape[-3:]
+
+    # Data between -10 and 10
+    data = 20 * np.random.random(shape) - 10
+    layer = Image(data)
+    assert np.all(layer.data == data)
+    assert layer.ndim == len(shape) - 1
+    assert layer.shape == shape[:-1]
+    assert layer.multichannel == True
+    assert layer._data_view.shape == shape[-3:]
+
+
 def test_non_rgb_image():
     """Test forcing Image layer to be 3D and not multichannel."""
     shape = (10, 15, 3)