Repository: scipy/scipy

Issue:
Inconsistent use of offset in ndimage.interpolation.affine_transform() (Trac #1020)
_Original ticket http://projects.scipy.org/scipy/ticket/1020 on 2009-10-14 by trac user jvrantwijk, assigned to unknown._

I am confused about the meaning of the "offset" parameter in scipy.ndimage.interpolation.affine_transform(). It seems to apply the offset before applying the scaling matrix, but only in case the scaling matrix is specified as a 1-D array.

The following example makes this more clear.
My test array looks like this:

```
>>> w
array([[ 0.,  1.,  2.,  3.],
       [ 4.,  5.,  6.,  7.]])
```

On this array, I run an affine transform scaling the second index by 0.5. If I do this without offset, it works just like I would expect:

```
>>> from scipy.ndimage.interpolation import affine_transform
>>> affine_transform(w, matrix=(1,0.5), offset=(0,0), order=1)
array([[ 0. ,  0.5,  1. ,  1.5],
       [ 4. ,  4.5,  5. ,  5.5]])
```

In the next test I add an offset of 1 to the second index. I would expect this offset to apply in the index space of w[], which means that I expect the number 1 in the top left corner of the result.
However:

```
>>> affine_transform(w, matrix=(1,0.5), offset=(0,1), order=1)
array([[ 0.5,  1. ,  1.5,  2. ],
       [ 4.5,  5. ,  5.5,  6. ]])
```

Ok, so apparently it applied the offset to the index before scaling the index space. Fine. It seems counterintuitive, but fine.

However, now I try the same thing but specifying the full 2x2 matrix instead of just the diagonal entries:

```
>>> affine_transform(w, matrix=[[1,0],[0,0.5]], offset=(0,1), order=1)
array([[ 1. ,  1.5,  2. ,  2.5],
       [ 5. ,  5.5,  6. ,  6.5]])
```

And suddenly now the offset DOES work like I expected.

This is inconsistent. I think affine_transform() is simply wrong in the diagonal-matrix case.

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,20 @@ def affine_transform(input, matrix, offset=0.0, output_shape=None,
     outside the boundaries of the input are filled according to the given
     mode.
 
+    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.
+
+    .. versionchanged:: 0.18.0
+        Previously, the exact interpretation of the affine transformation
+        depended on whether the matrix was supplied as a one-dimensional or
+        two-dimensional array. If a one-dimensional array was supplied
+        to the matrix parameter, the output pixel value at index ``o``
+        was determined from the input image at position ``matrix * (o + offset)``.
+
     Parameters
     ----------
     input : ndarray
@@ -412,7 +426,12 @@ def affine_transform(input, matrix, offset=0.0, output_shape=None,
     if not offset.flags.contiguous:
         offset = offset.copy()
     if matrix.ndim == 1:
-        _nd_image.zoom_shift(filtered, matrix, offset, output, order,
+        warnings.warn(
+            "The behaviour of affine_transform with a one-dimensional "
+            "array supplied for the matrix parameter has changed in "
+            "scipy 0.18.0."
+        )
+        _nd_image.zoom_shift(filtered, matrix, offset/matrix, output, order,
                              mode, cval)
     else:
         _geometric_transform(filtered, None, None, matrix, offset,


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
@@ -33,6 +33,7 @@
 import warnings
 import math
 import sys
+from unittest import expectedFailure
 
 import numpy
 from numpy import fft
@@ -1980,6 +1981,42 @@ 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])
+
+    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)
+
+    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):