Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion scopesim/effects/spectral_trace_list_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1021,16 +1021,57 @@ def _xiy2xlam_fit(layout, params):
return xiy2x, xiy2lam


class BilinearImageInterpolation:
"""
Bilinear interpolation of an image on its integer pixel grid.

Drop-in replacement for
``RectBivariateSpline(arange(ny), arange(nx), image, kx=1, ky=1)``:
called with (j, i) pixel coordinates and ``grid=False``, it returns the
same values (coordinates outside the image are clamped to the edges),
but avoids FITPACK's slow scattered-point evaluation.
"""

def __init__(self, image):
self.image = np.asarray(image)

def __call__(self, jarr, iarr, grid=False):
if grid:
raise NotImplementedError(
"BilinearImageInterpolation only supports grid=False")
n_j, n_i = self.image.shape
jarr = np.clip(np.asarray(jarr, dtype=float), 0, n_j - 1)
iarr = np.clip(np.asarray(iarr, dtype=float), 0, n_i - 1)
j_0 = np.clip(jarr.astype(int), 0, n_j - 2)
i_0 = np.clip(iarr.astype(int), 0, n_i - 2)
w_j = jarr - j_0
w_i = iarr - i_0
img = self.image
return ((1 - w_j) * (1 - w_i) * img[j_0, i_0]
+ (1 - w_j) * w_i * img[j_0, i_0 + 1]
+ w_j * (1 - w_i) * img[j_0 + 1, i_0]
+ w_j * w_i * img[j_0 + 1, i_0 + 1])


def make_image_interpolations(hdulist, **kwargs):
"""Create 2D interpolation functions for images.

The interpolation functions are called with (j, i) pixel coordinates,
i.e. the first argument corresponds to the row (NAXIS2) axis of the
image, the second to the column (NAXIS1) axis.

For ``kx=1, ky=1`` (the case used for rectification) a direct bilinear
interpolator is returned, which evaluates large scattered coordinate
arrays orders of magnitude faster than FITPACK. Other spline degrees
fall back to ``RectBivariateSpline``.
"""
interps = []
for hdu in hdulist:
if isinstance(hdu, fits.ImageHDU):
if not isinstance(hdu, fits.ImageHDU):
continue
if kwargs.get("kx") == 1 and kwargs.get("ky") == 1:
interps.append(BilinearImageInterpolation(hdu.data))
else:
interps.append(
RectBivariateSpline(np.arange(hdu.header["NAXIS2"]),
np.arange(hdu.header["NAXIS1"]),
Expand Down
25 changes: 25 additions & 0 deletions scopesim/tests/tests_effects/test_SpectralTraceListUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,31 @@ def test_interpolation_is_accurate(self):
imginterp = interps[0](yy, xx, grid=False)
assert np.allclose(imginterp, img)

def test_bilinear_matches_spline_evaluation(self):
"""The kx=ky=1 fast path must reproduce RectBivariateSpline."""
from scipy.interpolate import RectBivariateSpline

rng = np.random.default_rng(3)
img = rng.random((50, 80))
hdul = fits.HDUList([fits.PrimaryHDU(), fits.ImageHDU(data=img)])
fast = make_image_interpolations(hdul, kx=1, ky=1)[0]
spline = RectBivariateSpline(np.arange(50), np.arange(80), img,
kx=1, ky=1)

# scattered points, deliberately extending beyond the image
jarr = rng.uniform(-2, 52, 1000)
iarr = rng.uniform(-2, 82, 1000)
np.testing.assert_allclose(fast(jarr, iarr, grid=False),
spline(jarr, iarr, grid=False),
rtol=1e-10)

def test_other_spline_degrees_fall_back_to_fitpack(self):
from scipy.interpolate import RectBivariateSpline
img = np.random.rand(20, 20)
hdul = fits.HDUList([fits.PrimaryHDU(), fits.ImageHDU(data=img)])
interps = make_image_interpolations(hdul) # default: cubic
assert isinstance(interps[0], RectBivariateSpline)

def test_works_with_non_square_image(self):
# The axes passed to RectBivariateSpline were transposed, which
# raised ValueError for any image with NAXIS1 != NAXIS2
Expand Down