Blink Comparator

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.

1. Loading images

At the top of the left column:

Files are sorted naturally — frame_2.fits comes before frame_10.fits, not after. The last-used folder is remembered across sessions.

1.1 Load scale (1:1 / 1:2 / 1:4 / 1:8)

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.

1.2 What Blink does to each frame on load

Every frame goes through the same pipeline:

  1. Load the raw image + header.
  2. Debayer if the file is a mono mosaic (CFA pattern detected from header).
  3. Optionally downsample per load-scale setting.
  4. Measure background on the pre-stretch linear data (SEP) and cache the result — this is what the Background and Bg Rate metrics report.
  5. Auto-stretch to a target median of 0.25 (per channel for colour, global for mono) for display purposes.
  6. Convert to a storage dtype chosen from available RAM: ≤ 16 GB → uint8, ≤ 32 GB → uint16, else float32. Larger sessions on smaller machines automatically land in the smaller dtype.

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.

2. The tree view

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.

2.1 Columns

The tree has nine columns:

Column widths and order are preserved across sessions. Drag column dividers to resize; drag column headers to reorder.

2.2 Show filter

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.

3. Blinking through frames

3.1 Playback controls

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.

3.2 Keyboard shortcuts

When the file tree has focus:

4. Preview area

4.1 Main preview

Occupies the right side of the window. The current frame is shown stretched for display. Controls:

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.

4.2 Aggressive Stretch toggle

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.

4.3 Zoom Panel (loupe)

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.

5. Interactive Metrics & Culling

Click Interactive Metrics & Culling to open a second window with seven live scatter plots — one per metric — across every loaded frame.

5.1 The seven metrics

5.2 Interacting with points

5.3 Threshold lines

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.

5.4 Guide lines

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.

5.5 Filter selector

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.

6. Satellite Trail Detection

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.

7. Stacking Suite integration

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).

8. Video Export

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.

9. Right-click menu

Right-clicking any leaf (file) in the tree opens a context menu with these actions:

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).

10. Other buttons

11. Persistence

Blink saves nearly everything about its state across sessions:

12. Keyboard shortcuts summary

ShortcutAction
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.

13. Technical details

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.

13.1 Per-frame SEP pipeline

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:

  1. Convert the storage image to float32 in [0, 1] (collapsing colour to a mean plane). NaN/Inf are replaced with 0 / 1 / 0 before SEP sees anything.
  2. Resize to half size with cv2.INTER_AREA.
  3. Build a SEP 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.
  4. Try the detection threshold cascade:
    \{\,7\sigma \text{ (minarea 4)},\; 5\sigma \text{ (4)},\; 4\sigma \text{ (3)},\; 3.5\sigma \text{ (2)}\,\}
    taking the first threshold that returns any stars. On very clean frames the 7σ pass finds hundreds of sources; on cloudy frames the cascade drops down until something comes back. This is what lets Blink continue to produce a star-count metric even on bad frames.

13.2 FWHM and eccentricity

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:

\mathrm{FWHM} \;=\; \mathrm{median}\!\left(2.3548\,\sqrt{a\,b}\right) \;\cdot\; 2 \;\cdot\; s_{\mathrm{load}}

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:

e \;=\; \sqrt{1 \;-\; (b/a)^2}

reported again as the median across sources. Perfect round stars give 0, highly elongated streaks give values approaching 1.

13.3 Weighted Score

A composite built from star count, eccentricity, and background:

W \;=\; N_{\star} \,\cdot\, (1 - e) \,\cdot\, \left(\frac{2}{\sqrt{\mathrm{bg}} + 1} \;-\; 1\right)

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.

13.4 Bg Rate (ADU per second)

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:

\mathrm{BG/s} \;=\; \frac{\mathrm{bg} \,\cdot\, (2^{B} - 1) \;-\; \mathrm{OFFSET}}{t_{\mathrm{exp}}}

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.

13.5 Robust sigma estimator for guides

The ±1σ, ±2σ, ±3σ guides on the metrics plots use the median-absolute-deviation estimator:

\hat{\sigma} \;=\; 1.4826 \,\cdot\, \mathrm{median}(|y - \tilde{y}|)

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.

13.6 Satellite detection

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.

13.7 Thread safety and numba threading layer

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:

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.

13.8 Header-aware exposure labels

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.

13.9 Push to Document

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.

Credits. Blink Comparator is SASpro's own tool. Star measurement uses SEP (Barbary 2016), the Python port of E. Bertin and S. Arnouts' Source Extractor (SExtractor), which is where the 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.