Image Processing & Computational Photography
Gamma Correction
The nonlinear transfer functions that map light to stored code values and code values back to light, such as sRGB, BT.709 and BT.1886, and why they decide how many visible steps an 8-bit image has.
beginner
Gamma correction is the nonlinear mapping between light and the numbers that represent it. A camera or renderer encodes linear light with a transfer function that is roughly a power law with an exponent near 1/2.2. It stores the result as 8- or 10-bit codes, and a display turns the codes back into light with a roughly inverse power law. The color spaces article gives the sRGB formula and shows why arithmetic on encoded values is not arithmetic on light. This article treats transfer functions as encodings: the standard curves, the reason they exist, and how they spend a fixed number of codes across the range from black to white. That last point decides whether an 8-bit image shows visible steps in dark gradients.
Definition
A transfer function relates a physical quantity of light, such as luminance or a linear RGB component, to a signal value. Video standards distinguish two directions:
- The opto-electronic transfer function (OETF) maps scene light to a signal at the source. ITU-R BT.709 specifies one for HDTV cameras.
- The electro-optical transfer function (EOTF) maps a signal to light at the display. ITU-R BT.1886 specifies the reference EOTF for flat-panel studio displays.
Gamma is the exponent of the power law that approximates either curve. Poynton’s Frequently Asked Questions about Gamma (1998, §5) gives the colloquial meaning: the exponent of a CRT’s power-law response of intensity to voltage. Strictly, he applies the term to the exponent at the monitor (§16). As of November 2026, the FAQ page of his website describes the document as about thirty years old and in need of an update; it is cited here for the CRT-era reasoning that the later standards record. Gamma correction is applying the compensating nonlinearity to the signal, which video does at the camera (§6). In image processing the same name is also used for any power-law point operation. scikit-image’s adjust_gamma, for example, says it “Performs Gamma Correction” by computing on values scaled to (scikit-image 0.25.2, exposure.py). That use is a tone adjustment, covered under point operations, not an encoding.
Intuition
There are two reasons to encode light nonlinearly, and historically they happened to coincide.
The first is the display. Poynton (1998, §5) states that a conventional CRT produces light approximately proportional to the applied voltage raised to the power 2.5. A signal proportional to scene light would therefore come out with its midtones far too dark, so the signal was pre-distorted at the camera with roughly the inverse power.
The second is perception, which matters even without a CRT. Vision responds to luminance nonlinearly. Poynton (§4) gives the example that a patch with 18% of a reference luminance looks about half as bright, and that two patches can be told apart when their intensities differ by about one percent. With only 256 codes available, a code step of a fixed size in light is far too coarse near black, where one percent of the light is tiny, and needlessly fine near white. Spacing the codes more evenly in perceived lightness uses them better. Poynton (§5) calls it “an amazing coincidence” that the CRT’s nonlinearity is close to the inverse of this lightness response: a signal coded for perception is decoded correctly by the CRT.
Flat panels have no electron gun, but the encoding stayed. BT.1886 (2011) records the reason on the display side: for consistent pictures it is desirable that newer display technologies have an EOTF that closely matches the CRT’s. The perceptual argument does not depend on the display at all.
Formal Definition
Power laws
The simplest encoding of a normalized linear value is a pure power law, and the display inverts it:
Stored with bits, the code is . Decoding gives the luminance of code , . Linear coding is the case . An encoding power law with has infinite slope at black. Poynton (1998, §6) explains the linear segment near black of the Rec. 709 curve as minimizing the effect of sensor noise in practical cameras and scanners, and sRGB has a linear segment too.
sRGB
The sRGB curve of IEC 61966-2-1, written out in the color spaces article, encodes with a linear segment of slope 12.92 below and with above it. Its decoding threshold is . Color Spaces notes that the curve approximates a pure power law with exponent about 2.2. The offset and the linear segment make it depart from that power law near black, as the example measures.
BT.709 and BT.1886
BT.709-6 (item 1.2) specifies the overall opto-electronic transfer characteristic at the source as
where is the luminance of the image, normalized to , and the corresponding electrical signal. Item 3.1 calls this a “conceptual non-linear pre-correction” with . A note to item 1.2 adds that in production practice the source encoding is adjusted until the picture looks right on a reference monitor with the BT.1886 decoding function. In our reading, the OETF is therefore a nominal reference, not a measurement rule. BT.709 also fixes the digital form. Each of , , and is quantized as (item 3.4; the colour-difference signals use 224 and 128), so 8-bit black and nominal peak sit at codes 16 and 235 (item 4.6). BT.709 calls , its weighted sum of nonlinear , and , the “luminance signal”. The color spaces article and Poynton (1998, §3, §11) call this quantity luma and keep “luminance” for the linear-light quantity.
For decades no standard defined the display side. BT.1886 (Appendix 2) states that the CRT’s EOTF was never documented, because all CRTs behaved similarly, and that a reference became necessary once other display technologies replaced CRTs. Its reference EOTF (Annex 1) is
where is screen luminance in cd/m², and are the screen luminances for white and black, and is the normalized signal. For BT.709 10-bit content, for code . The constants make give and give . In the standard’s terms, is the legacy “contrast” control (user gain) and the legacy “brightness” control (black-level lift). A footnote states that has been shown to be a satisfactory match to the legacy CRT.
Lightness
To judge how visible a code step is, the example uses CIE lightness . Poynton (1998, §4) describes as a modified cube root of relative luminance, with a linear segment of slope 903.3 near black (below ) and a range of 0 to 100. He takes a of one to be roughly the threshold of visibility. scikit-image computes it with the same threshold and an equivalent slope ():
for relative to white (xyz2lab in scikit-image 0.25.2). The CIE’s own definition is in CIE 015, not read here.
Properties
Codes per stop. A stop is a factor of two in light. For a pure power encoding, the stop from to spans the signal interval . It follows that each stop down receives a factor of the codes of the stop above: 0.73 for , against 0.5 for linear coding. Linear 8-bit coding spends 128 of its 256 codes on the brightest stop and leaves a single code for the eighth.
Step size per code. Between codes and , the relative change in light is for linear coding and for a pure power law. The power law’s relative step is larger at a given code, but the codes sit much lower in light. Over the cube-root part of , . So a linear 8-bit step of is worth about in , which exceeds one below , about code 15.
How many bits. Poynton (1998, §13) works the question with the 1% criterion over a 100:1 range of intensities. Linear coding must keep a step of 0.01 all the way up from 1 to 100, which takes about 9,900 codes, or about 14 bits. Coding in which each step is a constant ratio takes about 460 codes, or about 9 bits. He adds that 8 bits coded nonlinearly according to Rec. 709 suffice for broadcast-quality television at a contrast ratio of about 50:1, and that in an 8-bit linear system the 1% limit takes effect below code 100. The example below counts these steps under both visibility models.
End-to-end response. The display’s EOTF is not meant to be the exact inverse of the camera’s OETF. Poynton (1998, §7) explains that pictures reproduced at their physical intensity in a dim surround look lacking in contrast. An end-to-end power of about 1.1 or 1.2 compensates, and video builds this in by under-correcting at the camera, with an exponent of about 1/2.2 against the CRT’s 2.5. With BT.709 into BT.1886 the nominal exponents multiply to . The example shows that the BT.709 offset makes the effective exponent larger than that.
Linear and nonlinear processing. Poynton (1998, §19) separates the two domains by purpose. Simulating physics, such as a lens, needs linear-light values, so camera gamma must first be removed. A computation that should minimize the visibility of errors, such as the DCT quantization in JPEG, belongs in a perceptually uniform, nonlinear coding; he notes that applying JPEG or MPEG to linear-light data gives unacceptable results. Color Spaces makes the same point for averaging, blurring and resizing.
Exactly once. If an image originates in linear light, gamma correction must be applied exactly once (Poynton, 1998, §9). Without it the midtones are reproduced too dark, and applied twice they are too light. In code, the same error appears as decoding an image that is already linear, or encoding one twice before saving (our illustration).
Examples
What a code step is worth
The example measures how four encodings spend their codes:
- linear 8-bit;
- sRGB 8-bit, with the constants Color Spaces gives;
- a pure power law with in 8 bits;
- linear 12-bit.
Every encoding is applied as storage would apply it: encode, round to the nearest integer code, decode. Visibility is judged by the two thresholds Poynton gives: a step of one in , and a luminance ratio of 1%. The sources do not settle which one applies, so both are printed. is taken relative to display white at , with no black level or flare.
The script has four parts:
- Every code step. It counts the steps that exceed each threshold and prints, at six luminances, the width of the code interval that contains them.
- Codes per stop. It counts the codes in each of the ten stops below white.
- Two ramps. It stores and decodes two synthetic grey images whose linear luminance is known exactly: a dark ramp from to 0.05 (64 × 1024 pixels) and a full ramp from 0 to 1 (64 × 4001 pixels). The full ramp’s width was chosen at planning so that its samples do not coincide with the 12-bit codes, which would have made the 12-bit round trip exact. On each image it measures the number of distinct codes, the largest jump between neighbouring pixels (banding), and the largest and RMS error against the true ramp.
- Camera into display. It passes scene luminances through the BT.709 OETF and the BT.1886 EOTF with cd/m², and or cd/m². BT.1886’s Appendix 1 names 0.1 cd/m² as a moderate black level. It reports the effective end-to-end exponent .
The checks come first:
- the ramps are strictly increasing, with identical rows and the stated ends;
- the function equals scikit-image’s
xyz2labon greys; - every encoding round-trips without rounding to within ;
- no code is more than half a step from its exact value;
- the BT.1886 EOTF reaches and at its ends.
A further printed check (the last column of the ramp table) confirms a bound that follows from rounding. A value between codes and is stored as one of them, so its error cannot exceed that step. The script uses NumPy 2.3.5 and scikit-image 0.25.2 and runs in well under a second. Two runs printed identical output.
import os
os.environ["OMP_NUM_THREADS"] = "1" # one BLAS thread, set before NumPy loads
os.environ["OPENBLAS_NUM_THREADS"] = "1"
os.environ["MKL_NUM_THREADS"] = "1"
import numpy as np
from skimage.color import xyz2lab
# --- Transfer functions: relative luminance Y in [0, 1] <-> signal V in [0, 1] ---
def srgb_encode(Y):
return np.where(Y <= 0.0031308, 12.92 * Y, 1.055 * Y ** (1 / 2.4) - 0.055)
def srgb_decode(V):
return np.where(V <= 0.04045, V / 12.92, ((V + 0.055) / 1.055) ** 2.4)
ENCODINGS = { # name: (encode, decode, bits)
"linear 8-bit": (lambda Y: Y, lambda V: V, 8),
"sRGB 8-bit": (srgb_encode, srgb_decode, 8),
"power 2.2 8-bit": (lambda Y: Y ** (1 / 2.2), lambda V: V ** 2.2, 8),
"linear 12-bit": (lambda Y: Y, lambda V: V, 12),
}
def lstar(Y):
"""CIE L* of relative luminance Y (white = 1), constants as in skimage.color.xyz2lab."""
Y = np.asarray(Y, dtype=np.float64)
f = np.where(Y > 0.008856, np.cbrt(Y), 7.787 * Y + 16 / 116)
return 116 * f - 16
def quantize(Y, enc, dec, bits):
"""Encode, round to the nearest integer code, decode: what storing the value does."""
top = 2 ** bits - 1
code = np.round(enc(Y) * top)
return dec(code / top), code
# --- Synthetic images with known linear luminance: 64-row horizontal ramps ---
dark_ramp = np.tile(np.linspace(0.0, 0.05, 1024), (64, 1)) # black to Y = 0.05 (L* 26.7)
full_ramp = np.tile(np.linspace(0.0, 1.0, 4001), (64, 1)) # black to white
# --- Checks on exact data ---
print("checks")
print(f" ramps: monotone {bool(np.all(np.diff(dark_ramp[0]) > 0) and np.all(np.diff(full_ramp[0]) > 0))}, "
f"rows identical {bool(np.all(dark_ramp == dark_ramp[0]) and np.all(full_ramp == full_ramp[0]))}, "
f"ends {dark_ramp.min():.2f}-{dark_ramp.max():.2f} and {full_ramp.min():.2f}-{full_ramp.max():.2f}")
grey_xyz = full_ramp[0][:, None] * np.array([0.95047, 1.0, 1.08883]) # greys under D65
print(f" max |L* - skimage xyz2lab L*| on greys: {np.abs(lstar(full_ramp[0]) - xyz2lab(grey_xyz)[:, 0]).max():.1e}")
for name, (enc, dec, bits) in ENCODINGS.items():
Y = full_ramp[0]
exact = np.abs(dec(enc(Y)) - Y).max() # no rounding
v = enc(Y) * (2 ** bits - 1)
_, code = quantize(Y, enc, dec, bits)
print(f" {name:16s} unrounded round trip max |dY| {exact:.1e}; "
f"max |code - exact| {np.abs(code - v).max():.4f}")
# --- Part 1: what one code step is worth, across the whole range ---
print("\nper-code steps (all codes)")
print(f" {'encoding':16s} {'max dL*':>8s} {'at Y':>8s} {'steps dL*>1':>12s} {'steps dY/Y>1%':>14s} {'of':>5s}")
steps = {}
for name, (enc, dec, bits) in ENCODINGS.items():
top = 2 ** bits - 1
Yk = dec(np.arange(top + 1) / top) # luminance of every code
dL = np.diff(lstar(Yk))
weber = np.diff(Yk)[1:] / Yk[1:-1] # relative step above code k >= 1
steps[name] = (Yk, dL)
i = int(np.argmax(dL))
print(f" {name:16s} {dL.max():8.3f} {Yk[i]:8.5f} {int((dL > 1).sum()):12d} "
f"{int((np.round(weber, 12) > 0.01).sum()):14d} {weber.size:5d}")
print("\nthe code step containing a given luminance y: (Y[k+1] - Y[k]) / y in % | dL*")
levels = [0.001, 0.01, 0.05, 0.18, 0.5, 0.9]
print(f" {'encoding':16s}" + "".join(f"{'Y=' + str(y):>17s}" for y in levels))
for name, (Yk, dL) in steps.items():
cells = []
for y in levels:
k = int(np.searchsorted(Yk, y, side="right") - 1) # code just at or below y
cells.append(f"{100 * (Yk[k + 1] - Yk[k]) / y:8.2f} | {dL[k]:5.3f}")
print(f" {name:16s}" + "".join(f"{c:>17s}" for c in cells))
# --- Part 2: codes spent in each stop (factor of two in luminance) below white ---
print("\ncodes per stop below white (stop s holds 2^-s <= Y < 2^-(s-1); stop 1 includes white)")
print(f" {'encoding':16s}" + "".join(f"{s:>6d}" for s in range(1, 11)) + f"{'below':>7s}")
for name, (Yk, _) in steps.items():
pos = Yk[Yk > 0]
s = np.floor(-np.log2(pos)).astype(int) + 1 # Y = 1 falls in stop 1
counts = [int((s == j).sum()) for j in range(1, 11)]
below = int((s > 10).sum())
print(f" {name:16s}" + "".join(f"{c:6d}" for c in counts) + f"{below:7d}")
# --- Part 3: store the two ramp images and measure banding and round-trip error ---
print("\nramp images, stored and decoded: L* error against the true ramp")
print(f" {'image':5s} {'encoding':16s} {'levels':>7s} {'max jump dL*':>13s} {'max |dL*|':>10s} {'rms dL*':>8s} {'<= step':>8s}")
for label, img in (("dark", dark_ramp), ("full", full_ramp)):
for name, (enc, dec, bits) in ENCODINGS.items():
out, code = quantize(img, enc, dec, bits)
L_true, L_out = lstar(img), lstar(out)
jump = np.abs(np.diff(L_out, axis=1)).max() # largest step between neighbours
err = L_out - L_true
bound = steps[name][1][: int(code.max()) + 1].max() # largest step among the codes used
print(f" {label:5s} {name:16s} {np.unique(code).size:7d} {jump:13.3f} "
f"{np.abs(err).max():10.3f} {np.sqrt((err ** 2).mean()):8.3f} "
f"{str(bool(np.abs(err).max() <= bound + 1e-9)):>8s}")
# --- Part 4: camera curve into display curve (BT.709 OETF, then BT.1886 EOTF) ---
def bt709_oetf(L):
return np.where(L < 0.018, 4.5 * L, 1.099 * L ** 0.45 - 0.099)
def bt1886_eotf(V, Lw=100.0, Lb=0.0, g=2.4):
a = (Lw ** (1 / g) - Lb ** (1 / g)) ** g
b = Lb ** (1 / g) / (Lw ** (1 / g) - Lb ** (1 / g))
return a * np.maximum(V + b, 0) ** g
print("\nscene luminance -> BT.709 signal -> BT.1886 display (Lw = 100 cd/m2), relative to white")
ends = [bt1886_eotf(0.0) - 0.0, bt1886_eotf(1.0) - 100.0,
bt1886_eotf(0.0, Lb=0.1) - 0.1, bt1886_eotf(1.0, Lb=0.1) - 100.0]
print(f" EOTF(0) - Lb and EOTF(1) - Lw, cd/m2: largest |error| {max(abs(e) for e in ends):.1e}")
print(f" {'scene':>7s} {'signal':>7s} {'Lb=0':>8s} {'exponent':>9s} {'Lb=0.1':>8s} {'exponent':>9s}")
for L in (0.01, 0.05, 0.18, 0.5, 0.9):
V = bt709_oetf(L)
d0, d1 = bt1886_eotf(V) / 100, bt1886_eotf(V, Lb=0.1) / 100
print(f" {L:7.2f} {V:7.4f} {d0:8.5f} {np.log(d0) / np.log(L):9.3f} {d1:8.5f} {np.log(d1) / np.log(L):9.3f}")
Output:
checks
ramps: monotone True, rows identical True, ends 0.00-0.05 and 0.00-1.00
max |L* - skimage xyz2lab L*| on greys: 0.0e+00
linear 8-bit unrounded round trip max |dY| 0.0e+00; max |code - exact| 0.5000
sRGB 8-bit unrounded round trip max |dY| 4.4e-16; max |code - exact| 0.4998
power 2.2 8-bit unrounded round trip max |dY| 1.1e-16; max |code - exact| 0.4997
linear 12-bit unrounded round trip max |dY| 0.0e+00; max |code - exact| 0.5000
per-code steps (all codes)
encoding max dL* at Y steps dL*>1 steps dY/Y>1% of
linear 8-bit 3.542 0.00000 15 99 254
sRGB 8-bit 0.509 0.00913 0 226 254
power 2.2 8-bit 0.588 0.00902 0 220 254
linear 12-bit 0.221 0.00269 0 99 4094
the code step containing a given luminance y: (Y[k+1] - Y[k]) / y in % | dL*
encoding Y=0.001 Y=0.01 Y=0.05 Y=0.18 Y=0.5 Y=0.9
linear 8-bit 392.16 | 3.542 39.22 | 3.298 7.84 | 1.132 2.18 | 0.478 0.78 | 0.241 0.44 | 0.163
sRGB 8-bit 30.35 | 0.274 6.09 | 0.506 3.13 | 0.442 1.82 | 0.398 1.19 | 0.365 0.93 | 0.348
power 2.2 8-bit 20.94 | 0.189 7.01 | 0.583 3.38 | 0.479 1.87 | 0.411 1.19 | 0.363 0.91 | 0.338
linear 12-bit 24.42 | 0.221 2.44 | 0.205 0.49 | 0.070 0.14 | 0.030 0.05 | 0.015 0.03 | 0.010
codes per stop below white (stop s holds 2^-s <= Y < 2^-(s-1); stop 1 includes white)
encoding 1 2 3 4 5 6 7 8 9 10 below
linear 8-bit 128 64 32 16 8 4 2 1 0 0 0
sRGB 8-bit 68 51 37 29 21 16 12 9 6 3 3
power 2.2 8-bit 69 51 36 27 20 14 10 8 6 4 10
linear 12-bit 2048 1024 512 256 128 64 32 16 8 4 3
ramp images, stored and decoded: L* error against the true ramp
image encoding levels max jump dL* max |dL*| rms dL* <= step
dark linear 8-bit 14 3.542 1.766 0.656 True
dark sRGB 8-bit 64 0.509 0.251 0.130 True
dark power 2.2 8-bit 64 0.588 0.290 0.144 True
dark linear 12-bit 206 0.221 0.094 0.041 True
full linear 8-bit 256 3.542 1.736 0.177 True
full sRGB 8-bit 256 0.509 0.238 0.108 True
full power 2.2 8-bit 249 0.588 0.287 0.110 True
full linear 12-bit 4001 0.441 0.110 0.011 True
scene luminance -> BT.709 signal -> BT.1886 display (Lw = 100 cd/m2), relative to white
EOTF(0) - Lb and EOTF(1) - Lw, cd/m2: largest |error| 1.4e-14
scene signal Lb=0 exponent Lb=0.1 exponent
0.01 0.0450 0.00059 1.616 0.00386 1.207
0.05 0.1865 0.01776 1.346 0.03007 1.170
0.18 0.4090 0.11699 1.251 0.14112 1.142
0.50 0.7055 0.43293 1.208 0.45772 1.127
0.90 0.9491 0.88218 1.190 0.88858 1.121
Checks. All the checks passed:
- the ramps have the planned properties;
- the function reproduces scikit-image exactly;
- every encoding round-trips to within without rounding;
- the BT.1886 constants reach and to within cd/m²;
- the bound column is True in all eight rows.
The half-step check prints 0.5000 for the linear encodings because some ramp samples fall exactly halfway between codes; the sample , for instance, is code 127.5 in 8 bits and 2047.5 in 12 bits. That is still within the allowed half step, and NumPy rounds such ties to the even code.
The eight-bit bottleneck. The plan, written before the example was first run, predicted from the step formulas that linear 8-bit coding has about 15 steps above . The largest would be , from black to code 1. The other three encodings would have none: sRGB peaks at about 0.51 and the power law at about 0.59, both in the step just above the point where changes from its linear segment to its cube root, and 12-bit linear stays at 0.221 through the linear segment. All of this held. This is Poynton’s “eight-bit bottleneck” (1998, §14): linear light in 8 bits bands near black. On the dark ramp it shows as 14 grey levels with jumps of 3.5 , against 64 levels with jumps of about half an unit for the two gamma encodings and 206 levels with jumps of 0.221 for 12-bit linear. The planned levels and jumps on the dark ramp held for all four encodings. The power law also uses 64 levels, not the 66 its range spans (the ramp’s top, , rounds to code 65). Near black its codes are narrower than the spacing between ramp samples, so a few are skipped, as the plan derived. The round-trip predictions held on both ramps. Linear 8-bit errs by up to 1.77 (1.766 dark, 1.736 full), between the planned 1.5 and . sRGB errs by at most 0.251 and the power law by at most 0.290, both under the planned 0.5. 12-bit linear stays at or below 0.110, under the planned plus and below sRGB on both ramps. By the model, four extra bits of linear coding do better than 8-bit gamma encoding.
The 1% model gives a different verdict. By the 1% criterion, every encoding has visible steps. The planned counts held for all four encodings: linear coding exceeds 1% at codes 1 to 99, exactly as Poynton (§13) states for codes below 100. At code 100 the step is exactly 1%, and the code rounds before comparing so that this tie does not count. 12-bit linear has the same 99 steps, because its step relative to the value is again . sRGB exceeds 1% at 226 of 254 steps and the power law at 220. This is consistent with Poynton’s estimate that about 9 bits are needed for 1% steps over 100:1, and his remark that 8-bit Rec. 709 coding suffices at about 50:1. The table at fixed luminances shows why the two models disagree: at an sRGB step of 1.82% is worth only 0.398 . Over this range tolerates a few percent of luminance change per unit. Which threshold applies depends on viewing conditions that the code does not model.
Codes per stop. Linear 8-bit halves its codes in every stop: 128 in the top stop, one in the eighth and none below. The power law falls by roughly the predicted factor of 0.73 to 0.75 per stop, within the rounding of small counts, and sRGB does so down to stop 8. Below that sRGB’s count falls faster (9, 6, 3), towards the halving of linear coding, because its linear segment begins at , inside stop 9. The pure power law keeps 10 codes below where sRGB keeps 3, because sRGB’s linear segment caps the slope near black. That segment is also why sRGB’s steps at are coarser in relative terms than the power law’s (30.35% against 20.94%). The plan predicted every count with a separate counting formula and set the criterion that the example reproduce each one exactly. That check failed in one cell: for sRGB’s top stop the example prints 68 and the planning formula gave 67. The example’s count is the correct one. sRGB encodes as , or 187.5 codes, the value the color spaces example prints for its linear-light average, so the top stop holds codes 188 to 255, which is 68 codes; the planning formula undercounted that stop by one. Neither the planning script nor the example was changed. All other counts matched the formula; the hypothesis failed in that one cell only.
Full-ramp details. On the full ramp, 12-bit linear’s largest jump is 0.441, twice its step. The ramp samples are apart, slightly more than the 12-bit code width of , so some codes are skipped and neighbouring pixels can differ by two codes. The power law’s 249 levels have the same cause near black. RMS errors are smaller than the maxima by factors of about two to ten; in our reading this is because the largest steps are confined to a narrow dark range.
Camera into display. With a black display, the BT.709 signal shown through BT.1886 gives an effective end-to-end exponent of 1.251 at mid grey and 1.208 at half white. The plan predicted both within 1.15–1.30 from the formulas, and they are. The exponent rises to 1.616 at 1% of white. Every printed value exceeds the product , because the OETF’s offset of 0.099 (and, at , its linear segment) puts the signal below the pure power. Raising the display black to 0.1 cd/m² lowers every exponent, to between 1.121 and 1.207, and the exponent falls with luminance in both columns. All four planned predictions for this part held. The midtone values are close to the 1.1 to 1.2 that Poynton (§7) gives for dim-surround viewing. The camera and display curves are deliberately not inverses, so “undoing gamma” with the display curve does not recover scene-linear light (our reading of the two standards).
Common Misconceptions
- “Gamma correction is one operation.” The name covers three things: a camera’s encoding (OETF), a display’s response (EOTF), and an arbitrary power-law tone adjustment such as scikit-image’s
adjust_gamma. The first two are fixed by standards and come as a pair; the third is a point operation chosen for appearance. - “sRGB is a 2.2 power law.” It approximates one over most of the range. Near black its linear segment allocates codes differently: 3 codes below against the power law’s 10, and coarser relative steps at , as the example shows.
- “Decoding with the display curve gives scene light.” BT.709’s OETF and BT.1886’s EOTF combine to an end-to-end exponent of about 1.2 to 1.25 in the midtones with a black display, and more in the shadows. BT.709’s note on production practice also means the encoding of real footage is adjusted by eye, so, in our reading, its signal is not a calibrated measure of scene luminance. Methods that need scene-linear values need a calibrated camera response, as in HDR imaging.
- “Eight bits are always enough” or “never enough.” The answer depends on the visibility model and the contrast range. By , 8-bit gamma encodings have no visible steps on a ramp; by a 1% ratio over 100:1, they need about one more bit (Poynton, 1998, §13). Linear 8-bit fails both.
- “Values are linear unless stated.” Poynton (1998, §1) warns that image data in a file may or may not represent intensity, even if it is described as doing so. The digital camera article separates RAW values, close to proportional to light after black-level subtraction, from processed images that have passed through the full pipeline.
- “Luma is luminance.” BT.709 calls its weighted sum of encoded components the “luminance signal”, while Poynton and the color spaces article call it luma and reserve luminance for linear light. The source’s own term should be read with that in mind.
Where It Is Used
Every stored 8-bit image passes through a transfer function. The digital camera and image formation articles place it near the end of the image signal processor. Color spaces defines the sRGB curve and explains why averaging, blurring and resizing belong in linear light, and it notes that downsampling should respect linearity as well as sampling and aliasing.
Methods that model light undo the transfer function first. Photometric stereo and HDR imaging need values proportional to light, and HDR imaging estimates the camera response when it is not known; direct sparse odometry works on images corrected with a photometric calibration that includes the camera’s response. Noise statistics differ between linear raw data and encoded images, as image noise discusses.
Feature pipelines treat gamma either as a preprocessing choice or as a nuisance. Histogram of oriented gradients offers square-root gamma compression of each channel. Local binary patterns are unchanged by any increasing grey-level map, a gamma curve included, when their neighbours lie on pixel centres; with interpolated neighbours they are only approximately invariant, which that article measures. Super-resolution papers often compute PSNR on luma, which is built from encoded values; in our reading their errors are then weighted roughly by perceived lightness rather than by light. Display and tone reproduction close the loop: the image processing overview lists gamma encoding among display operations, and HDR imaging’s tone curves map scene radiance into the range an encoded display signal can carry.
Related
- Point Operations
Operations that compute each output pixel from the input value at the same pixel alone, such as negatives, log and power-law maps, contrast stretches, windows and thresholds; why every one is a look-up table, and what an 8-bit table merges, skips and cannot invert.
- Digital Camera
The camera as an imaging system, from lens, shutter and sensor to the image signal processor that demosaics, white-balances, colour-corrects, tone-maps and compresses its output; RAW against processed images, metadata, machine-vision interfaces, camera classes, and where processing breaks the assumptions of vision methods.
- Image Formation
How light leaving a scene becomes an array of pixel values, through illumination and reflectance, the lens, the sensor and its colour filters, sampling and quantization, and the camera's own processing.
- High Dynamic Range Imaging
Capturing scenes whose brightness range exceeds what one exposure can record, by bracketing exposures, recovering the camera response and a radiance map, merging with noise-aware weights and removing ghosts, or with single-shot HDR sensors and learned single-image reconstruction, and compressing the result for display by tone mapping or exposure fusion.
- Image Processing
Computing new images, or simple measurements, from images by point, neighborhood, global and geometric operations; how enhancement, restoration and learned methods build on them, and where image processing meets computer vision.
- Photometric Stereo
Recovering surface orientation and albedo at every pixel from several images taken from a fixed viewpoint under different lighting, from Woodham's Lambertian least squares to uncalibrated, robust and learned methods and the DiLiGenT benchmark.
References
- Poynton, C. (1998). Frequently Asked Questions about Gamma. Document dated 1998-01-04a.
- ITU-R (2015). Recommendation ITU-R BT.709-6: Parameter Values for the HDTV Standards for Production and International Programme Exchange.
- ITU-R (2011). Recommendation ITU-R BT.1886: Reference Electro-Optical Transfer Function for Flat Panel Displays Used in HDTV Studio Production.
- International Electrotechnical Commission (1999). IEC 61966-2-1:1999. Multimedia Systems and Equipment — Colour Measurement and Management — Part 2-1: Colour Management — Default RGB Colour Space — sRGB (not read).
- CIE (2018). CIE 015:2018 Colorimetry (4th ed.). International Commission on Illumination, Vienna (not read).
- scikit-image developers (2025). skimage.color.xyz2lab and rgb2xyz, skimage/color/colorconv.py, scikit-image v0.25.2 source code.
- scikit-image developers (2025). skimage.exposure.adjust_gamma, skimage/exposure/exposure.py, scikit-image v0.25.2 source code.