Repository: napari/napari

Issue:
Crash while thumbnail generation when setting `layer.data` (in case of label layers)
## 🐛 Bug

I'm just trying to port [napari-pyclesperanto-prototype](https://github.com/clesperanto/napari_pyclesperanto_assistant) (a napari plugin) from napari 0.4.5 to 0.4.7. When using the assistant, napari crashes inside a routine that draws thumbnails for the layer list. When surrounding the crash with a try-except-block, more things go wrong. Were there major changes in how layer.data and layer.name can be set?

## To Reproduce
* Install the assistant (which brings napari 0.4.5) and afterwards install napari 0.4.7
```
git clone https://github.com/clesperanto/napari_pyclesperanto_assistant
cd napari_pyclesperanto_assistant
pip install -e .
pip install napari==0.4.7
```
* Run the assistant demo:
```
python docs/demo/start_assistant.py
```
* Click on the "Binarize" button on the right. It will try to add a "Label" layer and crash while setting `layer.data` in [this line](https://github.com/clEsperanto/napari_pyclesperanto_assistant/blob/master/napari_pyclesperanto_assistant/_operations/_operations.py#L136) with this error message:
```
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
C:\Programs\miniconda3\envs\beetlesafari\lib\site-packages\napari\layers\labels\labels.py in _raw_to_displayed(self=, raw=array([[1., 1., 1., ..., 0., 0., 0.],
       [0....   [1., 1., 1., ..., 0., 0., 0.]], dtype=float32))
    704             try:
--> 705                 image = self._all_vals[raw]
        image = undefined
        self._all_vals = array([0.       , 0.1210181])
        raw = array([[1., 1., 1., ..., 0., 0., 0.],
       [0., 0., 1., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.],
       ...,
       [1., 1., 1., ..., 0., 0., 0.],
       [1., 1., 1., ..., 0., 0., 0.],
       [1., 1., 1., ..., 0., 0., 0.]], dtype=float32)
    706             except IndexError:

IndexError: arrays used as indices must be of integer (or boolean) type

During handling of the above exception, another exception occurred:

IndexError                                Traceback (most recent call last)
C:\Programs\miniconda3\envs\beetlesafari\lib\site-packages\magicgui\widgets\_bases.py in (*x=(,))
    429         self.changed = EventEmitter(source=self, type="changed")
    430         self._widget._mgui_bind_change_callback(
--> 431             lambda *x: self.changed(value=x[0] if x else None)
        x = (,)
        global self.changed = undefined
        global value = undefined
    432         )
    433 

C:\Programs\miniconda3\envs\beetlesafari\lib\site-packages\magicgui\events.py in __call__(self=, *args=(), **kwargs={'value': })
    601                     continue
    602 
--> 603                 self._invoke_callback(cb, event)
        self._invoke_callback = >
        cb = . at 0x0000022CC45800D0>
        event = 
    604                 if event.blocked:
    605                     break

C:\Programs\miniconda3\envs\beetlesafari\lib\site-packages\magicgui\events.py in _invoke_callback(self=, cb=.>, event=)
    619             cb(event)
    620         except Exception:
--> 621             _handle_exception(
        global _handle_exception = 
        self.ignore_callback_errors = False
        self.print_callback_errors = 'reminders'
        self = 
        global cb_event = undefined
        cb = . at 0x0000022CC45800D0>
        event = 
    622                 self.ignore_callback_errors,
    623                 self.print_callback_errors,

C:\Programs\miniconda3\envs\beetlesafari\lib\site-packages\magicgui\events.py in _invoke_callback(self=, cb=.>, event=)
    617     def _invoke_callback(self, cb, event):
    618         try:
--> 619             cb(event)
        cb = . at 0x0000022CC45800D0>
        event = 
    620         except Exception:
    621             _handle_exception(

C:\Programs\miniconda3\envs\beetlesafari\lib\site-packages\magicgui\widgets\_bases.py in (x=)
   1040         """Insert widget at ``key``."""
   1041         if isinstance(widget, ValueWidget):
-> 1042             widget.changed.connect(lambda x: self.changed(value=self))
        global widget.changed.connect = undefined
        x = 
        global self.changed = undefined
        global value = undefined
        global self = undefined
   1043         _widget = widget
   1044 

C:\Programs\miniconda3\envs\beetlesafari\lib\site-packages\magicgui\events.py in __call__(self=, *args=(), **kwargs={'value': })
    601                     continue
    602 
--> 603                 self._invoke_callback(cb, event)
        self._invoke_callback = >
        cb = . at 0x0000022CC4589790>
        event = 
    604                 if event.blocked:
    605                     break

C:\Programs\miniconda3\envs\beetlesafari\lib\site-packages\magicgui\events.py in _invoke_callback(self=, cb=.>, event=)
    619             cb(event)
    620         except Exception:
--> 621             _handle_exception(
        global _handle_exception = 
        self.ignore_callback_errors = False
        self.print_callback_errors = 'reminders'
        self = 
        global cb_event = undefined
        cb = . at 0x0000022CC4589790>
        event = 
    622                 self.ignore_callback_errors,
    623                 self.print_callback_errors,

C:\Programs\miniconda3\envs\beetlesafari\lib\site-packages\magicgui\events.py in _invoke_callback(self=, cb=.>, event=)
    617     def _invoke_callback(self, cb, event):
    618         try:
--> 619             cb(event)
        cb = . at 0x0000022CC4589790>
        event = 
    620         except Exception:
    621             _handle_exception(

C:\Programs\miniconda3\envs\beetlesafari\lib\site-packages\magicgui\function_gui.py in (e=)
    106         self._auto_call = auto_call
    107         if auto_call:
--> 108             self.changed.connect(lambda e: self.__call__())
        global self.changed.connect = undefined
        e = 
        global self.__call__ = undefined
    109 
    110         if show:

C:\Programs\miniconda3\envs\beetlesafari\lib\site-packages\magicgui\function_gui.py in __call__(self=, *args=(), **kwargs={})
    171         bound.apply_defaults()
    172 
--> 173         value = self._function(*bound.args, **bound.kwargs)
        value = undefined
        self._function = 
        bound.args = (, 'threshold_otsu', 1, 1, 0)
        bound.kwargs = {}
    174         self._call_count += 1
    175         if self._result_widget is not None:

c:\structure\code\napari_pyclesperanto_assistant\napari_pyclesperanto_assistant\_operations\_operations.py in binarize(input1=, operation_name='threshold_otsu', radius_x=1, radius_y=1, radius_z=0)
    136             binarize.self.viewer.add_labels(output, translate=input1.translate)
    137         else:
--> 138             binarize.self.layer.data = output
        global binarize.self.layer.data = array([[1., 1., 1., ..., 0., 0., 0.],
       [0., 0., 1., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.],
       ...,
       [1., 1., 1., ..., 0., 0., 0.],
       [1., 1., 1., ..., 0., 0., 0.],
       [1., 1., 1., ..., 0., 0., 0.]], dtype=float32)
        output = array([[1., 1., 1., ..., 0., 0., 0.],
       [0., 0., 1., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.],
       ...,
       [1., 1., 1., ..., 0., 0., 0.],
       [1., 1., 1., ..., 0., 0., 0.],
       [1., 1., 1., ..., 0., 0., 0.]], dtype=float32)
    139             binarize.self.layer.contrast_limits = (0, 1)
    140             binarize.self.layer.name = "Result of " + operation.__name__

C:\Programs\miniconda3\envs\beetlesafari\lib\site-packages\napari\layers\image\image.py in data(self=, data=array([[1., 1., 1., ..., 0., 0., 0.],
       [0....   [1., 1., 1., ..., 0., 0., 0.]], dtype=float32))
    333     def data(self, data):
    334         self._data = data
--> 335         self._update_dims()
        self._update_dims = >
    336         self.events.data(value=self.data)
    337         self._set_editable()

C:\Programs\miniconda3\envs\beetlesafari\lib\site-packages\napari\layers\base\base.py in _update_dims(self=, event=None)
    543         self._ndim = ndim
    544 
--> 545         self.refresh()
        self.refresh = >
    546 
    547     @property

C:\Programs\miniconda3\envs\beetlesafari\lib\site-packages\napari\layers\base\base.py in refresh(self=, event=None)
    927         """Refresh all layer data based on current view slice."""
    928         if self.visible:
--> 929             self.set_view_slice()
        self.set_view_slice = >
    930             self.events.set_data()
    931             self._update_thumbnail()

C:\Programs\miniconda3\envs\beetlesafari\lib\site-packages\napari\layers\base\base.py in set_view_slice(self=)
    791     def set_view_slice(self):
    792         with self.dask_optimized_slicing():
--> 793             self._set_view_slice()
        self._set_view_slice = >
    794 
    795     @abstractmethod

C:\Programs\miniconda3\envs\beetlesafari\lib\site-packages\napari\layers\image\image.py in _set_view_slice(self=)
    588         # Load our images, might be sync or async.
    589         data = SliceDataClass(self, image_indices, image, thumbnail_source)
--> 590         self._load_slice(data)
        self._load_slice = >
        data = 
    591 
    592     def _load_slice(self, data: SliceDataClass):

C:\Programs\miniconda3\envs\beetlesafari\lib\site-packages\napari\layers\image\image.py in _load_slice(self=, data=)
    599         if self._slice.load(data):
    600             # The load was synchronous.
--> 601             self._on_data_loaded(data, sync=True)
        self._on_data_loaded = >
        data = 
        global sync = undefined
    602         else:
    603             # The load will be asynchronous. Signal that our self.loaded

C:\Programs\miniconda3\envs\beetlesafari\lib\site-packages\napari\layers\image\image.py in _on_data_loaded(self=, data=, sync=True)
    623 
    624         # Pass the loaded data to the slice.
--> 625         if not self._slice.on_loaded(data):
        self._slice.on_loaded = >
        data = 
    626             # Slice rejected it, was it for the wrong indices?
    627             return

C:\Programs\miniconda3\envs\beetlesafari\lib\site-packages\napari\layers\image\_image_slice.py in on_loaded(self=, data=)
    136 
    137         # Display the newly loaded data.
--> 138         self._set_raw_images(data.image, data.thumbnail_source)
        self._set_raw_images = >
        data.image = array([[1., 1., 1., ..., 0., 0., 0.],
       [0., 0., 1., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.],
       ...,
       [1., 1., 1., ..., 0., 0., 0.],
       [1., 1., 1., ..., 0., 0., 0.],
       [1., 1., 1., ..., 0., 0., 0.]], dtype=float32)
        data.thumbnail_source = None
    139         self.loaded = True
    140         return True  # data was used.

C:\Programs\miniconda3\envs\beetlesafari\lib\site-packages\napari\layers\image\_image_slice.py in _set_raw_images(self=, image=array([[1., 1., 1., ..., 0., 0., 0.],
       [0....   [1., 1., 1., ..., 0., 0., 0.]], dtype=float32), thumbnail_source=array([[1., 1., 1., ..., 0., 0., 0.],
       [0....   [1., 1., 1., ..., 0., 0., 0.]], dtype=float32))
     94             image = np.clip(image, 0, 1)
     95             thumbnail_source = np.clip(thumbnail_source, 0, 1)
---> 96         self.image.raw = image
        self.image.raw = array([[1., 1., 1., ..., 0., 0., 0.],
       [0., 0., 1., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.],
       ...,
       [1., 1., 1., ..., 0., 0., 0.],
       [1., 1., 1., ..., 0., 0., 0.],
       [1., 1., 1., ..., 0., 0., 0.]], dtype=float32)
        image = array([[1., 1., 1., ..., 0., 0., 0.],
       [0., 0., 1., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.],
       ...,
       [1., 1., 1., ..., 0., 0., 0.],
       [1., 1., 1., ..., 0., 0., 0.],
       [1., 1., 1., ..., 0., 0., 0.]], dtype=float32)
     97 
     98         # save a computation of view image if thumbnail and image is equal

C:\Programs\miniconda3\envs\beetlesafari\lib\site-packages\napari\layers\image\_image_view.py in raw(self=, raw_image=array([[1., 1., 1., ..., 0., 0., 0.],
       [0....   [1., 1., 1., ..., 0., 0., 0.]], dtype=float32))
     71 
     72         # Update the view image based on this new raw image.
---> 73         self._view = self.image_converter(raw_image)
        self._view = array([[0.]])
        self.image_converter = >
        raw_image = array([[1., 1., 1., ..., 0., 0., 0.],
       [0., 0., 1., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.],
       ...,
       [1., 1., 1., ..., 0., 0., 0.],
       [1., 1., 1., ..., 0., 0., 0.],
       [1., 1., 1., ..., 0., 0., 0.]], dtype=float32)

C:\Programs\miniconda3\envs\beetlesafari\lib\site-packages\napari\layers\labels\labels.py in _raw_to_displayed(self=, raw=array([[1., 1., 1., ..., 0., 0., 0.],
       [0....   [1., 1., 1., ..., 0., 0., 0.]], dtype=float32))
    710                 )
    711                 self._all_vals[0] = 0
--> 712                 image = self._all_vals[raw]
        image = undefined
        self._all_vals = array([0.       , 0.1210181])
        raw = array([[1., 1., 1., ..., 0., 0., 0.],
       [0., 0., 1., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.],
       ...,
       [1., 1., 1., ..., 0., 0., 0.],
       [1., 1., 1., ..., 0., 0., 0.],
       [1., 1., 1., ..., 0., 0., 0.]], dtype=float32)
    713         elif (
    714             self.show_selected_label

IndexError: arrays used as indices must be of integer (or boolean) type
```
## Expected behavior

It should not crash when layer.data is set. 

## Environment

 - Please copy and paste the information at napari info option in help menubar here:
```
napari: 0.4.7
Platform: Windows-10-10.0.19041-SP0
Python: 3.8.8 (default, Feb 24 2021, 15:54:32) [MSC v.1928 64 bit (AMD64)]
Qt: 5.15.2
PyQt5: 5.15.2
NumPy: 1.19.2
SciPy: 1.6.1
Dask: 2021.03.0
VisPy: 0.6.6

OpenGL:
- GL version: 4.6.0 - Build 27.20.100.8681
- MAX_TEXTURE_SIZE: 16384

Screens:
- screen 1: resolution 1920x1080, scale 1.0

Plugins:
- clEsperanto: 0.7.3
- console: 0.0.2
- svg: 0.1.4
```

 - Any other relevant information:

## Additional context

<!-- Add any other context about the problem here. -->



Discussion:
from the traceback, it looks like #2415 might be involved.  So you might start looking there for clues.  And maybe @DragaDoncila has ideas?
Hmm yes, I wondered if this would come back to bite us. The reason it's crashing is because we now use the raw layer data to index into an array to get the colour value of each pixel.

Essentially all labels layers have to be int types. We convert on `__init__` but if you're setting the data directly then they'd never get converted. 
@haesleinhuepf how important is it to you to have float types as labels layers? We could update the data setter to make this conversion, but maybe we were too hasty in this change? Curious what @sofroniewn and @jni think. 
Great hints @tlambert03 and @DragaDoncila ! If I call `astype(int)` on my data before sending it to napari, the error goes away. So I can change that on my side; no problem. However, I do think that crash should not happen anyway. Any chance you can catch float-arrays and warn the user/developer?
@haesleinhuepf absolutely, we'll update the setter. Was just curious if you had a strong preference for float types for any other reason
> any other reason

Nope. float is just the only type we support currently in [pyclesperanto](https://github.com/clEsperanto/pyclesperanto_prototype).

Thanks for the instant reply!

Resolving patch:
diff --git a/napari/layers/labels/labels.py b/napari/layers/labels/labels.py
--- a/napari/layers/labels/labels.py
+++ b/napari/layers/labels/labels.py
@@ -32,7 +32,7 @@ class Labels(_image_base_class):
     Parameters
     ----------
     data : array or list of array
-        Labels data as an array or multiscale.
+        Labels data as an array or multiscale. Must be integer type or bools
     num_colors : int
         Number of unique colors to use in colormap.
     properties : dict {str: array (N,)}, DataFrame
@@ -85,7 +85,7 @@ class Labels(_image_base_class):
     Attributes
     ----------
     data : array
-        Integer valued label data. Can be N dimensional. Every pixel contains
+        Integer label data. Can be N dimensional. Every pixel contains
         an integer ID corresponding to the region it belongs to. The label 0 is
         rendered as transparent.
     multiscale : bool
@@ -330,6 +330,19 @@ def num_colors(self, num_colors):
         self._selected_color = self.get_color(self.selected_label)
         self.events.selected_label()
 
+    @property
+    def data(self):
+        """array: Image data."""
+        return self._data
+
+    @data.setter
+    def data(self, data):
+        data = self._ensure_int_labels(data)
+        self._data = data
+        self._update_dims()
+        self.events.data(value=self.data)
+        self._set_editable()
+
     @property
     def properties(self) -> Dict[str, np.ndarray]:
         """dict {str: array (N,)}, DataFrame: Properties for each label."""
@@ -374,24 +387,23 @@ def color(self, color):
         self.color_mode = color_mode
 
     def _ensure_int_labels(self, data):
-        """Ensure data is integer by converting from float if required"""
+        """Ensure data is integer by converting from bool if required, raising an error otherwise."""
         looks_multiscale, data = guess_multiscale(data)
         if not looks_multiscale:
             data = [data]
-        if any(np.issubdtype(d.dtype, np.floating) for d in data):
-            warnings.warn(
-                "Float dtypes are not supported for Labels layers. Converting data to integers..."
-            )
-            int_data = []
-            for d in data:
-                if d.dtype == np.float64:
-                    int_data.append(d.astype(np.int64))
-                elif d.dtype == np.float32:
-                    int_data.append(d.astype(np.int32))
-                else:
-                    int_data.append(d)
-            data = int_data
-
+        int_data = []
+        for data_level in data:
+            if np.issubdtype(data_level.dtype, np.floating):
+                raise TypeError(
+                    trans._(
+                        "Only integer types are supported for Labels layers, but data contains {data_level_type}."
+                    ).format(data_level_type=data_level.dtype)
+                )
+            if data_level.dtype == bool:
+                int_data.append(data_level.astype(np.int8))
+            else:
+                int_data.append(data_level)
+        data = int_data
         if not looks_multiscale:
             data = data[0]
         return data


Test patch:
diff --git a/napari/layers/labels/_tests/test_labels.py b/napari/layers/labels/_tests/test_labels.py
--- a/napari/layers/labels/_tests/test_labels.py
+++ b/napari/layers/labels/_tests/test_labels.py
@@ -3,6 +3,7 @@
 import numpy as np
 import pytest
 import xarray as xr
+from numpy.core.numerictypes import issubdtype
 from skimage import data
 
 from napari._tests.utils import check_layer_world_data_extent
@@ -55,25 +56,36 @@ def test_3D_labels():
 
 def test_float_labels():
     """Test instantiating labels layer with floats"""
-    shape = (10, 10)
     np.random.seed(0)
-    data = np.random.uniform(0, 20, size=shape)
-    with pytest.warns(UserWarning):
-        layer = Labels(data)
-        assert np.issubdtype(layer.data.dtype, np.integer)
+    data = np.random.uniform(0, 20, size=(10, 10))
+    with pytest.raises(TypeError):
+        Labels(data)
 
     data0 = np.random.uniform(20, size=(20, 20))
     data1 = data0[::2, ::2].astype(np.int32)
     data = [data0, data1]
-    with pytest.warns(UserWarning):
-        layer = Labels(data)
-        assert all(np.issubdtype(d.dtype, np.integer) for d in layer.data)
+    with pytest.raises(TypeError):
+        Labels(data)
+
+
+def test_bool_labels():
+    """Test instantiating labels layer with bools"""
+    data = np.zeros((10, 10), dtype=bool)
+    layer = Labels(data)
+    assert issubdtype(layer.data.dtype, np.integer)
+
+    data0 = np.zeros((20, 20), dtype=bool)
+    data1 = data0[::2, ::2].astype(np.int32)
+    data = [data0, data1]
+    layer = Labels(data)
+    assert all(issubdtype(d.dtype, np.integer) for d in layer.data)
 
 
 def test_changing_labels():
     """Test changing Labels data."""
     shape_a = (10, 15)
     shape_b = (20, 12)
+    shape_c = (10, 10)
     np.random.seed(0)
     data_a = np.random.randint(20, size=shape_a)
     data_b = np.random.randint(20, size=shape_b)
@@ -84,6 +96,14 @@ def test_changing_labels():
     np.testing.assert_array_equal(layer.extent.data[1] + 1, shape_b)
     assert layer._data_view.shape == shape_b[-2:]
 
+    data_c = np.zeros(shape_c, dtype=bool)
+    layer.data = data_c
+    assert np.issubdtype(layer.data.dtype, np.integer)
+
+    data_c = data_c.astype(np.float32)
+    with pytest.raises(TypeError):
+        layer.data = data_c
+
 
 def test_changing_labels_dims():
     """Test changing Labels data including dimensionality."""