The main lucky-imaging stacking engine — takes a video or image sequence loaded in the Planetary Stacker Viewer, scores every frame for sharpness, places alignment points automatically, aligns via multi-stage phase correlation and SSD refinement, optionally corrects atmospheric dispersion / field rotation / planetary axial derotation, and produces a drizzle-ready weighted stack of the best frames. This page documents the per-file Stacker dialog; the Batch Planetary Stacker runs this same engine across many files in sequence.
The quality metric that drives frame ranking, computed per frame on a downsampled mono preview:
— variance of the in-mask Laplacian (log-compressed) for sharpness, plus the mean-over-max of the in-mask gradient magnitude for feature richness. §14.1 derives it. Scores are then half-pedestal-corrected and used both for ranking (which frames to keep) and as weights at stack time.
The Stacker is a two-column dialog. The left column holds all the setup: Stack Settings, four collapsible sections (Drizzle, Planet Axial Rotation, Field Rotation, Calibration), Analyze settings, and the five action buttons at the bottom. The right column shows the live quality graph after Analyze finishes, plus a log window that narrates each phase of processing. A progress bar below the action buttons tracks the current phase.
The dialog is non-modal — it stays usable while open, so you can go back to the Viewer, re-pick the ROI or surface anchor, and the Stacker syncs those changes on window activation.
Inherits from the Viewer but can be overridden here:
What fraction of frames, by quality rank, actually makes it into the stack. The default is 20%. You can set it here (0.1 to 100), by dragging the vertical yellow cutoff line on the quality graph after Analyze, or by typing a different value.
Decodes a Bayer mosaic to RGB. For AVI sources opened from the Viewer, this should match whatever the Viewer's Bayer pattern dropdown is set to; for SER files, the pattern comes from the SER header automatically. Off = treat as mono.
Only enabled in Planetary mode. If ticked, every frame is shifted so the planet's centroid lands at the exact centre of the output image, rather than at the position of the reference frame's centroid. Useful when the reference happens to be an off-centre frame and you'd rather have the final stack centred for crop and framing.
Only enabled in Planetary mode with Debayer on. Splits each frame into R/G/B channels, finds each channel's centroid independently, and shifts R and B to align with G before the local AP warp — removing the small prismatic colour fringes that atmospheric refraction smears across a disc at low altitude. §14.12 covers the maths. Leave off for sources at the zenith; it's a noticeable win below about 30° altitude.
Read-only here — shows the anchor you set in the Viewer. In Surface mode it must be set, or Analyze refuses to start. The label shows the anchor in both ROI-space and (if ROI is active) full-frame coordinates for sanity-checking.
Drizzle (Fruchter & Hook 2002) upsamples each aligned frame and “drops” each input pixel's value onto a finer output grid, letting a well-sampled stack of many frames recover resolution beyond the native sampling. For planetary work it's useful when your camera's pixel scale under-samples the diffraction limit.
The “drop shrink” factor, in [0.30, 1.00], default 0.80. Lower values make each input pixel's drop smaller on the output grid (sharper reconstruction, but can produce gaps and noise if coverage is sparse); higher values spread each drop wider (smoother coverage, slightly softer).
Rule of thumb: 1.5× drizzle pairs well with pixfrac 0.80; 2× drizzle pairs well with pixfrac 0.70.
SASpro's drizzle uses a Gaussian kernel exclusively — square or circular kernels produce visible grid artefacts on planetary data. The Sigma field controls the Gaussian width in output pixels; 0 means “auto”, which sets σ = max(10⁻³, pixfrac × 0.5), which is a sensible default for most situations. Nudge up for smoother coverage, down for slightly sharper response.
For long clips of fast-rotating planets like Jupiter, the planet actually rotates during the clip, smearing features in the stack. SASpro's axial-derotation unwinds this by projecting each frame back to a geographic (latitude/longitude) map, shifting longitude by the known rotation rate times frame delta, and reprojecting (§14.13).
Master toggle for the feature.
Picks a sidereal rotation rate from a built-in table (based on standard values):
| Preset | Period | Rate (deg/min) |
|---|---|---|
| Jupiter (System III) | ~9.925 h | ~0.6050 |
| Saturn | ~10.656 h | ~0.5631 |
| Mars | ~24.623 h | ~0.2437 |
| Mercury | ~58.646 d | ~0.004264 |
| Venus (retrograde) | ~243.025 d | ~−0.001029 |
| Moon | ~27.321661 d | ~0.009155 |
| Uranus (retrograde) | ~17.24 h | ~−0.3480 |
| Neptune | ~16.11 h | ~0.3725 |
| Custom… | — | Enter manually |
Camera orientation and sky-side-up conventions can make the default sign of the rotation worse than nothing. Tick Reverse sign if derotation makes smearing worse rather than better; it flips the rate.
The literal rate used, filled in by the preset or editable with Custom. Negative values are allowed (retrograde).
After Analyze completes, click this to open SASpro's planetary disk-picker on the reference frame. You pick the disk centre, radius, pole position angle and sub-observer latitude ratio (b/a). These parameters feed the longitude-shift projection in §14.13. The status label shows cx, cy, r once set.
On alt-az mounts without a field rotator, the field rotates during a session. Over short clips the rotation per frame is often small enough to be absorbed into translation alignment, but over longer clips (or wider FOV) it compounds. Tick Correct field rotation to add a per-frame small-angle rotation search after the translation stage.
Divide every raw frame by a normalised master flat before stacking — the same slot you'd use in AutoStakkert for vignetting, dust motes, Newton's rings and per-pixel QE non-uniformity.
Accepted formats:
.ser — a flat video. Combined internally to a single flat using the Combine method (default Median; Mean also available for very clean runs)..tif, .tiff — a single already-averaged flat image..fit, .fits, .fits.gz, .fz — same, FITS.The flat must be at the same sensor resolution, binning and ROI setting as the lights. The pipeline normalises each channel of the flat to a mean of 1.0 before dividing, so you get back frames at roughly the same brightness as uncorrected ones, just with the response flattened.
The master toggle. If you pick a file via Browse… this auto-enables.
Analyze is the first (and most expensive) stage: score every frame for quality, build a reference frame, autoplace alignment points on the reference, then compute per-frame translation + optional rotation + optional dispersion in parallel.
By default the SSD refinement uses a hill-climb search over integer offsets with a subpixel quadratic polish (fast). Ticking brute force switches to a full radius-5 window scan at every pixel, which is slower but can rescue tough data where the hill-climb falls into a local minimum. See §14.7.
Alignment points are only placed on patches whose mean brightness exceeds this threshold. Default 0.03. Raise it to force APs onto the brighter central disc only; lower it to let APs populate dim limb or terminator regions as well.
Side length of each alignment patch in reference pixels, 16 to 256, default 64. Larger patches average over more detail (robust, but slow to update when the scene changes); smaller patches track finer detail. For planets at native resolution, 64 is a good start.
Centre-to-centre spacing of the AP grid during autoplace. Default 48 — which means APs overlap (spacing < size), giving good coverage. Reduce for denser grids; increase for sparser ones.
After Analyze completes, the right column shows the quality graph: a bright green curve of per-frame quality sorted from best (left) to worst (right). Reference lines:
(min + max) / 2.Click or drag on the graph to move the cutoff and set a new Keep % in one gesture. The Keep % spinbox stays in sync. The log reports every move so you can retrace your choices.
Looking at the curve shape:
Click (2) Edit APs… after Analyze to open the AP editor, a dedicated preview with every current alignment point drawn as a bright green outlined square on the reference image.
Changing the Size, Spacing or Min-mean controls at the bottom triggers an auto-relayout after a 250 ms debounce. On OK, SASpro automatically re-runs the alignment pass (shifts, rotation, dispersion) using the new AP layout — the quality scores and reference frame are preserved from the original Analyze run, so this is a fast operation.
When to edit APs: autoplace works well for most planetary scenes but can place too few APs on faint subjects, or waste APs on completely featureless areas (uniform background sky, saturated highlights). Manual placement lets you concentrate APs where features actually are — crater rims, limb edges, cloud bands.
Click (3) Blink Keepers to visually review every frame in your Keep % set and manually reject individual ones. The dialog shows one keeper at a time with its frame index and quality score; rejected frames are excluded from the stack and the final frame count on the quality graph updates.
The preview applies the same translation + field-rotation transforms that stacking will apply, so what you see is roughly what each frame will contribute to the stack. A Brightness slider at the bottom helps you see detail in faint frames. Rejected frames are drawn in red in the info line; the button to the right of the status also toggles reject.
Blink Keepers is strictly a cull step — it never adds frames you already culled with Keep %, only removes ones that look bad despite scoring well (satellite streaks across a bright patch, aircraft lights, dropped-sync frames whose quality metric happens to be high).
A powerful workflow trick: click (4) Export Aligned SER… after Analyze to write a new SER file where every frame has already been translated (and optionally rotated and derotated and locally warped) into reference alignment. The exported file is uint16 at the current ROI size.
The dialog lets you choose:
When would you want this?
Click (5) Stack Now to run the final integration. Each kept frame is loaded, flat-corrected (if a master flat is loaded), globally translated by its analysis (dx, dy), optionally rotated by its analysis angle, optionally derotated, optionally dispersion-corrected per channel, and then locally warped by the dense displacement field derived from the AP shifts. The warped frames are accumulated with quality weights (§14.15) or deposited into a drizzle grid (§14.14). The final result is a float32 image in [0, 1].
Stacking is heavily parallel: frames are warped in a thread pool whose worker count defaults to min(CPU count, 48). In non-drizzle mode the warped frames are accumulated serially on the main thread with their quality weights to avoid locking overhead; in drizzle mode, deposits land into shared buffers one frame at a time (also serially, to avoid Numba GIL contention under high parallelism).
On completion, the result is pushed as a new SASpro document named <source-basename>_stack (e.g., jupiter_2026_11_21_stack), with full history metadata including the stacking diagnostics (frames kept vs. total, track mode, drizzle settings, worker count). The document is opened in a sub-window ready for stretch, wavelets, deconvolution, or whatever else your post-processing pipeline needs.
The log window records the full run including every phase's elapsed time and the final kept-of-total count, so you have a durable record of exactly what was done.
The analysis and stacking pipeline, in order.
Every frame is downsampled to a mono preview (long axis ≤ 512 px) and scored in two components, both computed only over pixels with signal above a 0.05 floor — so empty sky, black background, and off-planet regions don't dilute the score with their uniform noise. Let:
Compute the Laplacian with a 5×5 kernel (cv2.Laplacian(ksize=5), slightly more noise-resistant than 3), then take the variance over the signal mask:
Variance (as opposed to mean absolute value) punishes frames where sharpness is spatially inconsistent — e.g., half the disc in focus while the other half is soft — which a mean-absolute metric would score the same as a uniformly-slightly-sharp frame.
Variance alone has arbitrary units, so SASpro log-compresses:
which keeps the dynamic range in a comfortable scoring range and prevents one extreme outlier frame from dominating.
Compute the gradient magnitude via Sobel operators:
then take the mean over the signal mask, normalised by the in-mask maximum:
This rewards frames with fine detail distributed across the signal region, as opposed to a single sharp limb edge that would peg the max but leave the rest soft.
Weighted sum (70% sharpness, 30% richness):
After scoring every frame, SASpro subtracts half the minimum from all scores and clips at zero:
This removes the DC pedestal that the log-compression introduces and broadens the relative spread, so differences between good and bad frames are more proportionally meaningful. The adjusted scores are used for both the Keep% ranking and the per-frame weights at stack time (§14.15).
In Planetary mode, the reference is either the highest-quality single frame (Best frame) or the mean of the top N by quality (Best stack (N)). In Surface mode, the reference is always frame 0 regardless of this setting — because the surface coarse-tracking pass locks every frame to frame 0 as its reference anchor; using a quality-ranked frame instead would make the (0, 0) shift of the reference itself ambiguous.
APs are placed on a regular grid over the reference frame. For each candidate position (cx, cy):
ap_min_mean, accept it.Grid spacing is ap_spacing, which can be less than ap_size for overlapping tiles (recommended). If no candidate passes the brightness test (e.g., a very dim target), SASpro falls back to a single AP at the image centre.
In Planetary mode, each frame's shift is computed by finding its brightness centroid and matching it to the reference centroid. The centroid formula:
with M = {(x,y) : I(x,y) > τ} and τ the Threshold setting from the Viewer (or 0.5 default), optionally after a per-frame normalisation. The pre-threshold image is first smoothed with a Gaussian of σ = 1.5 to suppress single-pixel noise spikes that would otherwise bias the moments.
Shift is then (dx, dy) = (ref_cx − cur_cx, ref_cy − cur_cy), with a confidence from the signal strength. If confidence is ≥ 0.25, the SSD refinement pass (§14.7) tightens it further.
Surface mode uses a two-stage approach. First, a coarse pass finds each frame's offset relative to frame 0 via template matching. Second, a fine SSD refinement polishes it.
Classical approach: find frame i's shift relative to frame i−1, chain them. Problem: errors accumulate. Over a long clip, the chain drifts and the final frames end up aligned to an entirely different piece of the Moon than frame 0.
SASpro's approach: lock every frame's template search to frame 0's template directly. Drift literally cannot happen because the reference never moves. The cost is a wider search window per frame (because the predicted offset for a late frame can be large), which SASpro handles with a per-frame predictor seeded by the previous frame's result.
σ_1 = 1.2 and σ_2 = 6.0, then z-normalised and Hann-windowed.search_radius = 96 pixels is centred on the predicted offset in the bandpassed current frame.cv2.matchTemplate with TM_CCOEFF_NORMED) finds the integer-pixel best match in the window:
cv2.phaseCorrelate on the matched patch:
0.65 × NCC + 0.35 × phase-corr-response.The whole clip is split into stride-16 chunks that run in parallel across a thread pool. To seed each chunk's predictor, SASpro first does a sequential keyframe-boundary pass with a wider search radius (2× the normal one), so each chunk starts from a known-good predictor. This gives near-linear speedup without the drift risk of fully-parallel chaining.
For every AP on the reference, SASpro computes the per-AP shift that aligns the current frame's corresponding patch to the reference patch via phase correlation. Each AP:
max_dim = 512).The result is per-AP arrays (ap_dx, ap_dy, ap_resp) that get combined into a global shift via the median (for a single global translation) or into a dense displacement field for local warping (§14.10).
After the coarse translation estimate (from centroid or AP phase correlation), SASpro polishes it by minimising the sum of squared differences between gradient images:
where G is the Sobel gradient magnitude (illumination-robust compared to raw intensity). The optimisation hinges on an aggressive precompute to make per-candidate SSD evaluation nearly free:
The reference gradient is computed once, cropped to the central 80% to avoid edge artefacts, and cached.
The current image is shifted by the coarse (dx_0, dy_0) once, its gradient is computed once, and that cropped gradient is cached.
For each candidate integer offset (i, j) in [−r, +r], SASpro computes SSD directly between slices of the two cached cropped gradients — no warps, no FFTs, no new gradients. Just two array views and a subtraction. This is dramatically faster than the typical “warp-then-correlate” approach that most planetary stackers use.
By default SASpro hill-climbs from (0, 0) through the 8 integer neighbours, moving to whichever lowers SSD most, until no improvement is found or 6 steps have been taken. This finds the global minimum when the SSD surface is smooth (the usual case). If SSD refine: brute force is ticked, the full (2r+1)² window is scanned instead, which is slower but robust to multiple local minima.
Once the best integer offset (i*, j*) is found, a separable quadratic fit at (i*±1, j*) and (i*, j*±1) gives subpixel offsets:
clipped to [−0.75, +0.75]. No additional warps are needed — the four extra SSD evaluations are the same slice-overlap arithmetic as the integer search.
Confidence is derived from how sharp the minimum is relative to the surrounding surface:
with scale λ = 0.002, clipped to [0, 1]. A single, very deep minimum gives high confidence; a flat surface or multiple comparable minima give low confidence, which gates downstream stages like field rotation.
When AP scale = Multi-scale, each AP's shift is computed at three sizes: 2× the base AP size, 1× (the base), and ½×. Each produces its own (dx_k, dy_k, c_k). These are combined into a single AP shift via confidence-weighted average with a slight bias toward the larger scales (which are more stable):
Combined confidence is the same weighted average over c_k. The 1.25/1.00/0.85 weighting is deliberate: large APs give more stable estimates but miss fine detail; small APs catch fine detail but can lock onto noise; the weighted combination gets you the best of both.
Not every AP produces a usable shift — some may land on featureless patches, saturated highlights, or just drift. SASpro rejects outlier APs via a Median Absolute Deviation (MAD) test, which is robust to the exact kind of extreme outliers that break mean/std-based rejection.
For each component (dx, dy), among APs with confidence > 0.15:
APs with z_x > 3.5 OR z_y > 3.5 are rejected. The surviving set drives the dense displacement field in §14.10.
After rejection, the surviving per-AP shifts need to be turned into a smooth per-pixel displacement field so the current frame can be warped into alignment with the reference. SASpro uses inverse-distance-weighted (IDW) interpolation on a coarse grid, then upsamples with cubic interpolation.
At each coarse-grid point p, the weight contributed by AP i at position p_i is:
with power q = 2, confidence c_i, and an optional cutoff radius (3× AP size by default) beyond which an AP's weight is zero. The displacement at p is the weighted average:
The coarse grid is 32×32 (adjusted for aspect ratio); the result is upsampled to full resolution with cv2.resize(INTER_CUBIC). Warping is then cv2.remap with linear interpolation and replicate-border behaviour. This correctly handles differential seeing across the disc — one AP may say shift up-left, another may say shift down-right, and the field smoothly transitions between them without any discontinuity artefacts.
With field rotation correction enabled, each frame's post-translation residual is checked for a small-angle rotation that would further reduce SSD against the reference. The search is two-pass with a hysteresis quality gate.
Sweep angles from −max_deg to +max_deg in steps of coarse_step (default 2°), with a small hysteresis extension so the fine pass can refine against neighbours that fall outside the initial range. For each angle, apply cv2.getRotationMatrix2D + cv2.warpAffine about the rotation centre (planet centroid or surface anchor), compute the gradient, compute SSD against the cached cropped reference gradient. Pick the coarse minimum.
Sweep angles around the coarse minimum in steps of step_deg (default 0.5°). Keep the running minimum.
Both reference and current are centre-cropped around the rotation centre to at most 640×640 pixels — this bounds the warpAffine cost without sacrificing accuracy (rotation signal is strongest near the centre anyway).
A frame's rotation is only accepted if the SSD at the best angle is lower than both the SSD at best_angle − hysteresis_steps × step and best_angle + hysteresis_steps × step, by a minimum lift of 0.2% of the best SSD. If the hysteresis check fails, the rotation is set to 0° (no correction applied) and the frame's quality score is set to 0 so it falls out of the Keep set.
Same quadratic fit as the translation refinement, giving sub-degree angle precision.
Atmospheric refraction depends on wavelength, so at low altitudes a planetary disc shows R and B offset from G — the classic “chromatic fringe”. In Planetary mode with Debayer and dispersion correction both on, SASpro corrects this before the local AP warp.
For each frame (after global translation):
This runs at analysis time, so the correction is per-frame rather than a single global offset — which matters because dispersion varies with altitude over the course of a session.
For long clips of Jupiter and similar fast rotators, the planet rotates measurably across the clip, smearing features in a naive stack. SASpro's derotation maps each frame to geographic coordinates on an oblate-spheroid model, shifts longitude by the known rotation rate times frame delta, and reprojects.
Given disk centre (cx, cy), radius r, pole position angle θ_p, and axis-tilt ratio b/a (semi-minor over semi-major), SASpro builds latitude/longitude grids for the current frame. The sub-observer latitude follows from the axis-tilt:
For derotation, each pixel's longitude is shifted by:
with ω the rotation rate (deg/min) and Δt the frame delta from the reference. The shifted lat/lon is then back-projected to pixel coordinates with the same oblate-spheroid model, and the frame is resampled via cv2.remap. Everything outside the disc is left as-is (no derotation applied).
Drizzle (Fruchter & Hook 2002) upsamples each input frame onto a finer output grid and deposits each input pixel's value with a kernel-weighted contribution. For planetary data, SASpro uses a Gaussian kernel exclusively:
with σ in output-pixel units. The output sigma is scaled by the drizzle factor to maintain consistent spatial smoothing:
and defaults to max(10⁻³, pixfrac × 0.5) when the user's sigma is zero. Deposits accumulate into two shared buffers: a weighted data buffer D and a weight buffer W. On finalisation:
Each input pixel's weight w_i is its frame's quality score — so better frames contribute more, exactly as in the non-drizzle weighted stack.
The implementation uses Numba JIT-compiled kernels (drizzle_deposit_kernel_mono_fast / drizzle_deposit_kernel_color_fast) shared with SASpro's Stacking Suite. A pre-allocated scratch tile is reused per frame to avoid allocation overhead at thousands-of-frames scale.
Without drizzle, the warped frames are accumulated as:
with w_i the half-pedestal-corrected quality score from §14.1. The accumulation happens serially on the main thread (while frames are warped in parallel), which avoids the overhead of per-pixel locks and keeps the memory footprint to one accumulator plus one weight scalar.
If a master flat is loaded, every frame is divided by the flat before anything else:
The flat is loaded once at full sensor size (via load_flat_for_run), then cropped to the current ROI per get_frame call via view-slicing (no copy). For SER-format flats with multiple frames, they're combined to a single master using the chosen method (median by default — robust to cosmic rays and satellite streaks in the flat sequence; mean for very clean runs). The resulting master is normalised to per-channel mean 1.0 before use, so the frames come out at the same brightness scale, just with the sensor's spatial response removed.
Values are clipped to [0, 1] after division to guard against the occasional negative pixel from a very dim master flat.
cv2.phaseCorrelate), template matching (cv2.matchTemplate), log-polar warp (cv2.warpPolar), affine warp (cv2.warpAffine), Sobel and Laplacian operators, Gaussian blur, resampling (cv2.resize, cv2.remap) and demosaic kernels. The drizzle deposit kernels are SASpro's own Numba JIT-compiled implementations (shared with the Stacking Suite). Astropy provides FITS I/O for flats; the SER reader is SASpro's implementation of the Lumenera SER v3 specification; FFmpeg (via SASpro's platform decoder) handles AVI/MP4/MOV/MKV frames. NumPy provides the array engine, PyQtGraph is not used here (the quality graph is custom QPainter-drawn), and PyQt6 provides the UI framework. The Planetary Stacker Viewer and Batch Planetary Stacker, which feed this engine, are documented separately.