Repository: napari/napari

Issue:
[Labels] IndexError after painting into 3D labels layer and toggling 3D
### 🐛 Bug Report

If you paint into a 3D labels layer and then toggle to 3D you get a spam of `IndexError: too many indices for array: array is 2-dimensional, but 3 were indexed`

Traceback:
<details>

```
IndexError                                Traceback (most recent call last)
File ~/dev/napari/napari/components/viewer_model.py:521, in ViewerModel._update_status_bar_from_cursor(self=Viewer(camera=Camera(center=(63.5, 63.5, 63.5), ...ouse_drag_gen={}, _mouse_wheel_gen={}, keymap={}), event=None)
    519 active = self.layers.selection.active
    520 if active is not None:
--> 521     self.status = active.get_status(
        active = <Labels layer 'Labels' at 0x28f2d0e10>
        self.status = 'Ready'
        self = Viewer(camera=Camera(center=(63.5, 63.5, 63.5), zoom=5.855859374999999, angles=(0.0, 0.0, 89.99999999999999), perspective=0.0, mouse_pan=False, mouse_zoom=True), cursor=Cursor(position=(63.5, 93.98228837467208, 121.98842848087597), scaled=True, style=<CursorStyle.CIRCLE: 'circle'>, size=10.0), dims=Dims(ndim=3, ndisplay=3, order=(0, 1, 2), axis_labels=('0', '1', '2'), range=(RangeTuple(start=0.0, stop=127.0, step=1.0), RangeTuple(start=0.0, stop=127.0, step=1.0), RangeTuple(start=0.0, stop=127.0, step=1.0)), margin_left=(0.0, 0.0, 0.0), margin_right=(0.0, 0.0, 0.0), point=(63.0, 63.0, 63.0), last_used=0), grid=GridCanvas(stride=1, shape=(-1, -1), enabled=False), layers=[<Image layer 'binary_blobs' at 0x28f2d2ed0>, <Labels layer 'Labels' at 0x28f2d0e10>], help='use <6> for pan/zoom, use <7> for transform, use <1> for activate the label eraser, use <3> for activate the polygon tool, use <4> for activate the fill bucket, use <5> for pick mode', status='Ready', tooltip=Tooltip(visible=False, text=''), theme='dark', title='napari', mouse_over_canvas=True, mouse_move_callbacks=[], mouse_drag_callbacks=[], mouse_double_click_callbacks=[], mouse_wheel_callbacks=[<function dims_scroll at 0x177f5e520>], _persisted_mouse_event={}, _mouse_drag_gen={}, _mouse_wheel_gen={}, keymap={})
        self.cursor.position = (63.5, 93.98228837467208, 121.98842848087597)
        self.cursor = Cursor(position=(63.5, 93.98228837467208, 121.98842848087597), scaled=True, style=<CursorStyle.CIRCLE: 'circle'>, size=10.0)
        self.cursor._view_direction = <class 'numpy.ndarray'> (3,) float64
        self.dims = Dims(ndim=3, ndisplay=3, order=(0, 1, 2), axis_labels=('0', '1', '2'), range=(RangeTuple(start=0.0, stop=127.0, step=1.0), RangeTuple(start=0.0, stop=127.0, step=1.0), RangeTuple(start=0.0, stop=127.0, step=1.0)), margin_left=(0.0, 0.0, 0.0), margin_right=(0.0, 0.0, 0.0), point=(63.0, 63.0, 63.0), last_used=0)
    522         self.cursor.position,
    523         view_direction=self.cursor._view_direction,
    524         dims_displayed=list(self.dims.displayed),
    525         world=True,
    526     )
    528     self.help = active.help
    529     if self.tooltip.visible:

File ~/dev/napari/napari/layers/labels/labels.py:1717, in Labels.get_status(self=<Labels layer 'Labels'>, position=(63.5, 93.98228837467208, 121.98842848087597), view_direction=<class 'numpy.ndarray'> (3,) float64, dims_displayed=[0, 1, 2], world=True)
   1695 """Status message information of the data at a coordinate position.
   1696 
   1697 Parameters
   (...)
   1714     Dict containing a information that can be used in a status update.
   1715 """
   1716 if position is not None:
-> 1717     value = self.get_value(
        self = <Labels layer 'Labels' at 0x28f2d0e10>
        position = (63.5, 93.98228837467208, 121.98842848087597)
        view_direction = <class 'numpy.ndarray'> (3,) float64
        dims_displayed = [0, 1, 2]
        world = True
   1718         position,
   1719         view_direction=view_direction,
   1720         dims_displayed=dims_displayed,
   1721         world=world,
   1722     )
   1723 else:
   1724     value = None

File ~/dev/napari/napari/layers/base/base.py:1297, in Layer.get_value(self=<Labels layer 'Labels'>, position=<class 'numpy.ndarray'> (3,) float64, view_direction=<class 'numpy.ndarray'> (3,) float64, dims_displayed=[0, 1, 2], world=True)
   1290         view_direction = self._world_to_data_ray(view_direction)
   1291         start_point, end_point = self.get_ray_intersections(
   1292             position=position,
   1293             view_direction=view_direction,
   1294             dims_displayed=dims_displayed,
   1295             world=False,
   1296         )
-> 1297         value = self._get_value_3d(
        self = <Labels layer 'Labels' at 0x28f2d0e10>
        start_point = <class 'numpy.ndarray'> (3,) float64
        end_point = <class 'numpy.ndarray'> (3,) float64
        dims_displayed = [0, 1, 2]
   1298             start_point=start_point,
   1299             end_point=end_point,
   1300             dims_displayed=dims_displayed,
   1301         )
   1302 else:
   1303     value = self._get_value(position)

File ~/dev/napari/napari/layers/labels/labels.py:1245, in Labels._get_value_3d(self=<Labels layer 'Labels'>, start_point=<class 'numpy.ndarray'> (3,) float64, end_point=<class 'numpy.ndarray'> (3,) float64, dims_displayed=[0, 1, 2])
   1221 def _get_value_3d(
   1222     self,
   1223     start_point: Optional[np.ndarray],
   1224     end_point: Optional[np.ndarray],
   1225     dims_displayed: List[int],
   1226 ) -> Optional[int]:
   1227     """Get the first non-background value encountered along a ray.
   1228 
   1229     Parameters
   (...)
   1242         non-zero value is not encountered, returns 0 (the background value).
   1243     """
   1244     return (
-> 1245         self._get_value_ray(
        self = <Labels layer 'Labels' at 0x28f2d0e10>
        start_point = <class 'numpy.ndarray'> (3,) float64
        end_point = <class 'numpy.ndarray'> (3,) float64
        dims_displayed = [0, 1, 2]
   1246             start_point=start_point,
   1247             end_point=end_point,
   1248             dims_displayed=dims_displayed,
   1249         )
   1250         or 0
   1251     )

File ~/dev/napari/napari/layers/labels/labels.py:1213, in Labels._get_value_ray(self=<Labels layer 'Labels'>, start_point=<class 'numpy.ndarray'> (3,) float64, end_point=<class 'numpy.ndarray'> (3,) float64, dims_displayed=[0, 1, 2])
   1207 bounding_box[:, 1] += 1
   1209 clamped = clamp_point_to_bounding_box(
   1210     sample_points,
   1211     bounding_box,
   1212 ).astype(int)
-> 1213 values = im_slice[tuple(clamped.T)]
        im_slice = <class 'numpy.ndarray'> (128, 128) int64
        clamped = <class 'numpy.ndarray'> (256, 3) int64
   1214 nonzero_indices = np.flatnonzero(values)
   1215 if len(nonzero_indices > 0):
   1216     # if a nonzer0 value was found, return the first one

IndexError: too many indices for array: array is 2-dimensional, but 3 were indexed
```

</details>

git bisect points to:
a6e15698bc25295588ac4fd0bdfd98bd4f3091c6
https://github.com/napari/napari/pull/6411
This does not occur on 0.4.18 or 0.4.19rc2

### 💡 Steps to Reproduce

In the GUI:
1. Open a 3D sample, e.g. Blobs 3D
2. Add a labels layer
3. Paint a dot
4. toggle 3D

This script is a simple way to test:
```
import napari
from skimage import data
from scipy import ndimage as ndi

blobs = data.binary_blobs(length=128, volume_fraction=0.1, n_dim=3)
labeled = ndi.label(blobs)[0]
viewer = napari.Viewer()
viewer.add_labels(labeled, name='blob ID')
```
Toggle to 3D, should be fine.
Go back to 2D, paint a dot anywhere.
Toggle to 3D, error.

### 💡 Expected Behavior

No error should occur, it should be possible to paint labels and toggle 3D.

### 🌎 Environment

napari: 0.5.0a2.dev430+ga59cdac8
Platform: macOS-14.1.2-arm64-arm-64bit
System: MacOS 14.1.2
Python: 3.11.4 | packaged by conda-forge | (main, Jun 10 2023, 18:08:41) [Clang 15.0.7 ]
Qt: 6.5.2
PyQt6: 
NumPy: 1.25.2
SciPy: 1.11.1
Dask: 2023.8.0
VisPy: 0.13.0
magicgui: 0.7.2
superqt: unknown
in-n-out: 0.1.8
app-model: 0.2.0
npe2: 0.7.2

OpenGL:
  - GL version:  2.1 Metal - 88
  - MAX_TEXTURE_SIZE: 16384

Screens:
  - screen 1: resolution 2056x1329, scale 2.0
  - screen 2: resolution 1920x1200, scale 1.0

Settings path:
  - /Users/sobolp/Library/Application Support/napari/napari-dev_36963bf0846bfe99dcf4f566a2cc780b6a723afa/settings.yaml
Plugins:
  - napari: 0.5.0a2.dev261+ge62c9dd3 (77 contributions)
  - napari-animation: 0.0.8.dev2+ga9ca7a7.d20230817 (2 contributions)
  - napari-console: 0.0.8 (0 contributions)
  - napari-ome-zarr: 0.5.2 (2 contributions)
  - napari-svg: 0.1.10 (2 contributions)

### 💡 Additional Context

First noticed by @melissawm here:
https://napari.zulipchat.com/#narrow/stream/298358-working-group-documentation/topic/multidimensional.20labels.20layer
[Labels] IndexError after painting into 3D labels layer and toggling 3D
### 🐛 Bug Report

If you paint into a 3D labels layer and then toggle to 3D you get a spam of `IndexError: too many indices for array: array is 2-dimensional, but 3 were indexed`

Traceback:
<details>

```
IndexError                                Traceback (most recent call last)
File ~/dev/napari/napari/components/viewer_model.py:521, in ViewerModel._update_status_bar_from_cursor(self=Viewer(camera=Camera(center=(63.5, 63.5, 63.5), ...ouse_drag_gen={}, _mouse_wheel_gen={}, keymap={}), event=None)
    519 active = self.layers.selection.active
    520 if active is not None:
--> 521     self.status = active.get_status(
        active = <Labels layer 'Labels' at 0x28f2d0e10>
        self.status = 'Ready'
        self = Viewer(camera=Camera(center=(63.5, 63.5, 63.5), zoom=5.855859374999999, angles=(0.0, 0.0, 89.99999999999999), perspective=0.0, mouse_pan=False, mouse_zoom=True), cursor=Cursor(position=(63.5, 93.98228837467208, 121.98842848087597), scaled=True, style=<CursorStyle.CIRCLE: 'circle'>, size=10.0), dims=Dims(ndim=3, ndisplay=3, order=(0, 1, 2), axis_labels=('0', '1', '2'), range=(RangeTuple(start=0.0, stop=127.0, step=1.0), RangeTuple(start=0.0, stop=127.0, step=1.0), RangeTuple(start=0.0, stop=127.0, step=1.0)), margin_left=(0.0, 0.0, 0.0), margin_right=(0.0, 0.0, 0.0), point=(63.0, 63.0, 63.0), last_used=0), grid=GridCanvas(stride=1, shape=(-1, -1), enabled=False), layers=[<Image layer 'binary_blobs' at 0x28f2d2ed0>, <Labels layer 'Labels' at 0x28f2d0e10>], help='use <6> for pan/zoom, use <7> for transform, use <1> for activate the label eraser, use <3> for activate the polygon tool, use <4> for activate the fill bucket, use <5> for pick mode', status='Ready', tooltip=Tooltip(visible=False, text=''), theme='dark', title='napari', mouse_over_canvas=True, mouse_move_callbacks=[], mouse_drag_callbacks=[], mouse_double_click_callbacks=[], mouse_wheel_callbacks=[<function dims_scroll at 0x177f5e520>], _persisted_mouse_event={}, _mouse_drag_gen={}, _mouse_wheel_gen={}, keymap={})
        self.cursor.position = (63.5, 93.98228837467208, 121.98842848087597)
        self.cursor = Cursor(position=(63.5, 93.98228837467208, 121.98842848087597), scaled=True, style=<CursorStyle.CIRCLE: 'circle'>, size=10.0)
        self.cursor._view_direction = <class 'numpy.ndarray'> (3,) float64
        self.dims = Dims(ndim=3, ndisplay=3, order=(0, 1, 2), axis_labels=('0', '1', '2'), range=(RangeTuple(start=0.0, stop=127.0, step=1.0), RangeTuple(start=0.0, stop=127.0, step=1.0), RangeTuple(start=0.0, stop=127.0, step=1.0)), margin_left=(0.0, 0.0, 0.0), margin_right=(0.0, 0.0, 0.0), point=(63.0, 63.0, 63.0), last_used=0)
    522         self.cursor.position,
    523         view_direction=self.cursor._view_direction,
    524         dims_displayed=list(self.dims.displayed),
    525         world=True,
    526     )
    528     self.help = active.help
    529     if self.tooltip.visible:

File ~/dev/napari/napari/layers/labels/labels.py:1717, in Labels.get_status(self=<Labels layer 'Labels'>, position=(63.5, 93.98228837467208, 121.98842848087597), view_direction=<class 'numpy.ndarray'> (3,) float64, dims_displayed=[0, 1, 2], world=True)
   1695 """Status message information of the data at a coordinate position.
   1696 
   1697 Parameters
   (...)
   1714     Dict containing a information that can be used in a status update.
   1715 """
   1716 if position is not None:
-> 1717     value = self.get_value(
        self = <Labels layer 'Labels' at 0x28f2d0e10>
        position = (63.5, 93.98228837467208, 121.98842848087597)
        view_direction = <class 'numpy.ndarray'> (3,) float64
        dims_displayed = [0, 1, 2]
        world = True
   1718         position,
   1719         view_direction=view_direction,
   1720         dims_displayed=dims_displayed,
   1721         world=world,
   1722     )
   1723 else:
   1724     value = None

File ~/dev/napari/napari/layers/base/base.py:1297, in Layer.get_value(self=<Labels layer 'Labels'>, position=<class 'numpy.ndarray'> (3,) float64, view_direction=<class 'numpy.ndarray'> (3,) float64, dims_displayed=[0, 1, 2], world=True)
   1290         view_direction = self._world_to_data_ray(view_direction)
   1291         start_point, end_point = self.get_ray_intersections(
   1292             position=position,
   1293             view_direction=view_direction,
   1294             dims_displayed=dims_displayed,
   1295             world=False,
   1296         )
-> 1297         value = self._get_value_3d(
        self = <Labels layer 'Labels' at 0x28f2d0e10>
        start_point = <class 'numpy.ndarray'> (3,) float64
        end_point = <class 'numpy.ndarray'> (3,) float64
        dims_displayed = [0, 1, 2]
   1298             start_point=start_point,
   1299             end_point=end_point,
   1300             dims_displayed=dims_displayed,
   1301         )
   1302 else:
   1303     value = self._get_value(position)

File ~/dev/napari/napari/layers/labels/labels.py:1245, in Labels._get_value_3d(self=<Labels layer 'Labels'>, start_point=<class 'numpy.ndarray'> (3,) float64, end_point=<class 'numpy.ndarray'> (3,) float64, dims_displayed=[0, 1, 2])
   1221 def _get_value_3d(
   1222     self,
   1223     start_point: Optional[np.ndarray],
   1224     end_point: Optional[np.ndarray],
   1225     dims_displayed: List[int],
   1226 ) -> Optional[int]:
   1227     """Get the first non-background value encountered along a ray.
   1228 
   1229     Parameters
   (...)
   1242         non-zero value is not encountered, returns 0 (the background value).
   1243     """
   1244     return (
-> 1245         self._get_value_ray(
        self = <Labels layer 'Labels' at 0x28f2d0e10>
        start_point = <class 'numpy.ndarray'> (3,) float64
        end_point = <class 'numpy.ndarray'> (3,) float64
        dims_displayed = [0, 1, 2]
   1246             start_point=start_point,
   1247             end_point=end_point,
   1248             dims_displayed=dims_displayed,
   1249         )
   1250         or 0
   1251     )

File ~/dev/napari/napari/layers/labels/labels.py:1213, in Labels._get_value_ray(self=<Labels layer 'Labels'>, start_point=<class 'numpy.ndarray'> (3,) float64, end_point=<class 'numpy.ndarray'> (3,) float64, dims_displayed=[0, 1, 2])
   1207 bounding_box[:, 1] += 1
   1209 clamped = clamp_point_to_bounding_box(
   1210     sample_points,
   1211     bounding_box,
   1212 ).astype(int)
-> 1213 values = im_slice[tuple(clamped.T)]
        im_slice = <class 'numpy.ndarray'> (128, 128) int64
        clamped = <class 'numpy.ndarray'> (256, 3) int64
   1214 nonzero_indices = np.flatnonzero(values)
   1215 if len(nonzero_indices > 0):
   1216     # if a nonzer0 value was found, return the first one

IndexError: too many indices for array: array is 2-dimensional, but 3 were indexed
```

</details>

git bisect points to:
a6e15698bc25295588ac4fd0bdfd98bd4f3091c6
https://github.com/napari/napari/pull/6411
This does not occur on 0.4.18 or 0.4.19rc2

### 💡 Steps to Reproduce

In the GUI:
1. Open a 3D sample, e.g. Blobs 3D
2. Add a labels layer
3. Paint a dot
4. toggle 3D

This script is a simple way to test:
```
import napari
from skimage import data
from scipy import ndimage as ndi

blobs = data.binary_blobs(length=128, volume_fraction=0.1, n_dim=3)
labeled = ndi.label(blobs)[0]
viewer = napari.Viewer()
viewer.add_labels(labeled, name='blob ID')
```
Toggle to 3D, should be fine.
Go back to 2D, paint a dot anywhere.
Toggle to 3D, error.

### 💡 Expected Behavior

No error should occur, it should be possible to paint labels and toggle 3D.

### 🌎 Environment

napari: 0.5.0a2.dev430+ga59cdac8
Platform: macOS-14.1.2-arm64-arm-64bit
System: MacOS 14.1.2
Python: 3.11.4 | packaged by conda-forge | (main, Jun 10 2023, 18:08:41) [Clang 15.0.7 ]
Qt: 6.5.2
PyQt6: 
NumPy: 1.25.2
SciPy: 1.11.1
Dask: 2023.8.0
VisPy: 0.13.0
magicgui: 0.7.2
superqt: unknown
in-n-out: 0.1.8
app-model: 0.2.0
npe2: 0.7.2

OpenGL:
  - GL version:  2.1 Metal - 88
  - MAX_TEXTURE_SIZE: 16384

Screens:
  - screen 1: resolution 2056x1329, scale 2.0
  - screen 2: resolution 1920x1200, scale 1.0

Settings path:
  - /Users/sobolp/Library/Application Support/napari/napari-dev_36963bf0846bfe99dcf4f566a2cc780b6a723afa/settings.yaml
Plugins:
  - napari: 0.5.0a2.dev261+ge62c9dd3 (77 contributions)
  - napari-animation: 0.0.8.dev2+ga9ca7a7.d20230817 (2 contributions)
  - napari-console: 0.0.8 (0 contributions)
  - napari-ome-zarr: 0.5.2 (2 contributions)
  - napari-svg: 0.1.10 (2 contributions)

### 💡 Additional Context

First noticed by @melissawm here:
https://napari.zulipchat.com/#narrow/stream/298358-working-group-documentation/topic/multidimensional.20labels.20layer


Discussion:

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
@@ -1051,8 +1051,10 @@ def _raw_to_displayed(
         mapped_labels : array
             Encoded colors mapped between 0 and 1 to be displayed.
         """
+
         if data_slice is None:
             data_slice = tuple(slice(0, size) for size in raw.shape)
+            self._cached_labels = None
         else:
             self._setup_cache(raw)
 


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
@@ -1639,6 +1639,15 @@ def on_event():
     assert event_emitted
 
 
+def test_invalidate_cache_when_change_slice():
+    layer = Labels(np.zeros((2, 4, 5), dtype=np.uint8))
+    assert layer._cached_labels is None
+    layer._setup_cache(layer._slice.image.raw)
+    assert layer._cached_labels is not None
+    layer._set_view_slice()
+    assert layer._cached_labels is None
+
+
 class TestLabels:
     @staticmethod
     def get_objects():