Batch-register a stack of frames to a reference, write aligned FITS files to disk, and produce an alignment_transforms.sasd sidecar — the standalone counterpart to Stacking Suite's registration stage.
*_n_r.fit file on the reference's pixel grid, plus a single alignment_transforms.sasd sidecar that records every frame's transform for later drizzle integration. This is the same engine Stacking Suite uses — just exposed as a standalone step for the cases where you want aligned frames without the integration (feeding another tool, inspecting individual frames post-align, or splitting registration and integration across different sessions).
The reference image defines the pixel grid every input frame will be warped onto. Two source modes:
A reference needs to be either plate-solved (if you'll later use the .sasd file with WCS-aware tools downstream) or at minimum star-rich — the solver matches input frames against it, so it needs enough detectable stars to form robust triangle matches.
Two ways to populate the file list:
Both actions deduplicate against what's already in the list, so running Select Files twice on overlapping sets doesn't produce duplicates. Supported extensions: .fits, .fit, .xisf, .tif, .tiff, .png, .jpg, .jpeg.
Files appear in a tree below the file-selection buttons, one row per file (basename only — hover for the full path). Standard tree selection behavior: click to select, Ctrl+click to multi-select, Shift+click for ranges.
Select Output Folder opens a directory picker. All aligned frames and the .sasd sidecar land here. If the folder doesn't exist yet, create it first in your file manager — the dialog doesn't offer a "create new" option.
_n_r.fit: M31_light_0001.fit → M31_light_0001_n_r.fit. These end in .fit regardless of input format, so a JPEG input becomes a FITS output. Writing them into the source folder mixes originals and aligned versions, which Stacking Suite and other downstream tools will then try to re-register in a subsequent run.
Start Registration kicks off the pipeline. The status label updates in real time with per-pass progress, per-frame results, and running counts. The status format tracks across three phases:
Refinement Pass 1/3…, Astroalign delta for M31_light_0012.fit: dx=1.42, dy=0.87, and so on. Up to three passes by default; stops early on convergence.Pass 1 delta shifts: [12.4, 8.9, 11.2, ...] and Within tolerance (≤ 0.20px): 42 frame(s). Frames already within tolerance are skipped in subsequent passes.Finalizing aligned outputs with 8 processes…, then per-frame Wrote M31_light_0001_n_r.fit [affine]. This is the single full-resolution read + warp + write per frame.At the end, a summary box reports Valid frames: X/Y — how many frames produced output versus how many were in the input list. Frames whose alignment failed entirely (astroalign couldn't find enough triangles even after backoff) are reported in-log and skipped rather than written as broken outputs.
Common status-line patterns that mean something's wrong:
| Status message | What it means | What to do |
|---|---|---|
Astroalign failed for X: List of matching triangles exhausted | Even the strictest detection pass couldn't find enough shared triangles between the frame and the reference. | Check whether the frame actually overlaps the reference. Common on frames that drifted far during the session or on panels from a different mosaic region. |
Astroalign failed for X – skipping (no transform returned) | Detection worked but RANSAC couldn't find a consistent transform. | Usually indicates a frame with heavy cloud contamination, a satellite trail covering the brightest stars, or a defocused frame. |
low coverage src=0.37, ref=0.82 for crop solve | Control points on the source cover only 37% of the frame — the solution may be geometrically biased. | Usually a warning, not a failure. If downstream stacking shows one-sided bleed or rotation inconsistency, this frame may be the culprit. |
Within tolerance (≤ 0.20px): N frame(s) | N frames converged in this pass and will be skipped by the next pass. | Nothing to do — healthy progress indicator. Convergence after pass 1 is normal for well-tracked sessions. |
Two kinds of output land in the chosen directory:
*_n_r.fit — one aligned FITS per successful input. The _n suffix is retained from calibrated frames (SASpro's normalization step appends it); the _r is appended by registration. The result is a float32 FITS with the reference's pixel dimensions, each frame's pixels warped and resampled via the anti-ring Lanczos-plus-bilinear blend.alignment_transforms.sasd — a plain-text sidecar listing every file's absolute path, transform kind (affine, homography, poly3, poly4), and the full matrix (2×3, 3×3, or UNSUPPORTED for polynomial models). Format details in §12 below.Each FITS output also carries SASDZ* header keywords — a self-description stamp that lets drizzle integration reconstruct the frame's transform even without the .sasd file.
If your frames went through calibration with satellite trail rejection enabled, trail pixels are baked as NaN in the calibrated frames. Star Registration preserves this: NaN pixels bloom through the Lanczos warp (naturally covering the ring halo around the original trail), and the final file retains them as no-data markers. Integration downstream drops NaN pixels per-lane via isfinite(), so trails don't contaminate the stack.
The star detector handles trail NaN by folding non-finite pixels into the SEP ignore mask on a detection-only copy. The saved frame keeps its NaN markers; only the detection pass sees a filled copy for background estimation.
.sasd; then run Stacking Suite with "Skip Registration and Integrate" twice, once per method. Each integration reads the same aligned frames with no re-registration cost.
.sasd is overwritten each run — so if you want the per-session .sasd preserved, rename it (alignment_night3.sasd) before the next session.
The internals here are literally the same code Stacking Suite uses — see the Stellar Alignment documentation for the full algorithmic treatment of star detection (SEP with hot-pixel rejection on FWHM, ellipticity, and pixel count), uniform-grid control point allocation (coverage and spread gates), triangle-invariant matching via astroalign, the center-crop solve with coordinate lift, the downsample-and-lift affine math, and the anti-ring Lanczos+bilinear warp.
The relevant preferences live under stacking/align/* in SASpro's settings and are shared across every alignment-using tool: the detection threshold, max control points, downsample factor, RANSAC reprojection threshold, hot-pixel FWHM/ellipticity limits, and finalize worker count. Tune them in one tool, they apply in all of them.
The batch pipeline's structure, multi-pass refinement, progress accounting, and finalize phase.
Registration runs in two phases: the refinement loop and the finalize pass. The refinement loop iterates on downsampled grids to compute accurate transforms; the finalize pass performs the single full-resolution read + warp + save per frame. Separating them matters because the refinement loop may touch each frame several times (reading full-resolution each time would be wasteful) while the finalize pass must touch each frame exactly once (and the full-res warp is the expensive operation).
Each refinement pass solves for the delta transform between the currently-warped frame and the reference. The accumulated transform is updated by matrix composition:
Pass 0 starts from identity, so the first delta is also the initial transform. Each subsequent pass warps the original frame with the accumulated transform, solves for whatever residual drift remains, and composes it in. The scalar delta is the translation magnitude:
Convergence is declared frame-by-frame when the delta falls below shift_tolerance (default 0.2 px). Converged frames are added to a skip set and excluded from subsequent passes:
The default maximum is 3 passes. For well-tracked sessions, most frames converge after pass 1; pass 2 handles frames with mild residual drift; pass 3 is rare and usually catches a few outliers with large initial offsets.
The progress bar reports unit progress rather than wall-clock percentage. Total units for a run of N frames with P passes:
which simplifies to:
Each refinement worker completion = 1 unit; each finalize worker completion = 1 unit. Convergence shortens the actual time by skipping already-converged frames in later passes, but the unit count stays fixed — "100% complete" means every frame has been attempted in every pass plus finalized once. Progress updates fire every 500ms via a Qt timer to avoid flooding the UI with per-frame updates.
A subtle but important design choice: the refinement pass always reads the original input frame, not the previous pass's intermediate. The accumulated transform is applied to the original each time, giving the solver the pre-warped frame to work against. This has two benefits: no intermediate files are written during refinement (saving I/O and disk space), and no cumulative resampling error builds up across passes (each pass sees the full-resolution source pixels, not a progressively-degraded warp of a warp of a warp).
The finalize pass is where disk writes happen — exactly once per frame, with the final accumulated transform applied at full resolution via the anti-ring warp.
The finalize pass uses a smaller worker pool than refinement because each worker holds a full-resolution frame plus warp buffers in RAM. To keep CPU cores busy without multiplying memory, each worker allocates multiple OpenCV threads for its warp:
Default is 8 workers × 1 cv2 thread on 8-core machines, or 4 workers × 2 cv2 threads on 8-core machines when finalize_workers is tuned down for memory reasons. The split is auto-balanced via the SASPRO_FINALIZE_CV2_THREADS environment variable passed to each worker process.
The default executor is ProcessPoolExecutor with spawn-mode contexts (matches both frozen builds and Qt safety requirements). On frozen (PyInstaller) builds, freeze_support() at the top of the entry point catches child re-execs so spawned workers don't launch new SASpro windows.
When process pool initialization fails — common on mixed-environment Windows installations where child workers die on NumPy/SciPy import with BrokenProcessPool — the engine falls back to a ThreadPoolExecutor. The thread pool serializes astroalign calls through the global _AA_LOCK (SEP is not thread-safe at the C level), so parallelism is limited to the warp phase, but registration still completes rather than failing hard.
The pipeline polls a cancellation event between frame boundaries. In-flight worker calls finish naturally; new jobs are not dispatched after the event fires. This gives clean cancellation without corrupting partially-written output files or leaving zombie worker processes. User-visible cancellation latency is bounded by the longest in-flight frame's solve time, typically 1-3 seconds.
A plain-text file in a line-oriented format, UTF-8:
Fields:
REF_SHAPE: H, W — reference image dimensions in pixelsREF_PATH — reference file path or the sentinel __ACTIVE_VIEW__MODEL: mixed — informational header; actual model is per-file in each KIND blockUNSUPPORTED for polynomial frames whose transforms are dense coordinate maps rather than small matrices)Numbers are formatted via {:.16g} for maximum precision without trailing zeros. Polynomial frames fall back to an affine written as # fallback affine from refinement comment plus the 2×3 matrix, so downstream tools that only understand affine can still process poly-registered stacks (less accurately but functionally).
Every aligned FITS carries header keywords that let drizzle integration reconstruct the frame's transform without needing the .sasd file:
SASDZ = T — sentinel indicating this frame is a SASpro-aligned outputSASDZKND — transform kind ("affine", "similarity", "homography", "poly3", "poly4")SASDZRFH, SASDZRFW — reference grid dimensionsSASDZORG — source frame basename (full path in a COMMENT card)SASDZNR, SASDZNC — matrix row and column counts (for affine/homography kinds)SASDZ00 through SASDZ11 — matrix elements, flattened row-major (FITS keyword names cap at 8 characters, so a 3×3 homography packs into 9 cards from SASDZ00 to SASDZ08)This stamp means a drizzle integration can be launched from a folder of aligned FITS files even if the .sasd has been deleted or overwritten by a later run — the drizzle integrator reads the stamp from each file's own header and reconstructs the per-frame geometry. Polynomial-registered frames carry only the kind and reference dimensions (their transforms are dense maps, not small matrices), and drizzle treats them as already-warped — deposited on an identity canvas.
.sasd sidecar format, the FITS SASDZ* self-description stamp, and the process-vs-thread executor fallback — is original work by Franklin Marek for SetiAstro Suite Pro. The underlying alignment math (star detection, triangle matching, RANSAC, warp) is the shared engine documented in Stellar Alignment; see that document for citations to astroalign, SEP, OpenCV, scikit-image, scipy, astropy, and the SIP convention.