Selective Color Correction

A hue-selective colour editor: build a mask from a colour range (either a preset or a custom arc on the hue wheel), refine it with chroma, lightness, shadows/highlights, edge blur, a freehand lasso region, or an imported external mask, and then push CMY / RGB / Luminance / Chroma / Saturation / Contrast adjustments through it. The whole pipeline previews live, zoomed and pannable, with an optional red overlay so you can see exactly what the mask is selecting before you commit.

The underlying idea is classical: “only touch the reddish regions” or “shift only the OIII blues toward cyan”. What distinguishes this implementation is how many ways you can carve up the selection — preset hues, draggable hue-wheel handles, luminance gates, a freehand lasso, or an imported mask — and how fast the preview updates thanks to the small-preview downsample.

1. The two-pane layout

The dialog's size, splitter position, small-preview toggle, live-preview toggle, mask-overlay toggle, and current zoom are all persisted to QSettings under the selcolor/ namespace.

2. Building a mask

2.1 Preset or custom hue range

The Preset dropdown offers seven classical colour presets plus Custom. Picking any preset snaps the hue wheel handles and the two Hue sliders to that preset's range:

PresetHue range (°)
Red340–360 and 0–15 (wraps around)
Orange15–40
Yellow40–70
Green70–170
Cyan170–200
Blue200–270
Magenta270–340

Any manual change to the hue wheel or either Hue slider automatically switches the preset to Custom so you don't accidentally wipe your range by nudging a knob.

2.2 The hue wheel

A compact HSV ring with two draggable white handles for the start and end of the hue arc. Dragging either handle updates the Hue Start and Hue End sliders live, and vice versa. The highlighted arc on the wheel is always the forward arc from start to end in the positive direction — so a start at 340° and an end at 15° selects reds wrapping across 0°, not the complementary 345° wedge going the other way around.

2.3 Chroma and lightness gates

2.4 Smoothness

Range 0–60°, default 10°. Feathers the hue band at both ends, so a pixel whose hue sits just outside the selected range still gets a partial weight. The feather is local — it only adds a ramp right after the end and right before the start, so widening the smoothness doesn't balloon into the whole wheel. See § 13.1 for the exact band formula.

2.5 Invert hue range

Takes the complement of the hue band on the wheel before the chroma and lightness gates kick in. So “Green inverted with min chroma 0.1” selects every saturated non-green pixel. The chroma and lightness filters still apply after the inversion.

2.6 Shadows / Highlights

Luminance gates that attenuate the mask outside a brightness range:

Both gates use smoothstep ramps, not hard thresholds (see § 13.2).

2.7 Edge blur

Gaussian blur applied to the mask after all other mask steps, 0–150 px radius. Useful when a hard-edged selection produces visible seams in the adjusted preview. Set to 0 for the sharpest selection the hue+chroma gates give.

2.8 Intensity

Scalar multiplier on the final mask, range 0.0–2.0, default 1.0. Think of it as the strength of the adjustment: 0 effectively disables the effect even if sliders are moved; values above 1 over-saturate the mask (any pixel above the cap just gets clipped to 1.0).

2.9 Show mask overlay

Toggles a red semi-transparent overlay over the preview showing exactly which pixels the current mask is selecting (and how strongly — alpha scales with the mask value). This is the single most useful control in the dialog: turn it on while tuning hue range and chroma/lightness, turn it off once the mask looks right.

2.10 Soft (proportional)

Switches the chroma/lightness gates from hard thresholds to smoothstep feathering. In soft mode the mask takes partial values based on how strongly a pixel matches the colour (chroma proxy S×V), which reduces the blobby, hard-edged look you get from a pure threshold. For selecting faint nebular hues rather than vivid dust lanes, soft mode is almost always what you want.

2.11 Imported mask

Two controls:

Use this when you've already painted a precise mask in another tool, or when you want to export a mask from Selective Color (see § 7.3) and apply the adjustments again later via the imported-mask path.

2.12 Lasso region mask

Hold Alt and drag in the preview to draw a freehand closed region. On release, the polygon is filled, converted to a binary mask at full resolution, and AND'd with the hue-based (or imported) mask. The resulting mask is only non-zero where both constraints hold: inside the lasso and matching the colour selection.

While drawing, the lasso shows as a dashed orange outline with a soft orange fill. Once committed, the region border appears as a dashed orange outline over the preview; a glow underneath makes it visible on bright backgrounds. The region-mask readout at the bottom-right confirms the point count and offers the × Clear Region Mask button to remove it.

3. Preview and interaction

Mouse:
• Click — sample the hue at that pixel. The wheel shows a black-and-white marker at the picked hue; the readout shows the numeric value.
• Shift + Click — sample and set the hue range to ±15° around that hue.
• Ctrl + Drag — pan the preview.
• Alt + Drag — draw a lasso region mask.
• Mouse wheel — zoom cursor-anchored (plain or Ctrl, with Ctrl using a bigger step).
• Zoom buttons — zoom in / out / 1:1 / fit.

3.1 Small-sized Preview (fast)

The toggle at the top-left of the dialog controls whether the preview path operates on a downsampled copy of the image (longest side ≤ 1200 px) or the full resolution. Small-preview mode is on by default and makes the mask/preview debounce feel instantaneous even on 24-megapixel composites. The applied output always uses the full resolution regardless of this toggle.

3.2 Preview changed image

Toggles between showing the raw image and the adjustment-applied image. Useful for before/after comparison without undoing anything. The mask overlay (§ 2.9) works independently of this.

3.3 Colour boost mode

In the LSC group, the Color boost mode dropdown picks which of two colourfulness-shaping sliders is active:

The inactive slider is greyed out; the one you selected is live.

4. The adjustment sliders

4.1 Complementary colours (CMY)

Three sliders, each in the range [−0.2, +0.2]:

These match the classical darkroom “add complementary cyan” convention: positive values pull a channel away from its primary.

4.2 RGB primaries

Three sliders, each in the range [−0.2, +0.2]:

Pair with CMY for a “shift the Ha reds slightly toward magenta without lifting the blue” move: set Red → −0.03, Magenta → −0.03.

4.3 Luminance / Chroma / Saturation / Contrast

Four sliders, range [−0.2, +0.2]:

All four are gated through the mask multiplied by Intensity, so their effect is strictly confined to the selected region.

5. Reset and Clear Region Mask

6. Applying the result

6.1 Apply

Runs the full pipeline at full resolution on the current self.img, writes the result back to the source document, then promotes the result to be the new working base and resets all control knobs to defaults. The dialog stays open so you can chain multiple selective passes (red first, then green, then blue) without reopening.

6.2 Apply as New Document

Same full-resolution pipeline, but opens the result as a new SASpro document titled <original name> [SelectiveColor] and switches the dialog's target to the new document. Subsequent Apply calls then write to the new document instead of the original.

6.3 Export Mask

Pushes the current mask (as a 3-channel grayscale image, each channel equal to the mask value) to a new document titled <original name> [SelectiveColor MASK]. The exported mask can be re-loaded via Pick mask from view on another Selective Color session, or fed to any other SASpro tool that accepts a mask input.

7. Technical details

All images handled as float32 in [0, 1]. HSV conversions go through OpenCV (cv2.cvtColor, 8-bit intermediate for speed). Hue is measured in degrees on [0, 360); OpenCV returns H on [0, 180], and SASpro converts both ways. Core pipeline runs on the GUI thread with debounced timers (200 ms for mask and adjustment paths) so moving a slider doesn't fire a cascade of recomputes.

13.1 Hue band formula

The band is defined on the circular hue axis in degrees. Let h_s be the start angle, h_e the end angle, s the smoothness in degrees, and h the pixel hue. First compute the forward arc length and the forward distance:

L \;=\; (h_e - h_s) \;\mathrm{mod}\; 360
d_{\mathrm{fwd}}(h) \;=\; (h - h_s) \;\mathrm{mod}\; 360

and the backward distance:

d_{\mathrm{bwd}}(h) \;=\; (h_s - h) \;\mathrm{mod}\; 360

The band value is 1.0 strictly inside the forward arc, with linear feathers of width s at both ends:

\mathrm{band}(h) \;=\; 1,\quad\text{if } d_{\mathrm{fwd}}(h) \leq L
\mathrm{band}(h) \;=\; 1 - \frac{d_{\mathrm{fwd}}(h) - L}{s},\quad L < d_{\mathrm{fwd}}(h) < L + s
\mathrm{band}(h) \;=\; 1 - \frac{d_{\mathrm{bwd}}(h)}{s},\quad 0 < d_{\mathrm{bwd}}(h) < s

Preset ranges that wrap (Red: 340–360 and 0–15) are expressed as multiple bands and max-combined. The Invert checkbox replaces the result with its complement before the chroma/lightness gates.

13.2 Smoothstep

Used by Soft mode and the shadows/highlights gates:

t(x; e_0, e_1) \;=\; t_c^{\,2}\,(3 - 2\,t_c),\quad t_c \;=\; \mathrm{clip}\!\left(\frac{x - e_0}{e_1 - e_0},\; 0,\; 1\right)

13.3 Chroma / lightness gates (soft mode)

In soft mode, the chroma proxy is HSV saturation times value:

C \;=\; S \cdot V

and the mask is weighted by the smoothstep ramp t(C; c_{lo}, c_{lo} + 0.35), so only vivid colour reaches full strength. Lightness gates use a feather width of 0.10:

w_V \;=\; t(V;\; V_{\min} - 0.10,\; V_{\min}) \cdot (1 - t(V;\; V_{\max},\; V_{\max} + 0.10))

13.4 Shadows / Highlights gate

With s = Shadows value, h = Highlights value, b = internal balance (default 0.5), and luminance L:

f \;=\; 0.08 + 0.12\,b
w_s \;=\; t(L;\; s - f,\; s)
w_h \;=\; 1 - t(L;\; h,\; h + f)
\mathrm{mask}' \;=\; \mathrm{mask} \cdot w_s \cdot w_h

13.5 Luminance basis

Rec. BT.601 weights, used throughout for the chroma-preserving boost and the shadows/highlights gate:

Y \;=\; 0.2989\,R + 0.5870\,G + 0.1140\,B

These are the classical NTSC weights, not Rec. 709. Astrophotography data doesn't particularly care which basis is used — the two differ by about 2% in the green weight — but it's worth knowing if you're exporting the mask for use with tools that assume one or the other.

13.6 Adjustment math

Let m be the per-pixel mask value (already multiplied by Intensity and clipped). All values are in [0, 1] after the final clip.

CMY (positive values pull a channel toward its complement):

R' \;=\; R + (-\,c)\,\cdot m,\quad G' \;=\; G + (-\,m_{\mathrm{mag}})\,\cdot m,\quad B' \;=\; B + (-\,y)\,\cdot m

RGB primaries (direct additive):

R'' \;=\; R' + r \cdot m,\quad G'' \;=\; G' + g \cdot m,\quad B'' \;=\; B' + b \cdot m

Luminance (additive brightness):

\mathrm{out}' \;=\; \mathrm{out} + \ell \cdot m

Contrast (around 0.5 midtone):

x' \;=\; (x - 0.5) \cdot (1 + \kappa \cdot m) + 0.5

Chroma (L-preserving):

\mathrm{rgb}' \;=\; Y + (\mathrm{rgb} - Y) \cdot (1 + \chi \cdot m)

Scales the colour-component vector (per pixel, around the per-pixel luminance) by a mask-weighted factor. At chroma = 0 this is the identity; at −1 it collapses to grey; at +1 it doubles the colour deviation.

Saturation (HSV S):

S' \;=\; S \cdot (1 + \sigma \cdot m)

Operates in HSV space, converting back to RGB via OpenCV when done. Changes perceived brightness because unlike the Chroma mode it doesn't anchor to Y.

13.7 Intensity scaling

The final mask handed to the adjustment code is:

m_{\mathrm{eff}} \;=\; \mathrm{clip}(m \cdot I,\; 0,\; 1)

where I is the Intensity slider value, so Intensity acts as a global strength on everything simultaneously.

13.8 Region mask composition

If a lasso region is active, the base mask (hue-based or imported) is multiplied element-wise by the region mask after all other mask steps:

\mathrm{mask}_{\mathrm{final}} \;=\; \mathrm{mask}_{\mathrm{hue/imported}} \cdot \mathrm{mask}_{\mathrm{region}}

The region mask is binary at the resolution it was drawn and bilinearly resampled when it needs to match a different image size. This happens for both the live preview and the full-resolution apply paths, so what you see is what you get.

13.9 Debounced pipeline

Two QTimers coordinate the preview:

Slider release and spin-box editing-finished also force an immediate preview update so you don't have to wait after letting go of a knob.

13.10 OpenCV dependency

Selective Color requires OpenCV (cv2) at import time for the HSV conversions, lasso polygon fill, Gaussian edge blur, contour outline rendering for the region overlay, and image resize. The _rgb_to_hsv01 helper raises RuntimeError if cv2 isn't available. A pure-NumPy fallback exists only for the lasso polygon fill (via matplotlib.path.Path) and for the imported-mask resize (nearest-neighbour via index arrays).

Credits. The selective-colour-correction concept — a hue-selective mask coupled with CMY and RGB additives — is a classical darkroom technique dating to film-era colour printing and reproduced across many image-editing platforms. The specific mask-building pipeline here (local hue-band feather, HSV S×V chroma proxy, luminance-feathered shadows/highlights, lasso region AND'd with hue mask, imported-mask override) and the UI around it (hue wheel widget, modifier-key interactions, dual debounced timers, mask overlay) are Frank's implementation for SASpro. HSV conversions and Gaussian blur use OpenCV. The luminance-preserving chroma formula (Y + (RGB - Y)(1 + k)) is the standard chroma-decomposition approach used across colour-processing tools.