BookNotesSlidesP-sets Syllabusdraft · v0.2.49.1
◀▲⌂▶
💬Comments welcome. To leave a note, select any text and click the note / highlight button that pops up — or open the panel with the tab at the top-right (‹). Notes are visible only inside our private review group.
💡 In a hurry? Jump to this chapter’s 1 big lesson ↓

8.5 Color point operations⧉

Changing saturation or tint requires considering the channels together. A color point operation maps an RGB triple to another triple at the same pixel. It can preserve a chosen brightness coordinate, but different coordinates and gamut handling produce different results.

8.5.1 Basic color enhancement: saturation and vibrance⧉

A simple saturation control moves colors toward or away from a gray of equal chosen brightness. The definition of brightness matters: preserving an encoded weighted sum does not preserve physical luminance or perceived lightness.

That split is literally one line of code. Let $Y$ be the pixel's luma: a weighted sum

$$Y = 0.2126\,R + 0.7152\,G + 0.0722\,B \qquad (\text{Rec. 709; Rec. 601 uses } 0.299 / 0.587 / 0.114),$$

computed here on the gamma-encoded $R,G,B$ (so it is a luma $Y'$, not a linear luminance) and broadcast back to all three channels; then its chrominance is just $\text{RGB} - Y$ (what is left after the gray is removed), and saturation scales it:

$$\text{RGB}' = Y + s\,(\text{RGB} - Y).$$

The weights sum to one, so this operation preserves the same weighted channel sum before clipping. At s=0 it gives gray, at s=1 the input, and at s>1 it extrapolates away from gray. Here we use sRGB-encoded values as a simple appearance control. The same construction in linear RGB preserves luminance before clipping; a perceptual-space chroma edit has different behavior.

Vibrance is a gentler cousin: it boosts the saturation of muted colors more than already-vivid ones, and typically spares skin tones, so portraits do not turn garish (Figure 8.5.2). It is the same formula, but with the constant $s$ replaced by a per-pixel gain that is large where a pixel is dull and fades to nothing where it is already colorful (and near skin hues):

$$g = 1 + v\,(1 - \text{sat})\,w_\text{skin}(\text{hue}), \qquad \text{RGB}' = Y + g\,(\text{RGB} - Y).$$

For this example, use HSV saturation in $[0,1]$, with zero at black. A chroma-to-luminance ratio is not automatically in that range and cannot be substituted without normalization. The optional hue weight reduces changes near selected orange hues, not verified skin. It can affect wood and fabric too. A neutral pixel stays neutral because its chroma is zero, even where the nominal gain is largest.

HSV is a convenient coordinate system for this teaching implementation; commercial vibrance controls need not use it. For nonnegative RGB, with M=max(R,G,B), m=min(R,G,B), and C=M−m:

fig-xkcd-648
Figure 8.5.1. xkcd #648 "Fall Foliage," Randall Munroe, xkcd.com/648 (CC BY-NC 2.5).
fig-point-op-saturation-vibrance
Figure 8.5.2. Saturation versus vibrance. Left, the input, a vivid red flower against muted green foliage. Center, a uniform saturation boost (constant $s = 1.6$): the dull foliage improves, but the already-vivid flower is pushed past the gamut and clips, its petals turning flat and garish, because every pixel is scaled the same amount. Right, vibrance (per-pixel gain $g$, here $v = 0.6$ so the gray-point gain $1+v$ matches $s$): the muted foliage gets the full boost while the saturated flower barely moves, because the gain fades as a pixel's own saturation rises. The fourth panel plots the chroma gain each method applies against a pixel's saturation: flat at $s$ for saturation, tapering from $1+v$ down to $1$ for vibrance, and damped further on skin hues (dashed). Same intended "more color," very different treatment of the colors that were already fine.

One caveat: "how far from gray" and "leave lightness alone" both depend on the color space you measure them in, and doing this well means scaling chrominance in a perceptually uniform space rather than naively in RGB. That choice of space, and why skin needs protecting at all, is the subject of the color part, so it is what we defer to Chapter 4.1, Linear color reproduction and CIE XYZ; the mechanics are the two equations above. Both saturation and vibrance are color operations, not tone operations.

For C>0, the HSV coordinates are

$$ V=M,\qquad S=\begin{cases}C/M&M>0\\0&M=0,\end{cases} \qquad H=60^\circ\begin{cases} ((G-B)/C)\bmod 6&M=R,\\ (B-R)/C+2&M=G,\\ (R-G)/C+4&M=B. \end{cases} $$

At C=0, hue is undefined; an implementation can store zero by convention and avoid evaluating the divisions by C. Value is the largest channel, and saturation is the channel range relative to that value. HSL instead uses $L=(M+m)/2$ as its lightness coordinate. Neither this L nor HSV value is physical luminance. These coordinates are inexpensive controls for selecting and changing colors, not perceptually uniform measurements.

Any space that separates a lightness from a chroma supports the same saturation formula, and the choice of space changes the result (Figure 8.5.3).

fig-saturation-methods
Figure 8.5.3. One saturation factor $s$, applied in nine spaces. Each method scales what its own space calls chroma: $\text{RGB}-Y'$ on the encoded values (this section's formula), $\text{RGB}-Y$ in linear light, S in HSV and in HSL, $a^*b^*$ in CIELAB, $ab$ in Oklab, $C_TC_P$ in ICtCp and $a_zb_z$ in JzAzBz, plus the book's vibrance, whose gain is $s$ at a gray and fades as a color's chroma rises. Choose any two to see side by side, on a photograph, an upload, or a test chart of every hue at several lightnesses. The table measures what each one did to the displayed image in one common yardstick, Oklab: the chroma it added, how far it moved lightness, how far it turned hue, and how many pixels it pushed out of the sRGB gamut. Oklab adds exactly $s$ by construction, because it is also the yardstick; what matters is how the others compare. On most of the photographs, HSV and HSL move lightness the most, CIELAB and linear RGB turn hue the most, linear RGB pushes the most pixels out of gamut, and vibrance adds the least chroma while barely moving hue or lightness.

8.5.2 The color edits, in code⧉

The examples below take sRGB-encoded RGB in [0,1]. Saturation and the skin-agnostic vibrance variant preserve the chosen encoded weighted sum before clipping. Split toning deliberately adds color to grays; subtracting the weighted mean from its tint preserves that same sum, not neutrality. Final clipping can break this preservation.

Listing 8.5.1 · Saturation, vibrance, and a teal-orange gradeembed
import numpy as np

LUMA = np.array([0.2126, 0.7152, 0.0722])          # Rec.709 luma weights, on gamma/sRGB values

def saturate(rgb, s=1.6):
    """Uniform saturation: scale chroma about luma. s=1 unchanged, s=0 -> gray, s>1 more color."""
    Y = (rgb @ LUMA)[..., None]
    return np.clip(Y + s*(rgb - Y), 0.0, 1.0)

def vibrance(rgb, v=0.6):
    """Boost muted colors more than vivid ones: the gain fades as a pixel's own saturation rises."""
    mx = rgb.max(-1); mn = rgb.min(-1)
    sat = np.where(mx > 0, (mx - mn)/np.maximum(mx, 1e-6), 0.0)   # HSV saturation
    g = (1.0 + v*(1.0 - sat))[..., None]
    Y = (rgb @ LUMA)[..., None]
    return np.clip(Y + g*(rgb - Y), 0.0, 1.0)

SHADOW_TINT    = np.array([0.00, 0.05, 0.10])      # teal, added to shadows
HIGHLIGHT_TINT = np.array([0.10, 0.04, 0.00])      # orange, added to highlights
def teal_orange(rgb, amount=0.5):
    """Cinematic split-tone: teal shadows, orange highlights, weighted luma removed from tint; gray can become colored."""
    Y = (rgb @ LUMA)[..., None]
    tint = SHADOW_TINT*(1 - Y) + HIGHLIGHT_TINT*Y                 # blend tint by luma
    tint = tint - (tint @ LUMA)[..., None]                    # drop the DC: hue push only
    return np.clip(rgb + amount*tint, 0.0, 1.0)

8.5.3 Converting to black and white⧉

A black-and-white conversion maps three color values to one gray. That discards color distinctions, and the desired gray depends on whether the goal is luminance reproduction or an expressive rendering (Figure 8.5.4). One simple family is

$$ \text{gray} = w_R R + w_G G + w_B B, \qquad \sum_i w_i = 1, $$

with the sum constraint preserving neutral inputs, and the interesting questions are which weights and in which space.

fig-bw-conversions
Figure 8.5.4. One color photo, many grays. The same image converted by: a single channel (R, G, or B); a flat average $(R{+}G{+}B)/3$; weighted luminance (Rec. 601/709); and a red-filter channel mix that darkens a blue sky for dramatic clouds. The conversions genuinely differ, proving there is no one right answer.

A single channel can emphasize particular features but discards the other measurements. An equal RGB average gives a different rendering from a luminance-weighted sum. For linear sRGB/Rec.709 primaries, luminance uses weights 0.2126, 0.7152, and 0.0722; apply the output encoding afterward. Applying these weights directly to encoded RGB gives a luma-like gray. For saturated red or blue, the encoded linear-luminance result is brighter than that direct encoded weighted sum. CIELAB lightness, HSL lightness, and HSV value provide still other grayscale mappings; their numeric ranges and output interpretation must be specified.

The expressive end of the ladder is the custom channel mixer: choose the weights yourself (keeping $\sum w_i = 1$ to preserve neutral inputs). This is Lightroom and Photoshop's black-and-white mixer, and it is the digital descendant of putting a color filter on black-and-white film: a red filter darkens a blue sky for dramatic clouds (Ansel Adams's black skies are a red filter on panchromatic film), a yellow or green filter lightens foliage, an orange filter smooths skin. So a conversion is really a decision about how each color should map to gray. The hard case exposes the lossiness directly: two distinct hues with the same luminance (a red and a green of equal brightness) map to the same gray, erasing the edge between them. Contrast-preserving decolorization methods (Color2Gray, Gooch et al. 2005; Grundland & Dodgson 2007) optimize the mapping to keep color contrast rather than only luminance: the principled answer to "the green apple and the red apple should not vanish into one gray."

Video 8.5.1. Video: Color2Gray: Salience-Preserving Color Removal, Amy A. Gooch, Sven C. Olsen, Jack Tumblin, Bruce Gooch 2005 (YouTube).
💡 Big lesson callback — black-and-white is a lossy projection

A grayscale mapping loses color distinctions. A fixed weighted sum has a definite output, but its inverse is underdetermined. Choose weights or an optimization objective according to the contrast you want to retain; equal-luminance colors need not remain distinguishable.

Try a subject whose color separates it from the background but whose luminance is similar. A luminance conversion can merge the two; a different channel mixture can separate them. That does not make the mixture a better measurement of luminance. It makes it a different black-and-white interpretation. The photographer can use the weights to emphasize skin, foliage, or sky, but should inspect noise and clipping as well as tonal separation. The input representation matters too: a weighted sum of encoded values and an encoded weighted sum of linear values are different operations.

8.5.4 Video color grading⧉

Tone and color tools developed separately for stills and for motion pictures, and the two vocabularies never fully merged. Color grading is the motion-picture craft, a separate discipline from the stills/radiometric editing the rest of this chapter describes: less concerned with physical radiance, far more concerned with the look of a film, and equipped with its own tools and idioms. The two began to converge when film scans could be graded digitally: O Brother, Where Art Thou? (2000) is widely cited as the first feature film color-graded entirely through a digital intermediate, its green Mississippi summer turned a dusty gold in software rather than in the lab.

The colorist's primary controls are the lift / gamma / gain wheels. Rather than a single exposure or contrast slider, these split the tonal range into three overlapping bands (lift for the shadows, gamma for the midtones, gain for the highlights) and each wheel carries a color offset as well as a level, so you can, say, cool the shadows and warm the highlights independently. Corrections come in two layers: primary corrections act on the whole frame (overall balance and exposure), while secondary corrections are qualified (restricted to a range of hue or luma, or to a tracked mask) so you can grade just the sky, or one actor's shirt, without touching the rest.

A grading pipeline must state its input camera space, transfer function, working space, and output transform. ACES supplies a color-management framework; camera log is one possible input encoding. A fixed RGB-to-RGB portion of a grade can be approximated by a 3-D LUT. Tracked masks, spatial effects, and temporal adjustments cannot be represented by one global LUT.

A teal-and-orange split tone adds a cool tint to darker values and a warm tint to brighter values (Figure 8.5.5). The example below is keyed by luma, so it does not identify people or backgrounds: a bright blue object receives the highlight treatment and a dark face the shadow treatment. Hue-qualified or semantic grades can make different selections.

fig-point-op-teal-orange
Figure 8.5.5. The teal-and-orange grade, on any photograph. Left, the ungraded frame; middle, the graded frame, or the key that decided each pixel's push; right, the key itself. Each pixel gets a signed key between $-1$ and $+1$, and its color is pushed by strength times that key toward orange where the key is positive and toward teal where it is negative. Keyed by luma, as a split tone is, the key rises from teal below the split point to orange above it, so a bright blue sky counts as a highlight and warms while a face in shade cools. Keyed by hue, skin-like hues go orange and every other color goes teal in proportion to how colorful it is, and neutrals stay neutral: a crude hue-qualified secondary. Strength, split point, softness and the two push hues are all adjustable. Either way it is a fixed $\text{RGB} \to \text{RGB}$ mapping, the kind of creative look a colorist freezes and ships as a 3-D LUT (.cube) applied per frame.

The stills world has a close cousin: Lightroom's hue-saturation-lightness (HSL) / color-mixer and color-grading panels do much the same hue-selective work a colorist does with secondaries. We mostly speak the stills/radiometric dialect in this book, but the operations underneath are the same point operations seen through a different craft. The grading toolkit ties back to Tone point operations#Lookup tables (3-D LUTs as the delivery format), to Chapter 4.1, Linear color reproduction and CIE XYZ (color management, log encoding, ACES), and forward to tone mapping (the dynamic-range side of the same problem).


In CompPhotoRoom

The color matrix, Vibrance and the HSL panel of Chapter 8.13, A raw developer in the browser are these operations; the ? on each control opens the running method next to this chapter's derivation.

Camera log encodings, such as S-Log and LogC, provide another connection to the earlier arithmetic: over their logarithmic region, multiplicative exposure changes become additive code-value shifts. Their precise curves also include specified offsets, scales, and low-end behavior. A common working representation helps a colorist match material from different cameras, but matching still requires the appropriate input transforms and scene judgments. LUTs can carry the fixed color mapping of that work; they do not carry the tracked selections or every operation used to create it.

Recap: big lessons of this chapter

💡 Big lesson callback — black-and-white is a lossy projection

A grayscale mapping loses color distinctions. A fixed weighted sum has a definite output, but its inverse is underdetermined. Choose weights or an optimization objective according to the contrast you want to retain; equal-luminance colors need not remain distinguishable.