A direct port of Bill Blanshan's and Mike Cranfield's PixelMath narrowband normalization technique, with Bill's permission. Normalizes Ha / OIII / SII channels against each other so that classical palette mappings (SHO, HSO, HOS, HOO) land on balanced, controllable colour instead of whatever the raw channel intensities happen to produce.
Credits up front. The core normalization algorithm — the SHO / HSO / HOS / HOO scenario formulas, the linear and non-linear modes, the lightness-source mechanism, the highlight recover/reduction curves — is the original PixelMath work of Bill Blanshan (Another Astro Channel) and Mike Cranfield (cosmicphotons.com). This tool reproduces that algorithm in SASpro with their permission. What SASpro adds — linear fit integration, SCNR as a post-step, background neutralization, OSC synthesis, mapped-RGB view import, the async preview worker and the UI around it — is detailed in § 12.
Classic narrowband palettes (SHO, HOO, …) are a direct R / G / B channel-assignment: “SII in red, Ha in green, OIII in blue”, say. The visual result depends heavily on the relative integrated signal of each channel — a weak OIII makes everything look magenta; a dominant Ha drowns out subtle SII shock structure. Normalization reshapes each channel against a shared statistical baseline (minimum, median, highlight distribution) before the palette assignment, so you can control the final hue balance with a handful of parameters instead of fighting it in Curves afterward.
Load Ha / OIII / SII separately from the Narrowband channels group. Each button offers From View (any open SASpro document) or From File (PNG, TIF, FITS, XISF). The label underneath turns green with the source name when loaded (long paths and view names are elided and shown in full on hover). For HOO scenarios the SII slot is hidden; for all other scenarios all three are required.
From the OSC extractions group:
If a direct mono channel is loaded alongside the OSC, the mono takes priority; OSC only fills in channels that would otherwise be missing. This differs from Perfect Palette Picker, which averages them.
The Import mapped RGB view group at the top of column A has four buttons: Load SHO View, Load HSO View, Load HOS View, Load HOO View. Pick one, choose any currently open RGB document, and SASpro inverse-splits it back into Ha / OIII / SII channels according to the mapping you selected:
This is the fastest way to re-normalize an existing palette image from Perfect Palette Picker. The source document is tracked so that Apply to Current View writes back to the original mapped composite, not the temp split channels.
Dimension mismatch is detected automatically. If the loaded channels aren't the same pixel size, you're prompted once per session to resize everything to a reference frame (preferred: Ha, then OIII, then SII) using OpenCV's INTER_AREA / INTER_LINEAR.
Four palette targets, selected from the Scenario combo. The normalization math changes per scenario because each one needs different relationships between the channels to look balanced:
| Scenario | R | G | B | Needs |
|---|---|---|---|---|
| SHO | SII | Ha | OIII | Ha + OIII + SII |
| HSO | Ha | SII | OIII | Ha + OIII + SII |
| HOS | Ha | OIII | SII | Ha + OIII + SII |
| HOO | Ha | OIII | OIII | Ha + OIII only |
Switching scenario reshuffles which rows of the Normalization form are visible: HOO shows the Blend Mode and Ha Blend rows and only the first OIII Boost; the SHO family hides those and shows SII Boost plus its own OIII Boost.
Two modes, driven by the state of your input data:
Picking the wrong mode doesn't produce an error, just a less-good result. If you're not sure, Non-linear is the safer default for anything you'd open in a stretched state.
Controls which signal provides the final luminance of the normalized composite:
Range 0.0–1.0, default 0.25. Controls the blackpoint reference the normalization measures against:
At 0.0 the reference is the channel minimum; at 1.0 it's the channel median. Higher values lift the baseline (brighter background); lower values preserve darker blacks. The 0.25 default sits closer to minimum but still above pure black, which handles the usual case of calibrated channels with faint background sky.
Both range 0.5–2.0, default 1.0 (identity). These shape the upper end of each channel independently:
Minimum enforced value is 0.25 inside the normalization routine so you can't accidentally zero out highlight response.
Range 0.5–2.0, default 1.0. Overall multiplicative gain applied after the other shaping. Also floored at 0.25 inside the routine.
How the Ha and OIII signals combine in the green and blue channels. Options: Screen, Add, Linear Dodge, Normal. Screen is a soft-light-ish layer with less clipping; Add and Linear Dodge produce brighter, warmer results; Normal is a direct blend without any additive behaviour.
Range 0.0–1.0, default 0.6. How much Ha contributes to the green and blue channels in the bicolour composite. Lower values keep HOO looking distinctly cyan-blue; higher values warm it toward Ha-dominant.
Range 0.5–2.0, default 1.0. Independent gain on the OIII contribution before blending. Pair with Ha Blend to tune the Ha / OIII ratio without touching the input channel data.
Range 0.5–2.0, default 1.0. Boosts (or dims) the SII contribution. Especially useful in SHO where the red channel is driven entirely by SII — a weak SII integration gives a magenta-looking image until you lift it with this.
Range 0.5–2.0, default 1.0. Independent OIII gain for the SHO-family scenarios.
The SCNR (reduce green cast) checkbox applies subtractive chromatic noise reduction as a post-step after the main normalization. SASpro uses the same SCNR core as the Remove Green tool. Two sub-controls:
SCNR is hidden for the HOO scenario because HOO's green channel is deliberately OIII-driven and shouldn't be clamped.
The Linear Fit (highest signal) checkbox median-matches each channel to the one with the strongest signal before normalization. This evens out the input scales so the normalization math has a predictable starting point — particularly useful when one channel is noticeably weaker than the others. The reference is picked automatically from the loaded channels' medians (highest wins). See § 12.3.
The Background Neutralization (∇-descent) checkbox applies a gradient-descent background colour balance as a post-step on the normalized RGB. Uses an auto-detected 50×50 reference rectangle. See § 12.4.
Toggle for showing the preview with a statistical stretch applied (target median 0.25, linked channels). The underlying result is not stretched; only the display is. Use this if your input channels are linear and you want to see what the normalization is doing without the preview looking black.
apply_edit if the document
supports it (history + metadata recorded as “Narrowband
Normalization”). When channels came from a mapped RGB
view import, Apply writes to that original composite, not to
the temp split channels.While a job runs, the action buttons are disabled and the status line shows a percent + stage name from the worker.
Channels normalised to float32 in [0, 1].
Compute flows through a background QThread with sequence gating so
stale preview workers can't overwrite newer results. The core
normalization engine is Bill Blanshan's PixelMath formulas in Python
(implemented in imageops.narrowband_normalization);
everything else below is the SASpro wrapper around it.
normalize_narrowband(ha, oiii, sii, params)
— the Blanshan/Cranfield core._apply_scnr_rgb with the
user's amount and preserve-luminance setting
(§ 12.2).SCNR green suppression computes a per-pixel green-cap and blends with the amount slider:
where a is the SCNR Amount. When Preserve luminance
is enabled, the Rec. 709 luminance is computed before and after:
and the output RGB is rescaled so that L is
preserved, preventing the overall image from darkening when green
is pulled down. The relevant line in the SCNR core is
_apply_scnr_rgb(rgb, amount, mode="avg",
preserve_lightness=True, channel="G").
When enabled, SASpro picks the channel with the highest median
value as the reference and median-matches the others to it via
linear_fit_mono_to_ref from the Linear Fit module:
SASpro passes rescale_mode_idx=2 (“leave as
is”) so the fit does not clip or renormalize — it
just median-matches. The normalization routine downstream decides
what to do with the resulting dynamic range.
Uses the same auto-rect strategy as the standalone BG
Neutralization tool: a 50×50 pixel region is picked
automatically from a dark part of the frame, and
background_neutralize_rgb runs a gradient-descent fit
that balances the per-channel offset so the measured background
hits neutral grey. The result respects the destination document's
active mask when running inside Apply.
If an OSC frame is loaded and the corresponding mono channel is not:
and symmetrically for OSC2 (SII / OIII). Unlike Perfect Palette Picker, which blends 50/50 when both OSC and mono are present, NB Normalization gives priority to the mono channel — OSC is used only to fill in missing data. This is the stricter behaviour appropriate for a normalization step: mixing scales from different source types would defeat the point of normalizing.
The preview path targets < 2 megapixels and a maximum dimension of 1800 px. The computed scale is quantised to one of five buckets — 1.00, 0.75, 0.50, 0.33, 0.25 — so a small zoom in or crop doesn't change the preview path across runs. Downsampling uses OpenCV's INTER_AREA. The chosen scale is reported in the status bar during preview compute.
Preview and the full-resolution Apply/Push both run through a
_NBNWorker QThread. Each job increments a
_calc_seq counter; when a worker finishes, its result
is accepted only if the sequence still matches the current one.
Older jobs that finish after a newer one has started are silently
dropped. Progress is reported through a progress(int,
str) signal and updates the status label as
“<stage> (<pct>%)”.
For the three three-channel scenarios the inverse-split is direct:
SHO is (SII, Ha, OIII) = (R, G, B), HSO is
(Ha, SII, OIII) = (R, G, B), HOS is
(Ha, OIII, SII) = (R, G, B). For HOO the OIII channel
is reconstructed as the average of G and B since the bicolour
mapping doesn't preserve them separately:
and SII is left as None (HOO doesn't use it). This isn't a mathematically exact inversion when the forward mapping included boosts or lightness overrides — it's the right starting point for re-normalizing an already-assembled composite, not for round-trip exact reconstruction.
remove_green core, auto-rect
Background Neutralization via SASpro's
backgroundneutral, OSC synthesis, mapped RGB view
inverse-split import for Perfect Palette Picker interop, the
async _NBNWorker + sequence-gating preview, the
scaling-bucket downsample strategy, dimension-mismatch handling, and
the dynamic form that shows/hides HOO vs SHO-family controls.
Image loading uses SASpro's
legacy.image_manager.load_image; display stretch uses
SASpro's stretch_color_image.