Repository: scipy/scipy

Issue:
ndimage.geometric_transform endianness-linked bug when affine is 1d
Results are byteswapped when `out` is passed into `ndimage.affine_transform` and `out` has a non-native endianness.

This is a similar issue as https://github.com/scipy/scipy/issues/4127 when the affine transformation is given as a 1d array (in #4127, which was fixed in 0.15, the affine transformation was given as a 2d array).

### Reproducing code example
```py
import numpy as np
from scipy import ndimage

data = np.ones((2, 2))
out = np.empty_like(data, dtype='>f8')
ndimage.affine_transform(data, np.array([1., 1.]), output=out, order=0)
print(out)
```

### Expected behaviour
The affine I am passing is the identity so I expect to have `(out == data).all()`, i.e. `out` should contain only ones.

### Actual behaviour
`out` is:
```
[[  3.03865194e-319   3.03865194e-319]
 [  3.03865194e-319   3.03865194e-319]]
```

### Other comments
The fix should be very similar to https://github.com/scipy/scipy/pull/4150, i.e. you need to call `.byteswap` when `output.dtype.isnative` is `False`. I am guessing the code path for the 1d affine is slightly different than the one for the 2d affine, hence it was missed in the original fix. This bug has been present in scipy for a while (I tried as far back as 0.14).


Discussion:
I would like to have a go at fixing this.
@cdouglass that would be great, go for it!
As Loïc suspected, for 1D transformation matrices it is `zoom_shift`, not `geometric_transform`, that gets called, see here:

https://github.com/scipy/scipy/blob/master/scipy/ndimage/interpolation.py#L490
Thanks @jaimefrio! I have a test + fix locally and will make a PR once I've read the Contributing guide properly and made sure I'm doing it right.

Resolving patch:
diff --git a/scipy/ndimage/interpolation.py b/scipy/ndimage/interpolation.py
--- a/scipy/ndimage/interpolation.py
+++ b/scipy/ndimage/interpolation.py
@@ -34,6 +34,7 @@
 import numpy
 from . import _ni_support
 from . import _nd_image
+from functools import wraps
 
 import warnings
 
@@ -46,6 +47,24 @@ def _extend_mode_to_code(mode):
     return mode
 
 
+def _fix_endianness(f):
+    """
+    Decorator to work around endianness issues in _nd_image.geometric_transform
+    and _nd_image.zoom_shift
+    """
+    @wraps(f)
+    def wrapper(*args, **kwargs):
+        output = kwargs.get("output")
+        result = f(*args, **kwargs)
+        if isinstance(output, numpy.dtype) and not output.isnative:
+            result.byteswap(True)
+        elif isinstance(output, numpy.ndarray) and not output.dtype.isnative:
+            output.byteswap(True)
+        return result
+
+    return wrapper
+
+
 def spline_filter1d(input, order=3, axis=-1, output=numpy.float64):
     """
     Calculates a one-dimensional spline filter along the given axis.
@@ -121,22 +140,7 @@ def spline_filter(input, order=3, output=numpy.float64):
     return return_value
 
 
-def _geometric_transform(input, mapping, coordinates, matrix, offset, output,
-                         order, mode, cval, extra_arguments, extra_keywords):
-    """
-    Wrapper around _nd_image.geometric_transform to work around
-    endianness issues
-    """
-    _nd_image.geometric_transform(
-        input, mapping, coordinates, matrix, offset, output,
-        order, mode, cval, extra_arguments, extra_keywords)
-
-    if output is not None and not output.dtype.isnative:
-        output.byteswap(True)
-
-    return output
-
-
+@_fix_endianness
 def geometric_transform(input, mapping, output_shape=None,
                         output=None, order=3,
                         mode='constant', cval=0.0, prefilter=True,
@@ -254,11 +258,13 @@ def geometric_transform(input, mapping, output_shape=None,
         filtered = input
     output, return_value = _ni_support._get_output(output, input,
                                                    shape=output_shape)
-    _geometric_transform(filtered, mapping, None, None, None, output,
-                         order, mode, cval, extra_arguments, extra_keywords)
+    _nd_image.geometric_transform(filtered, mapping, None, None, None, output,
+                                  order, mode, cval, extra_arguments,
+                                  extra_keywords)
     return return_value
 
 
+@_fix_endianness
 def map_coordinates(input, coordinates, output=None, order=3,
                     mode='constant', cval=0.0, prefilter=True):
     """
@@ -353,11 +359,12 @@ def map_coordinates(input, coordinates, output=None, order=3,
         filtered = input
     output, return_value = _ni_support._get_output(output, input,
                                                    shape=output_shape)
-    _geometric_transform(filtered, None, coordinates, None, None,
-                         output, order, mode, cval, None, None)
+    _nd_image.geometric_transform(filtered, None, coordinates, None, None,
+                                  output, order, mode, cval, None, None)
     return return_value
 
 
+@_fix_endianness
 def affine_transform(input, matrix, offset=0.0, output_shape=None,
                      output=None, order=3,
                      mode='constant', cval=0.0, prefilter=True):
@@ -496,8 +503,8 @@ def affine_transform(input, matrix, offset=0.0, output_shape=None,
         _nd_image.zoom_shift(filtered, matrix, offset/matrix, output, order,
                              mode, cval)
     else:
-        _geometric_transform(filtered, None, None, matrix, offset,
-                             output, order, mode, cval, None, None)
+        _nd_image.geometric_transform(filtered, None, None, matrix, offset,
+                                      output, order, mode, cval, None, None)
     return return_value
 
 


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
@@ -1422,24 +1422,6 @@ def mapping(x):
                                                         order=order)
             assert_array_almost_equal(out, [1])
 
-    def test_geometric_transform01_with_output_parameter(self):
-        data = numpy.array([1])
-
-        def mapping(x):
-            return x
-        for order in range(0, 6):
-            out = numpy.empty_like(data)
-            ndimage.geometric_transform(data, mapping,
-                                        data.shape,
-                                        output=out)
-            assert_array_almost_equal(out, [1])
-
-            out = numpy.empty_like(data).astype(data.dtype.newbyteorder())
-            ndimage.geometric_transform(data, mapping,
-                                        data.shape,
-                                        output=out)
-            assert_array_almost_equal(out, [1])
-
     def test_geometric_transform02(self):
         data = numpy.ones([4])
 
@@ -1690,6 +1672,23 @@ def mapping(x, a, b):
                                 extra_keywords={'b': 2})
             assert_array_almost_equal(out, [5, 7])
 
+    def test_geometric_transform_endianness_with_output_parameter(self):
+        # geometric transform given output ndarray or dtype with non-native endianness
+        # see issue #4127
+        data = numpy.array([1])
+
+        def mapping(x):
+            return x
+
+        for out in [data.dtype, data.dtype.newbyteorder(),
+                    numpy.empty_like(data),
+                    numpy.empty_like(data).astype(data.dtype.newbyteorder())]:
+            returned = ndimage.geometric_transform(data, mapping,
+                                        data.shape,
+                                        output=out)
+            result = out if returned is None else returned
+            assert_array_almost_equal(result, [1])
+
     def test_map_coordinates01(self):
         data = numpy.array([[4, 1, 3, 2],
                                [7, 6, 8, 5],
@@ -1702,25 +1701,6 @@ def test_map_coordinates01(self):
                                        [0, 4, 1, 3],
                                        [0, 7, 6, 8]])
 
-    def test_map_coordinates01_with_output_parameter(self):
-        data = numpy.array([[4, 1, 3, 2],
-                            [7, 6, 8, 5],
-                            [3, 5, 3, 6]])
-        idx = numpy.indices(data.shape)
-        idx -= 1
-        expected = numpy.array([[0, 0, 0, 0],
-                                [0, 4, 1, 3],
-                                [0, 7, 6, 8]])
-        for order in range(0, 6):
-            out = numpy.empty_like(expected)
-            ndimage.map_coordinates(data, idx, order=order, output=out)
-            assert_array_almost_equal(out, expected)
-
-            out = numpy.empty_like(expected).astype(
-                expected.dtype.newbyteorder())
-            ndimage.map_coordinates(data, idx, order=order, output=out)
-            assert_array_almost_equal(out, expected)
-
     def test_map_coordinates02(self):
         data = numpy.array([[4, 1, 3, 2],
                                [7, 6, 8, 5],
@@ -1753,6 +1733,19 @@ def test_map_coordinates03(self):
         assert_array_almost_equal(out, [[0, 0], [0, 4], [0, 7]])
         assert_array_almost_equal(out, ndimage.shift(data[:,::2], (1, 1)))
 
+    def test_map_coordinates_endianness_with_output_parameter(self):
+        # output parameter given as array or dtype with either endianness
+        # see issue #4127
+        data = numpy.array([[1, 2], [7, 6]])
+        expected = numpy.array([[0, 0], [0, 1]])
+        idx = numpy.indices(data.shape)
+        idx -= 1
+        for out in [data.dtype, data.dtype.newbyteorder(), numpy.empty_like(expected),
+                    numpy.empty_like(expected).astype(expected.dtype.newbyteorder())]:
+            returned = ndimage.map_coordinates(data, idx, output=out)
+            result = out if returned is None else returned
+            assert_array_almost_equal(result, expected)
+
     # do not run on 32 bit or windows (no sparse memory)
     @dec.skipif('win32' in sys.platform or numpy.intp(0).itemsize < 8)
     def test_map_coordinates_large_data(self):
@@ -1773,21 +1766,6 @@ def test_affine_transform01(self):
                                                      order=order)
             assert_array_almost_equal(out, [1])
 
-    def test_affine_transform01_with_output_parameter(self):
-        data = numpy.array([1])
-        for order in range(0, 6):
-            out = numpy.empty_like(data)
-            ndimage.affine_transform(data, [[1]],
-                                     order=order,
-                                     output=out)
-            assert_array_almost_equal(out, [1])
-
-            out = numpy.empty_like(data).astype(data.dtype.newbyteorder())
-            ndimage.affine_transform(data, [[1]],
-                                     order=order,
-                                     output=out)
-            assert_array_almost_equal(out, [1])
-
     def test_affine_transform02(self):
         data = numpy.ones([4])
         for order in range(0, 6):
@@ -2065,6 +2043,30 @@ def test_affine_transform27(self):
         numpy.testing.assert_raises(ValueError,
                                     ndimage.affine_transform, data, tform_h2)
 
+    def test_affine_transform_1d_endianness_with_output_parameter(self):
+        # 1d affine transform given output ndarray or dtype with either endianness
+        # see issue #7388
+        data = numpy.ones((2, 2))
+        for out in [numpy.empty_like(data),
+                    numpy.empty_like(data).astype(data.dtype.newbyteorder()),
+                    data.dtype, data.dtype.newbyteorder()]:
+            with warnings.catch_warnings():
+                warnings.simplefilter("ignore", UserWarning)
+                returned = ndimage.affine_transform(data, [1, 1], output=out)
+            result = out if returned is None else returned
+            assert_array_almost_equal(result, [[1, 1], [1, 1]])
+
+    def test_affine_transform_multi_d_endianness_with_output_parameter(self):
+        # affine transform given output ndarray or dtype with either endianness
+        # see issue #4127
+        data = numpy.array([1])
+        for out in [data.dtype, data.dtype.newbyteorder(),
+                    numpy.empty_like(data),
+                    numpy.empty_like(data).astype(data.dtype.newbyteorder())]:
+            returned = ndimage.affine_transform(data, [[1]], output=out)
+            result = out if returned is None else returned
+            assert_array_almost_equal(result, [1])
+
     def test_shift01(self):
         data = numpy.array([1])
         for order in range(0, 6):