Skip to content

Commit

Permalink
Add option to disable roundtrip-checking of transforms
Browse files Browse the repository at this point in the history
  • Loading branch information
svank committed Mar 29, 2022
1 parent ddc2902 commit 5dc745f
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 65 deletions.
13 changes: 13 additions & 0 deletions docs/celestial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,19 @@ also be specified, and be given the shape of the output image using the Numpy
:class:`~astropy.io.fits.Header`, does not contain information about image
size).

For the interpolation and adaptive algorithms, an optional third argument,
``roundtrip_coords`` is accepted. By default, after coordinates are transformed
from the output plane to the input plane, the input-plane coordinates are
transformed back to the output plane to ensure that the transformation is
defined in both directions. This doubles the amount of
coordinate-transformation work to be done. In speed-critical situations, where
it is known that the coordinate transformation is defined everywhere, this
extra work can be disabled by setting ``roundtrip_coords=False``. (Note that
this is not a question of whether each output pixel maps to an existing *pixel*
in the input image and vice-versa, but whether it maps to a valid *coordinate*
in the coordinate system of the input image---regardless of whether that
coordinate falls within the bounds of the input image.)

As an example, we start off by opening a FITS file using Astropy::

>>> from astropy.io import fits
Expand Down
21 changes: 16 additions & 5 deletions reproject/adaptive/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,35 @@
import numpy as np

from .deforest import map_coordinates
from ..wcs_utils import efficient_pixel_to_pixel_with_roundtrip
from ..wcs_utils import (efficient_pixel_to_pixel_with_roundtrip,
efficient_pixel_to_pixel)


__all__ = ['_reproject_adaptive_2d']


class CoordinateTransformer:

def __init__(self, wcs_in, wcs_out):
def __init__(self, wcs_in, wcs_out, roundtrip_coords):
self.wcs_in = wcs_in
self.wcs_out = wcs_out
self.roundtrip_coords = roundtrip_coords

def __call__(self, pixel_out):
pixel_out = pixel_out[:, :, 0], pixel_out[:, :, 1]
pixel_in = efficient_pixel_to_pixel_with_roundtrip(self.wcs_out, self.wcs_in, *pixel_out)
if self.roundtrip_coords:
pixel_in = efficient_pixel_to_pixel_with_roundtrip(
self.wcs_out, self.wcs_in, *pixel_out)
else:
pixel_in = efficient_pixel_to_pixel(
self.wcs_out, self.wcs_in, *pixel_out)
pixel_in = np.array(pixel_in).transpose().swapaxes(0, 1)
return pixel_in


def _reproject_adaptive_2d(array, wcs_in, wcs_out, shape_out, order=1,
return_footprint=True, center_jacobian=False):
return_footprint=True, center_jacobian=False,
roundtrip_coords=True):
"""
Reproject celestial slices from an n-d array from one WCS to another
using the DeForest (2004) algorithm [1]_, and assuming all other dimensions
Expand All @@ -47,6 +55,9 @@ def _reproject_adaptive_2d(array, wcs_in, wcs_out, shape_out, order=1,
Whether to return the footprint in addition to the output array.
center_jacobian : bool
Whether to compute centered Jacobians
roundtrip_coords : bool
Whether to veryfiy that coordinate transformations are defined in both
directions.
Returns
-------
Expand Down Expand Up @@ -83,7 +94,7 @@ def _reproject_adaptive_2d(array, wcs_in, wcs_out, shape_out, order=1,
# Create output array
array_out = np.zeros(shape_out)

transformer = CoordinateTransformer(wcs_in, wcs_out)
transformer = CoordinateTransformer(wcs_in, wcs_out, roundtrip_coords)
map_coordinates(array_in, array_out, transformer, out_of_range_nan=True,
order=order, center_jacobian=center_jacobian)

Expand Down
8 changes: 6 additions & 2 deletions reproject/adaptive/high_level.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

def reproject_adaptive(input_data, output_projection, shape_out=None, hdu_in=0,
order='bilinear', return_footprint=True,
center_jacobian=False):
center_jacobian=False, roundtrip_coords=True):
"""
Reproject celestial slices from an 2d array from one WCS to another using
the DeForest (2004) adaptive resampling algorithm.
Expand Down Expand Up @@ -68,6 +68,9 @@ def reproject_adaptive(input_data, output_projection, shape_out=None, hdu_in=0,
averaged to produce values at the pixel grid points. This is more
efficient, and the loss of accuracy is extremely small for
transformations that vary smoothly between pixels. Defaults to False.
roundtrip_coords : bool
Whether to verify that coordinate transformations are defined in both
directions.
Returns
-------
Expand All @@ -89,4 +92,5 @@ def reproject_adaptive(input_data, output_projection, shape_out=None, hdu_in=0,

return _reproject_adaptive_2d(array_in, wcs_in, wcs_out, shape_out,
order=order, return_footprint=return_footprint,
center_jacobian=center_jacobian)
center_jacobian=center_jacobian,
roundtrip_coords=roundtrip_coords)
27 changes: 18 additions & 9 deletions reproject/adaptive/tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ def as_high_level_wcs(wcs):
@pytest.mark.array_compare(single_reference=True)
@pytest.mark.parametrize('wcsapi', (False, True))
@pytest.mark.parametrize('center_jacobian', (False, True))
def test_reproject_adaptive_2d(wcsapi, center_jacobian):
@pytest.mark.parametrize('roundtrip_coords', (False, True))
def test_reproject_adaptive_2d(wcsapi, center_jacobian, roundtrip_coords):

# Set up initial array with pattern
data_in = np.zeros((256, 256))
Expand All @@ -48,7 +49,8 @@ def test_reproject_adaptive_2d(wcsapi, center_jacobian):

array_out, footprint_out = reproject_adaptive(
(data_in, wcs_in), wcs_out, shape_out=(60, 60),
center_jacobian=center_jacobian)
center_jacobian=center_jacobian,
roundtrip_coords=roundtrip_coords)

# Check that surface brightness is conserved in the unrotated case
assert_allclose(np.nansum(data_in), np.nansum(array_out) * (256 / 60) ** 2, rtol=0.1)
Expand All @@ -64,7 +66,8 @@ def test_reproject_adaptive_2d(wcsapi, center_jacobian):

@pytest.mark.array_compare(single_reference=True)
@pytest.mark.parametrize('center_jacobian', (False, True))
def test_reproject_adaptive_2d_rotated(center_jacobian):
@pytest.mark.parametrize('roundtrip_coords', (False, True))
def test_reproject_adaptive_2d_rotated(center_jacobian, roundtrip_coords):

# Set up initial array with pattern
data_in = np.zeros((256, 256))
Expand All @@ -87,7 +90,8 @@ def test_reproject_adaptive_2d_rotated(center_jacobian):

array_out, footprint_out = reproject_adaptive(
(data_in, wcs_in), wcs_out, shape_out=(60, 60),
center_jacobian=center_jacobian)
center_jacobian=center_jacobian,
roundtrip_coords=roundtrip_coords)

# ASTROPY_LT_40: astropy v4.0 introduced new default header keywords,
# once we support only astropy 4.0 and later we can update the reference
Expand All @@ -98,7 +102,8 @@ def test_reproject_adaptive_2d_rotated(center_jacobian):
return array_footprint_to_hdulist(array_out, footprint_out, header_out)


def test_reproject_adaptive_high_aliasing_potential():
@pytest.mark.parametrize('roundtrip_coords', (False, True))
def test_reproject_adaptive_high_aliasing_potential(roundtrip_coords):
# Generate sample data with vertical stripes alternating with every column
data_in = np.arange(40*40).reshape((40, 40))
data_in = (data_in) % 2
Expand All @@ -118,7 +123,8 @@ def test_reproject_adaptive_high_aliasing_potential():

array_out = reproject_adaptive((data_in, wcs_in),
wcs_out, shape_out=(11, 6),
return_footprint=False)
return_footprint=False,
roundtrip_coords=roundtrip_coords)

# The CDELT1 value in wcs_out produces a down-sampling by a factor of two
# along the output x axis. With the input image containing vertical lines
Expand All @@ -138,7 +144,8 @@ def test_reproject_adaptive_high_aliasing_potential():
[np.sin(angle), np.cos(angle)]]
array_out = reproject_adaptive((data_in, wcs_in),
wcs_out, shape_out=(11, 6),
return_footprint=False)
return_footprint=False,
roundtrip_coords=roundtrip_coords)
np.testing.assert_allclose(array_out, 0.5)

# But if we add a 90-degree rotation to the input coordinates, then when
Expand All @@ -151,7 +158,8 @@ def test_reproject_adaptive_high_aliasing_potential():
[np.sin(angle), np.cos(angle)]]
array_out = reproject_adaptive((data_in, wcs_in),
wcs_out, shape_out=(11, 6),
return_footprint=False)
return_footprint=False,
roundtrip_coords=roundtrip_coords)

# Generate the expected pattern of alternating stripes
data_ref = np.arange(array_out.shape[1]) % 2
Expand All @@ -162,7 +170,8 @@ def test_reproject_adaptive_high_aliasing_potential():
wcs_out.wcs.pc = [[1, 0], [0, 1]]
array_out = reproject_adaptive((data_in, wcs_in),
wcs_out, shape_out=(11, 6),
return_footprint=False)
return_footprint=False,
roundtrip_coords=roundtrip_coords)
data_ref = np.arange(array_out.shape[0]) % 2
data_ref = np.vstack([data_ref] * array_out.shape[1]).T
np.testing.assert_allclose(array_out, data_ref)
Expand Down
12 changes: 9 additions & 3 deletions reproject/interpolation/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
from astropy.wcs import WCS

from ..array_utils import map_coordinates
from ..wcs_utils import efficient_pixel_to_pixel_with_roundtrip, has_celestial
from ..wcs_utils import (efficient_pixel_to_pixel_with_roundtrip,
efficient_pixel_to_pixel, has_celestial)


def _validate_wcs(wcs_in, wcs_out, shape_out):
Expand Down Expand Up @@ -56,7 +57,7 @@ def _validate_array_out(array_out, array, shape_out):


def _reproject_full(array, wcs_in, wcs_out, shape_out, order=1, array_out=None,
return_footprint=True):
return_footprint=True, roundtrip_coords=True):
"""
Reproject n-dimensional data to a new projection using interpolation.
Expand All @@ -80,7 +81,12 @@ def _reproject_full(array, wcs_in, wcs_out, shape_out, order=1, array_out=None,
indexing='ij', sparse=False, copy=False)
pixel_out = [p.ravel() for p in pixel_out]
# For each pixel in the ouput array, get the pixel value in the input WCS
pixel_in = efficient_pixel_to_pixel_with_roundtrip(wcs_out, wcs_in, *pixel_out[::-1])[::-1]
if roundtrip_coords:
pixel_in = efficient_pixel_to_pixel_with_roundtrip(
wcs_out, wcs_in, *pixel_out[::-1])[::-1]
else:
pixel_in = efficient_pixel_to_pixel(
wcs_out, wcs_in, *pixel_out[::-1])[::-1]
pixel_in = np.array(pixel_in)

if array_out is not None:
Expand Down
11 changes: 8 additions & 3 deletions reproject/interpolation/high_level.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
@deprecated_renamed_argument('independent_celestial_slices', None, since='0.6')
def reproject_interp(input_data, output_projection, shape_out=None, hdu_in=0,
order='bilinear', independent_celestial_slices=False,
output_array=None, return_footprint=True):
output_array=None, return_footprint=True,
roundtrip_coords=True):
"""
Reproject data to a new projection using interpolation (this is typically
the fastest way to reproject an image).
Expand Down Expand Up @@ -68,6 +69,9 @@ def reproject_interp(input_data, output_projection, shape_out=None, hdu_in=0,
extremely large files.
return_footprint : bool
Whether to return the footprint in addition to the output array.
roundtrip_coords : bool
Whether to verify that coordinate transformations are defined in both
directions.
Returns
-------
Expand All @@ -86,5 +90,6 @@ def reproject_interp(input_data, output_projection, shape_out=None, hdu_in=0,
if isinstance(order, str):
order = ORDER[order]

return _reproject_full(array_in, wcs_in, wcs_out, shape_out=shape_out, order=order,
array_out=output_array, return_footprint=return_footprint)
return _reproject_full(array_in, wcs_in, wcs_out, shape_out=shape_out,
order=order, array_out=output_array,
return_footprint=return_footprint, roundtrip_coords=roundtrip_coords)
Loading

0 comments on commit 5dc745f

Please sign in to comment.