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.
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.
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:
| Preset | Hue range (°) |
|---|---|
| Red | 340–360 and 0–15 (wraps around) |
| Orange | 15–40 |
| Yellow | 40–70 |
| Green | 70–170 |
| Cyan | 170–200 |
| Blue | 200–270 |
| Magenta | 270–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.
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.
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.
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.
Luminance gates that attenuate the mask outside a brightness range:
Both gates use smoothstep ramps, not hard thresholds (see § 13.2).
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
and the backward distance:
The band value is 1.0 strictly inside the forward arc, with
linear feathers of width s at both ends:
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.
Used by Soft mode and the shadows/highlights gates:
In soft mode, the chroma proxy is HSV saturation times value:
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:
With s = Shadows value, h = Highlights
value, b = internal balance (default 0.5), and
luminance L:
Rec. BT.601 weights, used throughout for the chroma-preserving boost and the shadows/highlights gate:
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.
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):
RGB primaries (direct additive):
Luminance (additive brightness):
Contrast (around 0.5 midtone):
Chroma (L-preserving):
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):
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.
The final mask handed to the adjustment code is:
where I is the Intensity slider value, so Intensity
acts as a global strength on everything simultaneously.
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:
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.
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.
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).
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.