Difference-imaging transient detector. Takes a reference frame and one or more search frames of the same field, optionally registers and background-flattens them in parallel, puts every search frame onto the reference's photometric system from the flux of shared stars, subtracts, matched-filters the difference at the point-spread-function scale, and flags the compact sources that got brighter — candidates for supernovae, novae, asteroids, comets, variable stars, or anything else that appeared or brightened between frames. An optional minor-body path plate-solves the reference, reads the catalogue at the exact observation time, propagates each found body to every frame's own epoch so moving objects track through the blink, and labels the candidates that are already-known asteroids and comets — so what's left over is the interesting set. Any catalogued body can be opened in the in-app 3D Orbit Explorer.
The signature move is detecting on linear, photometrically-matched data and matched-filtering at the PSF scale:
Two ideas carry the detector. First, the gain that puts the search frame onto the reference's brightness scale comes from the flux of stars present in both frames, not from a noise statistic — so a thin-cloud or transparency change rescales real light correctly and stars subtract to nothing. Second, a faint point source is not best found pixel-by-pixel: convolving the difference with a Gaussian matched to the star PSF is the optimal linear detector for a point source and buys roughly 3× in signal-to-noise, which is the difference between a 16th-magnitude asteroid being invisible and being obvious. Everything the detector touches is linear; the stretched image exists only so you have something to look at.
The frames do not need to be co-registered going in — the Align option (§3.2) registers them for you — but they must be the same field at roughly the same scale.
All of these have sensible defaults; most runs only ever touch Detection σ and PSF σ afterwards via Re-detect.
Cosmetic correction (hot/cold pixel suppression) — default off. Cleans hot and cold pixels on every frame before anything else, using SASpro's shared Numba kernel at 5σ hot / 5σ cold with a 3-pixel window. Turn it on when the search frames are raw single subs — a hot pixel present in a search sub but not the reference is a single-pixel “transient.” Leave it off for already-stacked integrations, whose rejection has done this. Cosmetic correction runs at full resolution before binning, because hot pixels are single-pixel events and binning would smear them below the sigma filter.
Default on. Registers every search frame to the reference before differencing, using the same astroalign star-matching and anti-ring warp stack as SASpro's Star Alignment tool. Leave this on unless your frames are already pixel-registered — pixel-wise subtraction of unregistered frames produces a false anomaly around every star and is the single biggest correctness trap in difference imaging. The alignment leaves a zero-valued wedge at the frame edge; that wedge is detected and excluded downstream (§9.7).
Default on, with Degree (polynomial degree 0–6, default 2) and RBF refinement (default on). Runs SASpro's modern Automatic Dynamic Background Extraction (abe_run) on every frame so a sky gradient on either side does not print itself into the difference. It samples away from stars, fits a low-order polynomial (optionally refined with a radial-basis function), and is told to ignore the no-data wedge an aligned frame leaves at its edges, so the fit is not dragged toward zero there. Degree 2 handles ordinary gradients; raise it only for strong, structured backgrounds.
These feed the detection stage and can all be re-tuned after a run with Re-detect:
| Control | Default | What it does |
|---|---|---|
| Detection σ | 5.0 | Threshold in units of the measured noise of the matched-filtered difference, sigma-clipped and computed per frame on valid pixels only. With true photometric matching the noise floor is real, so 3–4σ is usable for faint objects. |
| Min area (px) | 5 | Minimum number of significant pixels, counted on the matched-filtered image before the connectivity dilation. Raise to suppress marginal noise clumps. |
| PSF σ (matched filter) | 1.5 | Width of the Gaussian the difference is convolved with before thresholding, in binned pixels (≈ star FWHM / 2.35). The single most important faint-object control — worth about 3× in SNR over raw-pixel thresholding, and it rejects single-pixel cosmic rays for free. Set 0 to threshold the raw difference. |
| Max bbox dim (px) | 200 | Rejects large structures (nebulosity, aircraft or satellite trails) that are not point sources. |
| Border exclude (%) | 5.0 | Zeroes a margin on each edge so any residual misregistration there cannot masquerade as transients. |
| Control | Default | What it does |
|---|---|---|
| Parallel workers | ≈ ½ CPU count (max 16) | Processes used for each pipeline stage. Defaults leave headroom for the GUI and OS; raise it on headless or benchmark runs. |
| Bin factor | 2 | Downsamples every frame on load by N×N block averaging. 2 drops a 45 MP frame to ~11 MP, cuts memory and inter-process traffic ~4×, and slightly raises transient detection SNR (block averaging compresses read noise). Recommended for 24 MP+ sensors unless you need sub-pixel bounding boxes. The WCS is rescaled to the binned grid so minor-body positions still land correctly. |
When the run finishes, a non-modal Anomaly Results window opens. It is a single zoomable viewer fed on demand — not one tab per frame — so a 120-frame run opens instantly instead of building thousands of widgets.
Three mode buttons flip the single viewer without disturbing the playback:
Overlays: a red box marks an anomaly with no catalogue match (the interesting case — supernova, nova, variable, uncatalogued or faint asteroid, satellite, or artefact); a green box marks one that matched a catalogued minor body, labelled with the designation and absolute magnitude; a yellow circle marks every predicted minor body in the field whether or not an anomaly fell on it, so you can see which known bodies the threshold missed. Unmatched candidates are labelled with their peak significance in σ. Labels avoid colliding with one another and can be toggled off in a crowded field.
Play cycles the search frames in sequence at the chosen Rate (ms), so an actual moving object drifts visibly across the field while a static anomaly stays put — the whole reason to blink. Your zoom and pan are preserved across frames, so you can zoom into a corner, press Play, and watch the asteroid drift past it. Step one frame at a time with the ◀ / ▶| buttons. Double-clicking a row in the tree pauses playback and jumps to that frame.
The detection knobs are repeated here, next to what they found, because tuning a threshold is something you do while looking at the result. Re-detect re-thresholds the cached difference images with these settings in a fraction of a second. In addition to Detection σ, Min area, PSF σ and Max dim, the window exposes:
4, range 0–50) — grows the reference star mask by N pixels before it rejects detections. Turn this up when a star's halo or diffraction spikes shift slightly between frames and light up a ring of false positives around every bright star; a wider mask swallows that ring. This value is the full margin the mask extends past each star: 0 masks only the star cores, larger values reach further into the halo. Too wide and the mask starts covering real objects passing near a field star, so grow only as far as the residuals actually reach. Active only while Reject at ref stars is on.Click Catalogue Minor Bodies in Field in the results window. This is a deliberate, explicit step — a catalogue-plus-ephemeris query is not something to run on every frame automatically — and it opens a setup dialog first. A mirrored progress strip appears in the results window so you see activity even while it covers the main dialog.
An ephemeris is only as good as its timestamp, and a silently wrong DATE-OBS looks exactly like “no objects in this field.” The dialog therefore shows the header value, where it came from, and the effective UTC and Julian Date it will actually use, recomputed live as you change the settings:
DATE-OBS: UTC as written (the FITS standard); local time in this computer's timezone (converted with the daylight-saving rule in force on the night of the exposure, not today's); local time in a named observing-site timezone (DST resolved for that zone on that date, so it keeps working across the site's next DST change); or a fixed UTC offset for a site that does not observe DST. When the header carries a separate DATE-LOC, SASpro knows DATE-OBS is UTC by construction and says so.The anchor epoch is the frame you are looking at, not the reference — inspecting frame 108 of a night while anchoring on frame 1 would be a multi-hour epoch error, hundreds of pixels for a main-belt asteroid.
Minor-body prediction needs a real astrometric solution on the reference. SASpro checks the attached WCS for genuineness — a header with no WCS keywords yields a default object pointing at RA 0, Dec 0 at one degree per pixel, which looks valid and silently projects the whole sky off-frame — and rejects anything whose derived scale or centre is implausible. If no usable solution is present, SASpro plate-solves on demand, running its full chain (in-house Gaia DR3 → ASTAP seeded → ASTAP blind → Astrometry.net local then web). The solve runs on the binned linear reference — the exact pixel grid the anomaly coordinates live on — so no post-hoc rescaling is needed.
Read from the acquisition header (SITELAT / SITELONG / SITEELEV) or SASpro settings, and editable in the dialog. With a site set, positions are topocentric rather than geocentric — worth about 4″ on a main-belt asteroid at 2 AU and 18″ at 0.5 AU, enough to make a marker miss when confirming a specific object against a plate solution.
20) and Max asteroid (row cap, default 50000).15) and Max comet (default 5000).When the query returns, every anomaly that falls within the match radius of a predicted body is labelled with its designation. Crucially, each found body is then propagated to every frame's own epoch, so as you Play through the subs its yellow circle tracks it across the field — the drift that was the whole point of the hunt. Frames without a usable timestamp keep the anchor frame's positions. The progress bar reports through opening the database, selecting bright bodies, propagating orbits (the slow per-object step), and projecting onto pixels.
With bodies catalogued, Explore Orbit (3D) opens the in-app heliocentric orbit viewer for a chosen body at the frame's exact observation time — the Sun, the inner planets (plus Jupiter and beyond as the orbit requires), and the body with its full orbit, placed at that instant. Detection-matched bodies float to the top of the chooser. The viewer draws the body's current position, a drop line to the ecliptic plane (so its inclination reads at a glance), and the Earth→body sight line (the direction you imaged along), with its elements, perihelion/aphelion, period and current Sun/Earth distances in a side panel. It uses the same pyqtgraph.opengl renderer as the Gaia Stellar Neighborhood Explorer and shares the two-body geometry with the catalogue, so the marker lands where the cross-match placed the body. See the Orbit Explorer's own help for its controls.
Clears both selections, resets every option to its default, drops the cached images and differences, and clears the WCS / JD / minor-body state. Equivalent to reopening the dialog but keeping its position.
The parallel pipeline, the linear detector, and the minor-body path, in order.
The whole run executes in a QThread that drives a ProcessPoolExecutor (with a thread-pool fallback for frozen builds), emitting progress signals so the GUI never blocks and Cancel is honoured between results. Each per-frame operation — load/cosmetic/bin, align, ADBE, photometric match + difference, display building — is a top-level worker function dispatched across the pool. Binning is applied inside the load worker so the full-resolution buffer never crosses the process boundary, cutting inter-process traffic by the bin factor squared on top of the memory drop.
Each frame is loaded, optionally cosmetic-corrected at full resolution, then optionally binned by integer-factor block averaging. The order is deliberate: hot pixels are single-pixel events, and binning first would smear them so the sigma filter could no longer catch them cleanly. Every later stage runs on the binned buffer. Each frame's own observation time is read while the file is open.
When enabled, each search frame is registered to the reference with astroalign (asterism matching with a robustness back-off), and warped with SASpro's shared anti-ring warp so resampling does not ring around bright stars. Registration leaves a zero / non-finite wedge at the frame edge wherever rotation moved data out of frame; that wedge is tracked as invalid (§8.7).
abe_run fits the background with a low-order polynomial (degree 0–6) and an optional radial-basis-function refinement, sampling away from stars with anchored median rescaling. It is run with no-data detection on (ignore_nodata): without it the polynomial would sample the dark alignment wedge — the dimmest pixels in the frame — and try to pull the real sky toward zero at the nearest edge. Running the same extraction on both sides matches their gradients to the same flat-zero reference, so the difference sees only local structure.
Each search frame is put onto the reference's photometric system before subtraction:
bg is each frame's sigma-clipped sky level (an additive pedestal); gain is a multiplicative throughput ratio measured from the aperture flux of bright, compact, unsaturated stars found in the reference and measured in both frames. The aperture radius scales with the bin factor. When fewer than five clean stars are available (sparse field, heavy cloud, bad alignment) it falls back to a noise-ratio gain and the log says so.
Detection runs entirely on linear data. A separate, compact 8-bit stretched copy (target median 0.25) is built only for the results viewer. Keeping the stretch out of the mathematics is the central correctness fix of this revision; keeping the display copy as 8-bit mono rather than float RGB is what holds a large results set to a few hundred megabytes instead of tens of gigabytes.
Pixels where either frame has no data — the alignment wedge, or a NaN — are marked invalid and eroded to drop the resampling fringe at the wedge edge. Invalid pixels are excluded from both the noise estimate and detection; leaving the wedge in would inflate the noise and desensitise the whole frame.
The linear difference is diff = matched − I_ref, zeroed outside the valid region. Its noise is estimated by iterative MAD clipping on valid pixels only, so stars and genuine transients do not inflate the threshold. This cached (diff, valid, σ) per frame is all the detection stage needs.
A faint point source clears a few-sigma pixel cut on only one or two pixels, which then die to the min-area filter — too faint to pass on area, too bright to ignore. Convolving the difference with a Gaussian matched to the PSF is the optimal linear detector for a point source:
It attenuates the source peak by about 2× but suppresses pixel-to-pixel noise far more, a net SNR gain near 3× for typical seeing, and it rejects single-pixel cosmic rays for free. The noise is re-measured on the filtered image — filtering changes it by the root-sum-square of the kernel, roughly 6× smaller for a σ = 1.5 kernel — so reusing the raw-difference noise would set the threshold about six times too high and find nothing. Set PSF σ = 0 to threshold the raw difference instead.
A mask of the reference's stars (threshold at median + k·σ, k = 10) is stored undilated. At detection time it is dilated by the Star mask grow value and subtracted from the threshold mask, so alignment-residual and shifting-halo false positives at bright stars are removed. Storing it undilated means the grow margin is fully re-tunable per Re-detect without rebuilding the mask.
The 1-pixel connectivity dilation only ensures a source that threshold-fragments into islands is labelled once; area is still counted on the undilated mask, so Min area keeps meaning “significant pixels,” not “pixels after inflation.” Each surviving component reports its bounding box, area, centroid, matched-filter peak significance (how detections are ranked) and raw-difference significance.
Stage A (expensive: align, ADBE, photometric match, difference) is cached per frame. Stage B (cheap: filter, threshold, group, filter) is a pure function of the cached arrays plus the knobs. Re-detect runs Stage B alone against the cache, so sweeping Detection σ, Min area, PSF σ, Star mask grow or Reject-at-stars costs tens of milliseconds per frame instead of a full reprocess.
When binning is active the reference WCS is rescaled so pixel (x,y) in the binned frame still maps to the same sky position:
Both the CDELT+CROTA and CD-matrix conventions are handled, and SIP distortion terms are adjusted so the mapping is invariant under the pixel rescale. Without this the minor-body cross-match would land one bin factor off.
Positions are computed with astropy and a local two-body propagator — no network, no Skyfield frame issues. Mean-anomaly elements (asteroids) use the catalogue's own mean daily motion; perihelion elements (comets, including e ≥ 1) are propagated through Barker's equation or the hyperbolic Kepler equation as the conic requires. A light-time iteration places each body where it was when the light left it (12–17 minutes earlier for a main-belt asteroid, 6–9″ of motion); the result is the astrometric place, light-time corrected but not aberration corrected, which is the right quantity to compare against a WCS fitted to catalogue star positions.
Each anomaly's bounding-box centre is tested against every predicted body. The radius is given in arcseconds (60″ default) and converted to pixels from the actual plate scale of the reference WCS:
Matching is done per frame against that frame's own epoch, because over a night a main-belt body drifts far beyond any sane radius. All matches inside the radius are kept, nearest first.
Overlays are drawn with OpenCV onto a per-frame 8-bit cache built lazily the first time each frame is shown. Anomaly boxes carry a small margin; a catalogue match gets one combined designation + magnitude label on the box, and labels are pushed apart with leader lines so two never overlap. Predicted bodies get yellow circles, labelled unless the same body was already named on its detection box.
Pushing re-reads the original source file with its full metadata, substitutes the overlay-marked pixels, and creates a new SASpro document carrying the original FITS/XISF header, WCS and acquisition keywords intact. The result is a real astronomy image, not a screenshot — you can plate-solve it, feed it to another tool, or send it for independent confirmation.
QThread-driven process pool; detection on linear data with a display-only stretch; the star-flux photometric match (matched = (search−bg)·gain+bg_ref, gain from shared-star aperture flux) replacing the old MAD-ratio-on-stretched-data scaling; the PSF matched filter with noise re-measured on the filtered image; the reference star mask with its re-tunable grow margin; the validity/no-data wedge handling; integer-factor binning with full WCS (including SIP) rescaling; the two-stage cached-difference detection and sub-second Re-detect; the single-viewer blink results window (Search/Reference/Difference, drift-preserving zoom, lazy overlay cache); the minor-body path (genuine-WCS check that rejects the default unsolved solution, on-demand plate solve on the binned linear reference, DST-aware observation-time model with live effective-UTC/JD readout, mid-exposure handling, topocentric parallax, per-frame propagation so moving objects track through the blink, plate-scale-aware match radius); the metadata-preserving Push to New View; and the 3D Orbit Explorer hand-off.
abe_run, polynomial + optional RBF, no-data aware); star alignment (astroalign transform with robustness back-off plus the shared anti-ring warp); the midtones-transfer stretch used for the display copy; unified image I/O (load_image / save_image) for FITS, XISF, TIFF, PNG with metadata round-tripping; the Plate Solver fallback ladder (in-house Gaia DR3 → ASTAP seeded → ASTAP blind → Astrometry.net) reused for on-demand WCS; the minor-body catalogue infrastructure (minorbodycatalog, managed MPC + comet database, astropy/Kepler position computation); and the 3D Orbit Explorer (orbit_explorer, pyqtgraph.opengl).
connectedComponentsWithStats. Asterism-based registration follows astroalign (Beroiz, Cabral & Sanchez 2020, A&C 32:100384). Mid-exposure epoch handling and light-time correction are standard IAU practice for moving-source astrometry.
linalg.det for the plate scale); Astropy (WCS, Time with UTC↔TT scales and ISO-8601 parsing, units, FITS header I/O, and solar-system ephemerides for the position computation); astroalign (frame registration); pyqtgraph + PyOpenGL (the 3D Orbit Explorer); and PyQt6 (the QGraphicsView viewer, the results tree, and the QThread + process-pool worker pattern).