LRGB Combination

Build a single colour document from three mono channels, with an optional luminance channel — either from open views in the current session or from files on disk. Classical workflow for RGB/LRGB/narrowband assembly: process each filter's stack as mono (calibration, registration, denoise, stretch if you want pre-combine stretches) and bring them together here. Always creates a new document; nothing is written to the source channels.

1. Mode selector

Two radio buttons at the top of the dialog pick where the channels come from:

Only one mode is active at a time — the inactive mode's widgets are greyed out so there's no ambiguity about which inputs will be used. The Luminance row in each mode is additionally greyed out until Add a Luminance (L) channel is ticked (§ 4).

2. Views mode

Four combo boxes — Luminance, Red, Green, Blue — each showing every open document by its window title. Luminance sits first so the rows read in the conventional LRGB order, but it is ignored entirely unless the L option is switched on.

Automatic channel guessing

When the dialog opens, SASpro scores every open document against each channel and pre-selects the best match. Scoring is two-layer, and the layers are summed:

Layer 1 — the FITS FILTER keyword. This is the definitive signal when it's present, because unlike a filename a FILTER card can't accidentally masquerade as something else. The keyword is read from a flat FILTER metadata key, or from a nested header under original_header, fits_header, header or wcs_header. Matching ignores case, spaces, underscores and hyphens.

FILTER valueMaps toWeight
R / RED, G / GREEN, B / BLUEthat channel+100
L / LUM / LUMINANCE Luminance+100
Sloan SR/SG/SB and R'/G'/B' that channel+90
Anything starting LUM… / LUMINANCE… Luminance+90
CLEARLuminance+80
CLuminance+70
NONELuminance+60
HA / HALPHARed+60
SII / S2Red+55
Filter-wheel variants (Ha_7nm, S2narrow…) Red+55 / +50

A card that maps to a different channel scores roughly minus half its weight for the channel being considered, so a stack with FILTER = L is actively pushed out of the Red slot (−50) rather than being free to win it by default. OIII is deliberately not mapped, because observers split it to green or blue depending on the palette (HOO vs SHO) — it's left to the filename layer and your own choice.

Layer 2 — the filename. The fallback for files whose FILTER card is missing or non-standard. The full table is in § 6.3.

The best-scoring title is pre-selected; ties fall to the first match in dropdown order. If no title scores above zero for a channel, the first document is chosen as a neutral default and you're expected to pick manually. R/G/B are only auto-guessed when at least three documents are open; Luminance is guessed whenever there is anything to guess from. The heuristic is designed to Just Work on filenames like NGC7000_Ha.fits / NGC7000_OIII.fits / NGC7000_SII.fits or the more literal M31_red / M31_green / M31_blue, but it never locks a choice — the dropdowns are always user-overridable.

3. Files mode

Each Load… button opens an OS file dialog filtered to common astronomy formats. The chosen filename is shown next to its button; nothing is read from disk until Combine is pressed. Multi-channel files are supported — see § 6.1 for what happens to their channels, which differs between the colour channels and L.

4. Luminance (L)

Ticking Add a Luminance (L) channel enables the Luminance source row and runs the combined RGB through the same luminance-replacement pipeline as the standalone Recombine Luminance tool. The RGB keeps its colour; its brightness structure is replaced by L.

⚠ Do not combine a LINEAR L with LINEAR RGB.

Unless you have already matched the two stacks — total integration, sub-exposure length, pedestal/offset, filter throughput and camera response — the scale factor this tool applies is meaningless. You will get blown highlights or crushed shadows, and badly desaturated colour.

Normal workflow: combine RGB here without L, stretch it, stretch L separately, then use Recombine Luminance on the stretched pair.

4.1 Why linear LRGB goes wrong

Recombination computes the RGB image's own luminance Y and rescales every pixel by s = L / Y (the full maths is in § 6.5). That only works if L and Y sit on the same intensity scale. For stretched data they usually do, because stretching pushes both into roughly the display range. For linear data they almost never do:

The result is that s is roughly a constant wrong number across the whole frame. Too high and the highlights blow and the colour washes out; too low and everything crushes. Either way chroma suffers, because colour lives in the ratios between R, G and B and those only survive when the overall scale is right.

The What this means… button in the dialog opens this same explanation in a scrollable window.

4.2 Advanced options

Collapsed by default, and disabled until the L option is ticked. These are the same controls the standalone Recombine Luminance tool exposes, and identical settings produce identical results.

ControlRange / defaultWhat it does
Luma profileRec.709 The weight vector used to measure the RGB's own luminance. Standard profiles (Rec.709 / 601 / 2020, Equal, Max, Median, SNR) plus per-sensor profiles grouped by manufacturer. If the L source is a colour image, this also selects how L is extracted from it.
Blend0.00–1.00, default 1.00 1.00 fully replaces the luminance; 0.00 leaves the RGB untouched. Intermediate values mix the two linearly.
Highlight soft knee0.00–1.00, default 0.00 Compresses the per-pixel scale factor as it rises above 1, holding back highlights where L is much brighter than the RGB's luminance.
Pedestal0.000–0.500, default 0.050 Noise-floor lift applied to both images before scaling and removed afterwards, to stop hue skew near zero. 0.000 disables it, giving pure linear scaling.
Saturation boost−1.00–2.00, default 0.00 Applied to the RGB before luminance is measured, so it cannot skew the luma being placed. 0 = no change, 1.0 = double saturation, −1.0 = greyscale. Works in HSV to avoid hue drift.
Chrominance NR0.0–10.0 px, default 0.0 Gaussian blur on the Cb/Cr channels only, leaving luma untouched. Equivalent to a chrominance noise-reduction slider. 0 = off.
Linear-fit L to RGB luminanceoff See § 4.3.

4.3 Linear-fit L to RGB luminance

Runs SASpro's standard Linear Fit engine before recombining: L is scaled so its median matches the median of the combined RGB's own luminance, measured with whichever luma profile is selected. This is the minimum pre-work for recombining linear data.

It handles the scale error — the first four causes in § 4.1. It does not handle the last three:

Out-of-range handling is pinned to clip to [0, 1] rather than the Linear Fit dialog's own default of “normalize if needed”, because normalising subtracts the array minimum and rescales the whole frame, which would undo the median match that was the point of the fit.

If either median is at or below 1e-8 — a blank or near-black L, say — the fit is skipped rather than applied, because the engine's internal max(median, 1e-12) guard would otherwise yield an astronomical scale factor. The skip is recorded in the document metadata.

5. Combine

Reads the channels, verifies dimensions, stacks them into a new colour document, optionally applies the luminance, and opens a subwindow for it. The source documents and files are left untouched.

Document title

Views modeFiles mode
Without LRGB_Combined RGB_<red-file-stem>
With LLRGB_Combined LRGB_<red-file-stem>

WCS inheritance

In views mode, the new document inherits a World Coordinate System from the first donor that carries one, in the order Green → Red → Blue. Green is tried first because it sits in the middle of the visible spectrum and is the usual astrometric calibration channel in LRGB rigs; the other two are tried only if green has no WCS. The luminance document is not a WCS donor — if only your L master is plate-solved, the combined document will carry no WCS and you should plate-solve it afterwards. In files mode no WCS is inherited at all, since the source documents don't exist in-session to pull headers from; open the files as documents first and use views mode if you want WCS on the result.

Shape requirement

All three colour channels must have identical dimensions, and L — when used — must match them too. A mismatched set raises a message dialog naming the mismatch and no new document is created; register or crop upstream and retry. L is not silently resampled here, because a luminance that doesn't line up with the colour data pixel-for-pixel will smear detail rather than sharpen it.

6. Technical details

Everything happens on float32 [0, 1]. The combine is a plain stack-along-axis-2; the interesting work is in the input normalisation, the channel-match scoring, and the optional luminance pipeline.

6.1 Mono extraction

The colour channels and the luminance channel are read differently, on purpose.

R, G and B pass through _mono_from_any, producing a 2-D float32 [0, 1] array:

L passes through _as_is_from_any, which normalises identically but keeps the channel count. A mono L is used directly; a colour L is run through compute_luminance with the selected luma profile, so pointing L at an RGB document extracts a proper weighted luminance rather than silently taking its red plane. This is also what makes the Max, Median and SNR profiles meaningful — with a mono L they only affect the weights used to measure the target's Y.

6.2 Normalisation

_to_f01 coerces every input to float32 [0, 1] without assumptions about where the data came from:

That per-image division is applied to each channel independently, including L. For colour combine it's harmless, since any downstream balance operation redoes per-channel scaling anyway. For luminance recombination it is not: it is one more reason the L/Y ratio is meaningless on linear integer data, and it happens inside this dialog regardless of what you did upstream. Float data already within [0, 1] is left alone and is unaffected.

6.3 Channel-match scoring — filename layer

Each candidate title gets a per-channel score from case-folded regex matches, summed with the FILTER-keyword layer from § 2.

PatternLRGB
Full word (lum/luminance, red, green, blue) +10+10+10+10
Separator-bounded letter (_l_, _r_…) +8+8+8+8
Suffix / end-of-title letter +7+7+7+7
Ha emission line —+6——
SII emission line —+5——
OIII emission line ——+5+4
Conflicting colour word −10−10−10−10
Conflicting single-letter code −5−5−5−5
lum/luminance present —−8−8−8

Separator matching uses the character class [_\-\s\.] for “letter boundary”, so dots in filenames behave the same as underscores or hyphens. Note that a digit is not a boundary: this matters because SASpro's own gain prefix (…_G121_… on every master-light file) would otherwise false-match as a green channel on every file in the set, leaving them all tied and handing green to whichever happened to be first in the list.

The channel scorers run independently per title, and the title with the highest positive score wins its dropdown. Scores of zero or less leave the dropdown at index 0 as a neutral default — which is why, on a fresh session with generically-named documents, you'll sometimes see every channel defaulting to the first document; that's by design, not a bug.

6.4 Stack and clip

Once R, G, B are three identically-shaped 2-D float32 arrays:

RGB[y, x, 0] = R[y, x] RGB[y, x, 1] = G[y, x] RGB[y, x, 2] = B[y, x]

via np.stack([r, g, b], axis=2), then clipped once more to [0, 1] as a belt-and-braces guard against anything the per-channel normalisation might have missed in edge cases (NaN-dominated frames, etc.).

6.5 Luminance recombination

When L is enabled, the stacked RGB goes through this pipeline, in order:

  1. Saturation boost on the RGB (HSV), if non-zero.
  2. Chrominance NR on the result (Cb/Cr Gaussian blur), if non-zero. Both of these run before luminance is measured, so neither can skew the luma being placed.
  3. Build L — mono passes through; colour goes through compute_luminance with the selected profile.
  4. Linear fit of L onto Y, if enabled (§ 4.3).
  5. Recombine by per-pixel scaling.

The recombination itself lifts both images by the pedestal p and compresses by (1 + p), scales, then inverts the lift:

Y = wR·R + wG·G + wB·B s = L / (Y + eps) RGB' = RGB · s

With a soft knee k, s is replaced by s / (1 + k(s − 1)) before it is applied. With a blend β below 1, the output is RGB·(1−β) + RGB'·β. The transform is an identity when L equals the image's own Y, which is the round-trip guarantee the whole approach rests on.

The weight vector w comes from the selected profile. Sensor profiles supply their own measured weights. Equal uses (⅓, ⅓, ⅓). Rec.601 and Rec.2020 use their standard vectors. Max, Median and SNR have no weight vector of their own, so they fall back to Rec.709 for measuring Y — they only change how L is extracted when the L source is a colour image.

6.6 Document creation and WCS transfer

The result is handed to the document manager via whichever of open_array, open_numpy or create_document is available, with the metadata described in § 6.7 and the title derived from the mode (as in § 5). The main window's WCS helpers (_extract_wcs_dict, _apply_wcs_dict_to_doc) are then called in the G → R → B donor order until one yields a usable WCS, which is copied onto the new document before its subwindow is spawned. If no donor has a WCS, the new document carries none — a plate-solve can be run on it afterwards from any of SASpro's astrometry tools.

6.7 Metadata

Without L, the document carries only step_name = "RGB Combination". With L, step_name becomes "LRGB Combination" and the full settings are recorded so a finished document says exactly how its luminance was placed:

KeyMeaning
luminance_appliedAlways true when L was used.
luma_methodResolved method name (rec709, max, …).
luma_profile_keyThe profile as selected, including the sensor: prefix, so the exact profile can be recovered later.
luma_profileDisplay name, for sensor profiles only.
luma_weightsThe three weights actually used.
l_blend, l_soft_knee, l_pedestalThe recombine parameters.
saturation_boost, chrominance_nr_sigmaRecorded only when non-zero.
l_linear_fitWhether the fit was applied.
l_linear_fit_median_src, l_linear_fit_median_ref, l_linear_fit_scale The two medians and the resulting scale factor, when the fit ran. A scale far from 1.0 is a direct measure of how mismatched the two stacks were.
l_linear_fit_skippedPresent instead, with a reason, when the fit was requested but could not be computed.