7.19. Tonal corrections¶
Tonal corrections change how brightness and colour are distributed in a captured image – the fixes an application applies when a frame is too dark, too bright, too flat, or skewed toward the wrong colour.
The corrections belong to two families:
brightness-and-contrast adjustments that
redistribute brightness, and colour
adjustments that change which colour each
pixel reads as. Both have analogues in the
sensor’s ISP,
which corrects each frame on its way in; the
methods here apply to an already-captured
Image, after the fact, for the cases
where the frame needs more correction than the
ISP provided.
7.19.1. Histogram equalisation¶
The simplest contrast-stretching operation is
histogram equalisation. The idea is to
remap pixel values so the histogram of the
output is as flat as possible – every value
appears roughly equally often. The visual
effect is that a low-contrast image (one
whose histogram is concentrated in a narrow
band) becomes a high-contrast one whose
pixels cover the full 0 – 255 range.
histeq() runs the
equalisation:
img.histeq()
The mechanic is direct. The cumulative distribution function (CDF) of the source’s histogram is computed; each input pixel value is mapped to its position in the CDF, scaled to the output range. Where pixels were already evenly spread, the mapping is close to the identity; where pixels were piled up at one brightness, the mapping spreads them out by stretching that brightness across a wider range of output values.
The result is dramatic on low-contrast scenes – the difference between a dim indoor photograph and the same photograph after histeq is often the difference between “unreadable” and “perfectly legible.” The trade-off is that the operation amplifies everything, including sensor noise. On a scene with real low-contrast detail to recover, histeq is the right answer; on a clean, well-exposed scene that simply does not need it, histeq introduces visible noise.
7.19.2. CLAHE: adaptive equalisation¶
Histogram equalisation is global: it uses one CDF computed from the whole image and applies it everywhere. That works on images whose brightness range is roughly uniform, but fails on scenes with localised dark and bright regions – the CDF gets pulled toward the side that has more pixels, and the opposite side gets overcorrected.
The adaptive variant is Contrast Limited Adaptive Histogram Equalisation, commonly referred to as CLAHE. Instead of one global CDF, CLAHE computes a separate CDF for every small tile of the image, equalises each tile against its own CDF, and blends the tile boundaries together. The result is that brightness adjustments happen locally – the shadowed corner gets its own equalisation without the bright corner pulling it the wrong way.
The adaptive=True flag switches
histeq() into CLAHE mode:
img.histeq(adaptive=True, clip_limit=10)
The clip_limit parameter is the part of
CLAHE that the “contrast limited” in the name
refers to. Local equalisation can over-amplify
noise in flat regions where the CDF has very
few distinct values; the clip limit caps how
aggressively any single bin can be
redistributed, which prevents the noise
amplification while still allowing the
contrast stretching where it matters. A value
around 10 is a reasonable starting point;
larger values let CLAHE work harder at the
cost of more visible noise, smaller values
make it gentler.
CLAHE is more expensive than the global histeq, but produces cleaner results on scenes where the brightness is unevenly distributed – which is most real-world scenes.
7.19.3. Gamma, contrast, and brightness¶
Histogram equalisation is the data-driven
way to remap brightness. The
data-independent way is to apply a chosen
curve, parameterised by a few easy-to-tune
knobs. gamma() provides
three:
img.gamma(gamma=1.0, contrast=1.0, brightness=0.0)
Each parameter applies one specific transformation to every pixel:
gamma runs each pixel’s value through the
power function output = input ** (1 / gamma).
The standard meaning: values larger than 1.0
brighten the image and lift midtones (the
classical “monitor gamma” correction); values
smaller than 1.0 darken it. The
parameter is non-linear – it preserves the
black and white points and only reshapes the
distribution in between, which is the right
behaviour when the goal is to recover detail
in shadow or highlight regions without
crushing the existing extremes.
contrast runs each pixel through a
straight multiplication around the mid-grey
point. Values larger than 1.0 increase
contrast (dark gets darker, bright gets
brighter, mid-grey stays the same); values
smaller than 1.0 reduce contrast.
brightness adds a constant to every
pixel value. Positive values brighten,
negative values darken. The shift is
uniform – nothing is preserved – which
is rarely what an application wants by
itself, but pairs well with a contrast pass
to recentre the result.
The three parameters compose: a single
gamma() call can apply a
gamma curve, then a contrast multiplication,
then a brightness shift, all in one pass.
The order is gamma first, then contrast,
then brightness, which matches the order
that gives the most intuitive results when
all three are non-default.
7.19.4. Auto white balance¶
The colour family of tonal corrections starts
with auto white balance. The same mechanism
the sensor’s ISP runs as part of the imaging
pipeline – adjusting the relative gains on
the red, green, and blue channels so that an
average grey-coloured patch reads as actual
grey – is also available as a post-capture
operation on a finished Image:
img.awb()
The default uses the gray-world algorithm:
the average colour of the whole image is
assumed to be neutral grey, and the
per-channel gains are adjusted to make it so.
The alternative max=True form uses the
white-patch algorithm: the brightest pixels
are assumed to be neutral white, and the
gains are adjusted to make them so. Both work
on RGB565 and on raw Bayer; neither works on
grayscale (where there is no colour to
balance) or YUV (where the colour
representation is not what these algorithms
operate on).
When to reach for the post-capture form rather than the ISP’s auto white balance: when the ISP’s choice was a poor match for the particular scene, when the application is loading reference frames from disk that were captured under different conditions, or when the colour judgement matters enough that the application wants to re-run it with its own choice of algorithm.
7.19.5. The colour correction matrix¶
When the colour correction the image needs is
not the per-channel scaling that white balance
gives, but a more general
channel-mixing, the operation to reach for
is the colour correction matrix. The
ccm() method applies a
3-by-3 (or 3-by-4 with offset) matrix that
multiplies every pixel’s (r, g, b) vector
to produce a new (r, g, b) vector:
img.ccm([[1.1, -0.05, -0.05],
[-0.05, 1.1, -0.05],
[-0.05, -0.05, 1.1]])
The matrix lets the application correct crosstalk between the colour channels – where the red sensor’s response includes some green light, for example, the matrix can subtract a fraction of the green channel from the red output to compensate. Combined with a per-channel offset, the 3-by-4 form lets the application also re-zero each channel.
The ISP pipeline
material covers the why of colour correction
matrices. The post-capture form on the
Image is just the same operation,
applied after the fact.