Repository: scipy/scipy

Issue:
scipy.ndimage.interpolation.affine_transform lacks documentation about the exact transform applied
The documentation only says `matrix` and `offset` determine the mapping from input to output, but it is not clear how offset is used. I posted an attempt in #5692, but I later realised that it is actually wrong. Maybe someone who actually knows these functions could step in, as trying to guess from behaviour as I did obviously is error-prone.
Particularly, there should also be a note indicating if integer indices refer to the centre or the edges of the pixels.

scipy.ndimage.interpolation.affine_transform lacks documentation about the exact transform applied
The documentation only says `matrix` and `offset` determine the mapping from input to output, but it is not clear how offset is used. I posted an attempt in #5692, but I later realised that it is actually wrong. Maybe someone who actually knows these functions could step in, as trying to guess from behaviour as I did obviously is error-prone.
Particularly, there should also be a note indicating if integer indices refer to the centre or the edges of the pixels.



Discussion:

Resolving patch:
diff --git a/scipy/ndimage/interpolation.py b/scipy/ndimage/interpolation.py
--- a/scipy/ndimage/interpolation.py
+++ b/scipy/ndimage/interpolation.py
@@ -339,6 +339,25 @@ def affine_transform(input, matrix, offset=0.0, output_shape=None,
     outside the boundaries of the input are filled according to the given
     mode.
 
+    If a two-dimensional array is supplied to the matrix parameter,
+    given an output image pixel index vector ``o``, the pixel value
+    is determined from the input image at position ``np.dot(matrix,o) + offset``.
+
+    A diagonal matrix can be specified by supplying a one-dimensional
+    array-like to the matrix parameter, in which case a more efficient
+    algorithm is applied.
+
+    .. warning::
+        Currently, the exact interpretation of the affine transformation
+        depends on whether the matrix is supplied as a one-dimensional or
+        two-dimensional array. This is considered a bug, and starting from
+        scipy 0.18.0, the behaviour for one-dimensional matrix arguments will
+        be modified to match the two-dimensional case.
+
+        Until then, if a one-dimensional array is supplied to the matrix
+        parameter, the output pixel value at index ``o`` is determined from
+        the input image at position ``matrix * (o + offset)``.
+
     Parameters
     ----------
     input : ndarray
@@ -412,6 +431,11 @@ def affine_transform(input, matrix, offset=0.0, output_shape=None,
     if not offset.flags.contiguous:
         offset = offset.copy()
     if matrix.ndim == 1:
+        warnings.warn(
+            "The behaviour of affine_transform with a one-dimensional "
+            "array supplied for the matrix parameter will change in "
+            "scipy 0.18.0."
+        )
         _nd_image.zoom_shift(filtered, matrix, offset, output, order,
                              mode, cval)
     else:


Test patch:
diff --git a/scipy/ndimage/tests/test_ndimage.py b/scipy/ndimage/tests/test_ndimage.py
--- a/scipy/ndimage/tests/test_ndimage.py
+++ b/scipy/ndimage/tests/test_ndimage.py
@@ -1980,6 +1980,44 @@ def test_affine_transform21(self):
                                                      (2,), order=order)
             assert_array_almost_equal(out, [1, 9])
 
+    def test_affine_transform22(self):
+        # shift and offset interaction; see issue #1547
+        data = numpy.array([4, 1, 3, 2])
+        for order in range(0, 6):
+            out = ndimage.affine_transform(data, [[2]], [-1],
+                                           (3,),order=order)
+            assert_array_almost_equal(out, [0, 1, 2])
+
+    def test_affine_transform23(self):
+        # shift and offset interaction; see issue #1547
+        data = numpy.array([4, 1, 3, 2])
+        for order in range(0, 6):
+            out = ndimage.affine_transform(data, [[0.5]], [-1],
+                                           (8,),order=order)
+            assert_array_almost_equal(out[::2], [0, 4, 1, 3])
+
+    @dec.knownfailureif(True,"Backwards-incomatible change will be fixed in 0.18.0")  # issue #1547;
+    def test_affine_transform24(self):
+        # consistency between diagonal and non-diagonal case; see issue #1547
+        data = numpy.array([4, 1, 3, 2])
+        for order in range(0, 6):
+            out1 = ndimage.affine_transform(data, [2], -1,
+                                                order=order)
+            out2 = ndimage.affine_transform(data, [[2]], -1,
+                                                order=order)
+            assert_array_almost_equal(out1, out2)
+
+    @dec.knownfailureif(True,"Backwards-incomatible change will be fixed in 0.18.0")  # issue #1547;
+    def test_affine_transform25(self):
+        # consistency between diagonal and non-diagonal case; see issue #1547
+        data = numpy.array([4, 1, 3, 2])
+        for order in range(0, 6):
+            out1 = ndimage.affine_transform(data, [0.5], -1,
+                                                order=order)
+            out2 = ndimage.affine_transform(data, [[0.5]], -1,
+                                                order=order)
+            assert_array_almost_equal(out1, out2)
+
     def test_shift01(self):
         data = numpy.array([1])
         for order in range(0, 6):