Watch a capture folder, stack every new sub-exposure as it lands, and see a cumulative image build in real time — with on-the-fly calibration, star alignment, μ–σ clipping rejection, per-filter mono-to-colour composition, star-trail mode, live metrics plotting and a culling folder for bad frames. The during-session companion to the batch Stacking Suite.
The core incremental update, applied per pixel for every good frame x arriving at count n+1:
During bootstrap this runs over raw normalized frames; after n reaches the bootstrap threshold, it runs over μ–σ-clipped frames with per-pixel Welford statistics (§13.9).
The window is a vertical stack: a top row of controls, a status line showing where files come from and go to, a zoom toolbar with a Settings wrench, the Show Metrics button, the large live-preview area, brightness/contrast sliders, and the Open in New View ▶ button at the bottom. A dynamic Mode label (“Linear Average” / “μ-σ Clipping Average” / “Star-Trail” / per-filter variants) and a Status label at the very bottom narrate what's happening.
The preview is a true QGraphicsView, so pan with click-drag and zoom with the toolbar. The dialog is non-modal — you can leave it running and keep working in SASpro.
Click Select Folder… and pick the directory your capture software writes to. Live Stacking recurses into subfolders, so if your capture program organizes by filter or session into subdirectories, it still works.
By default every supported file type in the folder is scanned:
| Category | Extensions |
|---|---|
| FITS | .fit, .fits |
| TIFF & XISF | .tif, .tiff, .xisf |
| Camera RAW | .cr2, .cr3, .nef, .arw, .dng, .raf, .orf, .rw2, .pef |
| Everyday | .png, .jpg, .jpeg |
If your capture program writes preview JPGs, autostretch TIFFs, debug thumbnails or other derived files alongside the real subs, tick Ignore non-FITS files in Settings (§9) — the scanner then limits itself to *.fit and *.fits only, which is also what you want for most dedicated astro cameras.
Capture software writes a file in chunks and often holds the handle open for a moment after the last byte lands. Reading it half-written would be a disaster. Live Stacking's file-readiness probe handles this:
The watch loop polls every 1.5 s. A re-entrancy guard prevents overlapping scans if a frame takes longer than one tick to process.
Live Stacking supports a single master dark, a single master flat, and a per-filter flat dictionary for mono-to-colour sessions.
FILTER header if present, otherwise by its filename stem. Stored as a dictionary keyed on the filter letter (H, O, S, R, G, B, L). The badge then lists the loaded filter names.During processing (§13.3), per-filter flats take precedence over the global master flat when the incoming frame's FILTER header matches a key. Masters are automatically shape-coerced to match each incoming frame: a mono master applied to a colour frame broadcasts to all three channels; a colour master applied to a mono frame is collapsed to its channel mean. This means you can load a mono dark and still have it correctly subtract from debayered RGB frames.
Click Select Cull Folder… to pick a destination for rejected frames (frames whose star metrics fail the thresholds in §7). When set, flagged frames are moved there with shutil.move; when not set, they're simply logged in the status line and skipped. The folder is created on first use if it doesn't exist.
Four run buttons sit at the right of the top row:
The Frames counter and Total Exp label (format HH:MM:SS) in the status row tick up with each accepted frame; exposure is accumulated from the FITS EXPOSURE or EXPTIME header, or from EXIF on RAW.
Live Stacking has three top-level modes, two of them toggled by checkboxes next to the run buttons.
With both checkboxes off, Live Stacking runs a classical two-phase integration: the first Bootstrap frames (default 24) go into a plain linear running mean while per-pixel statistics are collected; after the bootstrap, the stack switches to μ–σ clipping at Clip threshold σ (default 3.5σ) with Welford's incremental mean/variance updates (§13.9). The Mode label narrates the transition.
Tick Mono → Color Stacking to bucket incoming mono frames by their FILTER header into separate per-filter stacks and composite them live into an RGB preview according to a Narrowband Mapping (§6). Each filter stack goes through its own independent bootstrap→Welford transition. The composite is rebuilt from scratch each new frame, so adding a late OIII frame updates the whole preview immediately.
Mono-to-colour mode works entirely from the FILTER header — frames with no FILTER are not routed to any channel. OSC frames with a BAYERPAT header are still debayered and stacked as colour, bypassing the mono buckets.
Tick ★★ Star-Trail Mode ★★ to replace the running-mean pipeline with an elementwise per-pixel maximum accumulator. No alignment is performed and no clipping is applied; each frame is calibrated, debayered if needed, stretched to target_median = 0.3, and then combined with the current stack as:
— the classic star-trail blend, where every bright pixel wins. Enabling Star-Trail disables Mono→Color (they're mutually exclusive).
When Mono→Color is on, the Narrowband Mapping setting controls how per-filter stacks are composited into the live RGB preview. Change it any time in Settings (§9) — the next preview refresh uses the new mapping.
SASpro recognises these FILTER values (case-insensitive, with common synonyms):
| Key | Matches |
|---|---|
| H | H, Ha, HAlpha, H-Alpha |
| O | O, O3, OIII |
| S | S, S2, SII |
| R | R, Red, Rd |
| G | G, Green, Grn |
| B | B, Blue, Bl |
| L | L, Lum, Lumi, Luminance |
Natural (default) — auto-selects the composite based on what's loaded:
SHO, HSO, OSH, SOH, HOS, OHS — direct three-letter mappings. The letters assign to R, G, B in order:
R = SII, G = Hα, B = OIII — the classic Hubble palette.R = Hα, G = SII, B = OIII.R = OIII, G = SII, B = Hα.R = SII, G = OIII, B = Hα.R = Hα, G = OIII, B = SII.R = OIII, G = Hα, B = SII.Missing channels in a direct mapping default to zero, so an SHO composition with only Hα and OIII loaded shows up red-less until the first SII frame lands.
Every frame is measured for star quality (§13.6) after calibration and alignment but before it gets combined into the stack. A frame is flagged and rejected if any of the three thresholds in Settings is exceeded:
| Threshold | Default | Meaning |
|---|---|---|
| Max FWHM | 15.0 px | Reject frames with median star FWHM above this. Catches heavy defocus and bad seeing spikes. |
| Max Eccentricity | 0.9 | Reject frames with median eccentricity above this. Catches tracking slips, wind gusts, cable snags. |
| Min Star Count | 5 | Reject frames with fewer detected stars than this. Catches clouds, satellite streaks saturating detection, and dome lights. |
Flagged frames appear as red dots on the live metrics plot. If a cull folder is set, the file is moved there; otherwise the status line notes it and continues. The frame counter and exposure accumulator don't tick up for culled frames — what you see in Frames: N is the integrated count only.
Click Show Metrics to open a separate 2×2 scatter window that updates per frame:
Blue dots are kept frames; red dots are culled. Both colours stay plotted for the whole session so you can see when and why conditions changed. The window is independent of the main dialog — close it any time without stopping the stack; reopen with Show Metrics.
Click the orange wrench button in the zoom toolbar to open the combined Live Stack & Culling settings dialog. All values persist across sessions via QSettings under the LiveStack/ key.
| Setting | Default | Range | Effect |
|---|---|---|---|
| Switch to μ–σ clipping after | 24 | 1 – 100 | Bootstrap frame count. The linear running mean collects frames up to this count, then the stack switches to Welford-based clipping. |
| Clip threshold | 3.5 | 0.1 – 10.0 | μ–σ rejection multiplier (§13.9). Lower = more aggressive rejection. |
| Max FWHM (px) | 15.0 | 0.1 – 50.0 | Culling threshold on median star FWHM. |
| Max Eccentricity | 0.9 | 0.0 – 1.0 | Culling threshold on median star eccentricity. |
| Min Star Count | 5 | 0 – 5000 | Culling threshold on detected star count. |
| Acquisition Delay | 3.0 s | 0.0 – 60.0 s | File stability window. Size+mtime must stay unchanged for this long before a file is considered writeable-complete. |
| Star Detection σ | 7.0 | 1.0 – 20.0 | SEP detection threshold above background. Lower = more faint stars (and more false positives in a bright sky). |
| Ignore non-FITS files | off | — | When on, only *.fit/*.fits are picked up. Use when the capture app writes derived files beside the raw subs. |
| Narrowband Mapping | Natural | — | Composition rule for Mono→Color (§6). |
Click Open in New View ▶ to push the current stack (or composite) into SASpro as a new document named LiveStack_<YYYYMMDD_HHMMSS>_<N>f where N is the integrated frame count. The result is always float32 clipped to [0, 1], with metadata recording the source and frame count.
Live Stacking tries a sequence of document-manager entry points (create_numpy_document, new_document_from_numpy, open_numpy, open_array, open_image_array, add_document_from_array) so it works across SASpro's variants; if none fit, it writes a temp 16-bit TIFF and asks the host window to open it as a fallback. If even that fails, the temp path is shown in a dialog so you can grab it manually.
The brightness and contrast sliders affect the preview only — the underlying stack is untouched. Both are pivoted at 0.3 (the normalization target median, so the preview midtone is where the sliders have most leverage):
with c the contrast slider (10–1000 mapped to 0.1–10.0) and b the brightness slider (-100–100 mapped to -1.0–+1.0). The result is clipped to [0, 1] for display.
The streaming pipeline, in order of each frame's processing.
The scanner runs on a 1500 ms QTimer. Each tick, the folder is reglobbed recursively against the current extension set, candidate paths are normalised (case-insensitive, absolute, platform-normalised slashes), and those not yet in the processed set are tested for readiness. Each path accumulates a probe record:
size and mtime from os.stat, refreshed each cycle.since — the last cycle where either size or mtime changed; the file is only ready once now - since ≥ FILE_STABLE_SECS.penalty_until — a cool-down timestamp. On a PermissionError or OSError during the final 1-byte read probe, the penalty is set to now + 10 s so repeated hammering doesn't make matters worse.fails — a counter incremented on every exception during process_frame. After three failures the file is permanently marked processed so it stops blocking the queue, and its name goes to the status line.A _poll_busy re-entrancy guard prevents a long-running frame from stacking up overlapping scans. A threading.Event (_stop_event) is checked at every major pipeline stage so Stop is responsive mid-frame rather than only between frames.
Non-RAW files go through SASpro's shared load_image, which returns the pixel array, a FITS-style header, bit depth and a mono flag. XISF, TIFF, FITS, PNG and JPG all land in the same code path.
Camera-RAW files are decoded through rawpy (LibRaw) with use_camera_wb=True, no_auto_bright=True, output_bps=16, then normalised to float32 in [0, 1] by dividing by 65535. Exposure time, ISO and DateTimeOriginal are pulled from EXIF via exifread and planted into a synthetic FITS header so the rest of the pipeline sees a RAW frame as if it had come from a FITS-aware camera. If EXIF parsing fails the defaults "Unknown" are kept and processing continues.
Calibration is a two-step process applied once per frame after loading:
Dark subtraction is a plain NumPy subtraction in float32. Flat division is dispatched through apply_flat_division_numba, a JIT-compiled helper shared with the Stacking Suite. The flat used is the per-filter flat F_k when (a) Mono→Color is active, (b) the frame's FILTER header matches a key in the per-filter dictionary; otherwise the global master flat is used.
Shape coercion is automatic:
(H, W, 1) so it broadcasts across the three channels.Up-front shape compatibility between the master dark and master flat themselves is checked at load time, so you can't mix a 3D dark with a 2D flat without noticing.
If a frame is marked mono by the loader but its header carries a BAYERPAT, Live Stacking debayers it with debayer_fits_fast (SASpro's shared vectorised debayerer, same one the Stacking Suite uses) before the alignment stage. Supports RGGB, GRBG, GBRG and BGGR patterns.
The first good frame becomes reference_image_2d — a 2D intensity plane used as the alignment reference for the rest of the session (reset clears it, so a new reference is picked on the next good frame after a Reset). Every subsequent frame is reduced to a 2D plane (channel mean for colour, or the mono plane in Mono→Color), aligned to the reference via Astroalign's star-triangle matching, and the resulting 2×3 affine transform:
is applied to the full image (full RGB tensor if the frame is colour, else the mono plane). If Astroalign fails to find a transform (too few stars, catastrophic defocus), SASpro falls back to the identity transform rather than discarding the frame — so even badly-degraded frames at least land on top of the stack rather than vanishing, and will usually be culled by the FWHM/eccentricity thresholds anyway.
Before stacking, each frame is pulled to a common median via SASpro's stretch_mono_image / stretch_color_image helpers (shared with Stacking Suite's normalization block), both with target_median = 0.3. Mono frames pass through the mono stretcher directly; colour frames use the colour stretcher with linked=False (each channel pulled independently) so a per-channel exposure imbalance doesn't skew the composite.
Target 0.3 is deliberately above the typical sky median so the running stack has useful contrast on the live preview from the first frame.
Per-frame star metrics come from SASpro's measure_stars_sep, which wraps SEP (Barbary 2016; the Python port of Bertin & Arnouts' Source Extractor). The call uses:
sep.Background() with default mesh parameters.deblend_nthresh = 32.This matches the metrics SASpro's Blink tool uses, so the FWHM you see live is directly comparable with the quality numbers you'd get from running Blink on the same subs later.
The fourth metric plot — (μ–ν)/σ — is a cheap, scale-stable SNR proxy computed on the cumulative stack (the running stack for colour sessions; the composite for Mono→Color sessions; the current frame if nothing's integrated yet). The construction:
cvtColor with COLOR_RGB2GRAY if available, else a channel mean).C.P of up to 200×200 pixels inside C.If σ_central is zero (empty stack, uniform fill), the proxy returns 0. The result isn't a calibrated SNR — it's a monotonic quantity that should grow roughly as √N for a stack of similar frames, giving you a clean visual indicator that integration is actually helping. Noisy nights with gradients show up as a slower rise; a sudden flattening usually means clouds or focus drift.
Running mean and running variance of the normalized frames are built up in two phases.
Phase 1 — linear bootstrap. For frames 1 through B (the bootstrap frame count), the pixel-wise running mean uses the standard incremental form:
and every frame is also appended to an in-memory buffer. The stack is therefore available from the very first frame — no “warmup” delay before you see anything.
Phase 2 — Welford initialization. When n reaches B, the buffered frames are collapsed into the Welford summary:
and the frame buffer is discarded. The Mode label flips to μ-σ Clipping Average.
Phase 3 — incremental μ–σ clipping. From frame B+1 onwards, each new frame goes through the clipping update in §13.9. In Mono→Color mode every filter runs through this three-phase transition independently at its own frame count, so Hα might still be bootstrapping while OIII has already switched to clipping.
For each incoming post-bootstrap frame, pixels further than k = clip_threshold standard deviations from the running mean are rejected and replaced with the running mean itself before being combined into the stack:
The clipped frame x' is combined into the running mean with the same incremental formula as the bootstrap, and the Welford mean/variance summary is updated in place using the standard online algorithm (Welford 1962):
This never requires revisiting past frames and uses O(H × W) memory regardless of integration count — critical when you're going to stack hundreds of frames over the course of a session. Replacing rejected pixels with the current μ rather than skipping them keeps the running mean's denominator consistent and prevents the variance estimate from drifting when hot pixels or satellite trails are rejected.
Each incoming mono frame whose FILTER key is recognized (§6) goes into its own stack (filter_stacks[key]) with its own frame counter, bootstrap buffer, and Welford summary. The RGB preview is rebuilt from scratch after every new frame using the current mapping. For the Natural mode, the branch decision is made at composite time, not at frame-ingestion time, so adding the first SII frame to an already-running HOO session automatically promotes the composite to the SII palette on the next refresh.
For direct 3-letter mappings (SHO, HSO, …), missing filter stacks default to zeros in that channel, so a partial dataset simply shows the available channels and the missing one as black. If an invalid mapping string is somehow set, the composite falls back to Natural and the mapping is reset.
Star-trail mode replaces steps 7–10 of the normal pipeline (align, normalize, measure, cull, integrate) with a direct per-pixel maximum:
Each frame is still calibrated and debayered as usual and still stretched to target_median = 0.3 — stars across the frames may have different brightnesses but the max accumulator always keeps the brightest sample at each pixel. No alignment is performed (that's the whole point — tracking off, let the stars sweep), no metrics are collected and no culling is done. The frame counter still ticks and the exposure accumulator still accumulates, so you can see how long you've been building the trail.
Every pipeline stage is bracketed by _should_stop() checks so Stop is responsive mid-frame. Exceptions during process_frame are caught and reflected to the status line without crashing the dialog; the file's fail counter in the probe record is incremented and a 10 s penalty window is set. After three failures the file is marked permanently processed so the queue drains past it; this is the right call for e.g. a half-written file the capture app aborted and left behind.
QApplication.processEvents() is called at key points through the pipeline so the UI stays responsive during long frames (large debayer, big affine warp). The dialog connects to QApplication.aboutToQuit to call stop_live() at app shutdown, so you never leave a polling timer running past exit.