Repository: napari/napari

Issue:
Napari crashes when encounters a 1D array (in .zarr/ folder)
## 🐛 Bug

<!-- A clear and concise description of what the bug is. -->

`napari file_1 file_2 ... file_n` would result in a crash if any of the files is a 1D array, as e.g. in `napari mydata.zarr/one-dimensional-array`.

## To Reproduce

Steps to reproduce the behavior:

1. Download and extract the test dataset (9KB) [timelapse.zarr.zip](https://github.com/napari/napari/files/4935727/timelapse.zarr.zip). It contains 3 numpy arrays with shapes (8, 20, 28), (20, 28), and (8,)
2. Run `napari timelapse.zarr/*` or `napari timelapse.zarr/time`
3. napari crashes on `timelapse.zarr/time`, which is a 1D numpy array

<!-- If you have a code sample, error messages, stack traces, please provide it here as well -->

```sh
$ napari timelapse.zarr/*
Traceback (most recent call last):
  File "/home/ximeg/.anaconda3/envs/sandbox/bin/napari", line 33, in <module>
    sys.exit(load_entry_point('napari', 'console_scripts', 'napari')())
  File "/home/ximeg/dev/napari/napari/__main__.py", line 181, in main
    view_path(
  File "/home/ximeg/dev/napari/napari/view_layers.py", line 207, in view_path
    viewer.open(
  File "/home/ximeg/dev/napari/napari/components/add_layers_mixin.py", line 874, in open
    self._add_layers_with_plugins(
  File "/home/ximeg/dev/napari/napari/components/add_layers_mixin.py", line 941, in _add_layers_with_plugins
    new = self._add_layer_from_data(*_data)
  File "/home/ximeg/dev/napari/napari/components/add_layers_mixin.py", line 1013, in _add_layer_from_data
    layer = add_method(data, **(meta or {}))
  File "/home/ximeg/dev/napari/napari/components/add_layers_mixin.py", line 230, in add_image
    return self.add_layer(layers.Image(data, **kwargs))
  File "/home/ximeg/dev/napari/napari/layers/image/image.py", line 159, in __init__
    multiscale, data = guess_multiscale(data)
  File "/home/ximeg/dev/napari/napari/layers/image/_image_utils.py", line 63, in guess_multiscale
    raise ValueError(
ValueError: Input data should be an array-like object, or a sequence of arrays of decreasing size. Got arrays of single shape: ()
```

## Expected behavior

<!-- A clear and concise description of what you expected to happen. -->

I think napari should show a warning, ignore the 1D dataset, and go on. Raising and error and crashing is annoying, especially if my objective is to see all images inside of a Zarr directory.

## Environment

```
napari: 0.3.6.dev30+gfc12b65
Platform: Linux-4.18.0-25-generic-x86_64-with-glibc2.10
Python: 3.8.3 (default, May 19 2020, 18:47:26) [GCC 7.3.0]
Qt: 5.14.2
PyQt5: 5.14.2
NumPy: 1.19.0
SciPy: 1.5.1
Dask: 2.20.0
VisPy: 0.6.4

GL version: 3.0 Mesa 18.2.8
MAX_TEXTURE_SIZE: 16384

Plugins:
- napari-plugin-engine: 0.1.6
- svg: 0.1.3
```

## Additional context

<!-- Add any other context about the problem here. -->
Related: #1406 


Discussion:
Thanks for the report @ximeg! This should definitely be fixed.
> I think napari should show a warning, ignore the 1D dataset, and go on. Raising and error and crashing is annoying, especially if my objective is to see all images inside of a Zarr directory.

We should definitely fix - we could either ignore the 1D dataset or we could change `Image` to expand the dims of any 1D datasets that it receives so that you would see a `1xN` image on the screen, so that the following call works too

```python
viewer.add_image(np.ones(10))
```

which currently errors on

```python-traceback
Traceback (most recent call last):
  File "test-examples/1D_image.py", line 7, in <module>
    viewer.add_image(np.ones(10))
  File "/Users/nsofroniew/GitHub/napari/napari/components/add_layers_mixin.py", line 230, in add_image
    return self.add_layer(layers.Image(data, **kwargs))
  File "/Users/nsofroniew/GitHub/napari/napari/layers/image/image.py", line 159, in __init__
    multiscale, data = guess_multiscale(data)
  File "/Users/nsofroniew/GitHub/napari/napari/layers/image/_image_utils.py", line 64, in guess_multiscale
    'Input data should be an array-like object, or a sequence of '
ValueError: Input data should be an array-like object, or a sequence of arrays of decreasing size. Got arrays of single shape: ()
```

I like the `expand_dims` idea, curious what @jni thinks. We might revisit this when we have better support for 1D plots.

A full solution to your use case might require some of the changes mentioned here https://github.com/napari/napari/issues/1406#issuecomment-660178206
> I like the expand_dims idea, curious what @jni thinks.

I'm into ignoring for now. I think it's very unlikely that these arrays are there to be seen as an image. More likely they are part of xarray groups to e.g. indicate scale along an axis. So I think it would be super weird to display them.
I'm using the 1D datasets inside of Zarr to store some additional data, like e.g. a time axis. I am also thinking about saving data frames with some metadata or image analysis results. Yes, it would be really weird to display them as images.

Resolving patch:
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
@@ -507,7 +507,7 @@ def add_image(
         Parameters
         ----------
         data : array or list of array
-            Image data. Can be N dimensional. If the last dimension has length
+            Image data. Can be N >= 2 dimensional. If the last dimension has length
             3 or 4 can be interpreted as RGB or RGBA if rgb is `True`. If a
             list and arrays are decreasing in shape then the data is treated as
             a multiscale image.
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
@@ -9,6 +9,7 @@
 from ...utils import config
 from ...utils.colormaps import AVAILABLE_COLORMAPS
 from ...utils.events import Event
+from ...utils.translations import trans
 from ..base import Layer
 from ..intensity_mixin import IntensityVisualizationMixin
 from ..utils.layer_utils import calc_data_range
@@ -35,7 +36,7 @@ class _ImageBase(IntensityVisualizationMixin, Layer):
     Parameters
     ----------
     data : array or list of array
-        Image data. Can be N dimensional. If the last dimension has length
+        Image data. Can be N >= 2 dimensional. If the last dimension has length
         3 or 4 can be interpreted as RGB or RGBA if rgb is `True`. If a
         list and arrays are decreasing in shape then the data is treated as
         a multiscale image.
@@ -86,7 +87,7 @@ class _ImageBase(IntensityVisualizationMixin, Layer):
     affine : n-D array or napari.utils.transforms.Affine
         (N+1, N+1) affine transformation matrix in homogeneous coordinates.
         The first (N, N) entries correspond to a linear transform and
-        the final column is a lenght N translation vector and a 1 or a napari
+        the final column is a length N translation vector and a 1 or a napari
         AffineTransform object. If provided then translate, scale, rotate, and
         shear values are ignored.
     opacity : float
@@ -186,6 +187,11 @@ def __init__(
         if isinstance(data, types.GeneratorType):
             data = list(data)
 
+        if getattr(data, 'ndim', 2) < 2:
+            raise ValueError(
+                trans._('Image data must have at least 2 dimensions.')
+            )
+
         # Determine if data is a multiscale
         if multiscale is None:
             multiscale, data = guess_multiscale(data)
diff --git a/napari/utils/io.py b/napari/utils/io.py
--- a/napari/utils/io.py
+++ b/napari/utils/io.py
@@ -206,6 +206,9 @@ def magic_imread(filenames, *, use_dask=None, stack=True):
     for filename in filenames_expanded:
         if guess_zarr_path(filename):
             image, zarr_shape = read_zarr_dataset(filename)
+            # 1D images are currently unsupported, so skip them.
+            if len(zarr_shape) == 1:
+                continue
             if shape is None:
                 shape = zarr_shape
         else:
@@ -220,6 +223,10 @@ def magic_imread(filenames, *, use_dask=None, stack=True):
             elif len(images) > 0:  # not read by shape clause
                 image = imread(filename)
         images.append(image)
+
+    if len(images) == 0:
+        return None
+
     if len(images) == 1:
         image = images[0]
     else:


Test patch:
diff --git a/napari/components/_tests/test_viewer_image_io.py b/napari/components/_tests/test_viewer_image_io.py
--- a/napari/components/_tests/test_viewer_image_io.py
+++ b/napari/components/_tests/test_viewer_image_io.py
@@ -1,19 +1,12 @@
+import os
 from tempfile import TemporaryDirectory
 
 import numpy as np
-import pytest
+import zarr
 from dask import array as da
 
 from napari.components import ViewerModel
 
-try:
-    import zarr
-
-    zarr_available = True
-except ImportError:
-    zarr_available = False
-
-
 # the following fixtures are defined in napari/conftest.py
 # single_png, two_pngs, irregular_images, single_tiff, rgb_png
 
@@ -74,7 +67,6 @@ def test_add_single_filename(single_tiff):
     assert viewer.layers[0].data.dtype == np.uint8
 
 
-@pytest.mark.skipif(not zarr_available, reason='zarr not installed')
 def test_add_zarr():
     viewer = ViewerModel()
     image = np.random.random((10, 20, 20))
@@ -88,7 +80,6 @@ def test_add_zarr():
         np.testing.assert_array_equal(image, viewer.layers[0].data)
 
 
-@pytest.mark.skipif(not zarr_available, reason='zarr not installed')
 def test_zarr_multiscale():
     viewer = ViewerModel()
     multiscale = [
@@ -111,6 +102,34 @@ def test_zarr_multiscale():
             np.testing.assert_array_equal(images, images_in)
 
 
+def test_add_zarr_1d_array_is_ignored():
+    # For more details: https://github.com/napari/napari/issues/1471
+    viewer = ViewerModel()
+    with TemporaryDirectory(suffix='.zarr') as zarr_dir:
+        z = zarr.open(zarr_dir, 'w')
+        z['1d'] = np.zeros(3)
+
+        image_path = os.path.join(zarr_dir, '1d')
+        viewer.open(image_path, plugin='builtins')
+
+        assert len(viewer.layers) == 0
+
+
+def test_add_many_zarr_1d_array_is_ignored():
+    # For more details: https://github.com/napari/napari/issues/1471
+    viewer = ViewerModel()
+    with TemporaryDirectory(suffix='.zarr') as zarr_dir:
+        z = zarr.open(zarr_dir, 'w')
+        z['1d'] = np.zeros(3)
+        z['2d'] = np.zeros((3, 4))
+        z['3d'] = np.zeros((3, 4, 5))
+
+        image_paths = [os.path.join(zarr_dir, name) for name in z.array_keys()]
+        viewer.open(image_paths, plugin='builtins')
+
+        assert [layer.name for layer in viewer.layers] == ['2d', '3d']
+
+
 def test_add_multichannel_rgb(rgb_png):
     image_files = rgb_png
     viewer = ViewerModel()
diff --git a/napari/utils/_tests/test_io.py b/napari/utils/_tests/test_io.py
--- a/napari/utils/_tests/test_io.py
+++ b/napari/utils/_tests/test_io.py
@@ -5,18 +5,11 @@
 
 import numpy as np
 import pytest
+import zarr
 from dask import array as da
 
 from napari.utils import io
 
-try:
-    import zarr
-
-    zarr_available = True
-except ImportError:
-    zarr_available = False
-
-
 # the following fixtures are defined in napari/conftest.py
 # single_png, two_pngs, irregular_images, single_tiff
 
@@ -119,7 +112,6 @@ def test_guess_zarr_path():
     assert not io.guess_zarr_path('no_zarr_suffix/data.png')
 
 
-@pytest.mark.skipif(not zarr_available, reason='zarr not installed')
 def test_zarr():
     image = np.random.random((10, 20, 20))
     with TemporaryDirectory(suffix='.zarr') as fout:
@@ -131,7 +123,6 @@ def test_zarr():
         np.testing.assert_array_equal(image, image_in)
 
 
-@pytest.mark.skipif(not zarr_available, reason='zarr not installed')
 def test_zarr_nested(tmp_path):
     image = np.random.random((10, 20, 20))
     image_name = 'my_image'
@@ -143,7 +134,6 @@ def test_zarr_nested(tmp_path):
     np.testing.assert_array_equal(image, image_in)
 
 
-@pytest.mark.skipif(not zarr_available, reason='zarr not installed')
 def test_zarr_multiscale():
     multiscale = [
         np.random.random((20, 20)),