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.
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).
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.
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 value | Maps to | Weight |
|---|---|---|
R / RED, G / GREEN,
B / BLUE | that channel | +100 |
L / LUM / LUMINANCE |
Luminance | +100 |
Sloan SR/SG/SB and
R'/G'/B' |
that channel | +90 |
Anything starting LUM… / LUMINANCE… |
Luminance | +90 |
CLEAR | Luminance | +80 |
C | Luminance | +70 |
NONE | Luminance | +60 |
HA / HALPHA | Red | +60 |
SII / S2 | Red | +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.
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.
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.
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:
s = L/Y is dominated by the
difference in offsets rather than by any real signal.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.
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.
| Control | Range / default | What it does |
|---|---|---|
| Luma profile | Rec.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. |
| Blend | 0.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 knee | 0.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. |
| Pedestal | 0.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 NR | 0.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 luminance | off | See § 4.3. |
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.
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.
| Views mode | Files mode | |
|---|---|---|
| Without L | RGB_Combined |
RGB_<red-file-stem> |
| With L | LRGB_Combined |
LRGB_<red-file-stem> |
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.
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.
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.
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.
_to_f01 coerces every input to float32 [0, 1] without assumptions
about where the data came from:
max(a) > 1 and finite, the array is divided by
that max; otherwise the data is kept as-is. This means 16-bit integer data
gets divided by its own max (not 65535), which keeps per-image contrast but
drops a frame's absolute scale information.Each candidate title gets a per-channel score from case-folded regex matches, summed with the FILTER-keyword layer from § 2.
| Pattern | L | R | G | B |
|---|---|---|---|---|
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.
Once R, G, B are three identically-shaped 2-D float32 arrays:
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.).
When L is enabled, the stacked RGB goes through this pipeline, in order:
compute_luminance with the selected profile.Y, if enabled (§ 4.3).The recombination itself lifts both images by the pedestal p and
compresses by (1 + p), scales, then inverts the lift:
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.
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.
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:
| Key | Meaning |
|---|---|
luminance_applied | Always true when L was used. |
luma_method | Resolved method name
(rec709, max, …). |
luma_profile_key | The profile as selected, including
the sensor: prefix, so the exact profile can be recovered
later. |
luma_profile | Display name, for sensor profiles only. |
luma_weights | The three weights actually used. |
l_blend, l_soft_knee,
l_pedestal | The recombine parameters. |
saturation_boost,
chrominance_nr_sigma | Recorded only when non-zero. |
l_linear_fit | Whether 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_skipped | Present instead, with a reason, when the fit was requested but could not be computed. |