Curves Editor
Freehand transfer curves on a monotone spline, per channel —
RGB, CIELAB (L*, a*, b*, Chroma) and Saturation — with a live
before/after histogram, presets, and a mask-aware, instant-apply pipeline.
1. Using the tool
The grid is your transfer curve: the horizontal axis is input tone, the
vertical axis is output. Each channel keeps its own curve, and the channels you
aren't editing are drawn as dim overlays so you always see the whole transform.
Shaping the curve
- Add a point — left-click an empty spot inside the grid; it
starts dragging immediately. (You can also drop one straight from the
image — see below.)
- Move a point — drag it. Interior points are free in output and
clamped in input between their two neighbours, so the curve can't fold back
on itself.
- Endpoints are edge-locked — the black point (bottom-left) slides
along the left or bottom edge, the white point (top-right) along the right
or top edge, so they double as black/white-point handles. They can't be
deleted.
- Delete a point — right-click it, double-click it,
or hover/select it and press Delete.
- Hover a point — it highlights and shows its exact
in / out values right next to it.
- Stacked points — clicking a spot where points overlap cycles
through them, so a handle buried under another (e.g. an endpoint pushed
into a corner) is still reachable on the next click.
- Point X / Y — with a point selected, type its exact input/output
values (0–1) into the spin-boxes.
Working from the image
- Hover the preview — vertical value-lines appear on both
the curve grid and the histogram, and the status line reads out the cursor
position and the pixel's values. The lines are drawn where that pixel sits
on the active channel's axis, so you can see exactly where a feature
lands on your curve before you touch a handle. (On K and R/G/B that's the
R/G/B levels; on a colour channel it's that channel's own value — see
§2.2.)
- Double-click the preview — drops a control point on the active
curve at that pixel's position on the active axis (nudged slightly if it
lands on an existing point), so you can anchor the curve to a tone or a
colour you picked straight off the image.
- Ctrl+click the grid — sets an inflection pivot and redistributes
the interior handles evenly around it, a quick way to seed a symmetric
contrast move.
Channels, presets & preview
| Control | What it does |
| Channel | K (brightness / RGB linked), R/G/B,
L*, a*, b*, Chroma, or Saturation —
each with an independent curve; the inactive ones draw as dim overlays. |
| Presets | Built-in shapes (S-curves, lift/crush shadows, highlight
roll-off, …) plus your own saved multi-channel presets. |
| Pop Out | Detach the editor into its own resizable window
(position/size remembered). |
| Histogram | Before (dim) vs after (bright) for R/G/B and
luminance, with log scaling and x-axis zoom/pan. |
| Toggle Preview / Apply / Reset | Show the result on the canvas,
commit it to the document, or return every channel to the identity line. |
| Grip (bottom-left) | Drag to the canvas to mint a Curves shortcut
with all channel curves baked in, or drop on an image to apply
headlessly. |
2. Working with the colour channels
L*, a*, b* and Chroma behave differently from the RGB curves in one
important way, and once that clicks they become the most precise colour tools in
the program. This section is the practical version; the maths is in §3.
2.1 Which way is which
CIELAB splits colour into two opponent axes. a* is the red–green
axis, b* is the yellow–blue axis. On both of them, raising the curve
moves toward the warm end and lowering it moves toward the cool end:
| Channel | Raise the curve (output above the diagonal) | Lower the curve |
| a* | toward red / magenta | toward green |
| b* | toward yellow | toward blue |
| L* | lighter | darker |
| Chroma | more saturated, hue unchanged | toward neutral grey |
| Saturation | more vivid (HSV) | toward grey |
Mnemonic: up is warm, down is cool — on
both colour axes. a* up is red, b* up is yellow; a* down is green,
b* down is blue.
2.2 The x-axis is colour, not brightness
This is the part that catches people. In the K and R/G/B curves,
the horizontal axis is tone — left is shadows, right is highlights. In
a* and b* it is not. The horizontal axis is the pixel's
existing position on that colour axis:
| Channel | Left of the grid | Centre | Right of the grid |
| a* | already green | neutral / grey | already red |
| b* | already blue | neutral / grey | already yellow |
So a handle placed on the right-hand side of the a* curve acts on the
red pixels and barely touches anything else — no mask required. That makes
a*/b* curves selective by colour in the same way an RGB curve is selective
by brightness. If you want to act on a region that happens to be a particular
colour, this is usually the fastest route to it.
The readouts follow the axis. On a colour channel the
grid's hover line and the double-click-to-place point both use the pixel's
actual L*/a*/b*/Chroma/Saturation value, not its brightness, so a feature
you pick off the image lands exactly where the curve will act on it. The status
line shows the R/G/B levels and that axis value side by side. The histogram
stays in R/G/B — it shows the tonal distribution, not the a*/b*
distribution — so ignore it when judging where to put a colour handle.
2.3 Neutral, and the part of the grid that's actually reachable
a* and b* are signed — zero means neutral, not black. They are mapped
into the grid as (a*+128)/255 and (b*+128)/255, so
a* = 0 and b* = 0 land at 0.502, essentially dead centre. The
identity diagonal passes through it, which is why an untouched curve leaves
greys grey.
If you want a contrast-style pivot that keeps neutrals neutral, put the pivot
point at 0.502 rather than 0.500 — type it into
the Point X box. Half a thousandth is invisible on most images, but on a
carefully neutralised background it is the difference between no cast and a
faint one.
The grids also run wider than any real colour can reach. a* and b* are mapped
over ±128 and Chroma over 0–200, but no displayable colour gets that
far — the most extreme primaries top out well short of the edges:
| Channel | Reachable input range | Dead zone |
| a* | about 0.16 – 0.89 | both ends |
| b* | about 0.08 – 0.87 | both ends |
| Chroma | 0.00 – about 0.67 | right third |
A handle parked outside those ranges does nothing at all, because no pixel
ever has that input value. On a real image the occupied range is narrower still
— most pixels cluster near the centre — so the useful work happens in
the middle of the grid and a handle dragged to the far edge is usually a handle
doing nothing. Keep moves close to the centre and they'll bite harder than they
look.
2.4 Four shapes worth knowing
- Whole line lifted or dropped — a global cast along that axis.
Everything, including the background, moves. Usually not what you want on a
calibrated image.
- Steeper through the centre — pushes colours apart along that
axis. Reds get redder and greens get greener. This is a colour
contrast move, and it is much better behaved than cranking Saturation
because it respects the opponent structure.
- Flatter through the centre — pulls everything toward neutral on
that axis only. Flattening a* alone kills red/green separation while
leaving yellow/blue untouched.
- One half moved, the other left alone — the targeted move. Lower
only the left half of b* and already-blue pixels get bluer while
yellows and reds stay exactly where they were.
2.5 Recipes
- Remove a green cast (SCNR-like). On a*, lift the left-hand
side toward the centre line. Green pixels compress toward neutral; red
pixels, which live on the right, are untouched.
- Neutralise a yellow-brown background. On b*, pull the region
just right of centre down slightly. Stars and genuinely yellow features
further right survive if you keep the move local.
- Deepen a reflection nebula without touching the emission. On
b*, lower the left half only. The blue dust goes deeper blue; the red
Hα regions never enter that part of the curve.
- Push an Hβ/OIII region blue in a red-dominant narrowband blend.
Lower b* on the left half to drive the blue harder, then lower
a* on the right half if the region still carries too much red. For a
bigger shift than the curves will give you comfortably, assign the channel
a colour directly in NB Integration instead and use curves for the
finishing touch.
- Separate an SHO palette. Steepen a* through the centre to open
the gap between the SII/Hα reds and the OIII greens, then use
Chroma to bring the overall intensity back down if it goes garish.
2.6 a*/b* vs Chroma vs Saturation
- a* / b* change where a colour sits — they move hue and
intensity together, and they target by colour. Reach for these when a
specific colour needs to become a different colour.
- Chroma scales the distance from neutral in the a*–b* plane and
leaves hue alone. Reach for it when the colours are right but too weak or
too strong. Because it is perceptual, it holds up far better than HSV
saturation on bright stars.
- Saturation operates on HSV and is the blunter of the two. It is kept
for compatibility with RGB-style workflows and for quick, mild moves.
Large a*/b* excursions can push colours out of gamut on the way
back from Lab, where each channel is hard-clamped to [0,1] — there is
no soft roll-off. Clamping one channel and not the others shifts the hue, so a
bright core can go flat or drift colour after a big move. Back the curve off and
finish with Chroma instead.
3. Technical details
A curve is a monotone spline through your control points,
baked to a lookup table and applied either per RGB channel or per colour-space
component. Everything runs on float data in [0,1].
3.1 The curve: a monotone PCHIP spline
The control points are interpolated with a PCHIP (Piecewise Cubic
Hermite) spline — a shape-preserving monotone cubic. Unlike a natural cubic
it never overshoots or rings between points, so the curve does exactly what the
handles say and the transfer stays monotone (no tonal inversions).
3.2 From curve to lookup table
The spline is sampled into a lookup table — 65 536 entries for the
final apply (256 for the live preview) — and every pixel is mapped through
it:
\mathrm{out}(x)=\mathrm{LUT}\left[\,\lfloor x\,(N-1)\rfloor\,\right],\qquad N=65536
Because the LUT is monotone, the before→after histogram can be remapped
exactly by resampling the input CDF onto the output axis — mass-preserving
and gap-free, so the displayed “after” histogram can't show a false
comb where the curve expands tones.
3.3 Channel modes
RGB. K applies one LUT to all three channels (linked brightness);
R/G/B curve a single channel. When both are set, the per-channel curve is
composed after K:
\mathrm{LUT}_{R}'=\mathrm{LUT}_{R}\circ\mathrm{LUT}_{K}
CIELAB (L*, a*, b*, Chroma). The image is converted to XYZ with the
D65 primaries matrix, applied directly to the working data — no sRGB
transfer function is decoded first, so the channel values are treated as linear.
The inverse omits it symmetrically, so the round-trip is exact:
[ X ] [ 0.412456 0.357576 0.180438 ] [ R ]
[ Y ] = [ 0.212673 0.715152 0.072175 ] [ G ]
[ Z ] [ 0.019334 0.119192 0.950304 ] [ B ]
then XYZ→Lab about the D65 white point
(Xn, Yn, Zn) = (0.95047, 1, 1.08883):
L^{*}=116\,f\!\left(\frac{Y}{Y_n}\right)-16
a^{*}=500\left(f\!\left(\frac{X}{X_n}\right)-f\!\left(\frac{Y}{Y_n}\right)\right),\qquad b^{*}=200\left(f\!\left(\frac{Y}{Y_n}\right)-f\!\left(\frac{Z}{Z_n}\right)\right)
f(t)=\sqrt[3]{t}\ \ (t>\delta^{3}),\qquad f(t)=\frac{t}{3\delta^{2}}+\frac{4}{29}\ \ (t\leq\delta^{3}),\qquad \delta=\frac{6}{29}
The selected component is normalised to [0,1], curved, and de-normalised
(L*/100, (a*+128)/255, (b*+128)/255) —
which is why neutral falls at 0.502 on the a*/b* grids (§2.3).
For Chroma the curve scales the colour's radius in the a*–b* plane
while keeping its hue:
C=\sqrt{a^{*2}+b^{*2}}\;,\qquad (a^{*},b^{*})\;\to\;(a^{*},b^{*})\cdot\frac{C_{\mathrm{new}}}{C}
Saturation curves the S channel of HSV and converts back.
3.4 Order of operations
When several channels are active they are applied in a fixed order so the
result is deterministic: first the RGB stage (K composed into R/G/B), then the
Lab stage (L*, a*, b*, Chroma via one round-trip), then Saturation (via HSV).
Mono images support K (applied directly) and L* (through the Lab lightness
nonlinearity); the colour-only channels are skipped.
3.5 Inflection pivot
Ctrl+clicking the grid (or double-clicking the image) sets a pivot
u and redistributes the interior handles evenly on either side of it
along the identity line — a quick way to seed a symmetric contrast curve
around a chosen tone.
3.6 Masking & apply
With an active mask the result is blended back against the source over the
mask:
\mathrm{out}=\mathrm{mask}\cdot\mathrm{out}+(1-\mathrm{mask})\cdot\mathrm{src}
- The preview runs on a downsampled copy; the full-resolution result is
computed in a background thread, keyed by a signature of every channel's
points plus the active mask, so Apply is usually instant (it commits
the already-computed result) and falls back to a synchronous compute only
if you apply before the worker finishes.
- The grip/headless preset is
kind="curves_multi" and carries
every channel's points, so a dropped shortcut reproduces the entire
multi-channel transform — not just the active curve.
- After Apply, all curves reset to the identity line on the
already-transformed image so you can keep stacking adjustments.