Resampling¶
Resampling moves an image from the pixel grid it was observed on to another
grid: a different projection, orientation, pixel scale, or a shared mosaic
grid. Both grids are described by a WCS, and a
kernel decides how source pixel values become target pixel values.
resample does this for one or
more images without combining them; coadd resamples and then
stacks.
Use it to put an image on another survey's grid, to align exposures before a pixel comparison, to make a cutout in a chosen projection, or to drizzle dithered, undersampled data onto a finer grid. To map an image onto the sphere, use HEALPix mapmaking; to combine several exposures with weights and outlier rejection, use coadds.
How It Works¶
Two families of kernels walk the grids in opposite directions.
flowchart LR
subgraph pull ["Interpolating (pull)"]
T1["target pixel centre"] -->|"target WCS → sky →<br/>source WCS"| S1["source position"]
S1 -->|"kernel weights over<br/>nearby source pixels"| V1["target value"]
end
subgraph push ["Drizzle and overlap (push)"]
S2["source pixel,<br/>shrunk by pixfrac"] -->|"corners: source WCS →<br/>sky → target WCS"| P2["polygon on target grid"]
P2 -->|"share value by<br/>overlap area"| V2["target values"]
end
An interpolating kernel maps each target pixel centre back to a position in the source image and evaluates a weighted sum of nearby source pixels: bilinear uses 2×2 pixels, Lanczos-\(a\) uses \(2a \times 2a\) pixels with weights \(L(x) = \operatorname{sinc}(x)\operatorname{sinc}(x/a)\) per axis. Interpolation assumes the image is well sampled (the PSF spans at least about two pixels) and does not conserve total flux exactly.
A drizzle kernel (Fruchter and Hook 2002) pushes each source pixel forward
instead. The pixel is shrunk about its centre by pixfrac (between 0 and 1),
its corners are projected onto the target grid, and its value is shared among
the target pixels it overlaps. With \(a_{ij}\) the overlap area of shrunk source
pixel \(i\) with target pixel \(j\), and \(w_i\) its weight,
Drizzle conserves flux and suits undersampled, dithered data. A small pixfrac
sharpens the result but leaves holes unless many dithers cover each target
pixel. conservative_polygon is the same exact overlap at pixfrac = 1.
For distorted source WCS (SIP, TPV), the target-to-source mapping is evaluated exactly on a coarse lattice and interpolated in between; each lattice cell is verified to stay within \(10^{-3}\) source pixel and refined or solved per pixel where it does not.
Validation. On 135 JWST NIRCam exposures, drizzle agrees with STScI
drizzle to better than \(2\times10^{-6}\) σ and Lanczos-3 with SWarp to a
median below 0.1 σ (performance highlights);
the package tests check adjoints, gradients, and flux conservation.
Kernels¶
| Kernel | Family | Conserves flux | Variance | Use for |
|---|---|---|---|---|
nearest |
interpolating | no | yes | masks, labels, integer data |
bilinear (default) |
interpolating | no | yes | quick looks, smooth images |
bicubic, catmull_rom, mitchell_netravali |
interpolating, cubic | no | yes | smoother than bilinear; definitions differ between tools |
lanczos2 … lanczos5 |
interpolating, windowed sinc | no | yes | well-sampled images; lanczos3 is the SWarp default |
fft_shift, fft_diagonal_affine, fft_linear_affine, periodic_linear_affine |
Fourier / periodic | no | none | same-shape grids related by a shift or affine map; periodic boundaries |
spectral_linear_affine, spectral_nonuniform |
Fourier series | no | none | band-limited data under a general mapping (CPU/CUDA, float64) |
drizzle_square |
drizzle | yes | yes | dithered, undersampled exposures |
drizzle_turbo |
drizzle | yes | yes | as drizzle_square, faster box overlap when the grids are axis-aligned |
drizzle_point |
drizzle, pixfrac → 0 |
yes | yes | many dithers; leaves holes otherwise |
drizzle_gaussian, drizzle_lanczos2, drizzle_lanczos3 |
drizzle, smooth drop | no | yes | compatibility with the STScI kernels of the same name |
conservative_polygon |
exact overlap | yes | yes | flux-exact regridding without pixfrac |
splat_point, splat_box |
forward deposition | approximately | yes | differentiable source positions in models |
drizzle_turbo and splat_box fall back to the exact polygon overlap when a
pixel pair is rotated or sheared. At the grid edge drizzle_point keeps drops
whose centre falls in the outer half-pixel rim (target coordinates in
\([-0.5, n - 0.5]\)), where STScI drizzle keeps only \([0, n - 1]\), so no
drop that overlaps the grid is lost.
Surface Brightness or Flux¶
A pixel value is either surface brightness (per unit solid angle, the
default) or flux (per pixel). Resampling to pixels a quarter of the area
leaves surface brightness unchanged and divides flux by four. Set
semantics="flux" for counts or flux-calibrated pixels whose sum must be
preserved, and semantics="surface_brightness" for calibrated intensity maps.
import torch
from torchsky.map import Observation, resample
source = {
"NAXIS": 2, "NAXIS1": 32, "NAXIS2": 32,
"CTYPE1": "RA---TAN", "CTYPE2": "DEC--TAN",
"CRPIX1": 16.5, "CRPIX2": 16.5, "CRVAL1": 150.0, "CRVAL2": 2.0,
"CDELT1": -1e-4, "CDELT2": 1e-4,
}
# Half-size pixels: four target pixels cover one source pixel.
target = {**source, "NAXIS1": 64, "NAXIS2": 64, "CRPIX1": 32.5, "CRPIX2": 32.5,
"CDELT1": -0.5e-4, "CDELT2": 0.5e-4}
image = torch.zeros(32, 32)
image[10:20, 12:22] = 1.0 # 100 source pixels of value 1
observation = Observation(data=image, wcs=source)
sb = resample(observation, target, kernel="drizzle_square", semantics="surface_brightness")
flux = resample(observation, target, kernel="drizzle_square", semantics="flux")
assert abs(float(sb.data.max()) - 1.0) < 1e-6 # value per unit area kept
assert abs(float(flux.data.sum()) - 100.0) < 1e-3 # total flux kept
A Photographic Plate in Another Projection¶
The Horsehead example reads a DSS plate of the Horsehead Nebula with torchfits and resamples it onto a rotated TAN grid and a plate carrée (CAR) grid. A round trip back to the original grid leaves a residual of 1.5% of the image standard deviation, and the mean surface brightness changes by \(10^{-4}\).
# horsehead.fits: examples/horsehead_wcs_tour.py downloads it from data.astropy.org.
import torch
import torchfits
from torchsky.map import Observation, plan, resample
image = torchfits.read("horsehead.fits", 0).to(torch.float32)
header = dict(torchfits.read_header("horsehead.fits", 0))
plate = Observation(data=image, wcs=header)
grid = plan([plate], projection="CAR") # CAR grid covering the plate
car = resample(plate, grid, kernel="lanczos3") # MapResult: data, weight, header

Recipes¶
Reproject onto an existing header¶
Pass any target WCS: a header dict, a WCS,
or a planned Target. Pixels outside
the source footprint get weight == 0; mask with result.weight > 0 rather than
testing values. With several observations and no target, resample plans one
grid covering all of them and returns one result per input.
Drizzle dithered exposures¶
Undersampled exposures with sub-pixel dithers recover resolution on a finer
grid: plan a target with a smaller pixel_scale_arcsec, then coadd with
kernel="drizzle_square" and pixfrac between 0.6 and 1.0. Check the weight
map: with too few dithers for the chosen pixfrac, weights vary strongly from
pixel to pixel and some reach zero. See
coadds and the
drizzle example.
Propagate variance¶
Declare noise as variance components (var_poisson, var_rnoise, var_flat)
or as an inverse-variance weight map with weight_is_inverse_variance=True.
Each output variance is the sum of the input variances times the squared kernel
coefficients. Neighbouring output pixels then share input noise, so the
per-pixel variance is smaller than the input variance while the noise is
correlated. result.correlation_factor reports the local ratio needed to
correct aperture errors, and aperture_uncertainty
computes the exact variance of an aperture sum. Fourier kernels return no
variance product.
import torch
from torchsky.map import Observation, resample
header = {
"NAXIS": 2, "NAXIS1": 32, "NAXIS2": 32,
"CTYPE1": "RA---TAN", "CTYPE2": "DEC--TAN",
"CRPIX1": 16.5, "CRPIX2": 16.5, "CRVAL1": 150.0, "CRVAL2": 2.0,
"CDELT1": -1e-4, "CDELT2": 1e-4,
}
noisy = Observation(data=torch.ones(32, 32), wcs=header, var_rnoise=torch.full((32, 32), 4.0))
shifted = resample(noisy, {**header, "CRPIX1": 16.8}, kernel="bilinear")
# Bilinear weights 0.7 and 0.3: variance 4 * (0.7**2 + 0.3**2) = 2.32.
assert abs(float(shifted.var_rnoise[16, 16]) - 2.32) < 1e-4
Differentiate through resampling¶
Resampling is linear in the pixel values, and autograd applies its exact adjoint, so a loss on the resampled image has gradients with respect to the input pixels. This is what forward models use to compare a model on one grid with data on another.
import torch
from torchsky.map import Observation, resample
header = {
"NAXIS": 2, "NAXIS1": 32, "NAXIS2": 32,
"CTYPE1": "RA---TAN", "CTYPE2": "DEC--TAN",
"CRPIX1": 16.5, "CRPIX2": 16.5, "CRVAL1": 150.0, "CRVAL2": 2.0,
"CDELT1": -1e-4, "CDELT2": 1e-4,
}
model = torch.rand(32, 32, dtype=torch.float64, requires_grad=True)
on_data_grid = resample(Observation(data=model, wcs=header), {**header, "CRPIX1": 16.8}, kernel="lanczos3")
loss = (on_data_grid.data * on_data_grid.weight).sum()
loss.backward()
assert model.grad is not None and torch.isfinite(model.grad).all()
Map a mosaic back onto an exposure¶
blot is the reverse step: it
interpolates a coadd onto the grid of each input exposure, for example to
compare an exposure with the median sky and flag cosmic rays. It is what
outlier rejection uses internally.
Large images and devices¶
Resampling runs on the device of the input tensors. Large targets are tiled to
a memory budget; the knobs are in coadds.
Exact drizzle, conservative polygon, splat, and Fourier-series kernels need
float64 and raise NotImplementedError on MPS; run them on CPU or CUDA.
Pitfalls¶
- Value semantics. Comparing a torchsky product with SWarp, Montage, or
reprojectoutput starts with the same semantics and the same treatment of pixel area. Most disagreements trace to those, not to the kernel. - Lanczos ringing. Lanczos kernels have negative lobes and ring around sharp features such as saturated stars and masked edges.
- Interpolation of undersampled data. Interpolating kernels assume a smooth image. For undersampled data use drizzle.
pixfracand holes. Point and small-pixfracdrizzle leave empty target pixels unless many dithers cover each one.- Masks. Resample masks with
nearest, or pass them asmask=on the observation so they become zero weight.
See Also¶
- Coadds: weights, background matching, outlier rejection, stacking.
- HEALPix mapmaking and Cubes and IFU.
- Reference:
resample,ResampleConfig. - Examples: Horsehead reprojection, MegaPipe mosaic, exact square drizzle.