免费开源的天体摄影堆叠软件,适用于 Celestron Origin 及其他彩色相机
AI 翻译
Free, open-source astrophotography stacker for the Celestron Origin and other colour cameras
取自仓库 README
正在加载完整 README。 以下为已保存的摘录。
Streaming FITS stacker for astrophotography — runs on ordinary hardware, scales to any frame count.查看完整 README ↗
Trifid Nebula (M20) from one Origin session. Left: the stack the Origin saves itself (FinalStackedMaster.tiff, shown with PixInsight's standard automatic stretch, because the file itself is mid-grey). Right: OriginStack on the same session folder with default settings and no edits afterwards.
OriginStack-<version>-setup.exe for Windows, the macos-arm64 (Apple silicon) or macos-x86_64 (Intel) zip for a Mac, or the Linux tarball. No Python needed.Stuck, or the result looks wrong? Ask in Discussions, or use Report a problem in the app: it fills in the version, system and log for you, as a GitHub issue or as text to paste into an email or a forum post. Nothing is sent until you send it.
Changelog · Code signing policy · Support on Ko-fi
OriginStack is a full-featured Python pipeline for stacking and processing astronomical images, built from scratch: no OpenCV, scikit-image, PyWavelets, astroalign, astroquery or ONNX runtime — just NumPy, SciPy, Astropy and Pillow. The hot paths run in Rust: 80+ multi-threaded native kernels, ~5–150× faster than NumPy/SciPy, about 1.5–1.8× faster end to end on real sessions (see Performance), with a pure-NumPy fallback wherever the module isn't built. It was designed for the Celestron Origin smart telescope, is tested on the ZWO Seestar S50 and S50 Pro, and works with any OSC/DSLR/mirrorless camera. Reads FITS, camera RAW (CR2/CR3/NEF/ARW/DNG/ORF/RW2/RAF/PEF/3FR/MRW/X3F/IIQ — needs rawpy), TIFF (needs tifffile), XISF, and SER (planetary/lucky-imaging video) — mix and match formats freely within one input directory. The core design principle is a streaming architecture: frames are loaded, processed, and freed one at a time, so memory usage stays constant regardless of how many frames you have.
| Galaxy | Emission Nebula |
|---|---|
| Whirlpool Galaxy (M51), eight nights combined | Omega Nebula (M17), 114 x 30 s |
| Emission / Reflection Nebula | Star Cloud |
|---|---|
| Orion Nebula (M42), seven sessions combined | Sagittarius Star Cloud (M24), 138 x 10 s |
All stacked and processed entirely with OriginStack from raw Celestron Origin FITS frames. The Whirlpool and the Orion Nebula were made with version 2.13's default settings from every night of them on hand (8 and 7); --preset nebula for the Omega Nebula and --preset starfield for the star cloud -- see Usage Examples. A multi-night folder is a hierarchical combine: each night is stacked separately, registered onto the deepest one, merged, and post-processed once.
Sky conditions. These are backyard/rooftop sessions under Bortle 7-9 skies (heavy light pollution, urban/suburban), not a dark site -- measured per session with estimate_bortle() (src/quality.py, a rough same-equipment sky-glow bucket from calibrated background level vs. exposure/gain; not survey-grade photometry, see the function's docstring for what it isn't). None of the samples above -- or the two added for the Andromeda Galaxy and Horsehead/Flame Nebula gallery entries on the website -- were shot from a dark site. The pipeline's background extraction, sky-floor correction and gradient removal are what make these usable at all under that much sky glow.
Input: C:/source/Astrophotography/Black_Eye_Galaxy_2026-03-14_21-20-36
Native accel: astro_native ACTIVE - Rust kernels (stacking combine, Lanczos warp, aniso diffusion)
Discovering frames...
Mode: Single folder
Found 84 FITS files: 81 lights, 1 darks, 1 flats, 1 bias
Creating master calibration frames...
[OK] Master bias: 1 frames -> 2048x3056
[OK] Master dark: 1 frames -> 2048x3056
[OK] Master flat: 1 frames -> 2048x3056
[OK] Hot pixel map: 214576 pixels from dark frame
Bias: pedestal=4990.7 ADU noise=0.8 ADU -> Good (low read noise)
Dark: median=4917.1 ADU (163.9030 ADU/s) temp=31.3°C/88.3°F exp=30.0s ISO=200 -> Poor (warm sensor — cool camera or use shorter darks)
Flat: R=0.819/G1=1.192/G2=1.201/B=0.987 vignetting=3.4% -> Good (low vignetting)
Session info (info.json): object='Black Eye Galaxy' bayer=RGGB WCS=yes GPS=yes
======================================================================
PHASE 1: PROCESSING & QUALITY ANALYSIS
======================================================================
Processing 81 frames in parallel (5 workers x 2 native threads)...
CFA equalisation: session-constant from 8 frames (G2 gain 1.00142, 2x2 green offsets -0.66, +0.85, +0.85, -1.05 ADU)
[OK] Accepted: 80/81 (98.8%)
[X] Rejected: 1 (Statistical outlier: 1)
Target: Black Eye Galaxy [Galaxy] conf=100% source=session
Auto Advisor: detected 'Galaxy'
Blend: 75% Galaxy, 4% Globular Cluster, 4% Planetary Nebula
Applied auto settings:
* galaxy_mode False -> True
* stack_method 'auto' -> 'sigma_clip'
* rejection_sigma 3.0 -> 2.8
...
======================================================================
PHASE 2: REGISTRATION
======================================================================
Reference frame: Light0055.fits (score=45.4)
Consensus reference: Light0045.fits (best quality in the middle of the session)
Residual RMS threshold: 6.0px (adaptive: ref FWHM=7.1px, SNR=1.7)
Transparency (relative flux of a fixed star ensemble, session median = 1.00): min 0.96 p10 0.98 max 1.03
======================================================================
PHASE 3: STACKING
======================================================================
Method: sigma_clip
[rust] fused patch-weighted + sigma-clip combine (4.4s)
proper coadd: 80/80 frames, PSF FWHM 6.97-8.55 px (median 7.94), transparency 0.90-1.11
WCS: session info.json solve refined against Gaia (94 stars, rms 0.422 px)
Colour calibration: R x0.739 G x1.000 B x1.114 (48 Gaia stars, white = G2V, B-R scatter 0.074 mag)
======================================================================
PHASE 4: POST-PROCESSING
======================================================================
[OK] Per-channel hot pixel removal: 34 pixels fixed (1.2s)
[OK] Dynamic Background Extraction (8.7s)
[OK] Chroma noise reduction (1.6s)
[OK] Sky pedestal: +702.11 (sky sigma=87.70)
[OK] Directional wavelet denoise (2.3s)
[OK] Star reduction (0.6s)
[OK] Local contrast enhancement (1.6s)
[OK] Sky flattened + neutralised to grey
======================================================================
SUMMARY
======================================================================
Frames stacked: 80 (98.8%)
Integration time: 26.7 minutes
Output: Black_Eye_Galaxy_2026-03-14_21-20-36_stacked.fits (2034x2989x3)
Avg FWHM: 8.17 px (best: 6.95)
Avg SNR: 1.7 (best: 1.8)
Processing time: 2m 43.2s
ProcessPoolExecutor; GPU acceleration via CuPy (--use-gpu)(3, H, W) layout are converted to the pipeline's channel-last (H, W, 3) layout during Phase 1.pip install rawpy; reads the undemosaiced sensor mosaic, so RAW lights go through the same debayer step as OSC FITSpip install tifffile; common intermediate/export format from N.I.N.A., SharpCap, PixInsight, DeepSkyStacker--export xisf).ser file's frames are treated as individual lights--no-reg-residual-reject to keep them)--transparency-min 0.8 drops frames thinned by cloud/haze that the FWHM/SNR gates miss--distortion-model, off by default) — one radial distortion (two coefficients) fitted from every frame's star matches and applied as per-frame corrections in the same single resample pass; only applied if it clearly helps on frames the fit did not see--elastic-registration, off by default) — fits a smooth per-frame local displacement field from matched-star residuals, correcting spatially-varying distortion (differential atmospheric refraction, field rotation, tube flexure) a single global affine can't. Composed into the same single resample pass as the affine warp (no extra blur pass), and works under --drizzle-scale too| Method | Best For |
|---|---|
auto (default) | Percentile rejection under 15 frames, sigma-clip above (tighter from 20 frames) |
sigma_clip | Most sessions (MAD-based iterative rejection) |
winsorized | Like sigma_clip but clips to the boundary instead of rejecting |
percentile | Small sessions: reject outside a percentile range |
esd | Small sessions (generalized ESD / Grubbs test) |
linear_fit | Linear Fit Clipping (robust to non-Gaussian tails) |
ivw | Inverse-variance weighting by each frame's measured noise, no rejection (--uncertainty-map writes the per-pixel error) |
wavelet | Wavelet-subband combine |
median | Robust, no tuning required |
mean | Fastest, no rejection |
After the combine, proper image coaddition (Zackay & Ofek 2017, on by default, --no-proper-coadd to turn off) recombines the frames weighting each spatial frequency by that frame's measured PSF, transparency and noise: sharper stars at no noise cost on the benchmark sessions.
Drizzle super-resolution (--drizzle-scale 2.0) uses Lanczos-3 sub-pixel accumulation by default; --drizzle-kernel {psf,magic} swaps in a PSF-matched or ringing-free Magic-Kernel footprint, and --super-res-iters N adds iterative back-projection refinement. --drizzle-method splat uses true area-overlap drops instead of a Lanczos gather (~6x faster, softer, no ringing); the default resample path accumulates in one native pass (1.4x, 3.9x with --drizzle-pixfrac < 1, same output).
--quality-threshold)Applied in order after stacking. Steps marked ✅ are on by default; ❌ must be explicitly enabled:
--denoiser wavelet; --wavelet-protect 0 turns the structure protection off; --denoiser curvelet is an alias)--denoiser bilateral--denoiser acdnr--denoiser aniso (native/Rust accelerated)--scnr--photometric-calibration (gray-locus; skipped when the stack was already calibrated against Gaia, see below)--deconvolve rl|tv|rl-sv|sparse (RL is GPU-accelerated with --use-gpu; rl-sv is spatially-variant, sparse is FISTA in this project's wavelet basis)--no-star-reduce to disable--no-local-contrast to disable--skip-step edge_bands) — removes the sky excess that rises toward the frame edges, then the final sky flattening + neutralisation (masked large-scale per-channel background → neutral grey)Opt-in variants of the chain: --starless-process runs the denoisers and local contrast (and deconvolution, if enabled) on a starless copy and adds the stars back untouched; --layered-stretch takes the preview's black/white points from a starless copy so a faint galaxy or nebula owns the tonal range instead of being squeezed under the stars' white point. --stretch masked blends a stronger shadow stretch into faint, above-sky structure using a smoothed luminance mask, while tapering the boost out toward bright highlights. --stretch multiscale uses separate broad and fine-scale luminance masks to enhance diffuse emission and filament ridges with different strengths. Both are preview-only; neither changes the FITS.
The auto-advisor enforces a single primary luma denoiser (the curvelet wavelet, with ACDNR only as the fallback sky smoother) — layering several full-frame smoothers erodes faint structure without adding selectivity. Pick explicitly with
--denoiser. NLM, BM3D and MMT were removed after a ground-truth benchmark (tools/bench_denoise_quality.py): NLM damaged star cores, MMT erased fine structure, BM3D was slow and licence-encumbered.
Eight built-in presets set a group of parameters at once (any flag you pass still wins). --auto (on by default) then tunes the rest for the detected target, so a preset is rarely needed:
--preset galaxy # sigma-clip, deconvolution, star reduction, stronger GHS stretch
--preset nebula # sigma-clip 2.5, wavelet + ACDNR denoising, stronger GHS stretch
--preset narrowband # tuned for Ha/OIII/SII: tighter rejection, no chroma NR
--preset starfield # no star reduction or local contrast, gentle stretch
--preset planetary # mean stack, no background extraction, deconvolution
--preset lunar # mean stack, no background extraction, linear stretch
--preset quick # mean stack (no outlier rejection), lighter processing (fastest)
--preset quality # sigma-clip 2.5, per-frame cosmic-ray rejection, deconvolution (slower)
--preset quality is not "the best": the defaults are the settings measured to help. Its per-frame cosmic-ray pass costs minutes for well under 1% change on deep rejection stacks, and deconvolution did not help at typical Celestron Origin signal-to-noise.
--from-stack STACK.fits) — Phase 4 on an earlier run's linear stack, with that run's saved settings; no light frames needed--no-color-calibrate turns it off; the gray-locus method (--photometric-calibration) is the fallback--stretch-color preserve) — the preview curve is applied to brightness only, so stars and galaxy cores keep their colour instead of washing out to white; the default, --stretch-color channel, applies the curve to each channel separately--stretch masked) — softly boosts faint above-sky detail while tapering back toward sky noise and bright highlights; preview JPEG only--stretch multiscale) — separately weights broad diffuse emission and fine positive filament ridges; preview JPEG only--stretch seti) — maps the normalized image median to a target display median of 0.20, with a sigma-based black point; --stretch-color channel (the default) applies it independently per channel, while preserve keeps pixel colour ratios--stretch noisefloor) — solves the GHS strength so the sky noise shows at a fixed size (about 4 of 255 display levels), so a short and a long integration get a curve suited to each instead of the preset's fixed one; preview JPEG only--stretch oklab) — the GHS curve applied to OKLab lightness: mid-tones keep the usual preview colour, while saturated star cores render bright and still coloured instead of being dimmed to fit; preview JPEG only--stretch clahe) — the GHS preview plus contrast-limited adaptive histogram equalisation, blended in only on extended signal (not sky, not stars) to bring out dust lanes and arms; preview JPEG only--photometry) — calibrates the stack against Gaia DR3: per-channel zero points (optional colour terms), airmass from the GPS + time, a Poisson error term from raw bias/flat pairs, and a star catalogue with magnitudes + uncertaintiesRequires Python 3.10+ (CI tests 3.11 and 3.12; the optional native extension targets the CPython 3.10 ABI).
# 1. Clone the repo
git clone https://github.com/hd152/originstack.git
cd originstack
# 2. Create a virtual environment (recommended)
python -m venv .venv
source .venv/bin/activate # Linux/macOS
.venv\Scripts\activate # Windows
# 3. Install core dependencies
pip install -r requirements.txt
# 4. Optional but recommended: native (Rust) acceleration of the hot paths
# (otherwise they run in NumPy). Needs a Rust toolchain + maturin.
# See "Native (Rust) acceleration" below.
pip install maturin
cd ext/astro_native && maturin develop --release # into a venv
# 5. Optional: GPU support (NVIDIA + CUDA) -- pick the CuPy wheel for your CUDA version
pip install cupy-cuda12x
Plate solving needs nothing extra: the built-in solver uses a local Gaia star index (see Plate Solving).
Optional dependencies — all gracefully degraded when absent:
| Package | Feature |
|---|---|
psutil | Memory-adaptive worker/memmap sizing (guarded with a fixed fallback everywhere) |
rawpy | Camera RAW input (CR2/CR3/NEF/ARW/DNG/…) |
tifffile | TIFF input and --export tiff output |
certifi | Up-to-date CA certificates for the HTTPS lookups (Gaia, SIMBAD, astrometry.net), on systems whose own store is missing or stale |
tomli | Reading --config TOML files on Python 3.10 (3.11+ has tomllib built in) |
cupy-cuda* | GPU acceleration (--use-gpu; see GPU Acceleration) |
astro_native (Rust) | 80+ native kernels: stacking combines (incl. Linear Fit Clipping, inverse-variance-weighted, proper coaddition), Lanczos warp (alignment + drizzle), RCD / Malvar / Menon2007 debayer, calibration and hot-pixel passes, L.A.Cosmic, median filters, DBE, anisotropic diffusion, bilateral filter, matched-filter star detection, rigid-transform RANSAC, 2D wavelet transform, blind star-pattern match, 1D + 2D Moffat/Gaussian PSF fits, aperture photometry, and — only in a build with the triage Cargo feature (off by default, so not in release builds; maturin build --release --features triage) — --transient-triage inference (pure-Rust tract ONNX) |
opencv-python, astroalign, scikit-image, PyWavelets, and astroquery are not used anywhere in this codebase — the debayers and the bilateral filter are native Rust kernels (numpy fallback if astro_native isn't built); --merge's cross-night registration (arbitrary field rotation between nights) is src/blind_match.py, also native; Richardson-Lucy's CPU fallback and satellite-trail detection are native/numpy; the wavelet denoiser and multiscale-entropy seeing metric's transform are native (src/wavelet.py); every network catalogue lookup (astrometry.net, Gaia, VizieR, SIMBAD, JPL Horizons) is direct HTTP via src/net_query.py (stdlib urllib) — no dependency for any of them.
python originstack.py -d lights/ -o stacked.fits
OriginStack will automatically detect any calibration frames (dark_*.fit, flat_*.fit, bias_*.fit) in the same directory, build master frames, stack the lights, and write a FITS file and a preview JPEG.
python originstack.py -d lights/ -o stacked.fits -v
Shows per-frame brightness, contrast, star count, SNR, and shift magnitude as each frame is processed.
--auto is on by default: after Phase 1 it recognises the target (from the session info.json, the FITS OBJECT header, the folder name and SIMBAD, then the frames themselves) and tunes the settings for it. Tell it what you imaged with --target-type when it guesses wrong, or turn it off with --no-auto:
python originstack.py -d lights/ -o stacked.fits --target-type globular_cluster
Explicit flags always win over what --auto picks.
For extended targets (galaxies and emission/reflection nebulae), --auto
also makes DBE's diffuse-emission mask more sensitive so faint outer structure
is less likely to be fitted as sky and subtracted. Other target types retain
the standard DBE threshold. The effective threshold is saved in the run config
and reused by --from-stack.
Without --auto, the baseline DBE patch size is 47 px and the preview GHS
settings are b=5.5613, SP=0.1125, HP=0.92, with a black point of
-0.0868 sky sigma. --auto can tune these settings for the detected target;
explicit command-line or config values remain available for deliberate overrides.
python originstack.py -d m51_sessions/ -o m51.fits -v
Where m51_sessions/ contains one subfolder per night. The sessions are pooled into one stack, or, when field rotation between them would crop the corners, stacked separately and merged onto one grid. See Folder Organization Modes.
Nights add up: five Whirlpool Galaxy nights (680 frames, 3.8 h) against the best single night (295 frames, 1.6 h) gave 30% lower sky noise on the same pixels and 12% narrower stars on the same 400 stars (tools/common_star_fwhm.py); the merge weights each night by its measured noise, so the nights with steadier air count for more, and maps every night onto one brightness scale first, so different ISO, exposure or sky brightness combine cleanly. To add a new night without keeping the old frames, stack it with --merge previous.fits; the output chains into the next merge.
A native window for anyone who'd rather not memorize CLI flags:
python desktop_app.py
On Windows, the packaged build needs no Python install at all: download OriginStack-<version>-setup.exe from the latest release and run it (per-user install, Start Menu entry, uninstaller). Prefer no install? OriginStack-<version>-windows-x64-portable.exe is a single file that runs from any folder (it unpacks itself on each start, so it opens a few seconds slower). Prefer the zip? Extract all of it first and run OriginStack.exe from the extracted folder — running the exe from inside the zip preview fails with "Failed to load Python DLL". On Linux, download OriginStack-<version>-linux-x64.tar.gz (and its .sha256) from the same page, extract it and run ./install.sh (per-user install, application-menu entry, ./install.sh --uninstall to remove); it needs glibc 2.35 or newer and has had less real-world use than the Windows build. See Packaging.
The window has two columns:
info.json, FITS header or folder name.--stretch and --stretch-color choice; Apply to JPEG re-renders the preview and overwrites the run's .jpg without re-processing (the FITS outputs are unchanged).Leave Output file blank and the stack is saved next to the light-frames folder, never overwriting an earlier one.
Closing the window while a run is in progress asks for confirmation first; a native OS notification fires when a run finishes, so you don't have to keep the window in view.
Languages. The app follows your system language: English, Deutsch, Español, Français, Italiano, Nederlands, Português (Brasil), 日本語 or 简体中文. Pick another from the menu in the header (it applies from the next start), or set ORIGINSTACK_LANG (e.g. ORIGINSTACK_LANG=fr). The log and the command line stay in English, so a log attached to a problem report reads the same for everyone. The translations are machine-drafted; corrections are welcome as issues or pull requests (src/locales/<code>.json, keyed by the English text).
python originstack.py -d lights/ -o galaxy.fits --target-type galaxy -v
--target-type galaxy tells --auto what it is (it would usually work it out from the session anyway): the galaxy's halo is kept out of background extraction, and stars are trimmed. --deconvolve rl can sharpen spiral arms on bright, high signal-to-noise data; on typical Celestron Origin sessions it was measured not to help.
python originstack.py -d lights/ -o nebula.fits --target-type emission_nebula -v
python originstack.py -d ha_lights/ -o ha_stack.fits \
--preset narrowband \
--white-balance none \
--scnr \
--stack-method sigma_clip --rejection-sigma 3.0 \
-v
python originstack.py -d frames/ -o jupiter.fits \
--preset planetary \
--deconvolve rl \
--no-background-extraction \
--no-star-reduce \
-v
python originstack.py -d lights/ -o starfield.fits \
--preset starfield \
--stack-method sigma_clip \
-v
The starfield preset skips star reduction (the whole point of the target)
and keeps post-processing minimal so points stay sharp and colour-true.
python originstack.py -d lights/ -o cluster.fits --target-type globular_cluster -v
python originstack.py -d lights/ -o m78.fits --target-type reflection_nebula -v
Neither has a preset; --target-type gives --auto the type, and it blends in matching denoise/stretch settings.
# First night: normal run; the output FITS is a mergeable linear stack
python originstack.py -d night1/ -o m51.fits --auto -v
# Later nights: process only the new frames, fold in the saved stack (seconds)
python originstack.py -d night2/ -o m51_v2.fits --auto --merge m51.fits -v
Each previous stack is registered onto the new session's grid (handles cross-night field rotation via a blind rigid star-pattern match, no assumption about the angle), mapped onto the new stack's flux scale (so sessions with different exposure or ISO combine correctly), and combined as a per-pixel mean weighted by each stack's measured noise. The output chains into future merges.
python originstack.py -d lights/ -o drizzled.fits \
--drizzle-scale 2.0 \
--drizzle-pixfrac 0.7 \
-v
Colour calibration against Gaia needs no flag: it is on by default whenever the stack has a sky position (a Celestron Origin session always does) and the run is online.
# Built-in solver: no API key needed (see "Plate Solving" below)
python originstack.py -d lights/ -o stacked.fits \
--plate-solve \
-v
# How did the night go? Drift, rotation, focus trend, transparency
python originstack.py -d lights/ -o stacked.fits --session-report
# Drop frames thinned by cloud (relative flux < 0.8), remove sensor banding
python originstack.py -d lights/ -o stacked.fits --transparency-min 0.8 --banding-removal
# Look for asteroids and stack along the first tracks
python originstack.py -d lights/ -o stacked.fits --moving-objects-stack
# Measured noise + which structure is repeatable, session-wide distortion fit
python originstack.py -d lights/ -o stacked.fits --noise-validate --distortion-model
# Period/transit search on the light curves (needs a session WCS)
python originstack.py -d lights/ -o stacked.fits --photometry-timeseries --lightcurve-analysis
# AAVSO report for a variable star, 5-minute bins
python originstack.py -d lights/ -o stacked.fits --photometry-timeseries --aavso-report --aavso-star "SS Cyg" --aavso-obscode ABC --aavso-bin 5
python originstack.py -d lights/ -o stacked.fits --debug registration
Writes PNG overlay images and shift statistics to _registration_debug/. Use this when frames aren't aligning correctly.
# Dry run: walk the whole tree, score every light, report what would be flagged
python originstack.py --quality-sweep -d "G:\astro\Astrophotography"
# Apply: rename flagged frames to *.fits.rejected (invisible to stacking)
python originstack.py --quality-sweep -d "G:\astro\Astrophotography" --apply
# Change your mind: restore every flagged file
python originstack.py --sweep-undo -d "G:\astro\Astrophotography"
Uses the exact same quality gate as stacking: hard failures (no stars, SNR < 0.5,
near-zero contrast), statistical outliers vs each folder, and scores below
--quality-threshold% of the folder's 90th-percentile reference.
python originstack.py -d lights/ --health-check
Analyses calibration quality (bias noise, dark thermal current, flat vignetting) and reports any ISO or dimension mismatches — without actually stacking anything.
Every run saves its effective settings next to the output as <output>_config.toml (including what --auto chose):
# See the resolved parameters without processing anything
python originstack.py -d lights/ -o stacked.fits --target-type galaxy --dry-run
# Reapply an earlier run's settings to new data
python originstack.py -d lights2/ -o stacked2.fits --config stacked_config.toml
A saved config fixes --auto's choices for the session it came from; re-running with --auto on different data is not the same thing.
OriginStack supports three ways of organizing your input files. Single folder and multiple sessions are auto-detected; mosaic needs a flag. Different targets go in separate runs.
Put all your FITS files (lights and optional calibration frames) in one directory and point -d at it.
lights/
├── bias_001.fit (optional)
├── dark_001.fit (optional)
├── flat_001.fit (optional)
├── light_001.fit
├── light_002.fit
└── ...
python originstack.py -d lights/ -o stacked.fits
OriginStack builds master calibration frames from any bias/dark/flat files it finds, then processes and stacks all light frames into a single output FITS.
Use this for the same target across several nights. Create one subfolder per session.
m51_sessions/
├── 2024-04-01/
│ ├── info.json (optional — Celestron Origin metadata)
│ └── light_001.fit ... light_NNN.fit
├── 2024-04-03/
│ ├── dark_001.fit
│ ├── info.json
│ └── light_001.fit ... light_NNN.fit
└── 2024-04-07/
├── flat_001.fit
├── info.json
└── light_001.fit ... light_NNN.fit
python originstack.py -d m51_sessions/ -o m51_deep.fits -v
OriginStack predicts from each session's metadata how far the field rotates between sessions (an alt-az mount rotates the field as the target tracks) and picks one of two ways to stack:
--combine-sessions to force): every light from every session is quality-analysed, registered and stacked together, with all calibration frames merged into shared masters. Used when the sessions line up.--hierarchical to force): used when the sessions differ by more than 3° of field rotation beyond what any one session spans, because pooling would crop the output to the small region every frame covers. Each session is stacked on its own (its own calibration, quality analysis and registration), the stacks are registered onto the one with the most integration time by a rotation-agnostic star match, and post-processing runs once on the combined stack.If the metadata can't be read, the sessions are pooled and the log says so.
info.json support: If a subfolder contains an info.json from the Celestron Origin app, OriginStack reads the target name, Bayer pattern, GPS position and WCS (RA/Dec/FOV/orientation) from it automatically. If the sessions report different Bayer patterns (e.g. mixing cameras), a warning is printed before stacking; per-frame FITS headers always take priority over info.json defaults.
--mosaic)Use this when your subfolders are adjacent sky panels of the same large target (for example the tiles of the Celestron Origin's Mosaic Mode), and you want them stitched into a single wide-field image. You usually do not need the flag: when the sessions' info.json pointings are more than a quarter of a field apart and their fields overlap, the run switches to mosaic mode by itself (--combine-sessions or --hierarchical override it).
panels/
├── panel_1/
│ ├── info.json (provides WCS — or use --plate-solve)
│ └── light_001.fit ... light_NNN.fit
├── panel_2/
│ ├── info.json
│ └── light_001.fit ... light_NNN.fit
└── panel_3/
├── info.json
└── light_001.fit ... light_NNN.fit
python originstack.py -d panels/ -o mosaic.fits --mosaic -v
Each subfolder is stacked on its own (Phases 1–3), then the panels are resampled onto one grid (the deepest panel's own projection, so it is not resampled), matched to each other in their overlaps with one least-squares solve for all of them (a gain per panel and channel for transparency, then a plane per panel and channel for residual gradient), and blended with feathered, inverse-noise-variance weights. Post-processing runs once, on the whole mosaic, so background extraction and the stretch see one image.
Requirements:
info.json (Celestron Origin, refined against Gaia) or from plate solving (--mosaic turns --plate-solve on)What's in -d | Mode selected |
|---|---|
| FITS files directly in the directory | Single folder |
| Subdirectories containing FITS files | Several sessions: pooled, or stacked separately and merged when field rotation would crop the corners |
| Subdirectories pointed at overlapping but different sky (mosaic tiles) | Mosaic: each panel stacked, then stitched |
Subdirectories + --combine-sessions / --hierarchical | Several sessions, forced pooled / forced separate-then-merge |
Subdirectories + --mosaic | Mosaic |
Most post-processing is on by default. Here are the disable flags:
| Feature | Default | Disable with |
|---|---|---|
| Background extraction (DBE) | ✅ on | --no-background-extraction |
| Proper image coaddition | ✅ on | --no-proper-coadd |
| Luma denoising (wavelet) | ✅ on | --denoiser none |
| Chroma noise reduction | ✅ on | --no-chroma-nr |
| Star reduction | ✅ on | --no-star-reduce |
| Colour calibration against Gaia (online, needs a sky position) | ✅ on | --no-color-calibrate |
| Colour-preserving stretch | ❌ off | --stretch-color preserve |
Star removal (writes a _starless.fits sidecar; main output keeps stars) | ❌ off | --remove-stars |
| ↳ learned star removal instead of the inpaint (EXPERIMENTAL; sidecar = starless linear stack) | ❌ off | --remove-stars-method nn |
| Local contrast enhancement | ✅ on | --no-local-contrast |
| Chromatic aberration correction | ✅ on | --no-ca-correction |
| Cosmic ray rejection | auto | --cosmic-ray-rejection / --no-cosmic-ray-rejection (auto-skipped on deep rejection stacks) |
| Quality filtering | ✅ on | --no-quality-filter |
| Affine registration | ✅ on | --no-affine |
| Elastic local registration | ⬜ off | --elastic-registration |
| Primary denoiser choice | auto (wavelet) | --denoiser {wavelet,acdnr,bilateral,aniso,none} |
| Deconvolution | ❌ off | --deconvolve {rl,rl-sv,tv,sparse} |
python originstack.py -d <dir> -o <output.fits> [options]
| Flag | Description |
|---|---|
-d, --directory | Input directory (required unless --from-stack) |
-o, --output | Output FITS path, or a folder to write <session>_stacked.fits into (default: <session>_stacked.fits in the current folder) |
--preset NAME | Apply named preset (quick, quality, galaxy, nebula, narrowband, starfield, planetary, lunar) |
--config PATH | Load parameters from TOML file |
--no-auto | Disable the heuristic target classifier (on by default; detects target type and optimises settings automatically) |
--target-type TYPE | Tell --auto what you imaged: galaxy, emission_nebula, reflection_nebula, planetary_nebula, globular_cluster, star_field, wide_field |
--stack-method METHOD | Stacking algorithm (auto, mean, median, sigma_clip, percentile, esd, winsorized, linear_fit, ivw, wavelet) |
--debayer-method METHOD | Debayer algorithm: rcd (default), malvar, menon2007 |
--white-balance METHOD | White balance (grayworld, whitepatch, none) |
--bg-method METHOD | Background extraction (dbe, mesh, wavelet; physical is experimental) |
--drizzle-scale N | Super-resolution scale (1.0 = off, 2.0 = 2×) |
--elastic-registration | Local (non-rigid) displacement correction on top of the global affine (off by default) |
--distortion-model | One radial distortion for the whole session, applied per frame (off by default; skipped with --elastic-registration) |
--transparency-min 0-1 | Drop frames whose relative transparency is below this (0 = report only) |
--banding-removal | Remove row/column banding from each calibrated frame (off by default) |
--cfa-drizzle | Bayer-aware drizzle of the measured samples (opt-in; for undersampled data) |
--noise-validate | Odd/even half-stacks: measured noise map + repeatable-structure map |
For the full CLI reference with all flags and defaults, see PROJECT_SPEC.md.
Measured end to end on real Celestron Origin data (2048×3056 frames, Windows 11, an 8-core / 16-thread CPU with 16 workers), comparing this release against the development build just before its optimisation work (same data, same config, same machine, run back to back):
| Session | Before | Now |
|---|---|---|
| Fireworks Galaxy, 148 × 10 s frames, single session | 3 min 50 s | 2 min 27 s – 2 min 40 s (~1.5×) |
| Orion Nebula, 7 sessions (456 frames) combined hierarchically | 15 min 12 s | 8 min 16 s (~1.8×) |
Where the time went on the Fireworks run:
| Phase | Before | Now |
|---|---|---|
| Phase 1 — load, calibrate, debayer, quality | 98 s | 51–52 s |
| Phase 2 — registration | 41 s | 31–40 s |
| Phase 3 — align + stack | 51 s | 26–33 s |
| ↳ per-frame alignment (141 frames) | 30 s | 12–15 s |
Drizzle (2×, 29 frames of the same session, Lanczos-3 kernel, just the accumulation loop):
| Mode | Before | Now |
|---|---|---|
Resample, --drizzle-pixfrac 1.0 | 12 s | 11 s |
Resample, --drizzle-pixfrac 0.7 | 49 s | 12 s (~4×) |
--drizzle-method splat (area-overlap drops, opt-in) | — | 2 s (~6× the resample loop) |
Notes on reading these numbers:
--no-session-cfa-eq restores the per-frame path), taking Debayer from ~1.26 s to ~0.45 s per frame under 16 workers; and the temporary frame memmaps are no longer flushed to disk just before being deleted. Together they cut a 158-frame run from 135 s to 126 s with a bit-identical stack.Same lights, same bias/dark/flat, one machine (Windows 11, 8-core / 16-thread CPU, 64 GB RAM), each tool at default-style settings. Three Celestron Origin sessions: Omega Nebula (114 × 30 s), Sunflower Galaxy (158 × 20 s) and Sculptor Galaxy (532 × 10 s). Reproduce on your own data with tools/bench_vs_siril.py. To compare a tool that has no command line (Astro Pixel Processor, PixInsight), stack the same lights in it, save the linear result as a colour FITS and pass it with --external STACK.fits --external-seconds <your time>; it is scored with the same star-width and noise measurements.
OriginStack first, Siril second. "Stack only" leaves out OriginStack's finishing steps (background extraction, denoising, stretch), which Siril's script does not have:
| Session | Stack only | OriginStack, finished image | Peak memory | Star width |
|---|---|---|---|---|
| Omega, 114 frames | 44 s / 54 s | 62 s | 12.0 / 6.5 GB | 4.42 / 4.56 px |
| Sunflower, 158 frames | 54 s / 67 s | 81 s | 14.3 / 8.9 GB | 3.51 / 3.60 px |
| Sculptor, 532 frames | 128 s / 159 s | 144 s | 9.1 / 11.2 GB | 2.66 / 3.01 px |
--offline) and does much that the other tools leave to you.All three sessions were re-measured on 2026-10-20 with this version, on an idle 8-core laptop (i9-10885H). Three sessions from one camera are a small sample.
How star width is measured matters more than it looks. It is a Gaussian fit to each of up to 400 bright, unsaturated, isolated stars, at the same positions in both stacks and on each stack's own pixel grid (no resampling), and the median is reported. An earlier version of this section compared each stack's FWHM over its own detected star list and concluded OriginStack's stars were tighter; that came from which stars were picked and was withdrawn. Measured properly, OriginStack's stars were then genuinely 13% wider than Siril's on Omega, until a hot-pixel filter that was clipping the cores of bright stars was found, by switching Phase 1 steps off one at a time, and fixed (see the changelog). Registration accuracy (about 0.1 px between frames), the warp kernel, rejection, weighting and normalisation were all checked and are not the cause of anything above.
Things these numbers do not show: three sessions from one camera are a small sample; Siril and DeepSkyStacker have many settings that were left alone; and DeepSkyStacker was not re-run after the fix.
Every Celestron Origin session folder also holds the telescope's own result, FinalStackedMaster.tiff. Same lights, four sessions from one Origin; OriginStack at its defaults, Siril with the settings of the comparison above (winsorized sigma clip 3/3, same bias/dark/flat). Reproduce with tools/bench_vs_origin.py (--siril-stack adds Siril).
| Session | Star width, OriginStack vs Origin | Noise at star scale R/G/B, OriginStack / Origin | Star width, OriginStack vs Siril | Noise at star scale R/G/B, OriginStack / Siril |
|---|---|---|---|---|
| Black Eye Galaxy, 81 × 20 s | 8.15 vs 8.38 px (4% sharper) | 1.00 / 1.04 / 1.06 | 8.21 vs 8.26 px (0.7% sharper) | 0.94 / 1.09 / 0.97 |
| Sunflower Galaxy, 158 × 20 s | 3.54 vs 4.25 px (17% sharper) | 0.96 / 0.93 / 0.95 | 3.50 vs 3.58 px (1.5% sharper) | 1.02 / 1.03 / 1.02 |
| Crab Nebula, 133 frames | 2.54 vs 3.50 px (27% sharper) | 1.00 / 0.92 / 0.95 | 2.54 vs 2.77 px (8% sharper) | 1.04 / 0.99 / 1.01 |
| Hercules Cluster, 56 frames | 5.67 vs 5.98 px (5% sharper) | 0.99 / 0.96 / 1.01 | 5.68 vs 5.74 px (1% sharper) | 1.00 / 1.04 / 1.01 |
ext/astro_native/ is an optional PyO3/maturin crate of 80+ hot-path kernels, each with a numpy fallback (absent module → pure-Python path). It covers the Phase-1 calibration/cosmic-ray/debayer hot paths, the Phase-2/3 warp + combine hot path, drizzle, background extraction, star detection, RANSAC, several denoisers, the photometry aperture loop, PSF profile fitting, and --transient-triage inference (pure-Rust tract ONNX runtime — no Python ONNX dependency). The triage path is behind the Cargo feature triage, off by default and not in release builds; without it --transient-triage disables itself with a warning. A representative sample:
| Kernel | Speedup vs numpy/scipy |
|---|---|
sigma_clip_combine (default stack method) | ~37× |
esd_combine / percentile_clip_combine / median_combine | ~24× / ~13× / ~6× |
| Fused patch-weighted + sigma-clip combine | ~100× |
| Per-frame Lanczos-3 warp (alignment + drizzle resample) | ~5× / ~26× |
| Malvar debayer | ~2× |
| Phase 1 calibration (bias/dark/flat/finite check/clip, one pass) | ~4× single-thread (40 → 9 ms) |
| Bayer hot-pixel repair / hot-pixel map replacement | ~25× / ~55× single-thread (715 → 29 ms / 666 → 12 ms) |
| RGB hot-pixel repair (luma + median + MAD + replace, fused) | ~22× single-thread (1174 → 52 ms) |
| Per-frame pre-gradient removal | ~40× (349 → 8 ms) |
| Debayer stage as a whole (medians, G1/G2 + grid equalisation in place) | ~2.2× under 16 workers (2207 → 1002 ms/frame) |
| L.A.Cosmic cosmic-ray rejection | ~2× under real parallel load |
| Median filter (3×3 network / larger windows) | ~13× / ~26×; the 3×3 picks an AVX2 version at run time (a further ~1.7×) |
| DBE surface fit + patch sampler | ~2.4× / ~31× |
| Anisotropic diffusion | ~37× |
Batch aperture photometry (--photometry / --photometry-timeseries) | ~150× |
CFA drizzle frame splat (--cfa-drizzle) | ~15.7× (407 s → 26 s, 148 frames) |
| White balance (default Phase 1 step, bit-identical to numpy) | ~6.9× single-thread |
| Lanczos-3 warp of a rotated frame (alignment, drizzle) | ~5× vs the previous kernel (2187 → ~415 ms/frame), and Phase 3 now warps only the common crop |
| Fused drizzle accumulate / area-overlap splat | ~1.4× (3.9× with pixfrac < 1) / ~6× |
Most Phase 1 kernels are bit-identical to the numpy code they replace; the rotated Lanczos warp takes its weights from an interpolated table (99.985% of output values identical, the rest one ulp off). Speedups were measured single-threaded unless noted; under a full worker pool memory bandwidth, not arithmetic, is the limit, which is why the whole-stage figures are smaller than the single-kernel ones.
See dev-notes/native-kernels.md for the full kernel-by-kernel notes.
Build (needs a Rust toolchain + pip install maturin):
# into a virtualenv:
cd ext/astro_native && maturin develop --release
# system Python (no venv): build a wheel and install it
cd ext/astro_native && python -m maturin build --release
pip install --force-reinstall target/wheels/astro_native-*.whl
# add --transient-triage support (tract ONNX runtime, longer first build):
python -m maturin build --release --features triage
At runtime the startup banner reports Native accel: astro_native vX ACTIVE …, and each accelerated step logs a [rust] … line. The aligned stack is a float32 memmap that Rust views zero-copy, so the streaming memory model is preserved. GPU (--use-gpu) additionally accelerates the registration warp and Richardson-Lucy deconvolution via cupy.
--from-stack STACK.fits re-runs only post-processing (Phase 4) on an earlier run's linear output, with that run's saved settings; flags you pass override them. No light frames are needed, and the slow early steps are cached next to the stack, so a re-run takes seconds to tens of seconds:
python originstack.py --from-stack stacked.fits -o tweak.fits --stretch arcsinh
An interrupted run resumes from its last checkpoint. With --keep-checkpoint the checkpoint also survives a successful run, so a re-run with different stacking settings reuses Phase 1 and one with different post-processing settings reuses Phases 1-3.
originstack.py ← thin backward-compatibility entry point
└── src/
├── cli.py ← argument parsing, process_directory(), main()
├── pipeline.py ← four-phase orchestrator (stack_target)
├── frame_processor.py ← Phase 1: parallel per-frame load/calibrate/quality
├── registration.py ← Phase 2: shift calculation, affine/RANSAC
├── stacking.py ← Phase 3: alignment, cropping, combine
├── postprocess.py ← Phase 4: up to 20-step post-processing chain
├── debayer.py ← Bayer demosaicing, hot pixels, white balance
├── quality.py ← star detection, FWHM, quality metrics
├── background.py ← DBE, mesh sky extraction, floor normalisation
├── denoising.py ← curvelet wavelet, bilateral, ACDNR, aniso, stretch
├── psf_deconvolution.py ← PSF estimation, Richardson-Lucy
├── io_fits.py ← FITS load/save, master frame creation
├── frame_discovery.py ← automatic frame classification
├── auto_settings.py ← heuristic target classifier (--auto)
├── plate_solve.py ← built-in Gaia solver, ASTAP, astrometry.net
├── desktop_app.py ← the desktop app window (tkinter)
├── desktop_control.py ← desktop app: form schema, form → CLI args, run manager
├── gpu_context.py ← CPU/GPU abstraction (numpy ↔ cupy)
├── models.py ← Config, FrameInfo, ProcessingStats
├── health_check.py ← calibration analysis
└── utils.py ← print helpers, formatting
See PROJECT_SPEC.md for a detailed architecture and feature reference.
pip install -r requirements-dev.txt # pytest, pytest-xdist, ruff
pytest -q
pytest -q -n auto # parallel, ~3x faster
# Lint (both gated in CI)
python -m ruff check .
python tools/lint_conventions.py
# Run a specific test
pytest tests/test_core.py::test_calculate_shift_recovery -v
# CI smoke test (generates synthetic data, then stacks it)
python tools/create_synthetic.py
python originstack.py -d synthetic_data -o ci_synthetic_stack.fits \
--debayer-method malvar --white-balance grayworld --stack-method median
Frames not aligning? Use --debug registration:
python originstack.py -d lights/ -o out.fits --debug registration
# Diagnostics written to _registration_debug/
Want shift and quality data per frame? Run with -v:
python originstack.py -d lights/ -o out.fits -v 2>&1 | tee run.log
Not sure what's happening? Run with --dry-run first:
python originstack.py -d lights/ -o out.fits --dry-run
See QUICK_REFERENCE.md for guidance on interpreting shift patterns and quality metrics.
--offlineOriginStack works entirely on your machine. It goes online in these cases only, and never uploads your frames except to astrometry.net when you choose that plate solver:
| What | When | What is sent |
|---|---|---|
| SIMBAD lookup of the target | On a normal run, when the object name (from the session file, the FITS OBJECT header, or the folder name) is not in the built-in table | The name string only |
| Gaia star index tiles | --plate-solve (built-in solver), and on a normal run with a session info.json solve (to correct that WCS on the stack; --no-wcs-refine turns it off), for sky areas not yet in the local index | Sky coordinates of a 5°×5° tile; tiles are cached, so each area is fetched once |
| astrometry.net | --plate-solve --plate-solver astrometry (or auto when the built-in solver fails) | The image, to solve its position (needs your API key) |
| Gaia / VizieR / SIMBAD catalogues | Colour calibration on a normal run with a sky position (--no-color-calibrate turns it off); --photometry, --photometry-timeseries, --annotate | Sky coordinates of the field. Gaia results are cached next to the star index (gaia_cones/), so the same field on another night or a --from-stack rerun is answered locally, also under --offline; ORIGINSTACK_NO_GAIA_CACHE=1 turns that off |
| AAVSO VSP | --aavso-report | The variable star's name or the target's sky coordinates, to fetch the comparison-star chart |
| JPL Horizons | Comet mode with a designation: on a normal run when the target is a comet (C/2025 R2, 3I/ATLAS, ... from the OBJECT header, session or folder name), or --comet-designation | The comet designation, three observation times and the session's GPS position rounded to 0.1° (about 10 km); three requests per run |
| Self-update check | Once per CLI run or desktop-app launch | Nothing — an anonymous GET of GitHub's public releases API, no request parameters, no identifying data |
Pass --offline (a checkbox in the desktop app's Core options) to make no network requests at all: the target lookup is skipped, and the features in the table that need the network are turned off with a note in the log. Everything else is computed locally; colour falls back to the field's own star colours (white balance and, where a preset enables it, the gray-locus calibration). This is enforced where the requests are made rather than caller by caller, and a test counts real connection attempts.
The self-update check is the one thing that isn't gated by --offline itself (it runs before a run's settings are even read), but it never blocks anything and fails silently: a background thread checks GitHub once, and if a newer release exists, the CLI prints one line at the end of the run and the desktop app shows a small clickable "Update available" note in the header (not a system notification). The desktop app passes the result to its Tkinter main-thread poll, so the check can finish without touching UI widgets from the worker thread. Set ORIGINSTACK_NO_UPDATE_CHECK (to anything) to disable it outright; it's also skipped automatically whenever --offline was used in the same process.
python originstack.py -d lights/ -o stacked.fits --plate-solve
The default solver (--plate-solver auto) is built in: it matches the stack's stars against a
local Gaia DR3 star index near a position hint — the session info.json solve on Celestron Origin
data, or the RA/DEC/OBJCTRA header keywords — in about a second. Index tiles it doesn't have
yet are downloaded once from Gaia and cached (%LOCALAPPDATA%\OriginStack\star_index on Windows,
~/.cache/originstack/star_index elsewhere, or $ORIGINSTACK_STAR_INDEX). For a machine that is
never online, fill the index ahead of time:
python tools/build_star_index.py --all # whole sky, ~1650 tiles, ~80 MB
python tools/build_star_index.py --ra 83.6 --dec 22 --radius 10
python tools/build_star_index.py --status
With --offline, the built-in solver still runs from the cached tiles. If it fails and an
astrometry.net key is set (ASTROMETRY_API_KEY, free from
nova.astrometry.net), auto falls back to
astrometry.net; --plate-solver local never goes online for the image.
When plate solving succeeds, WCS keywords (CRVAL, CRPIX, CD matrix) are written to the FITS header and the field's primary object is identified via the SIMBAD database. The output FITS will then display coordinate grids in DS9, AstroImageJ, PixInsight, and similar tools.
Alternatively, use the ASTAP solver:
python originstack.py -d lights/ -o stacked.fits --plate-solve --plate-solver astap
GPU acceleration uses CuPy and is opt-in (--use-gpu).
pip install cupy-cuda12x # or cupy-cuda11x
python originstack.py -d lights/ -o stacked.fits --use-gpu
Notes:
--use-gpu measured slower than CPU-only on a real session, because most of Phase 1 is CPU work and the GPU path limited the worker count. Phase 1 therefore runs on the CPU process pool unless the card can host one GPU worker per CPU core (--gpu-phase1 on forces it); later phases still use the GPU.OriginStack is free. If it saves you time, you can leave a tip on Ko-fi; the desktop app links there too ("Support OriginStack" in its header). Bug reports and sample sessions that show a problem help just as much: open an issue.
Copyright (C) 2026 Tom Davenport
OriginStack is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. It is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE for the full text.
Releases up to and including 2.10.1 were published under the MIT License, and those releases stay available under it. Third-party dependency licenses are listed in THIRD_PARTY_NOTICES.md.
info.json<output>_photometry.csv--photometry-timeseries) — aperture-photometers a fixed Gaia star list on every registered sub and ensemble-calibrates, writing per-frame + per-star CSVs with a variability flag; --photometry-target "RA,DEC" reports one star--banding-removal) — per-row/column offsets removed from each calibrated Bayer frame before debayering, per colour plane, only where significant; a clean frame is left essentially untouched--session-report) — <output>_session.png + .csv: FWHM, transparency, SNR, background, drift, field rotation, ellipticity, residual and temperature against time, with the drift rate, any periodic tracking error, the rotation rate and any focus-vs-temperature drift printed--noise-validate) — stacks the odd and even frames separately; their difference is an empirical noise map (<output>_noise.fits) and their local correlation shows which structure is repeatable (<output>_consistency.fits)--moving-objects, --moving-objects-stack) — links per-frame residuals into asteroid-like tracks by voting in velocity space; optionally stacks a window along each track so a mover too faint for any one frame comes up out of the noise--lightcurve-analysis, with --photometry-timeseries) — Lomb-Scargle period search with a false-alarm probability and a box-least-squares + trapezoid transit fit with errors and a BIC test--aavso-report, with --photometry-timeseries) — writes <output>_aavso.txt in the AAVSO Extended Format for WebObs upload: comparison stars from the AAVSO VSP chart (outliers such as saturated stars rejected), a per-frame ensemble zero point on the green channel (TG band), a check star, mid-exposure JD and optional time bins (--aavso-bin MINUTES). Name the star with --aavso-star "SS Cyg" (or --photometry-target RA,DEC) and set --aavso-obscode. On an Orion Nebula session (130 × 20 s), AAVSO comparison stars of V 10–13.6, each measured against the others, came out at TG − V +0.004 ± 0.035 mag; in 5-minute bins the errors were 0.015–0.04 mag at V 11–13. tools/aavso_report.py writes the report afterwards from an existing run--cfa-drizzle, opt-in) — recombines each frame's measured colour samples instead of debayer-interpolated ones (native kernel, 15.7× the numpy path). On well-sampled data it trades 13% lower luma noise for 2× colour noise and no sharpness gain, so it is meant for undersampled data_comet.fits)--mosaic, or detected from the sessions' pointing) — panels matched to each other in their overlaps (gain + gradient, one global solve), blended, and finished once as one image; no extra dependency--merge); output chains into future merges. Stacks from different exposure/ISO are mapped onto one flux scale first and weighted by measured noise, not frame countpython desktop_app.py, or the packaged OriginStack.exe): pick the target from picture cards and a run mode, see what the run will do before you start, then follow phase progress, the log, per-frame quality and an interactive preview (zoom/pan, before/after wipe compare) — see Desktop App below*.fits.rejected (--quality-sweep, dry-run by default, reversible with --sweep-undo)--keep-checkpoint); coalesces with --merge for fast tuning of merged stacks--debug diagnostic)--quality-report)--galaxy-mode, --galaxy-center X,Y) — protects a galaxy's broad halo from background extraction, so it isn't fit and subtracted as gradient; auto-enabled for galaxy targets by --auto--master-method robust_pca, --flat-from-lights) — separates true shared calibration pattern from session-specific outliers (dust motes, transient hot pixels) instead of a per-pixel median--live folds new subs into a running stack as they land; --stream two-pass streams an already-complete large directory at O(1) full-resolution memory--annotate) — labels bright stars and named deep-sky objects on a copy of the preview, using a WCS solution--moving-objects[-stack] | Find asteroid-like movers; optionally stack along each track |
--session-report | Write <output>_session.png / .csv diagnostics |
--lightcurve-analysis | Period + transit analysis of --photometry-timeseries light curves |
--aavso-report | AAVSO Extended Format report of the target's light curve (--aavso-star, --aavso-obscode, --aavso-bin) |
--denoiser NAME | Primary luma denoiser (auto — wavelet unless overridden —, wavelet, acdnr, bilateral, aniso, none; curvelet is an alias for wavelet) |
--wavelet-protect 0-1 | Structure protection for the wavelet denoiser (default 0.6; 0 = plain BayesShrink) |
--starless-process | Denoise / local-contrast / deconvolve a starless copy, add the stars back untouched |
--layered-stretch | Preview stretch points taken from a starless copy (preview JPEG only) |
--deconvolve {off,rl,rl-sv,tv,sparse} | Richardson-Lucy (global or spatially-variant), TV, or sparse-wavelet deconvolution |
--plate-solve | Plate solve (built-in local Gaia solver by default; --plate-solver picks ASTAP/astrometry.net) |
--from-stack STACK.fits | Re-run post-processing only, on an earlier run's linear stack |
--spike-reject | Remove cosmic-ray / hot-pixel spikes on the raw mosaic before debayering (cheap; --auto turns it on for < 20 frames, mean stacking or drizzle) |
--frame-store {auto,ram,disk} | Keep per-session frame arrays in RAM when there is room (auto), always (ram), or in temp files (disk) |
--compress-phase1 / --no-compress-phase1 | Phase 1 frames that do not fit the safe RAM budget go to temp files; they are compressed automatically only when they exceed 1.5x the available RAM (uncompressed temp files would thrash the page cache) or the temp disk cannot hold them uncompressed. Either flag forces compression on or off |
--no-wcs-refine | Keep the session info.json WCS as mapped onto the stack, without the Gaia refinement |
--gpu-phase1 {auto,on,off} | With --use-gpu: run Phase 1 on the GPU or the CPU pool (auto picks) |
--comet-mode | Also stack on the comet nucleus (<output>_comet.fits, <output>_comet_blended.fits); on automatically when the target is a comet designation (C/2025 R2, 12P/..., 3I/ATLAS), --no-comet-mode turns it off |
--offline | Make no network requests at all |
--no-proper-coadd | Plain combine only, without proper image coaddition |
--hdr-combine PATH | Blend short-exposure stack for HDR |
--mosaic | Stitch per-subfolder panel stacks into one mosaic (also detected automatically) |
--merge STACK.fits [...] | Incremental stacking: fold previous linear stacks into this run |
--quality-sweep [--apply] | Recursively flag poor lights across a collection (dry-run by default) |
--keep-checkpoint | Save raw pre-post-processing stack for re-processing |
--quality-report PATH | Write per-frame quality metrics to CSV |
--dry-run | Discover frames, show parameters, estimate resources — no processing |
--health-check | Analyse calibration and frames without stacking |
--debug KIND[,..] | Debug artefacts: registration, diagnostic, intermediates, masks |
--use-gpu | Enable CuPy GPU acceleration |
-j N, --parallel N | Worker count (0 = auto-detect) |
-v, --verbose | Detailed per-frame output |