Repository: napari/napari

Issue:
channel axis and affine transform problem
## 🐛 Bug
using the `channel_axis` kwarg causes problems when using a transformation matrix(```np.array```) throwing an ```IndexError```. When using ```napari.utils.transforms.Affine``` for the ```affine``` kwarg a ```TypeError``` is thrown as setting the `channel_axis` makes the other kwargs match the len of channels.

## To Reproduce

```python

from skimage.data import astronaut
import napari
from napari.utils import transforms

rgb_astro = astronaut()
mc_astro = np.rollaxis(rgb_astro, 2)

aff_mat = transforms.Affine(rotate=45).affine_matrix

with napari.gui_qt():
    viewer = napari.Viewer()
    viewer.add_image(
        rgb_astro,
        name="45 rot RGB",
        affine=aff_mat,
    )

    viewer.add_image(
        mc_astro,
        channel_axis=0,
        name="45 rot mc",
        affine=aff_mat,
    )
```
### error
```python
ERROR:root:Unhandled exception:
Traceback (most recent call last):
  File "C:\miniconda3\envs\bc\lib\site-packages\napari\_qt\event_loop.py", line 79, in gui_qt
    yield app
  File "<ipython-input-185-65466d9eb63b>", line 15, in <module>
    viewer.add_image(
  File "C:\miniconda3\envs\bc\lib\site-packages\napari\components\viewer_model.py", line 776, in add_image
    layer = self.add_layer(image_class(image, **i_kwargs))
  File "C:\miniconda3\envs\bc\lib\site-packages\napari\layers\image\image.py", line 209, in __init__
    super().__init__(
  File "C:\miniconda3\envs\bc\lib\site-packages\napari\layers\intensity_mixin.py", line 22, in __init__
    super().__init__(*args, **kwargs)
  File "C:\miniconda3\envs\bc\lib\site-packages\napari\layers\base\base.py", line 224, in __init__
    data2world_transform = Affine(
  File "C:\miniconda3\envs\bc\lib\site-packages\napari\utils\transforms\transforms.py", line 313, in __init__
    linear_matrix = affine_matrix[:-1, :-1]
IndexError: too many indices for array: array is 1-dimensional, but 2 were indexed
```


### doesn't work
```python
with napari.gui_qt():
    viewer = napari.Viewer()
    viewer.add_image(
        rgb_astro,
        name="45 rot RGB",
        affine=aff_mat,
    )

    viewer.add_image(
        mc_astro,
        channel_axis=0,
        name="45 rot mc",
        affine=transforms.Affine(rotate=45),
    )
```
```python
ERROR:root:Unhandled exception:
Traceback (most recent call last):
  File "C:\miniconda3\envs\bc\lib\site-packages\napari\_qt\event_loop.py", line 79, in gui_qt
    yield app
  File "<ipython-input-208-0a3b5c8d9a01>", line 9, in <module>
    viewer.add_image(
  File "C:\miniconda3\envs\bc\lib\site-packages\napari\components\viewer_model.py", line 772, in add_image
    layerdata_list = split_channels(data, channel_axis, **kwargs)
  File "C:\miniconda3\envs\bc\lib\site-packages\napari\layers\utils\stack_utils.py", line 76, in split_channels
    kwargs[key] = iter(ensure_iterable(val))
TypeError: 'Affine' object is not iterable
```
### works
```python
aff_mat = transforms.Affine(rotate=45).affine_matrix

with napari.gui_qt():
    viewer = napari.Viewer()
    viewer.add_image(
        rgb_astro,
        name="45 rot RGB",
        affine=aff_mat,
    )

    viewer.add_image(
        mc_astro,
        channel_axis=0,
        name="45 rot mc",
        affine=[
            aff_mat,
            aff_mat,
            aff_mat
        ],
    )
```
### doesn't work
```python
aff_mat = transforms.Affine(rotate=45).affine_matrix

with napari.gui_qt():
    viewer = napari.Viewer()
    viewer.add_image(
        rgb_astro,
        name="45 rot RGB",
        affine=aff_mat,
    )

    viewer.add_image(
        mc_astro,
        channel_axis=0,
        name="45 rot mc",
        affine=[
            aff_mat,
        ],
    )
```
## Expected behavior
When setting a channel axis, a single transform should transform all channels when passed in `affine`, but shouldn't be a requirement (for an instance where each channel needs corrective transformation). This seems to be handled here:
https://github.com/napari/napari/blob/d8d2d301b71af79a6a7f8f4bafa1f55a18317dd8/napari/layers/utils/stack_utils.py#L13

## Environment

('napari: 0.4.2'
 'Platform: Windows-10-10.0.19041-SP0'
 'Python: 3.8.6 | packaged by conda-forge | (default, Oct  7 2020, 18:22:52) '
 '[MSC v.1916 64 bit (AMD64)]'
 'Qt: 5.14.2'
 'PyQt5: 5.14.2'
 'NumPy: 1.19.2'
 'SciPy: 1.5.2'
 'Dask: 2.30.0'
 'VisPy: 0.6.5'
 
 'GL version:  4.6.0 - Build 26.20.100.7325'
 'MAX_TEXTURE_SIZE: 16384'
 
 'Plugins:'
 '  - napari_plugin_engine: 0.1.8'
 '  - svg: 0.1.4')





Discussion:
I think I have fixed this. I'll open a PR soon.

Resolving patch:
diff --git a/napari/layers/utils/stack_utils.py b/napari/layers/utils/stack_utils.py
--- a/napari/layers/utils/stack_utils.py
+++ b/napari/layers/utils/stack_utils.py
@@ -52,7 +52,13 @@ def split_channels(
     kwargs['blending'] = kwargs.get('blending') or 'additive'
     kwargs.setdefault('colormap', None)
     # these arguments are *already* iterables in the single-channel case.
-    iterable_kwargs = {'scale', 'translate', 'contrast_limits', 'metadata'}
+    iterable_kwargs = {
+        'scale',
+        'translate',
+        'affine',
+        'contrast_limits',
+        'metadata',
+    }
 
     # turn the kwargs dict into a mapping of {key: iterator}
     # so that we can use {k: next(v) for k, v in kwargs.items()} below
diff --git a/napari/utils/misc.py b/napari/utils/misc.py
--- a/napari/utils/misc.py
+++ b/napari/utils/misc.py
@@ -144,7 +144,8 @@ def ensure_sequence_of_iterables(obj, length: Optional[int] = None):
     In [4]: ensure_sequence_of_iterables(None)
     Out[4]: repeat(None)
     """
-    if obj and is_sequence(obj) and is_iterable(obj[0]):
+
+    if obj is not None and is_sequence(obj) and is_iterable(obj[0]):
         if length is not None and len(obj) != length:
             raise ValueError(f"length of {obj} must equal {length}")
         return obj


Test patch:
diff --git a/napari/layers/utils/_tests/test_stack_utils.py b/napari/layers/utils/_tests/test_stack_utils.py
--- a/napari/layers/utils/_tests/test_stack_utils.py
+++ b/napari/layers/utils/_tests/test_stack_utils.py
@@ -7,6 +7,7 @@
     split_channels,
     stack_to_images,
 )
+from napari.utils.transforms import Affine
 
 
 def test_stack_to_images_basic():
@@ -156,6 +157,8 @@ def test_images_to_stack_none_scale():
             'blending': None,
             'visible': True,
             'multiscale': None,
+            'rotate': None,
+            'affine': None,
         },
         {
             'rgb': None,
@@ -224,3 +227,45 @@ def test_split_channels_missing_keywords():
     for d, meta, _ in result_list:
         assert d.shape == (128, 128)
         assert meta['blending'] == 'additive'
+
+
+def test_split_channels_affine_nparray(kwargs):
+    kwargs['affine'] = np.eye(3)
+    data = np.random.randint(0, 200, (3, 128, 128))
+    result_list = split_channels(data, 0, **kwargs)
+
+    assert len(result_list) == 3
+    for d, meta, _ in result_list:
+        assert d.shape == (128, 128)
+        assert np.array_equal(meta['affine'], np.eye(3))
+
+
+def test_split_channels_affine_napari(kwargs):
+    kwargs['affine'] = Affine(affine_matrix=np.eye(3))
+    data = np.random.randint(0, 200, (3, 128, 128))
+    result_list = split_channels(data, 0, **kwargs)
+
+    assert len(result_list) == 3
+    for d, meta, _ in result_list:
+        assert d.shape == (128, 128)
+        assert np.array_equal(meta['affine'].affine_matrix, np.eye(3))
+
+
+def test_split_channels_multi_affine_napari(kwargs):
+    kwargs['affine'] = [
+        Affine(scale=[1, 1]),
+        Affine(scale=[2, 2]),
+        Affine(scale=[3, 3]),
+    ]
+
+    data = np.random.randint(0, 200, (3, 128, 128))
+    result_list = split_channels(data, 0, **kwargs)
+
+    assert len(result_list) == 3
+    for idx, result_data in enumerate(result_list):
+        d, meta, _ = result_data
+        assert d.shape == (128, 128)
+        assert np.array_equal(
+            meta['affine'].affine_matrix,
+            Affine(scale=[idx + 1, idx + 1]).affine_matrix,
+        )