A gallery of classical photographic effects wrapped in a shared live-preview dialog. Six effects at launch — Orton Glow, Soft Focus, Bloom / Star Glow, Vignette, Film Grain, Split Tone — each with its own parameter set and real-time preview. The dialog hot-swaps the controls panel when you pick a different effect from the dropdown. Downsampled preview buffer for fast interactive editing, full-resolution worker-thread apply, document mask integration, before/after toggle, and preset grip for every effect.
Michael Orton's 1980s darkroom technique, digitised: sandwich a sharp exposure with a soft-focus brighter one and blend them together. The result is a dreamy, hazy luminous look that works especially well on nebular imagery where you want to emphasise glowing structure without flattening the fine detail.
Parameters:
The simplest of the six: Gaussian blur a copy, blend back at partial opacity. No brightening, no mode selection — just diffusion. Classic portrait / glamour softening, useful on astronomical imagery for taming overly crunchy sharpening results or giving a wide-field a dreamier atmosphere.
Parameters:
Isolates just the bright highlights above a threshold, blurs only those, and screens them back onto the image. The result is a soft halo around every bright feature — stars, galaxy cores, planetary limbs, the bright edge of a lunar terminator — without touching the rest of the image. Essentially the engine HDR renderers use for their bloom pass.
Parameters:
Radial darkening from the frame centre outward — the classical post-processing framing cue, drawing the eye toward the subject. Not just for portraits: works beautifully on widefield astroimages to concentrate attention on the central nebula or cluster.
Parameters:
Adds organic monochrome or colour Gaussian noise. The random seed is fixed at 42 so the grain pattern stays stable while you drag sliders — you're sculpting one specific grain field, not watching it re-randomise on every tick. (If you want a different pattern, Reset and reopen.)
Parameters:
Tints shadows one hue and highlights another. The classical example is cyan shadows / orange highlights (teal-and-orange, much maligned but genuinely useful on astronomical imagery for giving nebular glow a warm/cool contrast). Preserves luminance structure; only colour shifts.
Parameters:
The ⇆ Toggle Before / After button is checkable; checked shows the original cached preview pixmap, unchecked shows the latest processed result. Both are cached, so switching is instant with no recomputation. Any slider or combo change automatically pops the button out of its “showing-before” state so you don't have to toggle back yourself.
Right side of the dialog. Supports:
Preview computation runs on a background QThread with cancellation: changing a slider mid-render cancels the in-flight preview and starts a new one after the 150 ms debounce fires, so no backlog of stale computations builds up during a slider drag.
For images larger than 1200 px on the long side, the preview pipeline operates on a downsampled copy (OpenCV's INTER_AREA area averaging). Interactive preview on a 24 MP frame therefore runs just as fast as on a 2 MP crop — the slider-drag response is driven by the preview buffer size, not the full resolution. The final Apply always runs on the full resolution for the committed result, which is why that stage disables the controls and shows a “processing full-resolution image…” status line while it works.
If the active document has a mask layer, the processed result is blended with the untouched original through the mask so only masked regions receive the effect. The mask is resampled with nearest-neighbour (OpenCV INTER_NEAREST) if its dimensions don't match. Both the live preview and the final Apply respect the mask identically.
The dialog listens for the main window's
currentDocumentChanged signal and switches its
preview target when you focus a different open image, so you can
auditioning the same effect across several frames without
reopening the dialog. The current effect selection and parameter
state carry across the document change.
The handle at the bottom-left of the controls column carries the full state: the selected effect and that effect's current parameters. Drag it onto the canvas to make a preset tile that runs that specific effect with those settings headlessly, or drop it directly on an image for immediate apply. Double-clicking a shortcut tile reopens this dialog pre-seeded with the saved effect and parameters.
All processing on float32 in [0, 1]. OpenCV provides the Gaussian blurs; everything else is numpy. Blend formulas follow the W3C Compositing Spec where they overlap with Photoshop.
Three blend modes are available (Orton Glow); the others use Screen directly.
Screen — symmetric commutative blend, inverse of Multiply, always lightens:
Lighten — per-pixel maximum, preserves whichever channel is brighter:
Soft Light — the W3C Compositing Spec formulation
that matches Photoshop's Soft Light. For base a and
blend b, the formula is piecewise on b.
When b ≤ 0.5:
and when b > 0.5 an auxiliary function
g(a) is used that itself is piecewise on
a:
This gives a dodging/burning response symmetric around
b = 0.5 that gently steepens contrast
without the hard breakpoints of Overlay or Hard Light.
Screen-blending two similar images runs away in brightness: for
identical layers, S(x, x) = 2x − x²,
which is always brighter than x for
0 < x < 1 (maximum excess of
0.25 at x = 0.5). This matters a lot for
Orton Glow and Bloom, where the blend layer is a brightened
near-copy of the base. Luma recovery pulls the result back toward
the average brightness of the two source layers.
For Rec. 709 luminance
L = 0.2126R + 0.7152G + 0.0722B,
the recovery target is the average of the two layers' luminance:
and the blend is rescaled per-pixel by the ratio:
with the final output being a dry/wet mix of the raw blend and the ratio-corrected version controlled by the Luma recovery slider. The scale is multiplicative on all channels so chroma is preserved exactly — only overall brightness is tamed.
End-to-end, with blur radius σ, glow brightness
β, blend mode ⊗, luma recovery
ρ, highlight protect h, and opacity
α:
Highlight protection fades the effect in regions already near white:
which starts at 1.0 everywhere, begins falling at
L = 0.7, and reaches
1 − h at L = 1.
The final effective opacity is:
and the composite:
The threshold slider produces a soft highlight mask by linear ramping from zero at the threshold to one at pure white:
The highlight-only image img · m
is blurred, boosted by the brightness slider, and Screen-blended
back with the same luma-recovery pass as Orton Glow. Because the
mask smoothly ramps rather than hard-cutting at the threshold, the
bloom halos fade naturally into non-highlight regions instead of
showing a visible brightness-level contour where the mask edge
would be.
The vignette uses a normalised radial distance in unit-centred coordinates where the frame edge midpoints sit at distance 1.0:
Mapping that distance through the radius/softness interval
[R − S, R + S] gives a
smooth transition weight:
which is zero inside the bright disc, linearly rises through the transition band, and clips at 1 outside. The multiplicative darkening factor is then:
reducing to 1 (no change) inside the clean centre and to
1 − A at the fully-vignetted edges.
Applied by direct multiplication, broadcast to all three channels
of RGB input.
A unit-variance Gaussian noise field N(0, 1)
of the same shape as the image (or per-channel for colour grain
mode) is generated with a fixed RandomState seed of 42, so the
pattern stays stable while you adjust sliders. If grain size
σ > 0.01, it's Gaussian-blurred
and renormalised to unit variance so the brightness-amplitude
interpretation of Intensity stays consistent:
Final composite is additive with Intensity (slider value
I) and a global 0.15 amplitude multiplier so even at
Intensity = 1 the effect stays in the visually sensible
range:
Mono mode (default) shares the same noise field across all three channels so the grain is colourless — the pattern of brightness variation is identical in R, G, and B. Colour mode uses independent noise per channel, which produces visibly chromatic grain reminiscent of early colour film stocks.
Hues are turned into pure HSV (h, 1, 1) RGB vectors:
The shadow/highlight split is weighted by luminance, with the Balance slider sliding the midpoint:
so Balance = 0 puts the crossover at
L = 0.5, positive Balance shifts it darker
(more of the image reads as highlight → more warm tint),
negative shifts it brighter (more cool-tint shadow). The per-pixel
tint is a weighted mix of the two hue vectors:
and the final result is a Strength-controlled mix between the
untouched image and the tinted version (where the tint multiplies
the image by 2·tint to approximately preserve
luminance when the tint averages roughly 0.5):
Two different workers handle computation:
FXWorker, a QThread) runs
the effect on the downsampled preview buffer. Supports
cancellation — a new slider change cancels the
in-flight preview and launches a new one as soon as you
stop moving._FXApplyWorker, a QObject
moved onto a QThread) runs the effect at full resolution
with no cancellation; the main thread does the document
write once it emits finished. Controls are
disabled during this run so the committed result always
corresponds to the exact slider values at the moment of
pressing Apply.Three module-level entry points:
fx_headless(doc, effect="orton_glow", **params)
— convenience wrapper for scripted use with
keyword-style params and sensible defaults for anything
missing.apply_fx_headless(doc, preset, main_window=None)
— the preset-driven dispatcher used by shortcut tiles
and the drag-to-canvas system. The preset dict carries the
effect key plus any of that effect's own
parameters; missing values fall back to defaults.replay_fx(main_window, target_sw=None)
— replays the last FX operation on the current base
document, used by Replay Last Action.colorsys. The FX dialog's composition —
the hot-swapping parameter panel driven by the ParamSpec registry,
the downsampled preview buffer with full-resolution threaded Apply,
the per-effect luma recovery pass for Screen-overshoot
compensation, the highlight-protect roll-off, the smooth bloom
mask ramp, the active-document tracking, and the preset grip
system — is SASpro-exclusive.