drizzle_resample — ASTROSTACK stack op

• Data kinds: images → image2d

• Call: import fullseye as fs; fs.ledger.drizzle_resample(frames, shifts=None, scale=2.0, pixfrac=1.0) (to call the implementation directly, import astrostack; astrostack.drizzle_resample(frames, shifts=None, scale=2.0, pixfrac=1.0); from the registry, opsastrostack.get("drizzle_resample"))

• Return value through the ledger: fullseye.ledger.drizzle_resample(...) returns only the declared out type image2d (the underlying function also returns auxiliary values). When you need what was dropped, use fullseye.ledger.drizzle_resample.raw(...) or call astrostack.drizzle_resample directly.

• Underlying return: (sci, wht) -> image2d

Usage

Drizzle — build a finer grid from several sub-pixel-shifted frames (area-conserving).

Fruchter & Hook, *Drizzle: A Method for the Linear Reconstruction of Undersampled Images*, PASP 114, 144 (2002). Each input pixel is regarded as a slightly shrunken "drop" (side pixfrac), and its flux is distributed in proportion to the overlap area with the pixels of the output grid. Because nothing is interpolated,

• Total flux is conserved. As long as the drops stay inside the output grid, sci.sum() equals the mean of the input frame totals exactly (measured: relative error 0.0 with shifts=0, scale=2, pixfrac=0.7). That is why it is built so that it can be cross-checked from the return value alone.

• The smaller pixfrac, the larger the fraction of each drop falling inside a single output pixel: resolution rises, but some output pixels are left uncovered (wht becomes small there). This tug-of-war is drizzle's only tuning knob.

It only helps when the sampling is insufficient — that is the measured conclusion. 48x48 with 10 stars, dithering 1.5 px, 16 frames (values are FWHM converted to input pixels)::

true FWHM single frame plain mean drizzle x2 (pixfrac=0.5)

1.0 1.773 2.249 1.312

1.3 1.929 2.876 1.572

1.8 2.262 2.648 3.086

2.5 4.057 4.295 3.586

Drizzle wins only where the FWHM falls below 2 pixels (violating Nyquist). Plainly averaging dithered frames actually blurs the image (1.773 → 2.249) —— because the offsets get averaged away; drizzle turns those very offsets into resolution. At 1.8 px and above the image is already adequately sampled, so drizzle gains nothing and only loses by the convolution with the drop.

It is easiest to see in whether two stars are resolved. 2 stars of σ=0.55 px at varying separations, dithered over 16 frames, stacked by mean and by drizzle x3 (pixfrac=0.4), then star_detect, measured: at a separation of 1.6 input pixels, the mean stack found 1, drizzle found 2 (at 2.0 px and above both find 2).

*shifts* is (N, 2) of (dr, dc): how far frame i is offset from the reference (truth["shifts"] of synth_frame_series uses this orientation). None means all 0.

★ Mind the sign —— the translation in the matrix returned by frame_align / align_frames points in the direction that brings the frame back to the reference, i.e. the (dr, dc) needed here with the opposite sign. Passing the estimate as-is does not cancel the offset but doubles it, and produces a double image without raising (measured: 6 frames 96x96 give est + truth ≈ 0; passing it as-is makes the residual 2 times as large). To build correctly oriented shifts from a matrix, use drizzle_shifts. Rotation is not accepted —— with rotation the axes no longer separate and the overlap area has no closed form, so undo it first with align_frames (paying the interpolation error there; it is more honest to keep that trade visible).

★ Where does an input pixel land in the output (this convention was not written down). By correspondence of pixel centres::

out = in * scale + (scale - 1) / 2

With scale=2 a star at input (8, 8) sits at output (16.5, 16.5). The interval [in, in+1) covered by input pixel in maps to [in*scale, (in+1)*scale) in the output, and its centre is the formula above —— not simply in*scale. Dropping the half pixel gives, at scale=4, a systematic offset of 1.5 output pixels, i.e. 0.375 input pixels, and photometry and double-star separations after registration silently go wrong by exactly that amount.

Measured (24x24, single-pixel star, pixfrac=1, measured by centroid): for scale 2 / 3 / 4 the difference from the formula is exactly 0. At scale=2.5 it is off by 0.05 output pixels, but this is not an error in the formula: the edge output pixels are only partially covered, so the centroid over indices deviates from the continuous centroid (with integer factors this remainder does not occur). When positions must be exact, use an integer scale.

Returns (sci, wht):

• sci —— (round(H*scale), round(W*scale)) float64, in total-flux units (the same electrons as the input). Drops that leave the grid reduce the sum, so the difference between sci.sum() and the input total is exactly "the amount lost at the edges".

• wht —— weight map of the same shape: how many drops' worth cover each output pixel (output pixel area = 1). With pixfrac=1 and shifts=0 the interior is exactly 1.0.

★ Use sci / wht when looking / measuring. sci keeps the "distributed amount" as-is in order to conserve total flux, so the coverage non-uniformity remains in the image. With a small pixfrac the coverage is uneven in a grid pattern, so running detection on raw sci turns that grid into stars —— measured (scale=3, pixfrac=0.4, 24 frames of 1 double star): star_detect returned 200 (stuck at the cap) on raw sci, and correctly 2 on sci / wht. The conservation quantity (sci) and the appearance (sci / wht) are different quantities; using one in place of the other is wrong without exception.

Raises ValueError: if *frames* is not a list / tuple / shapes do not match / *scale* is less than 1 / *pixfrac* is outside (0, 1] / *shifts* is not of shape (N, 2) / the output exceeds MAX_OUTPUT_ELEMENTS.

References (sample data, literature)

• Sample-data catalog (download URLs / licences) — 2-D uses skimage.data (BSD/public domain) plus synthetic images; 3-D lists download URLs for real data sources (Stanford, PDS, …).

• Operator provenance and references — the sources of the research/methods this op family came from.

• The canonical algorithm (author, year) and its uses are named in the family usage guide above.

Runnable examples (verified samples that actually call this op)

• astro_stacking — py -3.11 examples/astro_stacking.py

• poc_astro_photometry — py -3.11 examples/poc_astro_photometry.py

• poc_superresolution_limits — py -3.11 examples/poc_superresolution_limits.py

Ops the type connects to (they accept image2d as input)

frame_quality · noise_sigma · cosmic_ray_reject · star_detect · psf_fit · aperture_photometry · frame_align

Same category (stack)

sigma_clip_stack


*Provenance: astrostack.py — ASTROSTACK operator registry. This per-op note is generated by tools/opdocs.py md (do not hand-edit).*

© 2026 Kazufumi Furuse — Fullseye operator documentation. Licensed under Apache-2.0.