Layers Dock

Non-destructive layer compositing over any open view. Stack other views on top of your base image as layers, each with its own blend mode (30 to pick from), opacity, mask, transform, and per-layer levels — the base view is never touched until you explicitly merge. A floating preview window with a draggable before/after split shows the composite in real time, rendered in two tiers: an instant downsampled fast preview on the GUI thread, then a full-resolution render on a background worker. Drop another view into the dock to add it as a layer. Drop a view onto an existing layer to use it as a mask. When you're happy, merge the stack back to the view, into a new document, or just the selected layers into one baked raster.

1. The non-destructive promise

The Layers Dock is built around one rule: the base view's pixels are never changed by the layer stack. You can add a dozen layers, tweak their blend modes and opacities, add masks, scale and rotate them — and the view you're working on still contains the original image, untouched, with full undo intact. The composite you see lives entirely in the floating preview window. Only three operations ever write back:

Nothing is committed until you click Merge. Closing the preview, switching views, or quitting SASpro throws away the composite but keeps the layer stack recorded against the view, so you can come back to it. If you want to keep the result, merge it.

2. The dock layout

3. Adding a layer

Drag another open view's title bar (or thumbnail) into the Layers dock. A new layer appears at the top of the stack, with that view as its live source — if you later run a tool on the source view that changes its pixels, the composite re-renders automatically to pick up the new data. The layer name shows the source view's title and updates when you rename the view.

You can also drop the same view multiple times for different roles (one copy as a blend layer, another as a mask), or drop a document that isn't open as a subwindow — SASpro resolves it by document UID, pointer, or file path depending on what the drop payload carries.

4. The row controls

Each layer row has four sub-rows:

4.1. Main controls

4.2. Mask controls

Pick any open view from the Mask dropdown to use its luminance as this layer's mask. Invert flips the mask. Clear removes the mask assignment. Alternatively, drop a view directly onto an existing layer row — SASpro recognizes the mask MIME type and assigns it as this layer's mask automatically, including any invert and feather settings carried in the drop payload.

Luminance masks follow the layer. When you transform a layer (rotate it, scale it, nudge it), its mask gets the exact same transform applied so the two stay registered. A galaxy mask you painted at native orientation stays aligned to the galaxy after you rotate the whole layer 45°.

4.3. Per-layer levels

Each layer has its own levels adjustment that applies before the blend mode runs. Enable the checkbox, set black point, midtones, and white point (0 to 1 for all three). Uses the same PixInsight-style midtones transfer function as SASpro's main Levels tool, so adjustments translate predictably if you later bake the layer. Midtones = 0.5 is neutral; lower brightens, higher darkens.

The base view row (greyed at the bottom of the list) also has a levels block that applies to the base image before any layers are composited on top. Useful for setting a stretch on the base that you don't want to commit until you merge.

4.4. Sigmoid parameters

When the blend mode is set to Sigmoid, two extra controls appear: Center (0 to 1, default 0.5) and Strength (0.1 to 50, default 10). These control the shape of the luma-dependent blend curve — see §6 under Sigmoid.

5. The preview window

Click 👁 Preview to open the floating preview window. It's non-modal, so you can keep it visible while you tweak the dock. Three controls at the top:

Canvas navigation. Scroll wheel to zoom in/out (centered on the mouse cursor). Click and drag to pan. Double-click empty space to reset to fit. Hover near the split line and the cursor changes to a split-horizontal cursor — click and drag to reposition.

6. Blend modes

30 modes in six categories. All operate in float32 [0,1] space with 3-channel inputs; monochrome layers are promoted to RGB first.

Darken family

ModeFormula (base a, source s)Use for
Multiplya·sDarkening overlays, star-mask integration
Darkenmin(a, s)Per-channel darker of the two
Darker Colors if Y_s ≤ Y_a else aPick the darker whole pixel by luminance
Color Burn1 - (1-a)/sDeepen dark areas, enhance shadow contrast

Lighten family

ModeFormulaUse for
Screen1 - (1-a)(1-s)Classic stacking of light layers, nebula overlays
Lightenmax(a, s)Per-channel brighter of the two
Lighter Colors if Y_s ≥ Y_a else aPick the brighter whole pixel by luminance
Color Dodgea/(1-s)Boost highlights, HDR-ish bloom
Addclip(a + s)Linear stacking (handle with care; clips)
Relativistic Addition(a+s)/(1+a·s)Non-clipping addition — bounded in [0,1] by construction
Relativistic Addition is Frank's workhorse non-clipping alternative to Add. It's the same formula as the velocity addition rule in special relativity: two speeds under c compose to another speed under c. Here, two intensities under 1 compose to another intensity under 1. You can stack a dozen Add-mode layers at full opacity and the brightest regions still don't blow out — they asymptotically approach 1 instead of clipping to it. Perfect for integrating HII fluorescence into an SHO stack without posterizing the brightest cores.

Contrast family

ModeFormulaUse for
OverlayMultiply where a ≤ 0.5, Screen aboveClassic contrast boost based on base luminance
Soft Light(1-2s)·a² + 2sa (W3C)Gentler contrast; good for texture layers
Hard LightMultiply/Screen conditional on sOverlay's cousin, conditioned on source instead
Pin Lightmax(a, 2s-1) if s>0.5, else min(a, 2s)Selective shadow/highlight replacement

Difference family (ideal for gradient modeling)

ModeFormulaUse for
Difference|a - s|Alignment check, gradient reveal
Difference (Squared)(a - s)²Weighted difference emphasizing larger offsets
Exclusiona + s - 2asSofter Difference, less contrasty midtones
Negation1 - |1 - a - s|"Bright difference" — equal pixels go white
Subtractclip(a - s)Gradient / pedestal / synthetic-flat removal
Dividea/sFlat-fielding, illumination correction
Grain Extracta - s + 0.5Pull a gradient out as a mid-gray map for inspection
Grain Mergea + s - 0.5Re-add an extracted gradient layer
The gradient-modeling workflow. Drop your image onto itself as a layer, run a strong blur on the top copy (Convolution tool), set the top layer to Grain Extract. The preview now shows your gradient alone, centered at mid-gray — bright where the original was brighter than the blurred version, dark where darker. Save that as a document and you have a quantitative gradient model. Combine with Divide (as a classical flat-field pass) or Subtract (for additive gradients) depending on whether your contamination is multiplicative or additive. All non-destructive; the original stays clean.

Average family

ModeFormulaUse for
Average(a + s)/2Simple 2-image mean (fixed opacity)
Geometric Mean√(a·s)Sits between Multiply and Average; preserves zeros

Luminance-aware modes

ModeBehaviorUse for
LuminosityKeep base's color, use source's luminanceLRGB combination: swap in a sharper L
ColorKeep base's luminance, use source's colorChromatic overlays: paint color onto a mono master
SigmoidLuma-weighted blend; w = 1/(1+exp(-k(Y-c)))Blend only where base is bright (bright-area overlays)

The Luminosity mode uses the full luminancerecombine pipeline (same as SASpro's Luminance Recombine tool) so highlight soft-knee and other refinements are available. The Color mode is its inverse: it rescales the source channels by the ratio of base luma to source luma, preserving the base's brightness distribution while replacing its hue and saturation.

Sigmoid is unique: the blend weight is a logistic function of the base's luminance, with user-controlled center and strength. At center = 0.5, strength = 10 (defaults), dark base pixels keep mostly the base, bright base pixels transition smoothly to the source. Great for pasting bright stars from a short-exposure frame onto a long-exposure stack — the sharp stars replace the bloated ones only where the base is bright.

7. Transforms

The Transform… button on each layer row opens a dialog with five numeric inputs:

Changes apply live with a 120 ms debounce — tweak a slider and the preview catches up without a tool call per keystroke. Hitting Reset zeros everything. Hitting Cancel restores the transform that was active before you opened the dialog. Hitting OK commits.

The transform applies to both the layer pixels and the layer's mask (if any), so a rotated galaxy stays masked correctly. The identity transform (no translation, no rotation, scale = 1,1) is detected automatically and skipped entirely — no warp pass runs when the transform is a no-op, which is the common case for most layers and keeps composites fast.

8. Fast preview vs full-resolution composite

WIMI composites run in two tiers (both debounced to coalesce rapid changes):

Both tiers coalesce: dragging an opacity slider across its full range in half a second produces exactly one fast render per 80-ms window and one full-res render per 350-ms window — not one per slider tick. A generation counter on the worker queue silently discards any stale full-res result that lands after you've moved on.

While you drag transform parameters or adjust levels, you're interacting with the fast preview. When your mouse stops, the full-res catches up. If you push a long-running tool on a source view (say a 30-second deconvolution), the preview auto-refreshes once the tool completes — the dock listens to each source document's changed signal.

9. Merging

Merge → Push to View

Flattens the full-resolution composite and replaces the current view's image as a standard SASpro undoable edit (step name: "Layers Merge"). The layer stack is cleared from the view. If you don't like the result, Ctrl+Z gets your original back.

Merge → New Document

Flattens the composite into a brand-new document. The source view is left completely untouched and its layer stack remains active. The new document's title appends "_merged" to the source view's title. Useful when you want to keep iterating on the layers while also saving a snapshot of the current state.

Merge Selected → Single Layer

Select two or more layer rows (Ctrl+click or Shift+click), then click this. SASpro composites just those layers against whatever's below them, bakes the result as a new raster ImageLayer (no source document, just pixels), and replaces the selected range with that one baked layer. Layers above and below the selection are preserved exactly. Useful for consolidating a sub-stack you've finished tweaking so later changes to other layers don't trigger expensive recomposites of the frozen sub-group.

10. Clear All Layers

Removes every layer from the current view's stack. The base view returns to its untouched pixels. Doesn't delete any source documents — you can drop them back in to rebuild.

11. Workflow recipes

LRGB combine with sharper L. Load L and RGB images as separate views. In Layers Dock, select the RGB view. Drag the L view in as a layer. Set its blend mode to Luminosity and opacity to 1.0. The preview shows the RGB color on L's structure. Push to View to commit, or Merge → New Document to keep both.
Starless + star layer compositing. Load a starless version and the original. Drag starless onto the original as a layer. Set mode to Screen at 100% to re-add stars from the original (or Pin Light for selective replacement). Push to View.
HII overlay without blowouts. Load your SHO mix. Drag an Hα-only layer on top. Set mode to Relativistic Addition at full opacity. The brightest Hα cores never clip; they asymptote to pure white. Add a luminance mask from Hα to restrict the overlay to actual signal. Transform the Hα slightly if registration is off (the mask will follow automatically).
Extract and remove a gradient. Drop your light-pollution–contaminated image onto itself. On the top copy, run Convolution with a large Gaussian (σ ≈ 200 px). Set the top layer to Grain Extract. The preview now shows the gradient alone as a mid-gray map. Merge → New Document to save the gradient model. Switch back to the original, drop that gradient model in as a layer, set it to Subtract, and push.
Multi-session stacks that don't blow highlights. One view per night. Pick the deepest as the base. Drop each other session on top as a Relativistic Addition layer at 1/N opacity (N = total sessions). The composite looks like a mean-stacked frame but preserves hard-earned dynamic range in nebula cores. Merge → New Document for the final.
Localized tweak without touching the main image. Drop your working image onto itself as a layer. Set the top layer's levels to brighten (midtones down to 0.4). Drop a mask view (a painted region) onto the layer. Only the masked region brightens; the rest of the base shines through. Transform the whole layer if the mask needs rotating; both move together.
Transform-only workflow for registration. Drop a reference frame as a layer, enable only translate/rotate/scale, open the Transform dialog, nudge until features line up visually in the before/after split. The 120 ms live preview gives you instant feedback. Use the mask of your main galaxy to see only its registration, not the field.
Freeze a sub-stack to speed up iteration. You've got 8 layers, the bottom 5 are done. Select the bottom 5, click Merge Selected → Single Layer. Now there are 4 layers total (the top 3 originals plus the one baked raster), and composite recomputes are much faster. Later edits on the top 3 don't re-blend the 5 baked ones each pass.

12. Credits

Written by Franklin Marek as part of SetiAstro Suite.

Blend modes. Standard formulas follow the W3C Compositing & Blending Level 1 specification and the PhotoShop blend-mode conventions as documented across the color-math literature. The Soft Light formula uses the W3C simple form. Relativistic Addition is adapted from the velocity-composition rule in special relativity. Luminosity mode delegates to the SASpro Luminance Recombine pipeline (Rec. 709 luma with highlight soft-knee support).

Midtones transfer function. PixInsight-equivalent MTF: M(x; m) = ((m-1)x) / ((2m-1)x - m) with the boundary cases M(0) = 0, M(m) = 0.5, M(1) = 1.

Compositing engine. Pure NumPy with optional OpenCV fast paths for resize (cv2.resize INTER_NEAREST) and affine warping (cv2.warpAffine). Falls back to NumPy mgrid/indexing when OpenCV isn't available. Transform math built from standard 3×3 affine composition.

UI. PyQt6 — QDockWidget host, QListWidget-of-widgets for the layer rows (so each row carries its own slider/spinbox/combo state), QDialog preview window with custom QPainter canvas for the split view and zoom/pan. Background composite worker is a plain QObject on a QThread with a generation counter to discard stale results. Live transform dialog uses a 120 ms QTimer for debounced apply.

Drag & drop. MIME types MIME_VIEWSTATE (adds a layer from a view) and MIME_MASK (assigns a view as a mask on the hovered layer) are defined in setiastro.saspro.dnd_mime and shared with the SASpro view manager so any subwindow drag source works.

Technical details

13.1. The midtones transfer function

SASpro uses the PixInsight MTF for all levels adjustments (base and per-layer):

M(x; m) = \frac{(m - 1) \cdot x}{(2m - 1) \cdot x - m}

with boundary conditions enforced explicitly: M(0) = 0, M(m) = 0.5, M(1) = 1. Valid for midtone m in (0, 1); SASpro clamps to [1e-6, 1 - 1e-6] to avoid the singularity at m = 0.5 where the denominator would vanish for x = m. Vectorized across all channels in one pass.

13.2. The full levels pipeline

Per channel, in float32 [0, 1]:

t = \mathrm{clip}((x - b_p) / (w_p - b_p),\; 0,\; 1)
\mathrm{Levels}(x) = M(t;\, m),\quad t = \frac{x - b_p}{w_p - b_p}

Black point is subtracted first, white point normalizes the result to [0, 1] with clipping, then the midtones curve applies. If the user sets wp ≤ bp (an impossibility that would blow up the normalization), SASpro silently widens wp to bp + 1e-8 so the pipeline never crashes.

13.3. Luminance (Rec. 709)

All luminance-aware operations (Lighter Color, Darker Color, Sigmoid, Luminosity, Color, mask luminance extraction) use Rec. 709 primaries:

Y = 0.2126\,R + 0.7152\,G + 0.0722\,B

Clamped to [0, 1] after weighting. Chosen because it matches the sRGB gamut primaries most users' calibrated monitors are displaying, so the perceived luma matches what you see on screen.

13.4. The 30 blend-mode formulas

Each mode operates per-channel on base a and source s, both in [0, 1]. The exact implementations:

\mathrm{Multiply}: a \cdot s
\mathrm{Screen}: 1 - (1 - a)(1 - s)
\mathrm{Overlay}: a \leq 0.5 \Rightarrow 2as,\quad a > 0.5 \Rightarrow 1 - 2(1-a)(1-s)
\mathrm{Soft\ Light}: (1 - 2s) \cdot a^2 + 2 \cdot s \cdot a
\mathrm{Hard\ Light}: s \leq 0.5 \Rightarrow 2as,\quad s > 0.5 \Rightarrow 1 - 2(1-a)(1-s)
\mathrm{Color\ Dodge}: \mathrm{clip}(a / (1 - s),\; 0,\; 1)
\mathrm{Color\ Burn}: 1 - (1 - a) / s
\mathrm{Pin\ Light}: s > 0.5 \Rightarrow \max(a,\, 2s - 1),\quad s \leq 0.5 \Rightarrow \min(a,\, 2s)
\mathrm{Relativistic}: (a + s) / (1 + a \cdot s)
\mathrm{Grain\ Extract}: a - s + 0.5
\mathrm{Grain\ Merge}: a + s - 0.5
\mathrm{Negation}: 1 - |1 - a - s|
\mathrm{Difference}: |a - s|
\mathrm{Exclusion}: a + s - 2as
\mathrm{Geometric\ Mean}: \sqrt{a \cdot s}
\mathrm{Divide}: a / \max(s,\, \varepsilon)
\mathrm{Subtract}: \mathrm{clip}(a - s,\; 0,\; 1)
\mathrm{Lighter\ Color}: Y_s \geq Y_a \Rightarrow s,\; \mathrm{else}\; a

All division operations use ε = 1e-6 in the denominator to guard against division by zero. All results are clipped to [0, 1] where the formula could otherwise overshoot (Color Dodge/Burn, Add, Linear Burn, Linear Light, Vivid Light, Divide). Difference and Subtract naturally stay in range; Multiply, Screen, Soft Light, and Relativistic Addition are provably bounded in [0, 1] for inputs in [0, 1] and don't need explicit clipping.

13.5. Sigmoid blend

Unique among the modes because the blend weight depends on the base's luminance:

w(Y) = 1 / (1 + \exp(-k \cdot (Y - c)))
\mathrm{Sigmoid}: (1 - w) \cdot a + w \cdot s

Y is the Rec. 709 luminance of the base, c is the center (default 0.5), k is the strength (default 10). At the default, dark base pixels (Y < 0.3) stay essentially 100% base; bright base pixels (Y > 0.7) transition to essentially 100% source. The strength parameter controls how sharp the transition band is — small k gives a wide ramp, large k gives a nearly-binary mask.

13.6. Luminosity and Color (chrominance swap)

Luminosity delegates to the full SASpro luminancerecombine pipeline (compute Y_src with Rec. 709, then recombine_luminance_linear_scale keeps the base's chrominance). Color is the mirror operation implemented inline:

\mathrm{Color}: s \cdot (Y_a / Y_s)

This rescales every source channel by the ratio of base luma to source luma, which preserves the base's brightness distribution while replacing its hue and saturation with the source's.

13.7. Alpha composition with mask

After the blend mode produces a candidate pixel, SASpro combines it with the base using layer opacity (and mask if present):

\alpha_{\mathrm{map}} = \mathrm{clip}(\mathrm{opacity} \cdot M,\; 0,\; 1)
\mathrm{out} = (1 - \alpha_{\mathrm{map}}) \cdot \mathrm{base} + \alpha_{\mathrm{map}} \cdot \mathrm{blended}

With no mask, M = 1 everywhere and alpha_map degenerates to the scalar opacity. When Invert is on, M becomes 1 − M before the opacity multiplication.

13.8. Affine transform math

The 3×3 affine matrix is built from the user's parameters as a composition of translate, pivot-translate, rotate, scale, and inverse-pivot-translate:

M = T_t \cdot T_p \cdot R \cdot S \cdot T_{-p}

where T_t is translation by (t_x, t_y), T_p is translation by the pivot (default = image center), R is a 2D rotation by θ, S is diagonal scale with s_x and s_y, and T_{-p} is translation by the negative pivot. The composition reads right-to-left: translate to origin, scale, rotate, translate back to the pivot, then apply the user's translation.

For sampling, we invert and apply dest → source:

(x_s, y_s)^T = M^{-1} \cdot (x_d, y_d, 1)^T

Nearest-neighbor lookup fills the destination grid. OpenCV's cv2.warpAffine provides a fast C path when available; otherwise SASpro uses a vectorized NumPy implementation with explicit valid-region masking. Singular matrices (e.g. s_x = 0) are caught and return the fill color.

13.9. The two-tier preview architecture

Two debounced timers drive the preview:

A generation counter on LayersDock increments every time the composite is scheduled. When a worker emits its result, SASpro compares the emitted generation to the current one — if it's older, the result is silently discarded. This cleanly handles the case where you drag an opacity slider rapidly: workers for intermediate values still finish (they can't be interrupted mid-render) but their results don't flicker on-screen because they're stale by the time they arrive.

13.10. Preview downscale and transform scaling

The preview downscale is a straightforward ratio against the longest edge:

s = \mathrm{max\_dim} / \max(H,\, W)

Transform parameters that are specified in pixels (translate, pivot) are scaled by this factor so geometry stays correct:

(t_x',\, t_y') = s \cdot (t_x,\, t_y)

Rotation and scale factors don't need adjustment — they're angles and ratios. The transform is applied to the already-resized layer canvas, not the full-res one, so a translate-200-rotate-15 transform at full-res becomes translate-50-rotate-15 at 4× downscale, producing a geometrically identical preview at ¼ the cost.

13.11. Identity-transform skip

Most layers have no transform active most of the time. The composite engine checks each layer's transform against a small tolerance (1e-6 on each parameter) and if everything is identity, skips the warp step entirely. This is a significant optimization: a 10-layer stack with no transforms runs ~10× faster than one with transforms on every layer, because warps on full-res 4K+ images dominate composite time.

13.12. Base image cache

Converting the raw base image (which can be mono, uint8, uint16, float, 1-channel, 3-channel) to the composite engine's canonical form (float32, 3-channel, [0, 1]) is non-trivial. The dock caches this conversion keyed by id(base_doc.image) — Python's object identity — and only rebuilds when the base document actually produces a different array object (which happens when the base is edited through SASpro's normal pipeline). Base levels adjustments apply on top of the cached conversion, so moving the base black-point slider doesn't re-convert the whole image.

13.13. Event-driven auto-refresh

While the preview window is open, the dock listens to two signals on the current view:

Either signal triggers _force_refresh_preview, which drops the cached before-image identity (so the cached conversion rebuilds) and schedules a new composite via the standard debounced timers. This means: run Curves on a view you're using as a layer, and the composite in the preview updates on its own. Switch to a different base view and the dock rewires its watchers automatically.

13.14. Memory discipline during preview

Three design choices keep the layer stack cheap at preview time:

Full-resolution composites, triggered by the 350 ms timer, do run at native resolution — but on a worker thread, in the background, where even a multi-second render doesn't block the UI.