Narrowband Channel Extractor (NBExtract)

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.

SASpro-exclusive — Franklin Marek's extraction method. NBExtract is the dual-band narrowband counterpart to SSSC's spectrophotometric colour calibration. Both are built on the same core insight: for a well-defined linear problem (RGB ↔ emission lines, or RGB ↔ broadband colour), the stars in your own frame carry enough spectral information to fit the instrument's response empirically. Published filter and sensor QE curves are often promotional approximations and don't account for your specific optical train, filter angle-of-incidence shift, temperature, or dust — so NBExtract bypasses them entirely. The two tools share their star-fetching, SEP photometry, Gaia XP integration, and WCS infrastructure (NBExtract literally subclasses the SSSC dialog), differ only in what they're fitting on top of it.

1. What NBExtract does, and why it works

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.

2. When to use NBExtract

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.

3. Three-step workflow

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.

  1. Step 1 — Fetch Stars. Inherited button at the top of the dialog. Queries SIMBAD + Gaia for stars in the plate-solved field and marks which ones have Gaia XP spectra available for calibration. Shares its implementation with SSSC; see that tool's documentation for the catalogue mechanics.
  2. Step 2 — Calibrate Mixing Matrix. SEP re-detects stars at the configured threshold, matches them to the Gaia XP catalogue, integrates each XP spectrum over the two filter windows, fits the 3×2 mixing matrix by iterative sigma-clipped NNLS (§ 11.3), and auto-derives the Q regularisation values from the result's condition number (§ 11.5).
  3. Step 3 — Extract Channels. Solves the per-pixel inverse system (§ 11.6), applies the Q-blended regularisation (§ 11.7), optionally matches each output median to the corresponding source channel, and creates two new monochrome documents (one per emission line).

4. Narrowband Filter Configuration

Filter type presets

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.

Centre and FWHM spinners

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.

Max calibration stars

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.

5. Action row and matrix readout

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:

Calibrated from 284 Gaia XP stars (298 re-detected · 14 outliers clipped) Channel sensitivity (Ha % | OIII %): R : 87.3% Ha 4.1% OIII G : 10.6% Ha 61.8% OIII B : 2.1% Ha 34.1% OIII Condition number : 8.42 Q₁ = 0.85 Q₂ = 0.72

What to read here:

6. Diagnostic plots after Step 2

Step 2 draws a two-panel figure in the main area:

Left: Filter channel sensitivity bar chart

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:

Right: Fractional residual histogram

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:

7. Auto-Q and the Advanced panel

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.

8. Match output median to source

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.

9. Condition number and three-tier fallback

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:

κ rangeSeverityBehaviour
κ < 25OK Clean separation. Proceed to Step 3.
25 ≤ κ < 100Caution Moderate channel overlap. Auto-Q is conservative; the matrix label shows a warning glyph. Step 3 still enabled.
κ ≥ 100Severe 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).

10. Fun applications

11. Technical details

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.

11.1 The 3×2 mixing system

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:

\mathbf{b}_{\mathrm{meas}} \;=\; A\,\mathbf{s} \;+\; \mathbf{n}

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.

11.2 XP spectrum integration

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:

S_i \;=\; \int_{\lambda_i - \mathrm{BW}_i/2}^{\lambda_i + \mathrm{BW}_i/2} f(\lambda)\,d\lambda\,,\quad i = 1, 2

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

11.3 Iterative sigma-clipped NNLS fit

Each of the three measurement channels is fitted independently by non-negative least squares against the per-star (S1, S2) design matrix:

\min_{a_c, b_c \,\geq\, 0}\;\sum_{j=1}^{N} \left(y_{c,j} - a_c S_{1,j} - b_c S_{2,j}\right)^2

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:

\hat{A} \;=\; \arg\min_{A \,\geq\, 0} \sum_{j} \|\mathbf{y}_j - A\,\mathbf{s}_j\|^{2}

After each fit, per-star mean fractional residuals are computed over the three channels:

r_j \;=\; \frac{1}{3}\sum_{c \in \{R,G,B\}} \frac{|y_{c,j} - \hat y_{c,j}|}{\hat y_{c,j}}

and sigma-clipped at the 3σ MAD level:

\mathrm{MAD} \;=\; 1.4826 \cdot \mathrm{median}\!\left(|r_j - \tilde r|\right)\,,\qquad \mathrm{keep}_j \;=\; r_j \;<\; \tilde r \;+\; 3 \cdot \mathrm{MAD}

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

11.4 Condition number as a calibration quality metric

For the fitted 3×2 matrix, the condition number

\kappa(A) \;=\; \frac{\sigma_1}{\sigma_2}

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

11.5 Auto-Q derivation

11.5.1 Global baseline

The baseline Q value comes directly from the condition number with a logistic falloff:

Q_{\mathrm{global}} \;=\; \mathrm{clip}\!\left(\frac{1}{1 + 0.1\,\kappa},\; 0.2,\; 0.9\right)

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.

11.5.2 Per-channel SVD weighting

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:

Q_2^{\mathrm{svd}} \;=\; Q_{\mathrm{global}} \,\cdot\, \frac{\sigma_2}{\sigma_1}

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.

11.5.3 Row-similarity penalty

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:

\mathrm{sim} \;=\; 1 \;-\; \frac{|A_{i_1,2} - A_{i_2,2}|}{|A_{i_1,2}| + |A_{i_2,2}|}

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 κ²):

Q_2 \;=\; \mathrm{clip}\!\left(Q_2^{\mathrm{svd}} \,\cdot\, (1 \;-\; 0.8\,\mathrm{sim}^2),\; 0.2,\; 0.9\right)

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.

11.6 Per-pixel inversion

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:

\mathbf{L}(x,y) \;=\; A^{+} \;\mathbf{p}(x,y)\,,\qquad A^{+} = (A^{\top}A)^{-1}A^{\top}

followed by non-negativity clipping to prevent negative-flux pixels from the noise-driven corners of the inversion:

L_i \;=\; \max(0,\; L_i)

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.

11.7 Q-blended regularisation

Dominant-channel priors are built from the fitted matrix itself rather than hardcoded:

c_i \;=\; \arg\max_{c \in \{R,G,B\}} A_{c,i}

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:

\mathrm{line}_i \;=\; Q_i \,\cdot\, L_i^{\mathrm{NNLS}} \;+\; (1 - Q_i) \,\cdot\, \mathrm{prior}_i

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.

11.8 Noise amplification

The pseudoinverse maps measurement noise to output noise with gain bounded above by:

\mathrm{noise\;gain} \;\sim\; \kappa^{\,2}

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.

11.9 Stellar-flux colour-mixing fallback

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.

11.10 Headless API and stacker integration

Three module-level entry points cover scripted use:

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.

Credits. Gaia XP spectra are from ESA's Gaia mission (DR3+); SASpro accesses them via a local bulk-library of pre-cached spectra. SEP (Source Extractor for Python, Barbary 2016) is used for star re-detection and aperture photometry. SciPy provides the NNLS solver (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.