The sister tool to SSSC, applied to a different problem. Where SSSC solves broadband colour calibration without published quantum-efficiency curves by using the stars in your own image as spectrophotometric references, NBExtract solves dual-band narrowband channel extraction the same way. Instead of relying on the filter × sensor QE product from the manufacturer's datasheet, it fits the empirical 3×2 mixing matrix directly from Gaia XP-spectrum stars in your frame, then inverts it per-pixel to separate the two emission lines into independent monochrome channels.
A dual-band narrowband filter (L-eXtreme, Antlia ALP-T, STC Duo, Optolong L-Ultimate, Askar ColorMagic, …) passes two emission-line windows: typically Ha (656.28 nm) and OIII (500.70 nm), sometimes SII (671.64 nm) or Hβ (486.13 nm). Shot through a one-shot colour (OSC) sensor, the Ha signal lands mostly in the red channel, the OIII mostly in green — but both leak into all three channels, by amounts that depend on the sensor's spectral response at those specific wavelengths, your filter's actual passband centres, and the way the Bayer filters attenuate off-peak signal. The job of a channel extractor is to invert that leakage so you end up with two clean emission-line channels suitable for processing (and for recombination into SHO/HOO/false-colour palettes) just like separately filtered mono subs would be.
The traditional way to do this uses published QE curves: pick a sensor model, pick a filter model, multiply their response curves at the two line wavelengths, build the mixing matrix from those numbers. This has three problems:
NBExtract sidesteps all of that by fitting the mixing matrix from your actual data. For every star in the field that has a known Gaia XP low-resolution spectrum (~450 to 1050 nm continuum sampling), NBExtract integrates that known spectrum over the two filter windows to predict how much “line1 signal” and “line2 signal” the star should produce (even though the star isn't emitting those lines — its blackbody continuum passes through them), then compares to the actual R, G, B measured in the image. Do this for hundreds of stars with a wide range of spectral types and the mixing matrix falls out as a non-negative least squares fit. The system quantum efficiency uncertainty cancels out because you're fitting ratios against the same instrument that took the image.
What NBExtract is not for. Triband filters (Ha + OIII + SII in a single filter) will not be reliably separated into three lines by any three-equation two-unknown system; you still get two cleanly extracted channels but one is a blend. The dialog warns about this. Broadband (L-eNhance, UHC, CLS) filters are not supported — there is no well-defined pair of emission windows to extract.
The dialog is non-modal, so you can inspect the active document between steps, and it stays open after extraction so you can run on another sub.
The combo at the top of the configuration group picks the dual-band preset:
Picking a named preset snaps the centre spinners to the canonical rest wavelengths and the bandwidth spinners to typical narrowband FWHM values (7 nm for Ha/SII, 6.5 nm for OIII/Hβ); these are starting points and you should edit them to match your actual filter's spec sheet. Picking Custom leaves whatever you had set before.
Two rows, one per line. Centre (nm) is the emission line
rest wavelength (set automatically for presets; freely edit for
Custom). Bandwidth (FWHM) is the filter's full-width at
half-maximum in nm — use the number from your filter's spec
sheet, not the marketing “7 nm narrowband” label.
Internally these define a top-hat integration window of
centre ± FWHM/2 that each Gaia XP
spectrum is integrated over to produce the per-star
Sline1 and Sline2
predictions (§ 11.2). Approximating the real filter
transmission as top-hat is coarse but adequate: because the fit is
empirical and ratiometric, systematic shape mismatches are absorbed
into the fitted matrix coefficients.
Spinner, 6–2000, default 300. Caps the number of Gaia XP stars used to fit the mixing matrix. Candidates are sorted by SEP flux descending so the brightest 300 (which have the cleanest photometry and the best XP spectrum SNR) are selected. 300 is genuinely more than enough for a stable NNLS fit on most frames; increase it for very sparse fields where you don't have 300 bright XP stars to work with. The histogram above updates to show the limiting G magnitude the current cap would reach.
The two bold buttons — Step 2: Calibrate Mixing Matrix and Step 3: Extract Channels — sit beside the Match output median to source checkbox (§ 8) and the Max cal stars spinner.
After Step 2 the Mixing matrix readout below shows the calibration summary in human-readable form:
What to read here:
Step 2 draws a two-panel figure in the main area:
Two bars per RGB channel (red: line1, blue: line2), each normalised to its own line's peak channel so the plot shows the shape of each line's response across R/G/B rather than their absolute levels. The number above each bar is still the absolute percentage of that line's total sensitivity, so you can read both at once.
What to look for:
Distribution of
(measured − predicted) / predicted
across every star and every channel after the matrix fit, with the
display range set to ±3σ around the median
(uncapped so outliers appear as a natural tail rather than piling
on the edge). The dashed vertical lines mark zero (white) and the
median residual (red/tomato). The title shows the fit's standard
deviation and the count of stars beyond ±3σ.
What to look for:
Pure NNLS extraction (Q = 1) is mathematically ideal but noise-sensitive: when the mixing matrix is ill-conditioned, the inverse amplifies noise in the fainter line far more than the signal. The classical regularisation trick is to blend the NNLS result with a safer prior — in NBExtract's case, the raw dominant channel for each line. The blend factor Q controls where on the spectrum between “perfect NNLS” and “just use R for Ha, G for OIII” the output lands.
NBExtract derives Q⊂1⊂ and Q⊂2⊂ independently after calibration based on how well-conditioned each line's direction is in the fitted matrix (§ 11.5). Q⊂1⊂ depends on global conditioning; Q⊂2⊂ is additionally penalised when the two channels that are not dominant for line2 carry nearly identical line2 sensitivity — a common pathology that would otherwise amplify noise with no signal gain. The Advanced panel displays the auto-derived values with their explanation and lets you override:
Overriding Q doesn't require re-calibrating; the matrix fit stays, only the final blend changes. Press Step 3 again to see the new result.
Checkbox in the action row, on by default. When enabled, each extracted channel is statistically stretched so its median matches the corresponding source channel's median — so the output documents sit at the same linear intensity level as the input and pair naturally with downstream Combine, Blend, or recombination steps. Off is for callers (like a stacker) that will do their own normalisation across many subs.
Immediately after Step 2 finishes, NBExtract classifies the calibration by the matrix's condition number κ (§ 11.4) and either proceeds, flags a caution, or offers a fallback:
| κ range | Severity | Behaviour |
|---|---|---|
| κ < 25 | OK | Clean separation. Proceed to Step 3. |
| 25 ≤ κ < 100 | Caution | Moderate channel overlap. Auto-Q is conservative; the matrix label shows a warning glyph. Step 3 still enabled. |
| κ ≥ 100 | Severe | Noise would be amplified by roughly κ². Dialog offers the stellar-flux color-mixing fallback (§ 11.9). |
The fallback uses the same star-based 3×3 colour-mixing approach that white-balance uses, then picks the dominant channel for each line by wavelength (< 520 nm → B, > 600 nm → R, else G). Less precise than NNLS extraction but far more noise-robust when the matrix is singular (e.g. a synthetic HOO image where G = B exactly, or data from a sensor where G and B pixels respond identically at the OIII wavelength).
calibrate_only
headless API returns raw per-star photometry records that a
stacker can pool across many subs for a joint fit — one
shared mixing matrix calibrated across the full session,
applied to every sub.All processing on float32 in [0, 1]. SEP for star re-detection, SciPy NNLS for the matrix fit, Gaia XP for spectra. NaN-safe throughout — warp borders (uniformly zero across RGB) and non-finite pixels are masked from both the background estimation and the per-pixel inversion.
For a star of unknown spectral type observed through a dual-band filter, the three measured channel intensities should relate to the star's integrated flux in each emission-line window by a linear mixing:
where bmeas is the measured
(R, G, B) triplet, s is the
(S1, S2) integrated-flux
pair for the two filter windows, A is the 3×2
mixing matrix (the per-channel sensitivity to each line), and
n is photometric noise. Three equations in two unknowns
— overdetermined, well-conditioned when the filter actually
separates the two lines, and solvable per-pixel by the
Moore-Penrose pseudoinverse once A is known.
For each calibration star, NBExtract loads its Gaia XP
low-resolution spectrum (continuum flux density
f(λ) sampled from ~336 nm to
~1020 nm), normalises it to unit total flux, and integrates
over the two top-hat filter windows:
The integration is a trapezoid rule over the spectrum's native sampling, with linear interpolation at the window boundaries if they don't fall on sample points. The star is skipped if either integrated flux comes back zero or negative (window outside the XP coverage, degenerate spectrum, etc.).
Each of the three measurement channels is fitted independently by
non-negative least squares against the per-star
(S1, S2) design matrix:
Design matrix columns are median-normalised before the fit so
NNLS isn't fighting scale differences between
S1 and S2; the
coefficients are rescaled back after. The three per-channel
coefficient pairs become the rows of the 3×2 matrix
A:
After each fit, per-star mean fractional residuals are computed over the three channels:
and sigma-clipped at the 3σ MAD level:
The fit is repeated with the surviving stars for up to 3 iterations or until no star is clipped. The clip is held off if fewer than 6 stars would remain (NNLS becomes under-constrained below that count).
For the fitted 3×2 matrix, the condition number
(ratio of the largest to smallest singular value) governs how much noise the inversion will amplify. A condition number of 10 means the inversion amplifies RGB measurement noise by up to ~100× in the extracted lines; a condition number of 100 means ~10 000×. The three severity bands in § 9 are chosen to match this scaling — κ < 25 means noise amplification capped at < 625×, which is usually swamped by nebular signal; κ ≥ 100 means noise would dominate real signal in the fainter line.
The baseline Q value comes directly from the condition number with a logistic falloff:
which gives Q = 0.91 clipped to 0.9 at κ = 1, Q = 0.5 at κ = 10, Q = 0.33 at κ = 20, and clipped to the floor 0.2 for κ > 40. The clip bounds are there so the user always has some NNLS correction and never gets locked into 100% raw-channel output.
Line1 (the more cleanly resolved line in a typical dual-band
system — Ha in red is well-separated from OIII in green/blue)
takes Q1 = Qglobal
directly. Line2 is penalised by the second singular value's ratio
to the first:
This captures that the second principal direction of the fitted
matrix is less well-constrained; when
σ2 / σ1 is
small, the inversion along the line2 direction is being extrapolated
with little support.
The strongest noise-amplification pathology for the second line is not captured by the condition number alone: it occurs when the two channels that are not dominant for line2 carry nearly identical line2 sensitivity. In that case the inversion can't tell whether a bright line2 signal came from one or the other, and ends up extrapolating noise instead of measuring signal.
NBExtract measures this directly by computing the row similarity in the non-dominant line2 column:
where i1 and i2
are the two non-dominant rows for line2. Similarity 0 means the
two rows carry nothing in common; similarity 1 means they are
identical. The final Q⊂2⊂ applies a squared penalty that
mirrors noise amplification physics (gain scales as
κ²):
So sim = 0.7 cuts Q⊂2⊂ to 61% of its SVD-based value; sim = 0.9 cuts it to 35%; sim = 1.0 drives it to the 0.2 floor. This is the single most important part of the auto-Q logic for the common OIII-in-dual-band case where G and B channels often respond almost identically to the 500.7 nm window.
With A fitted and reduced to its Moore-Penrose
pseudoinverse A+, each pixel's
(R, G, B) triplet gets inverted to its
line1 and line2 contribution in a single vectorised step:
followed by non-negativity clipping to prevent negative-flux pixels from the noise-driven corners of the inversion:
Pixels where any channel is non-finite come out NaN in both
output lines (so downstream integrator NaN handling still works);
finite-zero warp borders stay zero because
A+ @ [0, 0, 0]T = [0, 0]T.
Dominant-channel priors are built from the fitted matrix itself rather than hardcoded:
If the top two channels for a line are within 5 percentage points of each other in their fractional sensitivity, their raw channels are averaged for the prior rather than one being arbitrarily chosen — they are genuinely tied. Otherwise the single dominant channel is used.
The final extracted line is a linear blend between the NNLS inversion and the raw prior:
with Q⊂i⊂ set to the auto-derived values from
§ 11.5 unless overridden in the Advanced panel. Both
LNNLS and prior are median-matched to the
corresponding source channel before the blend when
“Match output median to source” is on, so Q operates on
consistent intensity scales.
The pseudoinverse maps measurement noise to output noise with gain bounded above by:
which is why the Q-blend matters so much as κ climbs: at κ = 30, pure NNLS extraction amplifies noise ~900× while the raw-channel prior amplifies it 1×. A Q value of 0.4 means the output is dominated by the prior (noise gain ~1 from prior + ~145 from scaled NNLS), vs ~900 for Q = 1. The auto-Q logic places Q near 0.33 at κ = 20 and keeps falling, which is why extraction remains stable through the Caution band.
When the NNLS matrix fit fails outright (too few surviving stars, genuinely singular matrix from synthetic HOO data where G = B exactly), or the user accepts the Severe-band offer, NBExtract falls back to the stellar-flux colour-mixing approach shared with the white-balance tool: fit a 3×3 colour transform from the same star photometry, apply it to the image, then pick the dominant channel for each line by wavelength (< 520 nm → B, > 600 nm → R, else G). The result is cruder than NNLS extraction but far more noise-robust under singular conditions and still produces two usable, correctly-named output documents.
Three module-level entry points cover scripted use:
open_nbextract(doc_manager, data_path) —
the normal interactive dialog.run_nbextract_via_preset(app, doc, preset)
— builds a hidden dialog, runs Fetch → Calibrate → Extract
with no modals, returns the two output documents. Dispatched
by the command registry.calibrate_matrix_headless(doc_manager, data_path,
doc=..., preset=...) — just the Fetch → Calibrate
half. Returns (A, star_records, cond). The
star_records list carries raw per-star photometry
ready to be pooled across many subs for a joint fit.The stacker use case is to call calibrate_matrix_headless
on each sub, pool all star records, run a single joint fit, then
apply the one matrix to every sub via a lightweight pure-numpy call
to extract_channels_regularized. This gives one
consistent calibration across the full session rather than per-sub
drift.
scipy.optimize.nnls). SIMBAD (CDS, Strasbourg)
and the Pickles stellar spectral library are available as
alternate reference paths inherited from SSSC's infrastructure.
The NBExtract algorithm itself — the empirical
3×2 mixing matrix fit from Gaia XP stars, the
sigma-clipped iterative NNLS with design-matrix normalisation,
the condition-number-based auto-Q with its SVD and row-similarity
weightings, the matrix-driven dominant-channel detection with
5-percentage-point tie-breaking, the Q-blended regularisation,
the three-tier fallback with wavelength-driven channel assignment,
and the stacker-integration calibrate_only API —
is SASpro-exclusive, by Franklin Marek. It is the dual-band
narrowband counterpart to SSSC and shares its spectrophotometric
philosophy.