Stacking Suite

SASpro's complete calibration-and-integration pipeline: from raw camera RAW/TIFF through master darks and flats, debayer, cosmetic correction, satellite trail masking, registration against any alignment model up to 4th-order polynomial, drizzle, up to multi-panel mosaic sets, multi-frame deconvolution, and a final rejected-and-weighted integrated master — all driven from one window, with CPU, CUDA, MPS and DirectML backends shared across every heavy stage.

The Stacking Suite is laid out as six tabs along the top (Quick Stack, Convert Camera RAW/TIFF, Darks, Flats, Lights, Image Integration) with a header strip above them (wrench, folder path, Change…, Open Log, document) and almost every parameter you'll want to touch living in the orange-wrench Stacking Settings dialog. The sections below walk through each tab in the order you'll meet them, then cover the Settings dialog, then the maths and plumbing behind the scenes.

Contents

Top row & the wrench

A single strip sits above the tabs and stays visible at all times:

ControlWhat it does
πŸ”§ wrench (orange) Opens the Stacking Settings dialog — profiles, distortion/transform, alignment, performance, drizzle, MFDeconv tuning, rejection, cosmetic correction, comet, comet star removal. See §7.
Current Stacking Folder Live display of the folder every intermediate file and master will land in. Reads from stacking/dir and tooltips the full path.
Change… Picks a new stacking folder. The suite creates Calibrated/, Normalized_Images/, Aligned_Images/ and Masters/ underneath it on demand.
Open Log Raises the dockable Stacking Suite log window. Everything the suite prints is tee'd to this log and to stacking_log.txt inside the stacking folder, so you have a timestamped record of every run.
πŸ“„ document This help page. Added at the far right of the row.

1. Quick Stack

The default tab, and the easiest way to get a stack out the door. Five cards walk you top-left to bottom-right through the pipeline, each with a traffic-light indicator:

Workflow
1 Darks → 2 Flats → 3 Lights → 4 Calibrate → 5 Register + Integrate.
Cards go red when nothing is loaded, yellow when raw frames are present but no master has been built, and green when the step is complete.

1.1 Card controls

1.2 Pipeline buttons at the bottom

2. Convert Camera RAW/TIFF

A standalone batch converter from camera formats (CR2, NEF, ARW, RAF, DNG, TIFF, PNG, JPEG, … — anything rawpy can read, plus TIFF via tifffile) into debayered FITS. It never touches the Lights/Darks/Flats trees — the output goes to a folder you choose.

When to use this tab: only when your lights are in DSLR raw formats and you'd rather preprocess them to FITS once than let the Lights tab decode them on every run. Modern SASpro will happily eat raw files directly through the Lights tab too, so this is primarily for mixed sessions or archival.

3. Darks

Groups raw dark/flat-dark/bias frames and turns them into master darks. The tab is split in two: raw darks on the left, controls on the right, and the master-dark list across the bottom.

3.1 Left pane: raw darks tree

3.2 Right pane

3.3 Right-click & context

Right-click any raw-dark row for Set Session Tag…. Darks are session-agnostic for integration (see 3.1), but the tag is preserved in the tree for your reference.

3.4 Master Darks list

Shows every master dark currently loaded, with its exposure and file path. Load Master Dark adds a pre-built master from disk; Clear Selection removes items from the list and persists the change.

4. Flats

Same shape as Darks but with filter awareness and per-group dark-override controls.

4.1 Left pane: raw flats tree

4.2 Right pane

4.3 Right-click & context

Right-click any flat group for:

5. Lights

Where you load the actual signal data, apply the calibration pipeline, and optionally hand off straight to registration.

5.1 Lights tree

Five columns: Filter & Exposure, Metadata, Master Dark, Master Flat, Corrections. Grouped by filter at the top level and by (exposure, size) beneath. Leaves show individual frames with their status and chosen masters.

5.2 Options row

Manual Session field: same as Flats, shared via stacking/session_keyword.

5.3 Corrections row

5.4 πŸ”¬ Tune Cosmetic Correction σ…

Opens the standalone Cosmetic Correction dialog on your selected light (or the first available). Use the β—€ / picker / β–Ά bar at the top to bounce between frames. Adjust Hot σ and Cold σ with the live preview until you're happy with how it handles hot pixels without eating star cores. “Save σ Settings” writes the values back here so calibration uses them.

5.5 Satellite trail masking

5.6 Overrides & context

6. Image Integration

The back half of the pipeline. The tree lists calibrated lights ready for registration + integration; the row stack below configures tolerances, output cropping, drizzle, registration sets, MFDeconv, comet/trail modes, GPU backend and the big red launch buttons.

6.1 Calibrated Light Frames tree

Four columns: Filter – Exposure – Size, Metadata, Drizzle (per-group indicator), Set (registration-set membership). Grouping follows filter + (exposure ± tolerance) + size, with the tolerances set below.

6.2 Tolerance & output row

6.3 Drizzle controls

6.4 Reference frame

6.5 Registration Sets

A full subsystem for independent references — use it for mosaic panels or completely different targets loaded in one run. See §9.9 for the full logic. Controls:

Each registration set integrates separately into its own master, with its own reference. Unassigned frames use the Default set and the global reference above. “Already registered” precheck decisions are reused across every set in a single multi-set run so you only get prompted once.

6.6 MFDeconv — Multi-Frame Deconvolution

SASpro's multi-frame deconvolution engine, implementing the ImageMM method of Sukurdeep et al. (2025) with original engineering extensions for real astrophotography data — most notably a rejection-aware MM update that reuses the stacking rejection maps to exclude satellite trails, aircraft, transient defects and hot-pixel outliers from the per-iteration updates (this is exactly what the Rejection Strength control blends).

Implementation whitepaper — Franklin Marek (2026): “ImageMM: Multi-Frame PSF-Aware Deconvolution — Practical Notes & Implementation Diagram”, Zenodo working paper, DOI 10.5281/zenodo.17252270 (GPL v3). Documents SASpro's forward model, MM update, super-resolution path, and the rejection-aware extension that is original to this implementation.

Controls (see §9.8 for the maths):

6.7 Comet + Star-Trail row

6.8 Comet options row

6.9 Output options

Save rejection layers with master lights — when on (default), master light FITS files are saved as multi-extension FITS (MEF) with extra HDUs containing the rejection any-mask, fraction map, and count map — useful for post-mortem analysis and for feeding back into MFDeconv. Off = smaller single-HDU FITS.

6.10 Backend row

6.11 Action buttons

7. Stacking Settings dialog (πŸ”§ wrench)

Two scrollable columns. Every parameter is live-persisted to QSettings (stacking/… keys), and OK triggers save_stacking_settings for a few derived fields. Cancel reverts the dialog's uncommitted changes but keeps anything already persisted mid-session.

7.1 Profiles (top strip)

Snapshot/restore your entire stacking configuration under named profiles. New… saves the current state under a new name; Save overwrites the selected profile; Load replaces the current configuration (and restarts the Stacking Suite dialog to force every control to re-sync); Delete removes a profile; Export… / Import… round-trip profiles via JSON. See §8 for the detail.

7.2 Distortion / Transform

7.3 Alignment

7.4 Performance

7.5 Normalization & Gradient (ABE Poly²)

Optional 2nd-order background removal applied to every frame before stacking. See §9.3.

7.6 Drizzle (advanced)

7.7 Multi-frame Deconvolution (tuning)

7.8 Rejection

7.9 Cosmetic Correction (Advanced)

7.10 Comet (High-Clip Percentile tuning)

High-clip k / Percentile p — parameters of the High-Clip Percentile reducer used for comet nucleus stacks (defaults 1.30 / 25.0). See _high_clip_percentile in §9.6.

7.11 Comet Star Removal (Optional)

8. Profiles

Profiles exist to let you save a complete working configuration — sigma thresholds, weighting mode, drizzle, MFDeconv tuning, cosmetic sigmas, every setting in §7 — under a name, and bring it back later.

9. Technical details

What the suite actually does under the hood, in the order a stack passes through it.

9.1 Calibration pipeline

Calibration is a two-phase pipelined worker system (calibrate_lights):

Phase 0 — master upload. Every master dark, master flat and master bias is loaded to CPU, normalized where needed, and uploaded to the GPU exactly once. Interactive flat-strength dialogs (if enabled) are collected upfront so you can configure and walk away.

Phase 1 — three-thread pipeline. A producer thread reads raw lights from disk; a GPU thread runs dark subtraction, flat division and cosmetic correction; a consumer thread writes calibrated frames to Calibrated/. The three run concurrently so I/O, compute and I/O overlap. Per frame:

P → L' = \mathrm{clip}(L + P - D_{\mathrm{scaled}},\;0,\;\infty)
C = L' / F_{\mathrm{norm}}

where P is the pedestal (if enabled), D is the chosen master dark (bias-scaled when a master bias is loaded), and F_norm is the normalized master flat (median = 1 per channel). With interactive flat strength on, the effective flat is

F_{\mathrm{eff}} = 1 + \alpha(F_{\mathrm{norm}} - 1)

with your chosen α. Cosmetic correction then replaces hot and cold outliers with a 3×3 ring median using a MAD-based sigma estimate:

\sigma = 1.4826 \cdot \mathrm{MAD} = 1.4826 \cdot \mathrm{median}(|x - \mathrm{median}(x)|)
x_i \to \mathrm{median}(\mathrm{ring}_i)\quad \mathrm{if}\;|x_i - m_i| > k \cdot \sigma_i

with k taken from Hot σ / Cold σ. Patches above the protect_sigma threshold are left untouched (star / nebula cores). The GPU uses unfold for the 3×3 neighbourhoods; the Numba fallback handles cold-cluster guards.

Phase 2 — satellite trail removal. After Phase 1 fully completes, every master tensor and cosmetic buffer is freed and the CUDA cache emptied, then the satellite CNN runs on the saved frames one at a time. Separate phases mean the CNN never competes with the calibration working set for VRAM — critical on small cards where GPU memory cannot page to host.

9.2 Master-frame integration

Master darks group by (exposure, size, temperature bucket, gain, offset); master flats by (filter, size, session). Each group is read in row-band tiles sized for your RAM budget, stacked with the Calibration Rejection Algorithm (Winsorized Sigma Clipping by default), and written as 32-bit FITS with a header merged from the input group's representative frame.

Temperature bucketing:

T_{\mathrm{bucket}} = \mathrm{round}(T / \Delta T) \cdot \Delta T

with ΔT = 1°C by default (stacking/temp_group_step). Bias scaling for CCD users:

B^{*} = B + (\mathrm{median}(L) - \mathrm{median}(B))

9.3 Normalization & gradient

Before registration, each frame is normalized to a common target median. The method is min-offset + median-match on the luminance channel, with no clipping:

L = 0.2126\,R + 0.7152\,G + 0.0722\,B
f' = (f - \min L)\cdot \frac{m^{*}}{\mathrm{median}(L) - \min L}

Rec. 601 luminance for mono treatment of RGB, so colour balance is preserved and only the overall level is matched.

Optional ABE-style poly² gradient removal fits the model

G(x,y) = a + bx + cy + dx^2 + exy + fy^2

to robustly selected background sample points on a downsampled frame, then either subtracts or divides it back on the full resolution. The “Skip if strength <” guard compares peak-to-peak of G against the frame's robust noise so clean skies pass through untouched.

9.4 Registration

Reference selection: either you lock a frame (Pick… / Select Reference Frame), or the suite measures every candidate by its combined quality score (see §9.5) and picks the best. Reference lock survives tab changes; auto-accept unlocks it.

Each non-reference frame is solved against the reference with astroalign-style triangle matching on detected stars (SEP, with filters: min area, min FWHM, max ellipticity), then refined by the chosen transform model:

Accurate mode re-runs the solve 3 times with the previous solution as warm start; shift tolerance and max shift gate acceptance. Rejected frames are logged and skipped.

9.5 Frame weighting

Every frame gets a weight from five measured terms — detected star count (c), mean eccentricity (ε), background level (bg), FWHM proxy, and MAD-based noise estimate — combined multiplicatively with per-mode exponents:

w = c^{a_c}\,(1 - \varepsilon)^{a_e}\,b^{a_b}\,s^{a_s}\,n^{a_n}

with

b = \frac{2}{\sqrt{\mathrm{bg}} + 1} - 1
s = \mathrm{clip}\!\left(\frac{\mathrm{FWHM}_{\mathrm{ref}}}{\mathrm{FWHM}},\;\frac{1}{3},\;3\right)
n = \mathrm{clip}\!\left(\frac{\mathrm{noise}_{\mathrm{ref}}}{\mathrm{noise}},\;\frac{1}{3},\;3\right)

The modes change only the exponents (as):

Modecounteccbgsharpsnr
Balanced (default)1.01.01.01.00.0
PSF-weighted0.51.00.52.00.0
SNR-weighted0.50.51.00.52.0
Star-count-weighted2.00.50.50.50.0

The sharpness and SNR ratios are measured against the population median and clamped to [1/3, 3] so a single mismeasured frame can't dominate. A frame measured far tighter than the population (ratio < 0.4× or < 0.25×) is treated as a mismeasurement and set neutral.

9.6 Rejection algorithms

Rejection runs per output pixel across the registered stack, honouring per-frame weights where applicable. All reducers are implemented in NumPy (CPU) and PyTorch (GPU; cuDNN-free fallback and DirectML workarounds included). Each writes a 3-layer rejection map alongside its scalar output: any-mask, fraction (fraction of frames rejected), count (total frames rejected). These layers can be saved into master lights as additional FITS HDUs.

Weighted Winsorized Sigma Clipping (default)

The Pix Insight / APP style: seed with median and robust sigma, Winsorize at k sigma, re-measure, iterate:

\mu_{\mathrm{win}} = \mathrm{mean}(\mathrm{clip}(x,\,\mu - k\sigma,\,\mu + k\sigma))
\sigma_{\mathrm{winsor}} = 1.134\,\sqrt{\mathrm{Var}(x_{\mathrm{clipped}})}

The 1.134 factor is Huber's exact correction for k = 1.5. Outliers (satellite trails) cannot inflate σ, so the rejection bounds track the clean distribution; final output is the weighted mean of survivors only.

Kappa-Sigma Clipping

\mathrm{keep}_i = (\mu - \kappa\sigma \leq x_i \leq \mu + \kappa\sigma)

Median center, std of the kept sample, monotone shrink (rejected pixels stay rejected across iterations), weighted mean of survivors.

Extreme Studentized Deviate (ESD)

Generalized ESD (Rosner), externally studentized, vectorized per pixel. Sequentially removes the most extreme remaining value and tests it against the leave-one-out mean/std of the remaining sample (defeats masking); no early stop — the outlier count is the largest step whose test passed. The critical value adjusts for sample size:

R = \frac{|x_i - \overline{x}|}{s},\quad R > \lambda_{n,\alpha}
\lambda_{n,\alpha} = t_{n-2,\,1-\alpha}

so your threshold keeps its literal “sigmas” meaning on large stacks and inflates automatically on small ones (controls the false-rejection cascade a fixed cutoff would cause).

Biweight Estimator (Tukey)

\hat{\mu} = M + \frac{\sum_{|u_i|<1} (x_i - M)(1-u_i^2)^2}{\sum_{|u_i|<1}(1-u_i^2)^2},\quad u_i = \frac{x_i - M}{c\,\mathrm{MAD}}

with c from the tuning-constant setting. Smooth down-weighting of outliers, no hard clip.

Trimmed Mean

\overline{x}_{\alpha} = \frac{1}{N - 2k} \sum_{i=k+1}^{N-k} x_{(i)},\quad k = \lfloor \alpha N \rfloor

Drops the lowest and highest αN frames at each pixel before weighted averaging.

Modified Z-Score Clipping (Iglewicz & Hoaglin)

M_i = \frac{0.6745\,(x_i - \mathrm{median}(x))}{\mathrm{MAD}}

Rejects where |M_i| exceeds the threshold. Robust to small samples where mean/std fail.

Simple Average, Simple Median, Max Value

No rejection. Max Value is the engine behind Star-Trail mode: per-pixel maximum across all frames.

Comet reducers

Specialised for comet-aligned stacks where stars become trails that must be rejected: Comet Median, Comet High-Clip Percentile (parameters k and p in §7.10), Comet Lower-Trim (30%), Comet Percentile (40th). The high-clip percentile combines a percentile-centre with a sigma-style cap:

P_{40}(x_{xy}) = \mathrm{quantile}_{0.40}(x_{xy})
\mathrm{keep}: x_i \leq \mu + k\,|x_i - P_p|

9.7 Drizzle

Variable-pixel linear reconstruction, after Fruchter & Hook (2002):

I'(X,Y) = \frac{\sum_i w_i\,A_i(X,Y)\,I_i(x_i,y_i)}{\sum_i w_i\,A_i(X,Y)}

where each input pixel is “drizzled” onto the output grid as a kernel of area A_i(X,Y) centred on the warped position. The kernel is:

Scale 2× / 3× means the output grid has 4× / 9× the pixels of the input. With CFA Drizzle on, R/G/B samples from the Bayer pattern are drizzled directly into the three output channels — no interpolation step, which is the only way to gain resolution from one-shot colour.

9.8 Multi-frame deconvolution

ImageMM — a majorization-minimization (MM) framework for joint multi-frame restoration and super-resolution (Sukurdeep et al. 2025). The latent sky image I is recovered by iteratively minimising a surrogate of the robust data fit across all frames, with per-frame PSFs H_j and per-pixel noise variances v_j. The multiplicative update, with your κ cap and ρ loss:

I^{(k+1)} = I^{(k)} \cdot \mathrm{clip}\!\left(\frac{\sum_j H_j^{T}\!\left(\rho'(d_j)/v_j\right)}{\sum_j H_j^{T}(1/v_j)},\;\frac{1}{\kappa},\;\kappa\right)

where H_j is the forward operator (blur + warp + down-sample) for frame j, d_j = D_j - H_j I^{(k)} is the data residual, and v_j is the per-pixel variance:

v_j(x,y) = g\,I_j(x,y) + \sigma_r^2

Huber loss (L2 if you prefer the quadratic regime everywhere):

rho(d) = (1/2) d^2 if |d| <= delta delta|d| - (1/2) delta^2 if |d| > delta

Negative δ means scale×RMS (so δ = −2 uses 2×RMS as the cutoff). Lower |δ| → more pixels treated as outliers.

Star masks protect stellar cores from ringing by clamping update ratios near 1:

M = 1 - \mathrm{keep\_floor} \cdot (1 - M_{\mathrm{raw}})

Super-resolution reconstructs I on an r× grid; each H_j then includes an r× down-sampling step. Cost scales roughly as r^4.

Rejection-aware MM — SASpro extension (Marek 2026, DOI 10.5281/zenodo.17252270). Before MFDeconv runs, the suite performs its usual robust integration prepass and keeps the per-frame rejection maps that would normally live in the master light's extra FITS HDUs (any-mask, fraction, count — see §9.6 and §6.9). Those rejected pixels — satellite trails, aircraft, cosmic rays, transient defects, hot-pixel outliers — are then excluded from the forward operator for the frames that saw them, so the sums Σ_j H_j^T(·) only run over good samples per output pixel. The Rejection Strength control (§6.6) blends between the rejection-ignoring solve (0.0) and the fully rejection-aware solve (1.0); intermediate values run both endpoints and interpolate the final image. This is why the suite recommends leaving Save rejection layers with master lights on when you plan to run MFDeconv — those layers are the extension's input.

9.9 Registration Sets

A set is an orthogonal partition that sits above the filter / exposure grouping. Each set has its own reference and integrates to its own master. The queue runs sets serially, buffering the per-set summaries and flushing them as a single run report; the “already registered” precheck choice is remembered across all sets in one run so you aren't prompted repeatedly.

Filename-based assignment uses a regex equivalent to

keyword[\s_\-.]([^\s_\-.]+)

so M31_Panel 1_2024-01-02.fit, M31_Panel_02.fit and M31.Panel.3.fit all match. Unmatched frames stay in the Default set, which keeps the global reference.

9.10 GPU backends

The Stacking Suite shares one PyTorch runtime (installed via Install/Update GPU Acceleration…) across every heavy stage: calibration, cosmetic correction, satellite CNN, rejection reducers, MFDeconv. Backend auto-detection picks the first available of CUDA, MPS (Apple Silicon), DirectML (Windows GPUs via torch-directml), falling back to CPU. The stacking/use_hardware_accel master toggle disables the GPU path app-wide.

DirectML needs a workaround for a known torch-directml bug: nanmedian/nanquantile/nanstd can return empty tensors, so those reductions are forced to CPU and the results moved back to device (one-time warning per session). CUDA uses PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True to reduce fragmentation between the cosmetic pass and the satellite CNN; a device-wide RLock serialises the two so they don't both resident at once (which OOMs small cards on Linux/CUDA where VRAM cannot page).

Supported Python runtimes for GPU install: 3.12, 3.13, 3.14. The installer refuses older/newer versions with platform-specific tips.

9.11 File layout & cleanup

Inside your chosen stacking folder the suite creates:

FolderContents
Masters/Master darks, flats, biases — keep these.
Calibrated/Calibrated lights (*_c.fit). Delete after integration if disk is tight.
Normalized_Images/Normalized, pre-registration frames. Intermediate; always deletable.
Aligned_Images/Registered *_r.fit frames. Keep if you plan to re-integrate with different rejection.
loose .dat / .npy / .tmpMemmap scratch, transform caches, star catalogues. Always deletable.
stacking_log.txtTimestamped log of every run.

Both Clean Up buttons remove the intermediate folders and loose scratch files, but never Masters/ or Calibrated/.

Credits. SASpro's Stacking Suite is written and maintained by Franklin Marek (SetiAstro). It integrates many standard astronomy-image-processing conventions, which are credited to their originators here. Pipeline design, the quick-stack card UI, the shared GPU runtime, the three-thread calibration pipeline, Registration Sets, dual-band advanced split, the frame-weighting modes, and the per-group dark/flat overrides are original to this suite.