Repository: napari/napari

Issue:
Roll dimensions order of display crashes on 3D images
I was just testing out different features in the viewer, and napari crashes when I open a 3D image and press the Roll dimensions order of display button with the following error:

```
 WARNING: Traceback (most recent call last):
  File "/usr/lib/python3.7/site-packages/napari/_qt/qt_viewer_buttons.py", line 121, in <lambda>
    self.clicked.connect(lambda: self.viewer.dims._roll())
  File "/usr/lib/python3.7/site-packages/napari/components/dims.py", line 340, in _roll
    self.order = np.roll(self.order, 1)
  File "/usr/lib/python3.7/site-packages/napari/components/dims.py", line 130, in order
    self.events.order()
  File "/usr/lib/python3.7/site-packages/napari/util/event.py", line 508, in __call__
    self._invoke_callback(cb, event)
  File "/usr/lib/python3.7/site-packages/napari/util/event.py", line 529, in _invoke_callback
    cb_event=(cb, event),
  File "/usr/lib/python3.7/site-packages/napari/util/event.py", line 523, in _invoke_callback
    cb(event)
  File "/usr/lib/python3.7/site-packages/napari/components/viewer_model.py", line 88, in <lambda>
    self.dims.events.order.connect(lambda e: self._update_layers())
  File "/usr/lib/python3.7/site-packages/napari/components/viewer_model.py", line 1045, in _update_layers
    layer.dims.order = order
  File "/usr/lib/python3.7/site-packages/napari/components/dims.py", line 130, in order
    self.events.order()
  File "/usr/lib/python3.7/site-packages/napari/util/event.py", line 508, in __call__
    self._invoke_callback(cb, event)
  File "/usr/lib/python3.7/site-packages/napari/util/event.py", line 529, in _invoke_callback
    cb_event=(cb, event),
  File "/usr/lib/python3.7/site-packages/napari/util/event.py", line 523, in _invoke_callback
    cb(event)
  File "/usr/lib/python3.7/site-packages/napari/layers/base/base.py", line 176, in <lambda>
    self.dims.events.order.connect(lambda e: self._update_dims())
  File "/usr/lib/python3.7/site-packages/napari/layers/base/base.py", line 373, in _update_dims
    self._set_view_slice()
  File "/usr/lib/python3.7/site-packages/napari/layers/image/image.py", line 477, in _set_view_slice
    self._update_thumbnail()
  File "/usr/lib/python3.7/site-packages/napari/layers/image/image.py", line 536, in _update_thumbnail
    color_array = self.colormap[1][downsampled.ravel()]
  File "/usr/lib/python3.7/site-packages/vispy/color/colormap.py", line 293, in __getitem__
    return ColorArray(colors)
  File "/usr/lib/python3.7/site-packages/vispy/color/color_array.py", line 148, in __init__
    rgba = _user_to_rgba(color, clip=clip)
  File "/usr/lib/python3.7/site-packages/vispy/color/color_array.py", line 61, in _user_to_rgba
    if color.min() < 0 or color.max() > 1:
  File "/usr/lib/python3.7/site-packages/numpy/core/_methods.py", line 32, in _amin
    return umr_minimum(a, axis, None, out, keepdims, initial)
ValueError: zero-size array to reduction operation minimum which has no identity
WARNING:vispy:Traceback (most recent call last):
  File "/usr/lib/python3.7/site-packages/napari/_qt/qt_viewer_buttons.py", line 121, in <lambda>
    self.clicked.connect(lambda: self.viewer.dims._roll())
  File "/usr/lib/python3.7/site-packages/napari/components/dims.py", line 340, in _roll
    self.order = np.roll(self.order, 1)
  File "/usr/lib/python3.7/site-packages/napari/components/dims.py", line 130, in order
    self.events.order()
  File "/usr/lib/python3.7/site-packages/napari/util/event.py", line 508, in __call__
    self._invoke_callback(cb, event)
  File "/usr/lib/python3.7/site-packages/napari/util/event.py", line 529, in _invoke_callback
    cb_event=(cb, event),
  File "/usr/lib/python3.7/site-packages/napari/util/event.py", line 523, in _invoke_callback
    cb(event)
  File "/usr/lib/python3.7/site-packages/napari/components/viewer_model.py", line 88, in <lambda>
    self.dims.events.order.connect(lambda e: self._update_layers())
  File "/usr/lib/python3.7/site-packages/napari/components/viewer_model.py", line 1045, in _update_layers
    layer.dims.order = order
  File "/usr/lib/python3.7/site-packages/napari/components/dims.py", line 130, in order
    self.events.order()
  File "/usr/lib/python3.7/site-packages/napari/util/event.py", line 508, in __call__
    self._invoke_callback(cb, event)
  File "/usr/lib/python3.7/site-packages/napari/util/event.py", line 529, in _invoke_callback
    cb_event=(cb, event),
  File "/usr/lib/python3.7/site-packages/napari/util/event.py", line 523, in _invoke_callback
    cb(event)
  File "/usr/lib/python3.7/site-packages/napari/layers/base/base.py", line 176, in <lambda>
    self.dims.events.order.connect(lambda e: self._update_dims())
  File "/usr/lib/python3.7/site-packages/napari/layers/base/base.py", line 373, in _update_dims
    self._set_view_slice()
  File "/usr/lib/python3.7/site-packages/napari/layers/image/image.py", line 477, in _set_view_slice
    self._update_thumbnail()
  File "/usr/lib/python3.7/site-packages/napari/layers/image/image.py", line 536, in _update_thumbnail
    color_array = self.colormap[1][downsampled.ravel()]
  File "/usr/lib/python3.7/site-packages/vispy/color/colormap.py", line 293, in __getitem__
    return ColorArray(colors)
  File "/usr/lib/python3.7/site-packages/vispy/color/color_array.py", line 148, in __init__
    rgba = _user_to_rgba(color, clip=clip)
  File "/usr/lib/python3.7/site-packages/vispy/color/color_array.py", line 61, in _user_to_rgba
    if color.min() < 0 or color.max() > 1:
  File "/usr/lib/python3.7/site-packages/numpy/core/_methods.py", line 32, in _amin
    return umr_minimum(a, axis, None, out, keepdims, initial)
ValueError: zero-size array to reduction operation minimum which has no identity
[1]    16296 abort (core dumped)  napari FITC.tif

```

Is this by design?


Discussion:
Haha no, we try to design our apps *not* to crash! 😅

Thanks for the report! Would you be able to share the input image that caused this? We will definitely try to get this fixed ASAP.
Here's a code snippet that can crash napari

```
import napari
import numpy as np

img = np.empty((1, 2048, 2048))

with napari.gui_qt():
    viewer = napari.view_image(img)
```
Oh I see. So we are doing something wrong with singleton dimensions. It might have to do with computing the slider width. Thanks for the snippet!
Ah! No, it's not the sliders, it's creating a thumbnail. This crashes it without the need for a button press:

```python
import napari
import numpy as np

img = np.random.random((2048, 1, 2048))

with napari.gui_qt():
    viewer = napari.view_image(img)
```

We have actually run into this before. I guess the trick should be to have a minimal thumbnail side length of 1, even if the slice aspect ratio itself is squishier than 1/32. Either that or once we reach 1/32 we center and crop.

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
@@ -513,18 +513,21 @@ def _update_thumbnail(self):
         if dtype in [np.dtype(np.float16)]:
             image = image.astype(np.float32)
 
-        zoom_factor = np.divide(
+        raw_zoom_factor = np.divide(
             self._thumbnail_shape[:2], image.shape[:2]
         ).min()
+        new_shape = np.clip(
+            raw_zoom_factor * np.array(image.shape[:2]),
+            1,  # smallest side should be 1 pixel wide
+            self._thumbnail_shape[:2],
+        )
+        zoom_factor = tuple(new_shape / image.shape[:2])
         if self.rgb:
             # warning filter can be removed with scipy 1.4
             with warnings.catch_warnings():
                 warnings.simplefilter("ignore")
                 downsampled = ndi.zoom(
-                    image,
-                    (zoom_factor, zoom_factor, 1),
-                    prefilter=False,
-                    order=0,
+                    image, zoom_factor + (1,), prefilter=False, order=0
                 )
             if image.shape[2] == 4:  # image is RGBA
                 colormapped = np.copy(downsampled)
diff --git a/napari/util/status_messages.py b/napari/util/status_messages.py
--- a/napari/util/status_messages.py
+++ b/napari/util/status_messages.py
@@ -1,4 +1,4 @@
-from collections import Iterable
+from collections.abc import Iterable
 import numpy as np
 
 


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
@@ -455,6 +455,22 @@ def test_thumbnail():
     assert layer.thumbnail.shape == layer._thumbnail_shape
 
 
+def test_narrow_thumbnail():
+    """Ensure that the thumbnail generation works for very narrow images.
+
+    See: https://github.com/napari/napari/issues/641 and
+    https://github.com/napari/napari/issues/489
+    """
+    image = np.random.random((1, 2048))
+    layer = Image(image)
+    layer._update_thumbnail()
+    thumbnail = layer.thumbnail[..., :3]  # ignore alpha channel
+    middle_row = thumbnail.shape[0] // 2
+    assert np.all(thumbnail[: middle_row - 1] == 0)
+    assert np.all(thumbnail[middle_row + 1 :] == 0)
+    assert np.mean(thumbnail[middle_row - 1 : middle_row + 1]) > 0
+
+
 def test_xml_list():
     """Test the xml generation."""
     np.random.seed(0)