Supernova / Asteroid Hunter

Difference-imaging anomaly detector for transient discovery. Takes a reference frame and one or more follow-up search frames of the same field, matches their backgrounds, subtracts, and reports the groups of pixels that got brighter — candidates for supernovae, novae, asteroids, comets, variable stars, or anything else that changed between frames. An optional minor-body cross-match path can plate-solve the reference frame, query the SASpro minor-body catalog at the exact observation time, and label the candidates that correspond to already-known asteroids and comets so what's left is the interesting set.

Original work for SetiAstro Suite Pro. The difference-imaging transient-hunt orchestration — cosmetic-correct both sides optionally, apply per-channel background neutralization against a central sample patch with the (I - δ) / (1 - δ) dynamic-range-preserving correction, run SASpro's polynomial gradient removal on both sides so matching backgrounds subtract cleanly, apply asymmetric stretch to the matched target median so faint positive residuals pop out of the subtraction, convert to mono, subtract and clip to positive-only residuals (so disappearing stars don't flood the detections, only new or brightened sources do), threshold at a user-chosen level with the first/last 5% of each axis masked out to kill alignment-induced edge artefacts, group with 8-connected components, filter by area (≥ 25 px, ≤ 200×200) — is Franklin Marek's design. The optional minor-body cross-match path is also original: lazily plate-solve the reference via SASpro's Plate Solver when a WCS isn't already attached, derive mid-exposure JD from DATE-OBS + EXPTIME/2 with XISF FITSKeywords and FITS-header fallbacks (and the TT scale conversion where that matters), query SASpro's minor-body catalog for bright asteroids and comets, compute Skyfield ephemerides at the derived JD (topocentric when settings carry an observatory location), project the predicted RA/Dec through the reference WCS to pixel coordinates, cull predictions outside the FOV, and cross-match each anomaly's bounding-box centre against the predicted catalogue positions with a WCS-aware pixel search radius computed from the actual plate scale √|det CD| · 3600. The colour-coded preview (green for anomalies that match a catalogued minor body, red for unmatched candidates, yellow circles for every predicted minor body in field whether an anomaly fell on it or not), the Push to New View handler that re-reads the source file so the pushed anomaly-marked image keeps the original FITS/XISF header and metadata intact, the background-threaded QThread + QObject + moveToThread minor-body worker with progress reporting, and the candidate-list text summary with per-anomaly H-magnitude annotations are all original work.
Workflow
Select a reference image — usually the earlier of the two sessions, or a stacked master if you have one. Select one or more search images — the frames you want to hunt in. Optionally tick Apply Cosmetic Correction to clean hot/cold pixels before preprocessing. Pick a threshold (default 0.10). Click Process — the dialog preprocesses both sides, subtracts, finds anomalies, and shows a tree of results. Double-click an entry to open its anomaly-marked preview. From the preview you can zoom with Ctrl + wheel, pan by dragging, and push the marked image back into SASpro as a new document.

The signature move is positive-only residuals:

D(x, y) = \mathrm{clip}\!\left( I_{\mathrm{search}}(x, y) - I_{\mathrm{ref}}(x, y),\; 0,\; 1 \right)

Clipping negative residuals to zero asymmetrises the subtraction: a star that disappeared between the reference and the search produces D < 0 and is thrown away; a star that appeared or got brighter survives. For transient hunting this is exactly what you want — a supernova, a nova, or an asteroid passing through the field is a new source added onto the background, not something taken away.

1. Reference and search frames

The two paths at the top of the dialog select the inputs:

The two frames don't need to be bit-for-bit co-registered going in — the background neutralisation and gradient-removal steps leave the geometry alone and simply match brightness levels across the two sides — but they do need to be of the same field at the same approximate scale. If you have one with and one without an alignment pass, align first (SASpro's Star Alignment or the Stacking Suite's integration output both work).

2. Cosmetic correction option

Check Apply Cosmetic Correction before Preprocessing to clean hot and cold pixels on every search image before subtraction. Uses SASpro's shared Numba cosmetic correction kernel (the same code the Stacking Suite uses) at 5σ hot and 5σ cold with a 3-pixel window. Important when your search frames are raw single-sub exposures — one or two hot pixels in a search sub that aren't in the reference will produce single-pixel “transients” that pass the threshold. The area filter (§9.8) catches most of these anyway, but turning this on removes them at the source.

Leave it off when both sides are already-stacked integrations — the stacking rejection has already cleaned them.

3. Threshold slider

Slider from 0.01 to 0.50, default 0.10. The threshold is applied to the clipped difference image after normalisation and asymmetric stretch (target median 0.25), so 0.10 corresponds to roughly “40% dimmer than typical stretched star core”. Lower values catch fainter transients at the cost of more false positives; higher values find only bright anomalies.

For a 15-minute search frame against a deep stacked reference, 0.10 is a good starting point. For noisier shorter exposures, nudge up to 0.15 or 0.20.

4. Process

The Process button runs the whole pipeline in order:

  1. Cosmetic correction (if checked), on every search image.
  2. Preprocessing of the reference, then every search image.
  3. Difference imaging and anomaly extraction, per search image.
  4. Minor-body cross-match if a predicted catalogue is loaded (manual step, see §9).
  5. Results dialogs (summary tree + text report).

Each phase updates the status label and either the preprocessing or search progress label as it goes. The dialog stays responsive — processEvents is called between images so you can see progress live.

5. Results tree

After Process completes, a non-modal Anomaly Results window opens with a two-column tree: the search image's filename and the number of anomaly groups found in it. Double-click any row to open that image's anomaly-marked preview (§6).

A separate Detailed Anomaly Results text panel also opens, listing each image's anomalies with their bounding-box corners and any catalogued minor-body candidates that fell inside them.

6. Anomaly preview window

The preview is a zoomable, pannable, non-modal viewer with its own toolbar. On top of the stretched search image, SASpro draws:

Toolbar

Mouse

7. New Instance

Clears every selection, resets threshold to 0.10, drops the loaded preprocessed images, and clears the WCS / JD / predicted-minor-body state. Equivalent to closing and reopening the dialog but keeps your window position.

8. Minor-body cross-match

The minor-body path runs as a separate background operation after Process has produced anomalies. SASpro doesn't auto-trigger it on every search — it's a slow catalogue + ephemeris query, so you ask for it explicitly, and the dialog lets you tune the magnitude limits and time offset before kicking it off.

Limits dialog

When the minor-body search is requested, a modal dialog asks for:

Progress

The search runs in a background QThread-wrapped QObject worker, so the dialog stays responsive. A dedicated progress bar appears below the status label, reporting through the phases: opening the catalogue, selecting bright asteroids and comets, computing positions (asteroids then comets), projecting onto image pixels. On completion, SASpro re-opens the detailed results dialog with every anomaly annotated with its candidate matches.

9. Technical details

The preprocessing pipeline, difference extraction, and minor-body cross-match in order.

9.1 Preprocessing pipeline

Both the reference and every search image run through identical preprocessing before subtraction:

  1. Optional cosmetic correction on search frames (§2).
  2. Background neutralisation on colour frames (§9.2).
  3. Automatic Background Extraction via SASpro's PolyGradientRemoval (§9.3).
  4. Asymmetric stretch to target median 0.25 (§9.4).

The two sides are preprocessed independently with the same code path, so background levels, gradients, and brightness are matched. Any residual difference after preprocessing is — by construction — local structural difference, not global illumination.

9.2 Background neutralisation

For colour frames, SASpro removes any residual tint by subtracting a per-channel offset derived from a 10% central sample patch. For each channel c, taking med(C_c) as the sample-region median and the mean of all three channel medians as the target:

I'_c = \frac{I_c - \delta_c}{1 - \delta_c},\quad \delta_c = \mathrm{med}(C_c) - \overline{\mathrm{med}(C)}

The denominator (1 - δ_c) is the dynamic-range-preserving correction: a plain subtraction I_c - δ_c would push a bright white pixel below 1.0 wherever a channel was tinted up, and crush its headroom. Rescaling by 1 - δ_c preserves white at white while removing the tint. Clipped to [0, 1].

Only applied to colour frames (img.ndim == 3 and shape[2] == 3). Mono frames pass through unchanged.

9.3 Automatic Background Extraction

SASpro's PolyGradientRemoval fits a 2D polynomial of degree 2 to the background and subtracts it in the original (linear) domain. The fit runs on a 4× downsample with 100 sample points placed over the frame; sample locations are chosen to avoid stars and bright structures (the same algorithm as SASpro's standalone Polynomial Gradient Removal tool).

Why on both sides: the whole point of the subtraction is to isolate local differences. A residual uncorrected gradient on either frame would print the gradient into the difference image and pollute the anomaly search. Running the same ABE on both sides matches their gradients to the same reference (flat zero), so the difference image sees only local structure.

9.4 Asymmetric stretch

Both sides are stretched to the same target median:

\mu_{\mathrm{target}} = 0.25

via SASpro's shared midtones-transfer stretch (stretch_mono_image / stretch_color_image, same code used by GHS and Autostretch). The 0.25 target is deliberately brighter than a conventional linear or quarter-median stretch — it pushes faint positive residuals into a visible brightness regime where the threshold slider can bite on them, while leaving the deep background still noticeably dark. For visual transient hunting on web-exported JPEGs this is close to ideal.

Colour frames are stretched per-channel unlinked, so the ABE'd colour balance is preserved into the stretched output.

9.5 Difference imaging

Both frames collapse to mono (channel average for RGB) and the stretched reference is subtracted from the stretched search:

D(x, y) = \mathrm{clip}\!\left( I_{\mathrm{search}}(x, y) - I_{\mathrm{ref}}(x, y),\; 0,\; 1 \right)

Clipping the lower bound at zero makes this a positive-only detector — it will find things that appeared or brightened, never things that disappeared or dimmed. For transient-source discovery that's exactly the physics: supernovae, novae, dwarf-nova outbursts, and asteroids passing through all show as new / brightened pixels. A star variable that fades isn't being searched for here (and classical difference imaging with a two-sided threshold would catch it, but at much higher false-positive rates). The upper clip at 1.0 is defensive, not substantive — stretched values stay in [0, 1] already.

9.6 Border exclusion

A 5% border on each side of the frame is zeroed out of the detection mask:

\mathcal{M}(x, y) = 0 \;\; \mathrm{if}\;\; x < 0.05 W \;\mathrm{or}\; x > 0.95 W \;\mathrm{or}\; y < 0.05 H \;\mathrm{or}\; y > 0.95 H

Why: if the two frames weren't perfectly aligned going in (small rotation, differential drift, atmospheric refraction at low altitudes), the edges of the frame are where the misregistration prints the strongest, and they would produce rings of edge-aligned false transients. Masking the outer 5% off keeps the detector well-behaved on imperfect alignment at the cost of ignoring any real transients in the extreme corners.

9.7 Connected-components grouping

SASpro thresholds the difference image at the user's slider setting:

\mathcal{M}(x, y) = \mathbb{1}\!\left[ D(x, y) > \tau \right]

and runs 8-connected components labelling (cv2.connectedComponentsWithStats) to group the above-threshold pixels into distinct candidate anomalies. Each component gets a bounding box (minX, minY, maxX, maxY) and an area in pixels — the pieces of information the preview and the minor-body cross-match need.

9.8 Area filtering

Components are kept only if:

25 \leq \mathrm{area}(c) \quad \mathrm{and} \quad \max(w_c, h_c) \leq 200

The lower bound rejects noise speckle: a single hot pixel or an edge artefact that scraped past the border exclusion is tiny; a real transient on a stacked frame will seat on 25+ connected pixels after stretching. The upper bound rejects large structures — a globally-brighter nebulosity region, a passing airplane trail, or a satellite streak. For transient-point-source hunting both ends of the filter are appropriate.

If you need to catch something intermediate (a bright comet coma, say), consider raising the max bound or dropping this filter in a one-off edit.

9.9 Mid-exposure JD derivation

For minor-body cross-matching, SASpro needs the exact observation time of the reference frame. The clock used is the midpoint of the reference's exposure:

\mathrm{JD}_{\mathrm{mid}} = \mathrm{JD}_{\mathrm{obs}} + \frac{t_{\mathrm{exp}}}{2 \cdot 86400}

where JD_obs is derived from the DATE-OBS keyword and t_exp is the exposure time in seconds (EXPTIME or EXPOSURE).

Minor bodies move during the exposure; the midpoint is where they are “on average” across the frame and where their predicted centroid should land. Three retrieval paths:

  1. XISF path: DATE-OBS and EXPOSURE (or EXPTIME) from the file's FITSKeywords metadata, parsed through astropy's Time. Handles the ISO-8601-with-fractional-seconds format that SharpCap / N.I.N.A. / SGP emit.
  2. FITS path: DATE-OBS and EXPTIME/EXPOSURE from the FITS header. Converted to TT scale (t.tt.jd) because that's the planetary-ephemeris reference.
  3. None: minor-body search aborts with an explanation. Try plate-solving first if the file lost its header.

9.10 Lazy plate solve

Minor-body prediction requires a WCS on the reference. If one is already attached (because the frame came from a solved stacking integration, say), SASpro uses it directly. If not, it calls into the Plate Solver's four-pass core (_solve_numpy_with_fallback) on-demand, strips any existing WCS keywords from the acquisition header via _strip_wcs_keys, and merges the fresh solver WCS back in via _merge_wcs_into_base_header. The result is identical to what Plate Solver would produce as a standalone tool, so the fall-back ladder (Gaia DR3 engine → ASTAP seeded → ASTAP blind → astrometry.net) is available here too.

9.11 Catalogue query

The minor-body catalogue (SASpro's packaged MPC + comet orbit database, managed by minorbodycatalog) is queried for bright asteroids and comets up to the user-configured absolute-magnitude caps (H_ast_max, H_com_max) with row count caps for each (ast_max_count, com_max_count). The result is two DataFrames of orbital elements.

9.12 Ephemeris + WCS projection

Each selected body's position is propagated to the mid-exposure JD using Skyfield's catalogue ephemerides. Topocentric observation is used when the SASpro settings carry an observatory latitude, longitude, and elevation; otherwise geocentric (parallax error for an outer-main-belt asteroid at geocentric vs. topocentric is at most ~arcseconds, well below the default 60 arcsec cross-match radius).

The predicted RA/Dec is projected to pixel coordinates through the reference's WCS:

(\alpha, \delta) \;\;\overset{\mathrm{WCS}}{\to}\;\; (x, y)

Bodies landing outside the frame are discarded; the survivors are the predicted catalogue positions on this specific frame.

9.13 Cross-match

Each anomaly's bounding-box centre is tested against every predicted body position. The search radius is specified in arcseconds (60 arcsec default) and converted to pixels using the actual plate scale derived from the reference WCS's CD matrix:

s = \sqrt{\left| \det \mathrm{CD} \right|} \cdot 3600 \;\; [\mathrm{arcsec/pix}]

and the per-anomaly pixel search radius:

r_{\mathrm{pix}} = \frac{r_{\mathrm{arcsec}}}{s}

Pixel distance from the anomaly centre to each predicted body:

d = \sqrt{(x_{\mathrm{body}} - x_{\mathrm{anom}})^2 + (y_{\mathrm{body}} - y_{\mathrm{anom}})^2}

and acceptance:

d \leq r_{\mathrm{pix}} \;\;\Rightarrow\;\; \mathrm{candidate\ match}

All matches within the radius are kept and sorted by distance; the closest becomes the anomaly's primary match (the matched_body field), used to colour its box green in the preview. Multiple close candidates (two asteroids within an arcminute of each other) still both show up in the text results so you can inspect them.

9.14 Preview rendering

The anomaly-marked preview is drawn with OpenCV rectangle and circle primitives on an 8-bit RGB copy of the stretched image:

9.15 Push to new view

Pushing the anomaly-marked preview back into SASpro as a new document goes through a careful metadata-preserving path rather than just handing over the pixel buffer. The push handler:

  1. Looks up the source file path that the preview window was opened from.
  2. Re-reads the original file with load_image(..., return_metadata=True) to recover the complete header, bit-depth record, mono flag, and metadata dict.
  3. Takes the anomaly-marked pixels from the preview (normalising from [0, 255] to [0, 1] if needed).
  4. Creates a new SASpro document with the marked pixels but every piece of the original metadata attached — FITS header, XISF FITSKeywords, WCS, DATE-OBS, EXPTIME, the lot.

Why this matters: the new document is a legitimate astronomy image, not a screenshot. You can plate-solve it (it will solve to the same solution, since it's the same field), feed it back into another SASpro tool, save it as FITS with proper headers, or send it to a collaborator for independent confirmation.

Credits. The transient-hunt orchestration and minor-body cross-match workflow are new work by Franklin Marek for SetiAstro Suite Pro. SASpro-exclusive content: the cosmetic-correct → background-neutralise → ABE → stretch → subtract → threshold → connected-components → area-filter → cross-match → colour-code pipeline composition; the asymmetric positive-only difference (clip(S - R, 0, 1)) that filters the detector to new/brightened sources; the 5% border exclusion against alignment-edge artefacts; the minor-body cross-match path (lazy plate solve, mid-exposure JD from XISF or FITS header, Skyfield ephemerides topocentric when a site is configured, plate-scale-aware pixel search radius from √|det CD|·3600, nearest-match primary + full candidate list); the colour-coded anomaly preview (green matched / red unmatched / yellow predicted) and its interactive QGraphicsView with Ctrl + wheel cursor-anchored zoom; the metadata-preserving push-to-view that re-reads the source file so the pushed anomaly image keeps its original FITS/XISF header intact; and the background-threaded minor-body worker with its QThread + QObject + moveToThread lifecycle and granular progress callbacks are original work.

Reused SASpro primitives (also original SASpro work, cross-credited): bulk cosmetic correction via the shared Numba kernel (bulk_cosmetic_correction_numba, 5σ hot + 5σ cold); Polynomial Gradient Removal (PolyGradientRemoval, degree-2 polynomial, 4× downsample, 100 sample points); midtones-transfer stretch (stretch_mono_image / stretch_color_image, same code behind Autostretch and GHS); unified image I/O (load_image, save_image) for FITS, XISF, TIFF, PNG with metadata round-tripping; the Plate Solver four-pass fallback ladder (_solve_numpy_with_fallback, _strip_wcs_keys, _merge_wcs_into_base_header) which is reused here for on-demand WCS attachment; and the minor-body catalogue infrastructure (minorbodycatalog with its MPC + comet orbit database, ensure_minor_body_db managed-download, and Skyfield-driven position computation).

External algorithms and tools: difference imaging for transient detection is a classical technique in time-domain astronomy; the proper PSF-matched version is Alard & Lupton (1998, ApJ 503:325), and SASpro's simpler aligned-subtraction form is appropriate when input stacks have been co-registered and background-matched (as they are here by the shared preprocessing path). Connected-component labelling originates with Rosenfeld & Pfaltz (1966, J. ACM 13(4):471), with the modern two-scan implementation in OpenCV's connectedComponentsWithStats. Mid-exposure epoch handling for moving-source astrometry is standard IAU practice; the TT time scale (used for the FITS-fallback JD) is the correct reference for planetary and minor-body ephemerides.

Libraries: OpenCV (cv2) provides connected-components, rectangle and circle drawing, and the base image-processing primitives. NumPy provides array operations and linear-algebra helpers (linalg.det for the plate-scale derivation). Astropy provides the WCS implementation (astropy.wcs.WCS), the Time object with scale conversions (UTC ↔ TT) and ISO-8601 parsing (astropy.time.Time), units (astropy.units), and the FITS header I/O (astropy.io.fits.Header). Skyfield computes the minor-body ephemerides from the catalogue orbital elements using JPL DE-series planetary ephemerides. PyQt6 provides the UI framework: QGraphicsView / QGraphicsScene for the zoomable preview, QTreeWidget for the results tree, the QThread+QObject worker pattern, and the dialog toolkit.