A quality-assessment and culling station for a night's worth of sub-exposures. Load a folder, blink through the frames to spot clouds, trails, bad guiding, and focus drift, measure every frame's FWHM / eccentricity / background / star count / sensor temperature / background rate, drag threshold lines to auto-flag the losers, then hand the survivors to the Stacking Suite.
Blink is designed around the astrophotographer's actual Monday-morning workflow: you shot a hundred lights last night, roughly half are usable, and you need to decide which half quickly. Visual blinking catches things the metrics miss (satellites that dodged the detector, a single frame with reflection ghosts); the interactive metrics catch things the eye misses (gradual focus drift, a quietly-rising sensor temperature). Both views share the same list and the same flag state.
At the top of the left column:
.fit.gz,
.fits.gz, .fz), XISF, and raw camera
formats (CR2, CR3, NEF, ARW, DNG, RAF, ORF, RW2, PEF).Files are sorted naturally — frame_2.fits
comes before frame_10.fits, not after. The last-used
folder is remembered across sessions.
Choose how much to downsample on load. 1:1 keeps full resolution and is what you want for publishable work; 1:2, 1:4, and 1:8 progressively cut both dimensions in half, which cuts RAM use and makes blink / zoom responsive on very large sensors or long sessions. The setting persists across sessions. Star-size metrics still report in full- resolution pixel units — FWHM at 1:4 and FWHM at 1:1 are directly comparable.
Every frame goes through the same pipeline:
Failed loads are automatically retried twice with a short delay before being reported as failures — useful on NAS-backed folders where one or two files occasionally take a moment to become readable.
Loaded frames are grouped as Object → Filter → Exposure
→ files, inferred from each frame's FITS header
(OBJECT, FILTER, EXPTIME). Headers
missing a key fall back to “Unknown” so no file disappears.
This hierarchy makes multi-filter LRGB sessions readable at a glance.
The tree has nine columns:
Column widths and order are preserved across sessions. Drag column dividers to resize; drag column headers to reorder.
The Show dropdown above the tree filters what's visible:
Group headers (Object / Filter / Exposure) auto-hide when they'd be empty, so the view stays tidy.
The row of transport buttons below the Metrics / Sat buttons:
The Speed slider and spin box set playback rate from 0.1 to 10 FPS, synchronised to each other.
When the file tree has focus:
Occupies the right side of the window. The current frame is shown stretched for display. Controls:
Ctrl + wheel — cursor-anchored zoom (0.05×
to 3×). Also responds to trackpad pinch gestures on supported
platforms.Pan and zoom are preserved across frame switches, so blinking shows you the same patch of sky on consecutive frames — which is the whole point.
The amber Aggressive Stretch button applies an extra asinh boost on top of the already-stretched display, revealing faint signal and making subtle defects (dim streaks, gradient asymmetry) obvious at the cost of noise and star bloat. It's purely a display setting — nothing is written back to the frame data. Also noticeably slower to render large frames; turn off when you don't need it.
The right-hand pane shows a magnified crop of the current frame. Controls:
Drag the vertical splitter between the main preview and the loupe to adjust their relative widths.
Click Interactive Metrics & Culling to open a second window with seven live scatter plots — one per metric — across every loaded frame.
CCD-TEMP, DET-TEMP, and friends). Higher
= cooling slipped, dark current rising.Each plot has a red horizontal line you can drag. Dropping a frame on the wrong side of the line flags it automatically:
Thresholds are per-filter and persist for the session: your threshold for Luminance stays set when you switch to Ha. A frame is flagged if any group's threshold triggers it, which keeps the per-filter tuning from unflagging a frame someone else wanted gone.
The Show median and ±3σ guides checkbox draws dashed horizontal guides on every plot:
σ here is the MAD-based robust estimator (§ 13.5), so individual outliers don't skew it the way a plain standard deviation would.
The dropdown above the plots restricts the display to one FILTER value, so you can threshold Ha and OIII independently. “All” shows every frame. The status bar at the bottom reports “Flagged Items X/Y (Z%)” for whatever subset is on screen.
The Detect Satellite Trails button runs the Cosmic Clarity ResNet detection model (a two-stage classifier) over every loaded image, flagging frames that likely contain a trail. See § 13.6 for the algorithm. Results populate the Sat column: ⚠ (orange) for detected, ✓ (green) for clean.
This is a pre-pass, not a verdict — it can occasionally false-positive on big diffraction spikes or on bright edge stars. Use it to narrow down what to look at, not to automatically delete.
Once detection has run, right-click → Flag All Satellite Detections promotes every ⚠ to a flag in one shot.
Workflow suggestion. Detect sats first, then run Metrics, then blink through what's left. Each stage removes an obvious class of problem, leaving the subtle visual-only issues for the blinking pass.
Three buttons under the metrics / sat row push the current session to the Stacking Suite:
These buttons are also in the right-click menu (§ 9).
The Export Video… button creates an MP4 or AVI of the current session, suitable for timelapses or satellite-trail walkthroughs:
Blink tries multiple codec combinations in order (mp4v, avc1, H264, MJPG, XVID) and falls back to the imageio-ffmpeg backend if none of them opens. The best available is used automatically; if nothing works, Blink says so and suggests installing FFmpeg.
Right-clicking any leaf (file) in the tree opens a context menu with these actions:
Bad_) to every flagged frame's filename, so you can
cull later via a filename filter in your stacker of choice without
losing anything.Delete is permanent. “Delete Selected
Items” and “Delete All Flagged Images” both call
os.remove() — there is no undo and no trash. If
you're not sure, use the Move variants (which are reversible by moving
the files back) or Rename Flagged (which just renames).
Blink saves nearly everything about its state across sessions:
tree_header_state_v2).blink/last_folder).blink/thread_safety:
auto, safe, or fast —
see § 13.7).| Shortcut | Action |
|---|---|
| Space | Toggle play / pause (when tree has focus). |
| F | Flag or unflag the current frame, then advance. |
| ↑ / ↓ | Previous / next frame. |
Ctrl + wheel |
Zoom the main preview around the cursor. |
| Left-drag on preview | Pan. |
| Right-click on preview | Pin the Zoom Panel to this spot. |
Blink runs the metric computations in parallel
threads using ThreadPoolExecutor, with
SEP (Source Extractor Python) doing the per-frame source extraction and
numpy / OpenCV handling the pixel-level work. The threading
model — and specifically its interaction with numba's resolved
threading layer — is unusually subtle and is covered in
§ 13.7.
For each frame, metric computation runs on a 2×-downsampled copy of the storage image. The downsample is for speed and RAM — SEP's background modelling is the slow part, and a 2× downsample is enough to keep sub-pixel precision in the result. The pipeline:
cv2.INTER_AREA.Background model and extract the global
RMS. If RMS is non-finite (uniform background, pure sky), fall
back to None and let SEP decide its own.From each SEP source's second-moment axes a and
b (semi-major and semi-minor), the sigma of an equivalent
circular Gaussian is σ = sqrt(a·b), and
FWHM ≈ 2.3548 · σ. Blink reports the median across
all detected sources, scaled back to full-resolution pixel units:
where the factor of 2 compensates for the 2× downsample and
sload is the load-scale factor (1 at 1:1, 2 at
1:2, etc.), so FWHM values from any load scale compare directly.
Eccentricity from the same axes:
reported again as the median across sources. Perfect round stars give 0, highly elongated streaks give values approaching 1.
A composite built from star count, eccentricity, and background:
interpreting as: more stars is better, round stars are better, dark background is better. The three terms multiply, so a bad score on any axis drags the composite down — a 500-star frame with e = 0.6 scores worse than a 300-star frame at e = 0.1. Background enters as a saturating curve so a very dark frame doesn't dominate the metric. For most sessions, thresholding on Weighted Score alone catches ~80% of what the other three catch individually.
Converts the cached pre-stretch background from its stored normalised value back to raw ADU, subtracts the sensor OFFSET pedestal from the header, and divides by exposure time:
where B is the file's bit depth. OFFSET falls back to
0 when not in the header (common on SGP captures with default
templates, and some ASCOM drivers) — which means within-session
relative ordering is preserved for culling, only the absolute
ADU/s value is inflated by the missing pedestal / exposure. Blink
exploits this: BG/s is used for ranking, not for calibration, so a
missing OFFSET doesn't break the metric.
The ±1σ, ±2σ, ±3σ guides on the metrics plots use the median-absolute-deviation estimator:
where &ytilde; is the median. The constant 1.4826 is
1 / Φ⁻¹(0.75), which makes
σ̂ an unbiased estimator of the standard
deviation for Gaussian data. The point of using MAD rather than the
usual std is immunity to outliers: one or two really bad frames in a
session don't push the σ lines out so far that they become
useless.
A two-stage classifier runs on 256×256 interior tiles of the raw (linear, un-stretched) frame. A tile passes if both the first and second model return positive at probabilities > 0.5 and > 0.25 respectively. A frame is reported as having a trail if at least two spatially adjacent tiles pass (8-connected at the tile grid's natural stride) — this requires a continuous linear feature, which is what a satellite trail is, and rejects single-tile detections (bright stars, diffraction spikes).
A prefetch pipeline keeps CPU tile preparation one or more frames ahead of the GPU inference step, so the GPU rarely waits on disk I/O. Pool depth is 4 by default. If the GPU is not available (no PyTorch CUDA, no ONNX provider), inference runs on CPU and is correspondingly slower.
Blink's parallel loader is where a notorious cross-platform hazard
lives. SASpro's debayer path is accelerated with numba; numba's
resolved threading layer varies by platform and build. On boxes
whose numba fell back to the non-threadsafe workqueue
layer (seen on some StellarMate / Linux and macOS builds), running the
debayer kernel from several threads at once aborts the process.
Blink works around this by:
workqueue → serialize, anything threadsafe
(OpenMP / TBB) → concurrent.blink/thread_safety with values auto
(default), safe, or fast for people who
want to force a mode.Worker counts scale with the mode: safe mode caps at
min(cpus − 1, 8); fast mode uses
min(cpus − reserved, 60) with reserved =
25% of cores (capped at 4). The SEP metric path uses its own smaller
pool (4 to 12 workers depending on frame size) because SEP's internal
C allocator is itself contended.
Exposure values on the tree — the “300s” part —
are canonicalised so that 300, 300.0,
"300 s", and "300 seconds"
all end up in the same bucket. Temperature values are similarly
tolerant of units (“-10 C”, “−10 C”,
“-10 °C” are all recognised) and EXIF-style
shutter fractions (“1/125”) are parsed as floats. The
temperature keyword list prefers actual sensor temps (CCD-TEMP,
DET-TEMP, SENSOR-TEMP and friends) over cooler setpoints (SET-TEMP,
SETPOINT), matching the Stacking Suite's own precedence.
Right-click → Open in Document Window creates a new SASpro
document from the current frame, preserving the original FITS header,
bit depth, is-mono flag, and metadata dict. Only the numpy array is
new — everything else that load_image originally
returned rides along, so plate-solving, WCS, and any other
header-aware tool in SASpro will see the correct metadata.
a / b second-moment axes and the
background model come from. Satellite trail detection uses
Cosmic Clarity's ResNet dual-classifier model (see the Cosmic
Clarity suite documentation for model provenance). Image loading and
debayering use SASpro's existing engines
(setiastro.saspro.legacy.image_manager for loads,
setiastro.saspro.debayer for the canonical debayer path
shared with the Stacking Suite). The MAD-based robust-sigma guide line
uses the classical 1.4826 Gaussian-consistency factor
(Rousseeuw & Croux 1993). Video export uses OpenCV's
VideoWriter with codec fallbacks and imageio-ffmpeg as a
portable last resort.