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 3 big lessons ↓

8.3 Tone point operations⧉

📎 Problem set

PS1 implements brightness/contrast, gamma, and white balance as point operations. → Chapter 1.5, Problem sets (appendix).

The simplest image-editing operations are independent at each pixel: each output pixel depends only on the value of the input pixel at the very same spot, with no reference to its neighbors or to where it sits. These are point operations, and they cover a remarkable fraction of the edits photographers actually make: exposure, brightness, contrast, levels, freehand tone curves, gamma, and the lookup tables that bake them in. This chapter is about the tonal half of that family: what they can and cannot do, and the single question that decides whether each is done right or wrong, namely the numerical space it acts in. The color half, saturation and vibrance, black-and-white conversion, and video color grading, is the companion chapter Chapter 8.5, Color point operations.

Two framing cautions before the toolkit. First, these sliders are not independent: brightness, exposure, contrast, levels, and the tone curve all bend the same transfer line, so they overlap and partly undo one another; the general tone curve at the end of the chapter literally subsumes the rest, and any given look can be reached several ways. Second, they are not all equally meaningful: a few are objective, radiometric operations faithful to how light behaves (exposure is a multiply in linear light), while most are stylistic (brightness, contrast, the S-curve) with no single physically correct form, only a choice of space and taste. Knowing which kind you are using tells you whether "which space?" has a right answer (radiometric: yes) or merely a pleasant one (stylistic: pick the look).

8.3.1 Three kinds of operation⧉

Recall the distinction between changing values, remapping coordinates, and combining neighboring samples (Figure 8.1.13; Chapter 8.1, Image representation). These categories describe useful building blocks, not every possible image algorithm.

A point operation touches the range only. The output pixel depends solely on the input pixel's value at the same location, and one and the same rule is used everywhere:

$$ I_\text{out}(x, y) = f\big(I_\text{in}(x, y)\big). $$

There is a single input-to-output curve f, applied identically at every pixel. In words: read a pixel's value, find where that value lands on the curve, write the answer back: no neighbors consulted, nothing moved. Brightness, contrast, exposure, gamma, and lookup tables are all point operations, and we begin here because they are the simplest to reason about and the cheapest to run.

A domain operation maps output coordinates to input coordinates, $I_\mathrm{out}(x,y)=I_\mathrm{in}(x\prime,y\prime)$. For noninteger positions, interpolation combines neighboring samples and can change their values; see Chapter 6.5, Resampling and upsampling.

A neighborhood operation sits in between: the output pixel is computed from a whole window of input pixels around the same spot. Blur averages a pixel with its neighbors; sharpening, edge detection, and the bilateral filter all read a neighborhood. Those get their own chapter on Chapter 6.2, Convolution.

fig-operation-types
Figure 8.1.13 (reproduced). The three families of image operation, sorted by what each output pixel may read. Point / range (left): the output depends only on the input pixel's value at the same location: one curve applied everywhere (exposure, contrast, lookup tables). Domain (center): pixels are relocated, their colors unchanged (resize, rotate, warp). Neighborhood (right): the output is computed from a window of nearby input pixels (blur, sharpen, bilateral). This chapter is the left column.

We develop tonal curves on a scalar value L. Applying a nonlinear curve independently to RGB channels can change hue and saturation. Scaling the channels using a mapped luminance behaves differently; Chapter 8.5, Color point operations treats this choice.

8.3.2 The point-operation curve⧉

Since a point operation is just one curve f applied to every pixel, the curve is the operation, and photo editors hand it to you directly: the curves tool plots input value across the horizontal axis, output value up the vertical, and lets you bend the line by hand. The "do nothing" case (leave every value untouched) is the 45° diagonal. Everything in this chapter is some way of bending that diagonal: lift it to brighten, steepen it to add contrast, pin its ends to set black and white points, or just drag it freehand.

Controlling that mapping is not a digital invention. Photography has always been the craft of deciding how scene radiance becomes printed tone (film stock, exposure, development, paper grade, and the enlarger all shape it) and the digital tone curve is the most direct handle on the same problem. There is, though, one question we must answer before we bend anything, and it runs through the whole chapter: in which numerical space does the curve act?

The same image can be stored as linear-light values (proportional to the physical radiance the sensor measured), as gamma/sRGB-encoded values (the perceptually spaced numbers a PNG actually holds), or as log values (handy when the range is enormous). We met these encodings in Chapter 8.1, Image representation; here they stop being a storage detail and start changing the result. Apply the very same arithmetic in linear light versus in sRGB and you get visibly different pixels, so every operation below carries a note about the space it belongs in.

💡 Big lesson (L4.6) — additive vs. multiplicative → choice of encoding

Exposure multiplies linear-light values; independent contributions of light add. Log coordinates turn exposure ratios into differences. An offset in encoded values is a distinct appearance control. Choose the domain from the operation and its model.

8.3.3 Exposure: a multiply in linear light⧉

An exposure adjustment scales linear-light values. During capture, a longer shutter or larger aperture can collect more light; raising ISO changes the camera’s gain or rendering convention and does not itself add photons. A software exposure change scales an existing measurement without improving its capture SNR or recovering clipped data. For s stops, the multiplier is $2^s$:

$$ L_\text{out} = 2^{s}\, L_\text{in}. $$

If E denotes the output encoding, the displayed curve is $E(2^sE^{-1}(V))$. Its shape depends on E: a pure power law gives a scaled straight line before clipping, while sRGB also has a toe and offset. Values above display white can be retained in the working image even when the preview clips them (Figure 8.3.2).

fig-point-ops
Figure 8.3.1. A point-operation playground. Every pixel is mapped through the same function, independent of its neighbors: exposure, brightness, contrast, gamma, saturation, threshold, and invert all compose into the single input→output transfer curve shown at right. Adjust the controls (or load your own image) to watch the curve and the result change together; the rest of this section covers these one at a time.
fig-point-op-exposure
Figure 8.3.2. Exposure as a multiply in linear light. Left, the input. Center, the same photo at +1 stop (×2 in linear light), brighter throughout, with the brightest highlights clipped to white. Right, the transfer curve $L_\text{out} = 2^{s} L_\text{in}$ drawn in display (sRGB) space: it bows above the 45° diagonal, lifting the shadows strongly and flattening into the ceiling at 1. The 45° diagonal (no change) is shown for reference.

The "in linear light" qualifier is not pedantry. It marks the difference between a correct exposure change and a wrong one. The library makes the distinction concrete: imageops.exposure(img, stops) decodes to linear light, multiplies, then re-encodes; ask it for the naive version instead and it multiplies the gamma-encoded numbers directly, which is exactly what happens when you forget to linearize. The two part ways sharply, and the next figure is devoted to showing how.

💡 Big lesson (L8.3) — exposure scales linear light (a multiply) or shifts log values (an add)

Exposure scaling multiplies linear-light values by $k=2^s$ or adds $s$ in base-2 log units. For a pure gamma encoding $V=L^{1/\gamma}$ it multiplies V by $k^{1/\gamma}$. For an arbitrary transfer function, decode, scale, and encode. Output clipping is a separate operation.

fig-point-op-exposure-spaces
Figure 8.3.3. Why the space matters for exposure. The same "+1 stop" done two ways. Top, the correct way, multiply in linear light (decode → ×2 → re-encode): tones brighten smoothly and in proportion. Bottom, the naive way, multiply the gamma/sRGB numbers directly: the curve is a straight ramp of slope 2 that clips everything above input ½ to white, blowing out the entire upper half of the tonal range. Same intended edit, very different pixels; exposure belongs in linear light.

Doing it in the encoded domain, correctly. Sometimes you cannot leave the encoded values (an 8-bit buffer, a shader handed display-referred pixels), so it pays to know exactly what a stop costs there. For a pure gamma curve $V = L^{1/\gamma}$ the decode-scale-encode round trip collapses to a single multiply,

$$ V_\text{out} = \big(2^{s}\,V_\text{in}^{\gamma}\big)^{1/\gamma} = 2^{s/\gamma}\,V_\text{in}, $$

so an $s$-stop change on gamma-encoded values is just multiplication by $2^{s/\gamma}$ (at $\gamma=2.2$ a stop is $\times 2^{1/2.2}\approx 1.37$, not $\times 2$). The instant the transfer function is not a pure power the shortcut breaks: real sRGB carries a linear toe and an offset power segment, so there is no closed-form multiply and you must decode, scale, and re-encode,

$$ V_\text{out} = \mathrm{OETF}_\text{sRGB}\!\big(2^{s}\cdot\mathrm{EOTF}_\text{sRGB}(V_\text{in})\big). $$

The examples below accept input values in $[0,1]$. Positive exposure adjustments can produce outputs above 1; the code preserves them instead of clipping them:

Listing 8.3.1 · exposure on gamma-encoded valuesembed
def expose_gamma(V, stops, gamma=2.2):      # pure-gamma shortcut: one multiply
    return (2.0 ** (stops / gamma)) * V     # == ((2**stops) * V**gamma) ** (1/gamma)

def expose_srgb(V, stops):                  # real sRGB: decode, scale, re-encode
    lin = V / 12.92 if V <= 0.04045 else ((V + 0.055) / 1.055) ** 2.4
    lin *= 2.0 ** stops
    return 12.92 * lin if lin <= 0.0031308 else 1.055 * lin ** (1 / 2.4) - 0.055

Use the power-law shortcut only when that is the actual transfer function. Use the piecewise sRGB formula for sRGB, and the specified inverse/forward functions for other encodings.

8.3.4 Brightness vs. exposure⧉

Here brightness means a simple additive offset in a stated working space. Commercial sliders with that name may use more complicated curves. Our teaching version is

$$ L_\text{out} = L_\text{in} + b, $$

This shifts the curve upward by b and maps zero to b. Use an offset to change the black point deliberately; use exposure scaling when the intended change is multiplicative. Neither operation can decide which tone should appear black without information about the scene or a rendering choice.

fig-point-op-brightness-vs-exposure
Figure 8.3.4. Brightness (additive) versus exposure (multiplicative). Left, the input. Center, two outputs matched to lift the midtones by the same amount: brightness $L+b$ raises the blacks off zero, fogging the shadows into flat gray, while exposure $2^{s} L$ keeps black at black and scales in proportion. Right, both transfer curves: brightness is the diagonal shifted straight up (a parallel line, intercept b); exposure passes through the origin and fans upward. Only the multiplicative curve preserves true black.

The visible difference is easiest to see at black. Multiplication leaves zero unchanged; a positive offset raises it to b. A dark scene can therefore acquire a gray floor under this simple brightness operation. That can be a deliberate look, but it is a poor substitute for exposure scaling when the aim is to simulate a change in collected light.

8.3.5 Contrast: steepen about a pivot⧉

Where exposure moves all the tones the same way, contrast spreads them apart or pulls them together. The idea is to pick a pivot value, a mid-gray that stays put, and push every other value away from it (more contrast) or toward it (less). Values above the pivot get brighter, values below get darker, and the pivot itself never moves. As a curve, that is a straight line through the pivot whose slope is the contrast gain g:

$$ L_\text{out} = g\,(L_\text{in} - p) + p, $$

where p is the pivot. Read it as: measure how far a value sits from the pivot, scale that distance by the gain, add it back. A gain above 1 steepens the line and raises contrast; a gain below 1 flattens it and lowers contrast. The steeper the line, the sooner it slams into the floor at 0 and the ceiling at 1, so strong contrast clips both shadows and highlights (Figure 8.3.5).

💡 Big lesson (L8.4) — contrast is a pivot: choose what stays put

A contrast change rotates values about a chosen pivot: $L_\text{out} = p + g\,(L_\text{in}-p)$. The pivot $p$ is exactly what is left unchanged, and it also decides whether the picture gets brighter or darker overall: a high pivot pulls most of the image down (darkening), a low pivot pushes it up (brightening). Unlike exposure, contrast is not radiometrically meaningful, it is a stylistic stretch, so the encoding matters less (no single space is "correct"); what matters is a sensible pivot, and the sensible pivot does depend on the space: roughly 18% mid-gray in linear light, the numeric midpoint in gamma or log. Pick the pivot deliberately: it is the whole character of the operation.

fig-point-op-contrast
Figure 8.3.5. Contrast as a steepening about a pivot. Left, the input; center, increased contrast (gain > 1): shadows deepen, highlights brighten, midtones near the pivot barely move; right, the transfer curve, a straight line through the mid-gray pivot p whose slope is the gain. Steepening drives the line into the floor (0) and ceiling (1), clipping the darkest and lightest tones. The 45° diagonal (gain 1, no change) is shown for reference.

The pivot and the domain both affect contrast. A linear-domain adjustment preserves its chosen linear pivot; a log-domain scale changes luminance ratios by a power. An encoded-domain adjustment acts on displayed code differences. Choose the appearance you want and inspect the clipping at both ends (Figure 8.3.6). On the photographs in the figure, the three give recognizably different looks. Steepening the encoded values, what most editors do, pivots about a perceptual mid-gray and roughly tracks how the eye judges contrast. Steepening linear values weights the highlights more and, on these images, reads harsher, with shadows crushed faster. Steepening log values stretches ratios uniformly and reads gentlest across a wide range. None is wrong; they are three looks, and an editor can offer the choice.

fig-point-op-contrast-spaces
Figure 8.3.6. The same contrast boost in three spaces. The identical gain about a mid-gray pivot, applied in sRGB/gamma (perceptual mid-gray, the usual editor behavior), in linear light (acting on physical radiance, harsher in the highlights, crushes the shadows), and in log/density (a uniform stretch in ratios, gentlest across a wide range). For the log version the black is clamped to a small ε to avoid log(0). Each panel shows the output and its transfer curve; the look differs because the pivot and the spacing of tones differ between spaces.

8.3.6 Black point, white point, and levels⧉

Levels maps chosen input endpoints a and b to output black and white. For $b>a$, a simple version is

$$u=\operatorname{clip}\!\left(\frac{L-a}{b-a},0,1\right),\qquad L_\mathrm{out}=u^{1/\gamma_m},\quad\gamma_m>0.$$

The midpoint parameter $\gamma_m$ changes the interior without moving the endpoints. Handle coincident endpoints explicitly. Filling the whole output range is a choice: a foggy or low-contrast scene need not contain a true black or white.

Subtracting a calibrated additive light offset is meaningful in linear light. General haze also attenuates scene light and varies with depth, so an arbitrary black-point adjustment is not a complete haze inversion. A histogram minimum cannot identify the offset without further assumptions; see Chapter 8.2, What the numbers mean.

A display-space levels adjustment is an appearance operation. A sensor black-level correction is calibration in raw units. Keep them separate even if both lower dark values.

Use the histogram to see which values an endpoint choice will clip, then judge the image. Small percentile trims can avoid letting a single outlier set the range, but they still intentionally discard extremes (Figure 8.3.7).

fig-point-op-levels
Figure 8.3.7. Levels on a real photo. Left, the input, a flat, hazy coastal scene whose tones are bunched in the middle of the range, with no true black and a dull off-white that never reaches 1 (its luma histogram, below, is a narrow hump bounded by the black- and white-point markers). Center, after levels: the used range $[\text{black}, \text{white}]$ has been stretched out to fill $[0, 1]$, so the histogram spreads to both edges and the image gains contrast and snap. Right, the transfer curve: flat (clipped) below the black point, flat (clipped) at 1 above the white point, and a straight stretch between the two anchors (a mid-gray gamma would bow this middle segment).

An additive pedestal can explain a washed-out image. Add the same positive value to a dark patch and a bright patch: their difference stays the same, but their ratio moves closer to one. Subtracting a known pedestal in linear light reverses that part of the change. Choosing a black point by eye is less specific; it can improve a rendering without estimating the physical stray light. If the scene really has no black object, forcing its minimum to zero removes valid signal. Haze adds the further complication of depth-dependent attenuation as well as scattered light.

8.3.7 The general tone curve⧉

Exposure, contrast, and levels are all constrained shapes: a fan, a straight steepen, a clipped stretch. The most general point operation simply lets you draw the curve freehand: the tone curve tool gives you a handful of control points to drag wherever you please, and the editor threads a smooth interpolating curve through them (Figure 8.3.8). A monotone curve, one that never decreases, preserves the ordering of tones. A non-monotone curve deliberately reverses that ordering over part of its range, as in solarization effects. Catmull–Rom interpolation (Catmull & Rom 1974 (interpolating splines)) can overshoot even when the control points are ordered. Use a monotonicity-preserving interpolant when tone reversals must be excluded.

fig-tone-curve-spline
Figure 8.3.8. The general tone curve. A few draggable control points (dots) are interpolated by a smooth monotone spline (solid) into an arbitrary transfer curve; the 45° identity is shown dashed. Because the spline is kept monotone (never decreasing), a brighter input never maps to a darker output, so tones cannot invert. This single freehand curve subsumes the exposure bow, the contrast steepen, and the levels stretch of the earlier figures.

A freehand tone curve subsumes everything earlier in the chapter (you could draw the exposure scale, the contrast steepen, or the levels stretch by hand) which is why it is the photographer's most general tonal tool. The tone-mapping chapter pushes the idea to its physical conclusion with principled, automatically chosen curves (the Reinhard operator $L/(1+L)$ and relatives, Reinhard et al. 2002) for compressing high-dynamic-range scenes; here we simply note that the general curve is the container they all fit into.

8.3.8 Lookup tables⧉

Once a point operation is any fixed curve f, there is a cheap trick that applies to all of them at once. If pixel values come from a small finite set, the 256 levels of an 8-bit channel, say, there is no need to evaluate f at every pixel. Evaluate it once per possible value, store the 256 answers in a small table, and then for each pixel just look up its result. This precomputed table is a look-up table (LUT), and it turns an arbitrarily complicated tone curve into a single array index per pixel, the same cost whether f is a gamma curve, an S-curve, or a hand-drawn spline.

Independent per-channel one-dimensional LUTs represent separate R, G, and B curves, including diagonal white-balance gains in the appropriate linear space. They cannot represent a general color matrix or saturation adjustment, which mixes channels. A three-dimensional LUT maps an RGB triple to another triple and interpolates between a finite grid of samples. Its domain, interpolation, out-of-range behavior, and input/output encodings are part of the specification. A LUT stores an approximation to a mapping, not an invertibility guarantee.

For a 1-D table with 256 entries, one channel value chooses a position on one axis. A 3-D table with 33 entries per axis has 35,937 grid points, each holding an output RGB triple. Interpolation among nearby grid points approximates a function of all three input channels. That additional dependence lets a 3-D table express a saturation change that a separate curve on each channel cannot in general represent. Diagonal white-balance gains, however, already fit in three independent 1-D curves.

Both tables encode fixed mappings. If an adjustment depends on a neighborhood, a face mask, or statistics recomputed for each image, those dependencies must be supplied outside the table or used to choose its contents. Precomputing a color mapping does not remove its input-domain requirements.

8.3.9 Local point operations⧉

A fixed mask m allows a spatially varying point adjustment:

$$I_\mathrm{out}=(1-m)I+m\,f(I),\qquad 0\le m\le1.$$

This blends the original and adjusted values in the specified working space. For exposure, blending the two outputs gives a factor $1-m+m2^s$; scaling the stops by the mask instead gives $2^{ms}$. They agree at mask values zero and one but differ in between. A mask inferred from neighboring pixels makes the complete algorithm nonlocal, even though applying the finished mask is pointwise.

The connection to the darkroom is exact (Figure 8.3.10). Wherever the mask is bright, the adjustment acts; where it is dark, the pixel is untouched. Run a local exposure through a mask and you have reinvented dodging (locally brightening, holding light back in the printing easel) and burning (locally darkening, giving extra exposure): the only difference is that the mask is now a soft digital matte instead of a piece of cardboard waved over the paper. A radial gradient lifts a face out of the shadows; a linear gradient is the graduated-ND filter that holds back a bright sky; a painted brush burns down one distracting highlight. Everything earlier in the chapter, exposure, contrast, the tone curve, becomes a local tool the moment it is gated by a mask.

fig-local-point-ops
Figure 8.3.10. Local (masked) point operations: digital dodging and burning. The same point operation (here exposure) is applied only where a mask allows. Radial gradient: dodge (brighten) the center, lifting the subject. Linear gradient: burn the top, the digital graduated-ND that tames a bright sky. Paintbrush: burn a chosen region. The lower row shows each mask (white = full effect). Interactive: pick the mask shape and the adjustment (exposure for dodge/burn, or contrast), set the amount, and drag the mask's handle, or in brush mode paint directly on the image.

In CompPhotoRoom

The Exposure, Contrast, Whites and Blacks sliders and the point-curve editor of Chapter 8.13, A raw developer in the browser are this chapter's operations, run live: consecutive per-channel curves compose into one 4096-entry lookup table per channel, the LUT move above doing real work, and the ? on each slider shows the exact code beside this chapter's pseudocode.

Selections can come from a value or color threshold, a painted brush, a gradient, an edge-aware tool, or semantic segmentation. These choices change what the complete operation depends on. An edge-aware selection uses image structure to keep an adjustment within an object; its use of color similarity connects it to the affinities used by the bilateral filter. The finished mask can still be applied with the pointwise blend above.

Recap: big lessons of this chapter

💡 Big lesson (L4.6) — additive vs. multiplicative → choice of encoding

Exposure multiplies linear-light values; independent contributions of light add. Log coordinates turn exposure ratios into differences. An offset in encoded values is a distinct appearance control. Choose the domain from the operation and its model.

💡 Big lesson (L8.3) — exposure scales linear light (a multiply) or shifts log values (an add)

Exposure scaling multiplies linear-light values by $k=2^s$ or adds $s$ in base-2 log units. For a pure gamma encoding $V=L^{1/\gamma}$ it multiplies V by $k^{1/\gamma}$. For an arbitrary transfer function, decode, scale, and encode. Output clipping is a separate operation.

💡 Big lesson (L8.4) — contrast is a pivot: choose what stays put

A contrast change rotates values about a chosen pivot: $L_\text{out} = p + g\,(L_\text{in}-p)$. The pivot $p$ is exactly what is left unchanged, and it also decides whether the picture gets brighter or darker overall: a high pivot pulls most of the image down (darkening), a low pivot pushes it up (brightening). Unlike exposure, contrast is not radiometrically meaningful, it is a stylistic stretch, so the encoding matters less (no single space is "correct"); what matters is a sensible pivot, and the sensible pivot does depend on the space: roughly 18% mid-gray in linear light, the numeric midpoint in gamma or log. Pick the pivot deliberately: it is the whole character of the operation.

fig-point-op-contrast
Figure 8.3.5 (reproduced). Contrast as a steepening about a pivot. Left, the input; center, increased contrast (gain > 1): shadows deepen, highlights brighten, midtones near the pivot barely move; right, the transfer curve, a straight line through the mid-gray pivot p whose slope is the gain. Steepening drives the line into the floor (0) and ceiling (1), clipping the darkest and lightest tones. The 45° diagonal (gain 1, no change) is shown for reference.