Repository: scikit-image/scikit-image

Issue:
Bug when using the inverse of a Transform with params given as np.matrix
```
>>> import numpy as np
>>> m = np.matrix(np.eye(3))                                                                                                                
>>> from skimage import transform as tf                                                                                                     
>>> tform = tf.EuclideanTransform(m)
>>> tf.warp(np.ones((10, 10)), tform.inverse)                                                                                                             
Traceback (most recent call last):
  File "<ipython-input-86-faf11f9a042f>", line 1, in <module>
    tf.warp(img, tform.inverse)
  File "/home/emma/code/scikit-image/skimage/transform/_warps.py", line 898, in warp
    coords = warp_coords(coord_map, output_shape)
  File "/home/emma/code/scikit-image/skimage/transform/_warps.py", line 603, in warp_coords
    tf_coords = coord_map(tf_coords)
  File "/home/emma/code/scikit-image/skimage/transform/_warps.py", line 889, in coord_map
    return inverse_map(*args, **map_args)
  File "/home/emma/code/scikit-image/skimage/transform/_geometric.py", line 594, in inverse
    return self._apply_mat(coords, self._inv_matrix)
  File "/home/emma/code/scikit-image/skimage/transform/_geometric.py", line 558, in _apply_mat
    dst[dst[:, 2] == 0, 2] = np.finfo(float).eps
IndexError: too many indices for array
```
The Error disappears when an array is passed to `EuclideanTransform` (it probably happens also for other classes of transforms).



Discussion:
@emmanuelle do you think this is something we want to fix/support? Given than `np.matrix` is being deprecated anyway...
This is exactly the reason why np.matrix is being deprecated. I don't think we should be fixing this.

Edit: added should
@hmaarrfk did you inadvertently leave out a "should"? "I don't think we *should* be fixing this." =) In which case I agree. 
Nice catch.
I'm not sure this is a good first issue, this is actually quite esoteric and complex behavior. Catching subleties in multiple and matrix multiply is quite hard not easy for novice python programmers.

I would actually add a "won't fix" label.
OK, given the controversy for sure it's not a good first issue :-). However, I don't know when `np.matrix` is going to disappear, the docstring just says "the class may be removed in the future". I was thinking of all the people who started doing linear algebra when this was the recommended way (like me, I caught this bug when playing around with transforms). But I won't fight a lot for this issue either.
A simple fix would be to call `np.asarray` over the input argument, but the `EuclideanTransform` [documentation](https://scikit-image.org/docs/dev/api/skimage.transform.html#skimage.transform.EuclideanTransform) specifies that the `matrix` argument should be an `array`. So I would not consider this issue as a bug since 3×3 list of list would also raise an error :-(
I think @emmanuelle's historical context is quite important. Thanks for bringing that up.

I think it is especially appropriate in the context of homographies where matricues are used to describe the physics.


`np.asarray` works for me, if @emmanuelle wants to tackle this. It would be nice to support list-of-lists as input.

Resolving patch:
diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py
--- a/skimage/transform/_geometric.py
+++ b/skimage/transform/_geometric.py
@@ -42,7 +42,7 @@ def _center_and_normalize_points(points):
 
     Returns
     -------
-    matrix : (D+1, D+1) array
+    matrix : (D+1, D+1) array_like
         The transformation matrix to obtain the new points.
     new_points : (N, D) array
         The transformed image points.
@@ -93,9 +93,9 @@ def _umeyama(src, dst, estimate_scale):
 
     Parameters
     ----------
-    src : (M, N) array
+    src : (M, N) array_like
         Source coordinates.
-    dst : (M, N) array
+    dst : (M, N) array_like
         Destination coordinates.
     estimate_scale : bool
         Whether to estimate scaling factor.
@@ -112,6 +112,8 @@ def _umeyama(src, dst, estimate_scale):
             point patterns", Shinji Umeyama, PAMI 1991, :DOI:`10.1109/34.88573`
 
     """
+    src = np.asarray(src)
+    dst = np.asarray(dst)
 
     num = src.shape[0]
     dim = src.shape[1]
@@ -172,7 +174,7 @@ def __call__(self, coords):
 
         Parameters
         ----------
-        coords : (N, 2) array
+        coords : (N, 2) array_like
             Source coordinates.
 
         Returns
@@ -188,7 +190,7 @@ def inverse(self, coords):
 
         Parameters
         ----------
-        coords : (N, 2) array
+        coords : (N, 2) array_like
             Destination coordinates.
 
         Returns
@@ -247,7 +249,7 @@ class FundamentalMatrixTransform(GeometricTransform):
 
     Parameters
     ----------
-    matrix : (3, 3) array, optional
+    matrix : (3, 3) array_like, optional
         Fundamental matrix.
 
     Attributes
@@ -262,6 +264,7 @@ def __init__(self, matrix=None, *, dimensionality=2):
             # default to an identity transform
             matrix = np.eye(dimensionality + 1)
         else:
+            matrix = np.asarray(matrix)
             dimensionality = matrix.shape[0] - 1
             if matrix.shape != (dimensionality + 1, dimensionality + 1):
                 raise ValueError("Invalid shape of transformation matrix")
@@ -277,7 +280,7 @@ def __call__(self, coords):
 
         Parameters
         ----------
-        coords : (N, 2) array
+        coords : (N, 2) array_like
             Source coordinates.
 
         Returns
@@ -286,6 +289,7 @@ def __call__(self, coords):
             Epipolar lines in the destination image.
 
         """
+        coords = np.asarray(coords)
         coords_homogeneous = np.column_stack([coords, np.ones(coords.shape[0])])
         return coords_homogeneous @ self.params.T
 
@@ -294,7 +298,7 @@ def inverse(self, coords):
 
         Parameters
         ----------
-        coords : (N, 2) array
+        coords : (N, 2) array_like
             Destination coordinates.
 
         Returns
@@ -303,6 +307,7 @@ def inverse(self, coords):
             Epipolar lines in the source image.
 
         """
+        coords = np.asarray(coords)
         coords_homogeneous = np.column_stack([coords, np.ones(coords.shape[0])])
         return coords_homogeneous @ self.params
 
@@ -313,9 +318,9 @@ def _setup_constraint_matrix(self, src, dst):
 
         Parameters
         ----------
-        src : (N, 2) array
+        src : (N, 2) array_like
             Source coordinates.
-        dst : (N, 2) array
+        dst : (N, 2) array_like
             Destination coordinates.
 
         Returns
@@ -331,6 +336,8 @@ def _setup_constraint_matrix(self, src, dst):
             coordinates.
 
         """
+        src = np.asarray(src)
+        dst = np.asarray(dst)
         if src.shape != dst.shape:
             raise ValueError('src and dst shapes must be identical.')
         if src.shape[0] < 8:
@@ -367,9 +374,9 @@ def estimate(self, src, dst):
 
         Parameters
         ----------
-        src : (N, 2) array
+        src : (N, 2) array_like
             Source coordinates.
-        dst : (N, 2) array
+        dst : (N, 2) array_like
             Destination coordinates.
 
         Returns
@@ -443,12 +450,12 @@ class EssentialMatrixTransform(FundamentalMatrixTransform):
 
     Parameters
     ----------
-    rotation : (3, 3) array, optional
+    rotation : (3, 3) array_like, optional
         Rotation matrix of the relative camera motion.
-    translation : (3, 1) array, optional
+    translation : (3, 1) array_like, optional
         Translation vector of the relative camera motion. The vector must
         have unit length.
-    matrix : (3, 3) array, optional
+    matrix : (3, 3) array_like, optional
         Essential matrix.
 
     Attributes
@@ -462,8 +469,10 @@ def __init__(self, rotation=None, translation=None, matrix=None,
                  *, dimensionality=2):
         super().__init__(matrix=matrix, dimensionality=dimensionality)
         if rotation is not None:
+            rotation = np.asarray(rotation)
             if translation is None:
                 raise ValueError("Both rotation and translation required")
+            translation = np.asarray(translation)
             if rotation.shape != (3, 3):
                 raise ValueError("Invalid shape of rotation matrix")
             if abs(np.linalg.det(rotation) - 1) > 1e-6:
@@ -478,6 +487,7 @@ def __init__(self, rotation=None, translation=None, matrix=None,
                             -translation[1], translation[0], 0]).reshape(3, 3)
             self.params = t_x @ rotation
         elif matrix is not None:
+            matrix = np.asarray(matrix)
             if matrix.shape != (3, 3):
                 raise ValueError("Invalid shape of transformation matrix")
             self.params = matrix
@@ -494,9 +504,9 @@ def estimate(self, src, dst):
 
         Parameters
         ----------
-        src : (N, 2) array
+        src : (N, 2) array_like
             Source coordinates.
-        dst : (N, 2) array
+        dst : (N, 2) array_like
             Destination coordinates.
 
         Returns
@@ -549,7 +559,7 @@ class ProjectiveTransform(GeometricTransform):
 
     Parameters
     ----------
-    matrix : (D+1, D+1) array, optional
+    matrix : (D+1, D+1) array_like, optional
         Homogeneous transformation matrix.
     dimensionality : int, optional
         The number of dimensions of the transform. This is ignored if
@@ -567,6 +577,7 @@ def __init__(self, matrix=None, *, dimensionality=2):
             # default to an identity transform
             matrix = np.eye(dimensionality + 1)
         else:
+            matrix = np.asarray(matrix)
             dimensionality = matrix.shape[0] - 1
             if matrix.shape != (dimensionality + 1, dimensionality + 1):
                 raise ValueError("invalid shape of transformation matrix")
@@ -604,7 +615,7 @@ def __call__(self, coords):
 
         Parameters
         ----------
-        coords : (N, D) array
+        coords : (N, D) array_like
             Source coordinates.
 
         Returns
@@ -620,7 +631,7 @@ def inverse(self, coords):
 
         Parameters
         ----------
-        coords : (N, D) array
+        coords : (N, D) array_like
             Destination coordinates.
 
         Returns
@@ -683,11 +694,11 @@ def estimate(self, src, dst, weights=None):
 
         Parameters
         ----------
-        src : (N, 2) array
+        src : (N, 2) array_like
             Source coordinates.
-        dst : (N, 2) array
+        dst : (N, 2) array_like
             Destination coordinates.
-        weights : (N,) array, optional
+        weights : (N,) array_like, optional
             Relative weight values for each pair of points.
 
         Returns
@@ -696,7 +707,8 @@ def estimate(self, src, dst, weights=None):
             True, if model estimation succeeds.
 
         """
-
+        src = np.asarray(src)
+        dst = np.asarray(dst)
         n, d = src.shape
 
         src_matrix, src = _center_and_normalize_points(src)
@@ -725,6 +737,7 @@ def estimate(self, src, dst, weights=None):
         if weights is None:
             _, _, V = np.linalg.svd(A)
         else:
+            weights = np.asarray(weights)
             W = np.diag(np.tile(np.sqrt(weights / np.max(weights)), d))
             _, _, V = np.linalg.svd(W @ A)
 
@@ -827,7 +840,7 @@ class AffineTransform(ProjectiveTransform):
 
     Parameters
     ----------
-    matrix : (D+1, D+1) array, optional
+    matrix : (D+1, D+1) array_like, optional
         Homogeneous transformation matrix. If this matrix is provided, it is an
         error to provide any of scale, rotation, shear, or translation.
     scale : {s as float or (sx, sy) as array, list or tuple}, optional
@@ -873,6 +886,7 @@ def __init__(self, matrix=None, scale=None, rotation=None, shear=None,
         if params and dimensionality > 2:
             raise ValueError('Parameter input is only supported in 2D.')
         elif matrix is not None:
+            matrix = np.asarray(matrix)
             if matrix.ndim != 2 or matrix.shape[0] != matrix.shape[1]:
                 raise ValueError("Invalid shape of transformation matrix.")
             else:
@@ -960,9 +974,9 @@ def estimate(self, src, dst):
 
         Parameters
         ----------
-        src : (N, D) array
+        src : (N, D) array_like
             Source coordinates.
-        dst : (N, D) array
+        dst : (N, D) array_like
             Destination coordinates.
 
         Returns
@@ -971,6 +985,8 @@ def estimate(self, src, dst):
             True, if all pieces of the model are successfully estimated.
 
         """
+        src = np.asarray(src)
+        dst = np.asarray(dst)
 
         ndim = src.shape[1]
         # forward piecewise affine
@@ -1005,7 +1021,7 @@ def __call__(self, coords):
 
         Parameters
         ----------
-        coords : (N, D) array
+        coords : (N, D) array_like
             Source coordinates.
 
         Returns
@@ -1014,7 +1030,7 @@ def __call__(self, coords):
             Transformed coordinates.
 
         """
-
+        coords = np.asarray(coords)
         out = np.empty_like(coords, np.float64)
 
         # determine triangle index for each coordinate
@@ -1040,7 +1056,7 @@ def inverse(self, coords):
 
         Parameters
         ----------
-        coords : (N, D) array
+        coords : (N, D) array_like
             Source coordinates.
 
         Returns
@@ -1049,7 +1065,7 @@ def inverse(self, coords):
             Transformed coordinates.
 
         """
-
+        coords = np.asarray(coords)
         out = np.empty_like(coords, np.float64)
 
         # determine triangle index for each coordinate
@@ -1149,7 +1165,7 @@ class EuclideanTransform(ProjectiveTransform):
 
     Parameters
     ----------
-    matrix : (D+1, D+1) array, optional
+    matrix : (D+1, D+1) array_like, optional
         Homogeneous transformation matrix.
     rotation : float or sequence of float, optional
         Rotation angle in counter-clockwise direction as radians. If given as
@@ -1180,6 +1196,7 @@ def __init__(self, matrix=None, rotation=None, translation=None,
             raise ValueError("You cannot specify the transformation matrix and"
                              " the implicit parameters at the same time.")
         elif matrix is not None:
+            matrix = np.asarray(matrix)
             if matrix.shape[0] != matrix.shape[1]:
                 raise ValueError("Invalid shape of transformation matrix.")
             self.params = matrix
@@ -1230,9 +1247,9 @@ def estimate(self, src, dst):
 
         Parameters
         ----------
-        src : (N, 2) array
+        src : (N, 2) array_like
             Source coordinates.
-        dst : (N, 2) array
+        dst : (N, 2) array_like
             Destination coordinates.
 
         Returns
@@ -1241,7 +1258,6 @@ def estimate(self, src, dst):
             True, if model estimation succeeds.
 
         """
-
         self.params = _umeyama(src, dst, False)
 
         # _umeyama will return nan if the problem is not well-conditioned.
@@ -1287,7 +1303,7 @@ class SimilarityTransform(EuclideanTransform):
 
     Parameters
     ----------
-    matrix : (dim+1, dim+1) array, optional
+    matrix : (dim+1, dim+1) array_like, optional
         Homogeneous transformation matrix.
     scale : float, optional
         Scale factor. Implemented only for 2D and 3D.
@@ -1295,7 +1311,7 @@ class SimilarityTransform(EuclideanTransform):
         Rotation angle in counter-clockwise direction as radians.
         Implemented only for 2D and 3D. For 3D, this is given in ZYX Euler
         angles.
-    translation : (dim,) array-like, optional
+    translation : (dim,) array_like, optional
         x, y[, z] translation parameters. Implemented only for 2D and 3D.
 
     Attributes
@@ -1315,6 +1331,7 @@ def __init__(self, matrix=None, scale=None, rotation=None,
             raise ValueError("You cannot specify the transformation matrix and"
                              " the implicit parameters at the same time.")
         elif matrix is not None:
+            matrix = np.asarray(matrix)
             if matrix.ndim != 2 or matrix.shape[0] != matrix.shape[1]:
                 raise ValueError("Invalid shape of transformation matrix.")
             else:
@@ -1355,9 +1372,9 @@ def estimate(self, src, dst):
 
         Parameters
         ----------
-        src : (N, ndim) array
+        src : (N, 2) array_like
             Source coordinates.
-        dst : (N, ndim) array
+        dst : (N, 2) array_like
             Destination coordinates.
 
         Returns
@@ -1394,7 +1411,7 @@ class PolynomialTransform(GeometricTransform):
 
     Parameters
     ----------
-    params : (2, N) array, optional
+    params : (2, N) array_like, optional
         Polynomial coefficients where `N * 2 = (order + 1) * (order + 2)`. So,
         a_ji is defined in `params[0, :]` and b_ji in `params[1, :]`.
 
@@ -1414,6 +1431,8 @@ def __init__(self, params=None, *, dimensionality=2):
         if params is None:
             # default to transformation which preserves original coordinates
             params = np.array([[0, 1, 0], [0, 0, 1]])
+        else:
+            params = np.asarray(params)
         if params.shape[0] != 2:
             raise ValueError("invalid shape of transformation parameters")
         self.params = params
@@ -1461,13 +1480,13 @@ def estimate(self, src, dst, order=2, weights=None):
 
         Parameters
         ----------
-        src : (N, 2) array
+        src : (N, 2) array_like
             Source coordinates.
-        dst : (N, 2) array
+        dst : (N, 2) array_like
             Destination coordinates.
         order : int, optional
             Polynomial order (number of coefficients is order + 1).
-        weights : (N,) array, optional
+        weights : (N,) array_like, optional
             Relative weight values for each pair of points.
 
         Returns
@@ -1476,6 +1495,8 @@ def estimate(self, src, dst, order=2, weights=None):
             True, if model estimation succeeds.
 
         """
+        src = np.asarray(src)
+        dst = np.asarray(dst)
         xs = src[:, 0]
         ys = src[:, 1]
         xd = dst[:, 0]
@@ -1502,6 +1523,7 @@ def estimate(self, src, dst, order=2, weights=None):
         if weights is None:
             _, _, V = np.linalg.svd(A)
         else:
+            weights = np.asarray(weights)
             W = np.diag(np.tile(np.sqrt(weights / np.max(weights)), 2))
             _, _, V = np.linalg.svd(W @ A)
 
@@ -1518,7 +1540,7 @@ def __call__(self, coords):
 
         Parameters
         ----------
-        coords : (N, 2) array
+        coords : (N, 2) array_like
             source coordinates
 
         Returns
@@ -1527,6 +1549,7 @@ def __call__(self, coords):
             Transformed coordinates.
 
         """
+        coords = np.asarray(coords)
         x = coords[:, 0]
         y = coords[:, 1]
         u = len(self.params.ravel())
@@ -1576,7 +1599,7 @@ def estimate_transform(ttype, src, dst, *args, **kwargs):
     ttype : {'euclidean', similarity', 'affine', 'piecewise-affine', \
              'projective', 'polynomial'}
         Type of transform.
-    kwargs : array or int
+    kwargs : array_like or int
         Function parameters (src, dst, n, angle)::
 
             NAME / TTYPE        FUNCTION PARAMETERS
@@ -1642,9 +1665,9 @@ def matrix_transform(coords, matrix):
 
     Parameters
     ----------
-    coords : (N, 2) array
+    coords : (N, 2) array_like
         x, y coordinates to transform
-    matrix : (3, 3) array
+    matrix : (3, 3) array_like
         Homogeneous transformation matrix.
 
     Returns


Test patch:
diff --git a/skimage/transform/tests/test_geometric.py b/skimage/transform/tests/test_geometric.py
--- a/skimage/transform/tests/test_geometric.py
+++ b/skimage/transform/tests/test_geometric.py
@@ -6,16 +6,15 @@
 from numpy.testing import (assert_almost_equal, assert_array_almost_equal,
                            assert_equal)
 
-from skimage.transform._geometric import (_affine_matrix_from_vector,
+from skimage.transform import (AffineTransform, EssentialMatrixTransform,
+                               EuclideanTransform, FundamentalMatrixTransform,
+                               PiecewiseAffineTransform, PolynomialTransform,
+                               ProjectiveTransform, SimilarityTransform,
+                               estimate_transform, matrix_transform)
+from skimage.transform._geometric import (GeometricTransform,
+                                          _affine_matrix_from_vector,
                                           _center_and_normalize_points,
-                                          _euler_rotation_matrix,
-                                          GeometricTransform)
-from skimage.transform import (estimate_transform, matrix_transform,
-                               EuclideanTransform, SimilarityTransform,
-                               AffineTransform, FundamentalMatrixTransform,
-                               EssentialMatrixTransform, ProjectiveTransform,
-                               PolynomialTransform, PiecewiseAffineTransform)
-
+                                          _euler_rotation_matrix)
 
 SRC = np.array([
     [-12.3705, -10.5075],
@@ -258,7 +257,8 @@ def test_affine_init():
     assert_almost_equal(tform2.translation, translation)
 
     # scalar vs. tuple scale arguments
-    assert_almost_equal(AffineTransform(scale=0.5).scale, AffineTransform(scale=(0.5, 0.5)).scale)
+    assert_almost_equal(AffineTransform(scale=0.5).scale,
+                        AffineTransform(scale=(0.5, 0.5)).scale)
 
 
 def test_piecewise_affine():
@@ -298,11 +298,21 @@ def test_fundamental_matrix_residuals():
     assert_almost_equal(tform.residuals(src, dst)**2, [0, 0.5, 2])
 
 
-def test_fundamental_matrix_forward():
+@pytest.mark.parametrize('array_like_input', [False, True])
+def test_fundamental_matrix_forward(array_like_input):
+    if array_like_input:
+        rotation = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
+        translation = (1, 0, 0)
+    else:
+        rotation = np.eye(3)
+        translation = np.array([1, 0, 0])
     essential_matrix_tform = EssentialMatrixTransform(
-        rotation=np.eye(3), translation=np.array([1, 0, 0]))
-    tform = FundamentalMatrixTransform()
-    tform.params = essential_matrix_tform.params
+        rotation=rotation, translation=translation)
+    if array_like_input:
+        params = [list(p) for p in essential_matrix_tform.params]
+    else:
+        params = essential_matrix_tform.params
+    tform = FundamentalMatrixTransform(matrix=params)
     src = np.array([[0, 0], [0, 1], [1, 1]])
     assert_almost_equal(tform(src), [[0, -1, 0], [0, -1, 1], [0, -1, 1]])
 
@@ -407,10 +417,15 @@ def test_projective_weighted_estimation():
     assert_almost_equal(tform1.params, tform2.params, decimal=3)
 
 
-def test_projective_init():
+@pytest.mark.parametrize('array_like_input', [False, True])
+def test_projective_init(array_like_input):
     tform = estimate_transform('projective', SRC, DST)
     # init with transformation matrix
-    tform2 = ProjectiveTransform(tform.params)
+    if array_like_input:
+        params = [list(p) for p in tform.params]
+    else:
+        params = tform.params
+    tform2 = ProjectiveTransform(params)
     assert_almost_equal(tform2.params, tform.params)
 
 
@@ -448,10 +463,15 @@ def test_polynomial_weighted_estimation():
     assert_almost_equal(tform1.params, tform2.params, decimal=4)
 
 
-def test_polynomial_init():
+@pytest.mark.parametrize('array_like_input', [False, True])
+def test_polynomial_init(array_like_input):
     tform = estimate_transform('polynomial', SRC, DST, order=10)
     # init with transformation parameters
-    tform2 = PolynomialTransform(tform.params)
+    if array_like_input:
+        params = [list(p) for p in tform.params]
+    else:
+        params = tform.params
+    tform2 = PolynomialTransform(params)
     assert_almost_equal(tform2.params, tform.params)
 
 
@@ -526,6 +546,7 @@ def test_geometric_tform():
         # Ensure dst coords are finite numeric values
         assert(np.isfinite(dst).all())
 
+
 def test_invalid_input():
     with pytest.raises(ValueError):
         ProjectiveTransform(np.zeros((2, 3)))
@@ -630,7 +651,7 @@ def test_degenerate():
 
 def test_normalize_degenerate_points():
     """Return nan matrix *of appropriate size* when point is repeated."""
-    pts = np.array([[73.42834308, 94.2977623 ],] * 3)
+    pts = np.array([[73.42834308, 94.2977623]] * 3)
     mat, pts_tf = _center_and_normalize_points(pts)
     assert np.all(np.isnan(mat))
     assert np.all(np.isnan(pts_tf))
@@ -638,7 +659,6 @@ def test_normalize_degenerate_points():
     assert pts_tf.shape == pts.shape
 
 
-
 def test_projective_repr():
     tform = ProjectiveTransform()
     want = re.escape(textwrap.dedent(
@@ -683,22 +703,33 @@ def _assert_least_squares(tf, src, dst):
             assert new_ssq > baseline
 
 
-def test_estimate_affine_3d():
+@pytest.mark.parametrize('array_like_input', [False, True])
+def test_estimate_affine_3d(array_like_input):
     ndim = 3
     src = np.random.random((25, ndim)) * 2 ** np.arange(7, 7 + ndim)
-    matrix = np.array(
-        [[4.8, 0.1, 0.2, 25],
-         [0.0, 1.0, 0.1, 30],
-         [0.0, 0.0, 1.0, -2],
-         [0.0, 0.0, 0.0, 1.]]
-    )
+    matrix = np.array([
+        [4.8, 0.1, 0.2, 25],
+        [0.0, 1.0, 0.1, 30],
+        [0.0, 0.0, 1.0, -2],
+        [0.0, 0.0, 0.0, 1.]
+    ])
+
+    if array_like_input:
+        # list of lists for matrix and src coords
+        src = [list(c) for c in src]
+        matrix = [list(c) for c in matrix]
+
     tf = AffineTransform(matrix=matrix)
     dst = tf(src)
     dst_noisy = dst + np.random.random((25, ndim))
+    if array_like_input:
+        # list of lists for destination coords
+        dst = [list(c) for c in dst]
     tf2 = AffineTransform(dimensionality=ndim)
     assert tf2.estimate(src, dst_noisy)
     # we check rot/scale/etc more tightly than translation because translation
     # estimation is on the 1 pixel scale
+    matrix = np.asarray(matrix)
     assert_almost_equal(tf2.params[:, :-1], matrix[:, :-1], decimal=2)
     assert_almost_equal(tf2.params[:, -1], matrix[:, -1], decimal=0)
     _assert_least_squares(tf2, src, dst_noisy)