Astrophotography sub-frame quality evaluation tool.
Evaluates directories of FITS/XISF files for quality metrics including PSF FWHM, star eccentricity, background noise, and signal-to-noise ratio. Produces composite quality scores and per-frame rejection decisions, with CSV and interactive HTML reports.
Supports multi-filter sessions (Ha/OIII/SII) with per-filter tabbed reports, a live-updating watch mode, and optional remote file pull from an acquisition PC via SFTP.
Tuned for:
- Telescope: William Optics Redcat 51 (250 mm focal length, f/4.9) — use
--focal-lengthfor other scopes - Camera: QHY MiniCam 8M (pixel size read from FITS headers:
XPIXSZorPIXSIZE1) - Pixel scale:
206.265 × pixel_size_µm / focal_length_mmarcsec/pixel
If your camera doesn't write pixel size to FITS headers, set pixel_size_um in astro_eval.toml (see Config File) or the pixel scale falls back to the built-in default (3.1 arcsec/px for Redcat 51 + 3.76 µm).
Download astro-eval-setup.exe from the Releases page and run it. The installer:
- Installs astro-eval to
%LocalAppData%\Programs\astro-eval\(no administrator rights required) - Adds
astro-evalto your user PATH so it works from any Command Prompt - Adds a "Analyze with astro-eval" entry to the right-click context menu on folders in Windows Explorer
- Places a ready-to-use configuration file at
%APPDATA%\astro-eval\astro_eval.toml
After installing, right-click any folder containing FITS/XISF files and choose Analyze with astro-eval. The report opens automatically in your browser.
pip install -e .| Package | Purpose |
|---|---|
astropy |
FITS I/O, sigma-clipped statistics |
numpy |
Array operations |
scipy |
PSF curve fitting (Moffat/Gaussian), trail detection |
sep |
Source Extractor Python — star detection & background |
matplotlib |
Frame preview rendering (--serve) |
xisf |
XISF file format support |
paramiko |
SFTP remote file pull (--remote) |
Requires uv and Inno Setup 6.
build.batProduces Output\astro-eval-setup.exe.
# Single filter session — generates CSV report
astro-eval /path/to/ha_session
# With interactive HTML report
astro-eval /path/to/ha_session --html
# Multi-filter session (subdirs named Ha/, OIII/, SII/ etc.)
astro-eval /path/to/session_root --html
# Live watch mode — updates report every 30s as new frames arrive
astro-eval /path/to/session --watch --html
# Watch + pull new frames from a remote acquisition PC via SFTP
astro-eval ./staging --watch --html \
--remote astromini \
--remote-dir "C:\Users\AstroMini\Documents\N.I.N.A\2026-03-09\Soul Nebula\LIGHT" \
--remote-user AstroMini| Mode | Target | Key Metrics |
|---|---|---|
star |
Broadband (L, R, G, B, RGB, Clear) | FWHM, wFWHM, Eccentricity, Star count, SNR weight, PSFSignalWeight, Moffat β |
gas |
Narrowband (Ha, OIII, SII) | Background noise, SNR estimate (p95), Star count (transparency), PSFSignalWeight, wFWHM, Moffat β |
auto |
Auto-detect from FILTER header |
Dispatches to star or gas |
| Filter keyword | Canonical | Mode |
|---|---|---|
Ha, H-Alpha, H, HAlpha |
Ha |
gas |
OIII, O3, O-III, O |
OIII |
gas |
SII, S2, S-II, S |
SII |
gas |
R, Red, G, Green, B, Blue |
R/G/B | star |
L, Lum, Luminance |
L | star |
| Unknown / missing | — | star (with warning) |
If INPUT_DIR contains subdirectories with filter names (e.g. Ha/, OIII/, SII/), the tool automatically processes each filter independently with its own session statistics and rejection thresholds, and generates a single tabbed HTML report.
session_root/
├── Ha/ ← processed as gas mode
├── OIII/ ← processed as gas mode
└── SII/ ← processed as gas mode
Each filter's CSV is written as astro_eval_report_Ha.csv, astro_eval_report_OIII.csv, etc.
Point astro-eval at a folder that holds frames only in sub-folders — typically a N.I.N.A. night folder — and it analyses the whole session:
astro-eval "G:\Astro\NGC 7331 (Caldwell 30)\.SessionData\NIGHT_2026-09-18" --html
NIGHT_2026-09-18/
├── NGC 7331/LIGHT/<filter>/*.fits → one tab per filter (per target and filter if several targets)
├── FlatWizard/FLAT/<filter>/*.fits → flat sets, checked in the Calibration tab
└── DARK/, BIAS/ … → counted, not analysed
- Frames are found recursively and sorted by their
IMAGETYPheader (folder names as fallback)._REJECTEDfolders and calibrated / master frames are skipped. - Lights are grouped per filter. Subs shot with different camera settings from the rest of their filter (gain, offset, exposure, binning, readout mode, set temperature) are rejected as Different settings — they need their own darks — and are left out of that filter's statistics.
- Flats are grouped into sets — filter × exposure × gain/offset/readout × binning — because one folder often holds several flat runs, and each set makes its own master. Each flat is checked for exposure level, clipping, and noise against the rest of its set; the noise is measured by differencing consecutive flats, which cancels the pixel-response pattern the flats record. Each set gets a verdict from its master-flat SNR (per-flat SNR × √n, target
min_master_snr). - Calibration tab: for every light filter, the best flat set (good sets first, then exposures ≥ 1 s, then more frames) and the checks: missing flats, binning / frame-size mismatch, focuser offset between flats and lights, and a note when flats use a different gain/offset/readout mode than the lights (calibrate them with flat-darks or bias at the flat settings). Flat sets open as their own frame views, so bad flats can be inspected and moved like lights.
- Reports are written to the night folder (or
--output): one CSV per light filter,astro_eval_flats_<set>.csvper flat set, and one HTML report.--watchkeeps rescanning the folder and updates the report as frames arrive; moving a frame puts it in a_REJECTEDfolder next to it. - Running on a single
LIGHTfolder (filter sub-folders) or a flat folder of frames works as before;--flatand--remotekeep the per-folder behaviour.
astro-eval /path/to/session --watch --html- Polls for new FITS/XISF files every 30 seconds
- Re-evaluates session statistics and regenerates the report when new frames arrive
- Serves the report at
http://127.0.0.1:7420/and auto-refreshes the browser via SSE - Press
Ctrl+Cto stop
Pull frames from a remote Windows acquisition PC during a live session:
astro-eval ./staging --watch --html \
--remote HOSTNAME_OR_IP \
--remote-dir "C:\path\to\LIGHT" \
--remote-user USERNAME \
--remote-key ~/.ssh/id_ed25519 # optional, auto-discovers ~/.ssh/ keysINPUT_DIRis the local staging directory where files are downloaded- Remote filter subdirectories (e.g.
LIGHT\H\,LIGHT\S\,LIGHT\O\) are auto-detected via SFTP - The SSH connection is established once at startup and reused across polls
- Starting with an empty staging directory is fine — the tool waits for the first SFTP pull
On the evaluation PC:
ssh-keygen -t ed25519 -C "astro-eval"On the acquisition PC (if the user is an Administrator, the standard authorized_keys location is ignored — use the admin file instead):
# Copy public key to the admin-specific location
Copy-Item "$env:USERPROFILE\.ssh\authorized_keys" "C:\ProgramData\ssh\administrators_authorized_keys"
# Fix permissions
icacls "C:\ProgramData\ssh\administrators_authorized_keys" /inheritance:r /grant:r "SYSTEM:F" /grant:r "BUILTIN\Administrators:F"All CLI options can also be set in a TOML config file, making it easy to store per-scope or per-session defaults. CLI arguments always override the config file.
Search order (first file found wins):
--config FILE(explicit path)INPUT_DIR/astro_eval.toml— session-specific override%APPDATA%\astro-eval\astro_eval.toml— user global config (Windows)~/.config/astro_eval/astro_eval.toml— user global config (Linux/macOS)- Next to the executable — install directory fallback
- Current working directory
The installer places a ready-to-use config at %APPDATA%\astro-eval\astro_eval.toml.
Edit it once to set your telescope and camera defaults — it will be picked up automatically for every session without copying anything.
To override settings for a specific session, drop an astro_eval.toml directly in the FITS folder.
Key sections:
[telescope]
focal_length_mm = 250.0
[camera]
pixel_size_um = 2.9 # fallback if not in FITS headers
[rejection]
fwhm_threshold_arcsec = 0.0 # optional absolute cap; 0 = disabled
sigma_fwhm = 2.0
sigma_noise = 2.5
sigma_bg = 3.0
sigma_residual = 3.0 # PSF residual flag threshold (informational)
sigma_gradient = 2.0 # session-relative gradient flag: median + sigma × robust σ
gradient_threshold = 0.0 # optional absolute gradient hard cap; 0 = disabled
gradient_knee = 1.2 # scoring knee multiplier (× session median)
[scoring.star]
weight_fwhm = 0.30
weight_ecc = 0.25
weight_stars = 0.20
weight_psfsw = 0.25 # PSFSignalWeight (supersedes snr_weight)
[scoring.gas]
weight_snr = 0.30 # reduced; PSFSignalWeight captures overlapping SNR info
weight_noise = 0.20
weight_bg = 0.15
weight_stars = 0.20
weight_psfsw = 0.15 # PSFSignalWeight — star sharpness bonus, useful even in NBastro-eval INPUT_DIR [OPTIONS]
| Argument | Default | Description |
|---|---|---|
INPUT_DIR |
(required) | Directory with FITS/XISF files, filter subdirs, or local staging path |
| Option | Default | Description |
|---|---|---|
--config FILE |
auto | Path to astro_eval.toml config file |
--mode {star,gas,auto} |
auto |
Evaluation mode |
--output DIR |
INPUT_DIR |
Output directory for reports |
--focal-length MM |
250.0 |
Telescope focal length (mm) |
--fwhm-threshold ARCSEC |
5.0 |
Absolute FWHM rejection limit (arcsec) |
--ecc-threshold VALUE |
0.6 |
Eccentricity rejection threshold [0–1] |
--star-fraction FRAC |
0.7 |
Min star count as fraction of session median |
--snr-fraction FRAC |
0.5 |
Min SNR weight as fraction of session median |
--sigma-fwhm SIGMA |
2.0 |
Sigma multiplier for FWHM statistical rejection |
--sigma-noise SIGMA |
2.5 |
Sigma multiplier for noise statistical rejection |
--sigma-bg SIGMA |
3.0 |
Sigma multiplier for background level rejection |
--sigma-residual SIGMA |
3.0 |
Sigma multiplier for PSF residual flag (informational) |
--gradient-threshold SIGMA |
0 |
Optional absolute background gradient hard cap in noise σ units. 0 = disabled |
--gradient-knee RATIO |
1.2 |
Scoring knee: penalty steepens above knee × session_median gradient |
--detection-threshold SIGMA |
5.0 |
Star detection sigma threshold |
--workers N |
0 |
Parallel worker processes. 0 = all CPU cores |
--html |
off | Generate the interactive HTML report |
--serve |
off | Serve the HTML report at http://127.0.0.1:7420/ |
--port PORT |
7420 |
HTTP server port |
--watch |
off | Watch for new frames and update report every 30s (implies --serve) |
--remote HOST |
— | Hostname/IP of remote acquisition PC for SFTP pull |
--remote-dir DIR |
— | Remote directory to pull frames from (Windows paths OK) |
--remote-user USER |
current user | SSH username for remote |
--remote-key PATH |
auto | SSH private key path (auto-discovers ~/.ssh/ keys if omitted) |
--local-staging DIR |
INPUT_DIR |
Local directory for downloaded remote files |
--verbose |
off | Verbose progress output |
--version |
— | Show version and exit |
One file per filter: astro_eval_report.csv (single filter) or astro_eval_report_Ha.csv etc. (multi-filter).
| Column | Description |
|---|---|
filename |
FITS/XISF filename |
mode |
star or gas |
filter |
Filter name from header |
exptime_s |
Exposure time (seconds) |
n_stars |
Detected star count |
fwhm_median_arcsec |
Median FWHM in arcseconds |
fwhm_mean_arcsec |
Mean FWHM in arcseconds |
eccentricity_median |
Median stellar eccentricity [0–1] |
psf_residual_median |
Median normalized PSF fit residual |
snr_weight |
SNR weight proxy: Σflux² / (noise² × N) |
psf_signal_weight |
PSFSignalWeight: ΣA² / (2×noise²×N×FWHM²) — penalizes FWHM super-linearly |
wfwhm_arcsec |
wFWHM = FWHM / √N_stars — combined seeing+transparency metric |
moffat_beta |
Moffat β parameter (atmospheric seeing index; typical 2.5–5) |
snr_estimate |
Nebula SNR estimate via 95th-percentile method (narrowband) |
background_median |
Median sky background (ADU) |
background_rms |
Background noise RMS (ADU) |
noise_mad |
Robust noise estimate via MAD (ADU) |
background_gradient |
Sky gradient in noise σ units: (max−min)/noise_rms across an 8×8 grid of sigma-clipped sky cells. ~5–30 = uniform, ~20–80 = normal LP gradient, >100 = severe (sunrise/cloud edge) |
score |
Composite quality score [0–1] |
rejected |
1 if frame rejected, 0 if accepted |
rejection_reasons |
Pipe-separated rejection criterion names |
flag_* |
Per-criterion binary rejection flags |
A single self-contained HTML file — no CDN or web fonts, so it works offline at the telescope.
- Themes: dark (default), light, and night vision — everything in dim red to preserve dark adaptation. Frame status is always shown by shape and icon as well as colour, so it stays readable in red.
- Session summary: frames kept, usable integration time, seeing of the kept frames, session span (local time).
- Night timeline: aligned strips sharing one time axis — score, relative SNR, FWHM, sky background — each with its rejection limit as a dashed line. Rejected frames are crosses and shade the whole column, so clouds, focus drift and dawn stand out. Hover for values; click to open the frame.
- Why frames were rejected: counts per reason, in plain language ("Soft stars", "Low SNR", "Partly obstructed"…).
- Frames table: filter by status, search, sort any column; Essential or All metrics columns. Flagged values are marked.
- Frame panel (click a row): stretched preview of the sub (server mode), each failed check with the measured value against its limit, all measurements compared with the session median, and the 5×5 FWHM map (oriented like the preview). Browse with ↑/↓, toggle keep/reject with X.
- Full-size viewer (click the preview): opens the sub at 100 % centred on the clicked point — pan by dragging, zoom with the wheel or Fit / 100 / 200 / 400 %, pixels shown unsmoothed above 150 %. ←/→ step through frames keeping the same zoom and the same stars in place: frames are aligned on the stars using the registration from the transparency step (dither, field rotation and meridian flips included), so you can compare star shapes (elongation, focus, halos) frame by frame. Align (A) toggles it; hot pixels then visibly move while stars stay put. The preview uses an auto-stretch that keeps star cores unclipped.
- Tune thresholds: preview the effect of a different FWHM σ, minimum relative SNR, minimum score or eccentricity live on the counts, timeline and table. Keep anyway / Reject overrides are remembered in the browser. Neither changes the CSV — the page says so while a preview is active.
- Move to _REJECTED: select frames (or Select rejected); with
--serve/--watchthe files are moved directly, otherwise a.batscript is downloaded. - Multi-filter: an Overview tab (integration kept per filter and its main cause of loss, score of every filter through the night, per-filter cards) plus one tab per filter. Filter colours follow the Hubble palette idea: OIII blue, SII orange, Ha green.
- Watch mode: a Live indicator, and the page reloads in place when new frames arrive, keeping the open tab, scroll position and frame panel.
--analysisadds the AI session analysis to the summary.
FWHM — Full Width at Half Maximum of the stellar PSF, fitted using a 2D elliptical Moffat profile (Gaussian fallback). Measured in pixels, reported in arcseconds. Lower is better. Typical range: 1.5–5 arcsec.
wFWHM — Siril-inspired weighted FWHM: FWHM_arcsec / √n_stars. Combines seeing quality and sky transparency — a frame with poor seeing or few stars both result in a higher (worse) value. Lower is better.
Eccentricity — Departure from circular PSF: √(1 − (b/a)²). 0 = perfect circle, approaching 1 = elongated. Caused by tracking errors, wind, or collimation issues. Default rejection threshold: 0.6.
SNR Weight — Σ(flux²) / (noise² × N). Higher means brighter stars relative to noise floor. Useful for detecting thin clouds or transparency loss.
PSFSignalWeight — PixInsight-inspired metric: (Σ amplitude_i)² / (2 × noise² × N × FWHM_px²). Unlike SNR weight, penalizes FWHM super-linearly (~1/FWHM²), so frames with sharp stars rank significantly higher than blurry frames with equal total flux. Higher is better.
Moffat β — Power-law exponent of the fitted Moffat PSF profile. Reflects atmospheric turbulence: β ≈ 2.5 for strong atmospheric seeing, β ≈ 4–5 for better conditions. Informational only — not used in rejection decisions.
PSF Residual — Normalized median absolute deviation between the fitted PSF and the actual pixel data: MAD(fitted − actual) / amplitude. High values indicate distorted or trailed stars, optical aberrations, or double stars. Informational by default; flagged if > session_median + sigma_residual × std.
Star Count — Number of detected sources above the SNR threshold. Drops significantly with clouds or focus shift.
SNR Estimate — (p95 − background_median) / background_rms where p95 is the 95th percentile pixel value. This threshold-independent method gives SNR ≈ 1.6 for pure background (normal distribution p95 = μ + 1.645σ) and higher values for frames with genuine nebula signal. Higher is better. On faint targets it barely rises above the pure-noise value (≈ 1.9 on a typical Bortle 9 Ha session), so it is now only a fallback: the score and rejection use matched-star Rel SNR when available.
Background RMS — Noise level of the sky background (ADU). Higher values indicate light pollution, moon contamination, or sky glow. The primary rejection criterion for narrowband.
Star Count — Used as a transparency proxy even in narrowband — fewer detected stars indicates reduced sky transparency.
Background Gradient — Sky spatial non-uniformity expressed in noise σ units: (max_cell_bg − min_cell_bg) / noise_rms, where the image is divided into an 8×8 grid of sigma-clipped sky cells. Normalising by the noise floor (rather than sky level) is critical because auto-stretch dramatically amplifies subtle linear gradients — a gradient that looks enormous in a viewer may only be 1–2% of the sky ADU level but still hundreds of σ above noise. Typical values: uniform sky ~5–30 σ, normal LP gradient ~20–80 σ, severe gradient burning part of the frame ~100–1000+ σ. By default, hard rejection uses a session-relative threshold (sigma_gradient); gradient_threshold is an optional absolute cap (default disabled). Applies to both star and gas modes.
PSFSignalWeight, wFWHM, Moffat β, FWHM, Eccentricity — PSF fitting is also run in gas mode (same algorithm as star mode). These metrics are populated in the CSV/HTML output and psf_signal_weight contributes to the Gas Score (weight 0.15). They are informational in the context of narrowband imaging — star shape doesn't affect nebula detail — but help discriminate between otherwise similar frames and catch severe tracking or focus issues.
Star count is a coarse transparency proxy: it depends on where the detection threshold falls and does not scale linearly with sky clarity. astro-eval instead follows the same stars from frame to frame:
- Each frame records fixed-aperture photometry (local annulus background) of its ~800 brightest stars.
- After all frames are measured, the frame with the most stars becomes the reference. Every other frame is registered to it — translation, rotation and scale, including a 180° meridian flip — and its stars are matched (typically 350+ per frame).
- One aperture radius is used for the whole session (≥ 2.5 × the session's 90th-percentile FWHM), because real stars have extended halos and a per-frame aperture would make soft frames look brighter.
From the matched stars:
Transparency — flux per second of the same stars relative to the session median (1.0 = typical). Stars that should be in the frame but were not found count as lost flux, so clouds and obstructions lower it. It is computed from the brighter half of the stars, which stay detectable even in shallower frames (shorter exposure, brighter sky).
Rel SNR — (star flux / pixel noise) relative to the session median: the frame's signal-to-noise on an extended source compared with a typical frame. This is the number that matters for stacking. For an equal-weight average, a frame lowers the stack's SNR when its SNR is below ≈ 0.71 × typical, hence the default min_relative_snr = 0.70. If you integrate with per-frame weights (PixInsight/Siril), low-SNR frames still help a little and you may lower this threshold. A bright sky (dawn, moon, LP clouds) leaves transparency at ~1.0 but lowers Rel SNR.
Region — the frame is divided into a 4×4 grid; each cell's transparency is computed the same way, and Region = worst cell / 75th-percentile cell (1.0 = uniform). A tree, roof, dew shield or cloud bank covering part of the field drives it towards 0 even when the frame as a whole still has plenty of stars. Typical values for clean frames are 0.85–1.0.
In gas mode, Rel SNR replaces the p95 SNR estimate in the score.
The console summary prints session-level statistics for each metric. Each row is a per-frame measurement; the columns (median, mean, std, min, max) describe how that measurement varies across all frames in the session. The row and column together answer a specific diagnostic question.
| Row | Column | Diagnostic question |
|---|---|---|
fwhm_median |
median |
What was the typical seeing this session? |
fwhm_median |
std |
Did seeing stay stable, or did it drift during the night? High std = unstable atmosphere. |
fwhm_mean |
std |
Same as above, slightly more sensitive to frames with a few very blurry outlier stars. |
fwhm_std |
median |
Within a typical frame, how consistent are star sizes across the field? High = field curvature, sensor tilt, or anisoplanatic seeing. |
fwhm_std |
std |
Did the across-field PSF spread change between frames? High = focus drift or temperature-induced flexure during the session. |
moffat_beta |
median |
Typical atmospheric profile shape (β ≈ 2.5 = poor seeing, β ≈ 4–5 = good seeing). |
moffat_beta |
std |
Were atmospheric conditions steady, or did the turbulence profile keep changing? |
wfwhm |
median |
Combined seeing + transparency quality for the session (lower is better). |
| Row | Column | Diagnostic question |
|---|---|---|
eccentricity_median |
median |
How well did tracking/guiding perform on average? |
eccentricity_median |
std |
Were there isolated tracking failures, or was guiding consistently poor? High std with low median = occasional wind gusts or guide star lost briefly. |
psf_residual_median |
median |
How well does a Moffat profile fit the stars? High = distorted PSF from aberrations, coma, or trailing. |
psf_residual_median |
std |
Was the PSF distortion consistent (optical issue) or intermittent (tracking or wind)? |
| Row | Column | Diagnostic question |
|---|---|---|
n_stars |
median |
How transparent was the sky on average? |
n_stars |
std |
Did transparency fluctuate? High std = passing clouds or variable extinction. |
background_median |
median |
Typical sky brightness level (ADU) — driven by light pollution and moon. |
background_median |
std |
Did sky brightness change during the session? High std = moonrise/set or worsening LP. |
background_rms |
median |
Typical noise floor for the session. |
background_rms |
std |
How stable was the noise floor? High std = variable sky conditions. |
background_gradient |
median |
Typical gradient severity — how uneven the sky background is across the frame. |
background_gradient |
std |
Were gradients consistent (persistent LP source) or spiky (cloud edges, twilight encroachment)? |
| Row | Column | Diagnostic question |
|---|---|---|
psf_signal_weight |
median |
Typical combined signal quality (amplitude × sharpness) for the session. |
psf_signal_weight |
std |
Did signal quality vary? High std = intermittent clouds or transparency loss in some frames. |
snr_weight |
median |
Typical raw SNR proxy (flux² / noise²). Less sensitive to FWHM than PSFSignalWeight. |
snr_estimate |
median |
(Gas mode) Typical nebula signal level above background. |
snr_estimate |
std |
(Gas mode) Was the nebula signal stable, or did sky conditions affect it frame to frame? |
Score = 0.30×r(FWHM) + 0.25×r(AxisRatio) + 0.20×r(Stars)
+ 0.25×r(PSFSignalWeight) + 0.00×r(SNRWeight)
snr_weight is retained in the output (CSV/HTML) for reference but has a default weight of 0 — PSFSignalWeight supersedes it because it already captures the amplitude/noise ratio with an additional 1/FWHM² correction. It can be re-enabled via weight_snr in astro_eval.toml if PSF fitting is unreliable in your data.
BaseScore = 0.30×r(RelSNR) + 0.20×r(Noise) + 0.15×r(BG)
+ 0.20×r(Stars) + 0.15×r(PSFSignalWeight)
Score = BaseScore × trail_penalty × gradient_multiplier
PSF fitting is also run in gas mode to populate psf_signal_weight, wfwhm, and moffat_beta.
gradient_multiplier is a multiplicative penalty (not additive) with a knee at 1.2× the session median gradient:
gradient ≤ gradient_knee × median→ multiplier = 1.0gradient > gradient_knee × median→ multiplier drops exponentially (floored at 0.05)
This means frames with a normal LP gradient are barely penalised, while frames where one side of the sky is dramatically brighter (sunrise, twilight, cloud edge) are pushed toward 0. The gradient multiplier is applied in gas mode scoring.
r(x) is the frame's ratio to the session median, capped at 1: x / median for higher-is-better metrics, median / x for lower-is-better ones (FWHM, noise, background). PSFSignalWeight and SNRWeight are SNR²-type quantities, so their ratio is square-rooted to keep every term in linear SNR units. Eccentricity enters as the axis ratio b/a = √(1 − e²).
A score of 1.0 means "as good as a typical frame in this session, or better"; 0.7 means roughly 30 % worse than typical. Frames above the median are deliberately not ranked against each other — differences of a few percent there are mostly measurement noise. Because the score is anchored to the median rather than stretched between the session's best and worst frame, the fixed min_score cut (default 0.7) only removes frames that are genuinely degraded; a homogeneous session loses essentially nothing.
Statistics are computed independently per filter in multi-filter mode.
σ-based thresholds are session_median + max(k × σ, min_deviation × median), where σ is the robust session spread 1.4826 × MAD (a handful of bad frames cannot widen the thresholds and hide one another) and min_deviation (default 0.10) stops a very steady session from rejecting frames that are only a few percent worse than typical.
| Criterion | Condition | Hard reject? |
|---|---|---|
high_fwhm |
FWHM > σ-threshold (--sigma-fwhm) OR FWHM > --fwhm-threshold (optional absolute cap, default disabled) |
Yes |
low_relative_snr |
Matched-star relative SNR < --min-relative-snr (default 0.70) |
Yes |
partial_obstruction |
Worst region transmits < --min-region-transparency × the clear part (default 0.50) |
Yes |
low_stars |
Stars < session_median × --star-fraction |
Yes |
low_score |
Composite score < --min-score (default 0.7) |
Yes |
high_gradient |
Gradient > σ-threshold (--sigma-gradient) OR > --gradient-threshold |
No (informational; gas-mode score is penalised via gradient_knee) |
airplane_trail |
Airplane/double contrail detected | Yes |
satellite_trail |
Single satellite trail detected | No (informational) |
| Criterion | Condition | Hard reject? |
|---|---|---|
high_eccentricity |
Eccentricity > --ecc-threshold |
Yes |
low_snr_weight |
SNR weight < session_median × --snr-fraction |
Yes |
high_residual |
PSF residual > σ-threshold (--sigma-residual) |
No (informational) |
| Criterion | Condition | Hard reject? |
|---|---|---|
high_noise |
Background RMS > σ-threshold (--sigma-noise) |
Yes |
high_background |
Background median > σ-threshold (--sigma-bg) |
Yes |
low_snr |
p95 SNR estimate < session_median × --snr-fraction — only when matched-star transparency is unavailable |
Yes |
Note: Vignetting is intentionally not a rejection criterion (it is calibratable), and neither are gradients on their own (correctable with ABE/DBE).
Satellite and airplane trails are detected automatically:
| Trail type | Classification | Rejection |
|---|---|---|
| Satellite | Single thin trail, uniform brightness | Borderline (flagged, not rejected) |
| Airplane | Double contrail or strobe pattern | Hard rejected |
Detection uses PCA on connected components in a downsampled, background-suppressed image, followed by perpendicular cross-section analysis to distinguish single vs double trails.
A pySiril script is included (astro_eval_siril.py) for users who process their data in Siril. It runs astro-eval from within Siril and automatically deselects rejected frames in the loaded sequence.
Requirements: Siril 1.4+, astro-eval installed via the Windows installer.
Usage:
In Siril's script console:
pyscript C:\Users\YourName\AppData\Local\Programs\astro-eval\astro_eval_siril.py
Or add the install directory to Siril's script search paths in Siril preferences, then simply:
pyscript astro_eval_siril.py
What it does:
- Opens a folder picker dialog (pre-filled with Siril's current working directory)
- Runs
astro-eval <folder> --html— the full evaluation pipeline runs exactly as from the command line - Parses the CSV report to identify rejected frames
- If a sequence is loaded, deselects rejected frames (so they are excluded from stacking)
- Opens the HTML quality report in your browser
- Logs a summary (accepted/rejected counts) in Siril's log panel
Multi-filter sessions are handled automatically — all per-filter CSV reports (astro_eval_report_Ha.csv, etc.) are parsed.
The script does not modify astro-eval's processing in any way — it is a pure wrapper that calls the exe and acts on its output.
- Minimum frames: Session statistics require at least 3 frames for reliable thresholds.
- Multi-extension FITS: Automatically searches image extensions if primary HDU has no data.
- 3D FITS arrays: RGB (3×H×W) converted to luminance; multi-plane uses first plane.
- Existing report: If a report already exists, the tool prompts before reprocessing.
- SEP byte order:
byteswap().newbyteorder()applied before all SEP calls as required. - Saturated stars: Skipped during PSF fitting (SEP flag bit 4).
- Edge sources: Excluded within 50 px of image border.
astro_eval/
├── __init__.py Package exports
├── image_loader.py FITS/XISF I/O + header parsing
├── background.py Background estimation (SEP + sigma-clip)
├── star_detection.py SEP star detection + quality filtering
├── psf_fitting.py Moffat/Gaussian PSF fitting
├── metrics.py Star + gas metric computation
├── trail_detection.py Satellite/airplane trail detection
├── scoring.py Session stats, rejection flags, composite scores
├── transparency.py Matched-star photometry, transparency, partial obstruction
├── report.py CSV exports + HTML report entry points
├── report_html.py HTML report payload and page shell
├── report_assets.py Report stylesheet and client-side app (self-contained)
└── cli.py CLI, HTTP server, watch loop, SFTP sync