Source code for ndcube.wcs.wrappers.resampled_wcs
import numpy as np
from astropy.wcs.wcsapi.wrappers.base import BaseWCSWrapper
__all__ = ['ResampledLowLevelWCS']
[docs]
class ResampledLowLevelWCS(BaseWCSWrapper):
"""
A wrapper for a low-level WCS object that has down- or
up-sampled pixel axes.
Parameters
----------
wcs : `~astropy.wcs.wcsapi.BaseLowLevelWCS`
The original WCS for which to reorder axes
factor : `int` or `float` or iterable of the same
The factor by which to increase the pixel size for each pixel
axis. If a scalar, the same factor is used for all axes.
offset: `int` or `float` or iterable of the same
The shift of the lower edge of the 0th pixel (i.e. the pixel
coordinate -0.5) of the resampled grid relative to the lower
edge of the 0th pixel in the original underlying pixel grid,
in units of original pixel widths. (See the schematic in the
Notes section for a graphical example.) If a scalar, the grid
will be shifted by the same amount in all dimensions.
Notes
-----
Below is a schematic of how ResampledLowLevelWCS works. The asterisks show the
corners of pixels in a grid before resampling, while the dashes and
pipes show the edges of the resampled pixels. The resampling along the
x-axis has been performed using a factor of 2 and offset of 1, respectively,
while the resampling of the y-axis uses a factor of 3 and offset of 2.
The right column and upper row of numbers along the side and bottom of the
grids denote the edges and centres of the original pixel grid in the original
pixel coordinates. The left column and lower row gives the same locations in
the pixel coordinates of the resampled grid. Note that the resampled pixels
have an (x, y) shape of (2, 3) relative to the original pixel grid.
Also note, the left/lower edge of the 0th pixel in the resampled grid (i.e. pixel
coord -0.5) is shifted relative to the left/lower edge of the original 0th pixel,
and that shift is given by the offset (+1 in the x-axis and +2 along the y-axis),
which is in units of original pixel widths.
::
resampled original
factor=3
offset=2
0.5 4.5 *-----------*-----------*-----------*-----------*
| |
2/6 4 | |
| |
1/6 3.5 * * * * *
| |
0 3 | |
| |
-1/3 2.5 * * * * *
| |
-2/6 2 | |
| |
-0.5 1.5 *-----------*-----------*-----------*-----------*
| |
-4/6 1 | |
| |
-5/6 0.5 * * * * *
| |
-1 0 | |
| |
-1-1/6 -0.5 * * * * *
-0.5 0 0.5 1 1.5 2 2.5 3 3.5 original pixel indices
-1 -0.75 -0.5 -0.25 0 0.25 0.5 0.75 1 resampled pixel indices: factor=2, offset=1
"""
def __init__(self, wcs, factor, offset=0):
self._wcs = wcs
if np.isscalar(factor):
factor = [factor] * self.pixel_n_dim
self._factor = np.array(factor)
if len(self._factor) != self.pixel_n_dim:
raise ValueError(f"Length of factor must equal number of dimensions {self.pixel_n_dim}.")
if np.isscalar(offset):
offset = [offset] * self.pixel_n_dim
self._offset = np.array(offset)
if len(self._offset) != self.pixel_n_dim:
raise ValueError(f"Length of offset must equal number of dimensions {self.pixel_n_dim}.")
def _top_to_underlying_pixels(self, top_pixels):
# Convert user-facing pixel indices to the pixel grid of underlying WCS.
factor = self._pad_dims(self._factor, top_pixels.ndim)
offset = self._pad_dims(self._offset, top_pixels.ndim)
return (top_pixels + 0.5) * factor - 0.5 + offset
def _underlying_to_top_pixels(self, underlying_pixels):
# Convert pixel indices of underlying pixel grid to user-facing grid.
factor = self._pad_dims(self._factor, underlying_pixels.ndim)
offset = self._pad_dims(self._offset, underlying_pixels.ndim)
return (underlying_pixels + 0.5 - offset) / factor - 0.5
def _pad_dims(self, arr, ndim):
# Pad array with trailing degenerate dimensions.
# This make scaling with pixel arrays easier.
shape = np.ones(ndim, dtype=int)
shape[0] = len(arr)
return arr.reshape(tuple(shape))
[docs]
def pixel_to_world_values(self, *pixel_arrays):
underlying_pixel_arrays = self._top_to_underlying_pixels(np.asarray(pixel_arrays))
return self._wcs.pixel_to_world_values(*underlying_pixel_arrays)
[docs]
def world_to_pixel_values(self, *world_arrays):
underlying_pixel_arrays = self._wcs.world_to_pixel_values(*world_arrays)
top_pixel_arrays = self._underlying_to_top_pixels(np.asarray(underlying_pixel_arrays))
return tuple(array for array in top_pixel_arrays)
@property
def pixel_shape(self):
# Return pixel shape of resampled grid.
# Where shape is an integer, return an int type as its required for some uses.
if self._wcs.pixel_shape is None:
return self._wcs.pixel_shape
underlying_shape = np.asarray(self._wcs.pixel_shape)
int_elements = np.isclose(np.mod(underlying_shape, self._factor), 0,
atol=np.finfo(float).resolution)
pixel_shape = underlying_shape / self._factor
return tuple(int(np.rint(i)) if is_int else i
for i, is_int in zip(pixel_shape, int_elements))
@property
def pixel_bounds(self):
if self._wcs.pixel_bounds is None:
return self._wcs.pixel_bounds
top_level_bounds = self._underlying_to_top_pixels(np.asarray(self._wcs.pixel_bounds))
return [tuple(bounds) for bounds in top_level_bounds]