Repository: napari/napari

Issue:
Divide by zero warnings when working with data that is size 1 along an axis
## 🐛 Bug

Moving my text from https://github.com/napari/napari/issues/3159#issuecomment-910773616 into a new issue.

When playing with 3D data that is size 1 along the slice axis, I get a couple different "divide by zero" warnings. Despite these warnings, the data does load and I am able to interact with it as expected.

```
napari/components/viewer_model.py:285: RuntimeWarning: divide by zero encountered in true_divide
      np.array(self._canvas_size) / np.array(size[-2:])
```

The underlying issue here is that size is 0 along one axis (`Viewer.layers.extent.world` is size 0 on the last axis).

And if I transpose the axes in the 2D view so that the singleton dimension corresponds to the slider axis, then the slider disappears (I suppose this is as expected given there are no other slices to slide to) and the message is:

```
napari/_vispy/vispy_camera.py:111: RuntimeWarning: divide by zero encountered in true_divide
    zoom = np.min(canvas_size / scale)
```

where some debugging confirms that scale = [239.57143095 0. ] and the value of 0.0 corresponds to self._view.camera.rect.height.


#2875 addressed the case where scale == [0, 0], but that was due to a different cause. In my case only one of the two values is zero.

<!-- If you have a code sample, error messages, stack traces, please provide it here as well -->

## Expected behavior

We should be able to view data with singleton dimensions without these warnings. 

## Environment

current napari development branch (around 0.4.11rc3) 

 - Please copy and paste the information at napari info option in help menubar here:

 - Any other relevant information:

## Additional context

I suspect there is an off-by-one error in determination of the extent. I am looking into it now.



Discussion:
Example data files that are single slice 3D are here: https://github.com/grlee77/parrec_oblique/tree/master/NIFTI. This format can be opened with either `napari-nibabel` or `napari-itk-io` plugins.
If I comment out the following line it resolves the errors:
https://github.com/napari/napari/blob/8f1126c82bfbabd6cafd9739fa8c747ddff79583/napari/layers/image/image.py#L405

However, that creates a worse problem where the sliders have an extent that is one larger than desired and pulling the slider past the image extent will cause an IndexError. So, I have to look a bit more to see whether:

1.) keep this subtract by 1 here as-is and add 1 elsewhere
or
2.) remove this subtract by 1 and then subtract one from the slider extents elsewhere

I would argue that the extent of the display canvas should not have 1 subtracted from the shape if we are treating each point as a square pixel in the display. 

As a specific example, in 1D illustration, 5 points has extent 4:
```
                   *         *         *         *         *
                   |<------------------------------------->|
```
but 5 pixels has extent five since each pixel has a width of 1 pixel spacing.
```
                | pixel 1 | pixel 2 | pixel 3 | pixel 4 | pixel 5 |

                |<----------------------------------------------->|
```

This type of thing came up a lot in the past for scikit-image and scipy.ndimage coordinates used by interpolation routines (example: https://github.com/scikit-image/scikit-image/pull/5472#issuecomment-903148472)

Thanks for making a new issue, we should avoid having any of the scale values be 0.

As for the -1 / +1 stuff this could be bug/ artifact/ remnant of previous days before we had standardized more of our transforms. If you think we're getting this wrong here you're probably right!

I recall @kevinyamauchi once flagging some funny behaviour here too. Maybe @jni, @DragaDoncila or @andy-sweet want to double check things. I can take a more detailed look later too.

Resolving patch:
diff --git a/napari/_vispy/vispy_camera.py b/napari/_vispy/vispy_camera.py
--- a/napari/_vispy/vispy_camera.py
+++ b/napari/_vispy/vispy_camera.py
@@ -106,8 +106,7 @@ def zoom(self):
             scale = np.array(
                 [self._view.camera.rect.width, self._view.camera.rect.height]
             )
-            if np.allclose(scale, [0, 0]):  # fix for #2875
-                scale = [1, 1]
+            scale[np.isclose(scale, 0)] = 1  # fix for #2875
         zoom = np.min(canvas_size / scale)
         return zoom
 
diff --git a/napari/components/viewer_model.py b/napari/components/viewer_model.py
--- a/napari/components/viewer_model.py
+++ b/napari/components/viewer_model.py
@@ -281,8 +281,10 @@ def reset_view(self, event=None):
         if np.max(size) == 0:
             self.camera.zoom = 0.95 * np.min(self._canvas_size)
         else:
+            scale = np.array(size[-2:])
+            scale[np.isclose(scale, 0)] = 1
             self.camera.zoom = 0.95 * np.min(
-                np.array(self._canvas_size) / np.array(size[-2:])
+                np.array(self._canvas_size) / scale
             )
         self.camera.angles = (0, 0, 90)
 


Test patch: