Research cut-off: 7 August 2026
Audience: color scientists, codec and video-pipeline
engineers, browser/graphics implementers, finishing and mastering
engineers
Companion files: the Simplified-Chinese edition and a
reproducible NumPy script are supplied separately.
Every substantive historical or technical claim is marked by source class:
A citation such as [A-04] refers to the classified bibliography in §11.4. When an exact historical claim could not be supported by a primary source, the text says so explicitly.
Unless otherwise stated:
The numerical tables were computed in double precision. The exact
formulas and assumptions are restated beneath each table and are
implemented in gamma-analysis-repro.py.
A file does not contain “brightness” in a self-sufficient sense. It contains code values plus, sometimes, declarations about what those values mean. The final image is produced by a chain:
scene light
-> camera/scene encoding
-> RGB or Y'CbCr representation
-> quantization and legal range
-> codec bitstream
-> container metadata
-> decoder and color conversion
-> application working space
-> effects, scaling, compositing, tone mapping
-> OS compositor and display profile
-> display EOTF and viewing environment
-> perceived appearance
A difference anywhere in that chain can be reported colloquially as a “gamma shift,” even when the actual error is an affine range remap, a 601/709 matrix mismatch, a display-profile conversion, an HDR tone mapper, a chroma-resampling phase error, or arithmetic performed on nonlinear samples. That loose vocabulary is the principal reason the subject remains confusing.
The most useful engineering rule is therefore:
Do not diagnose a gamma error from appearance alone. First identify the signal domain, metadata, range, matrix, transfer function, color primaries, working space, output transform, and display path.
A genuine transfer-function mismatch has a characteristic nonlinear relationship between corresponding samples. Full/limited-range confusion has a characteristic affine relationship. Matrix confusion changes colors in a direction-dependent way. Tone mapping is generally content- and luminance-dependent and cannot be represented by one scalar exponent. These distinctions are measurable.
The rest of this article keeps these questions separate.
| Layer | Correct question | Typical false diagnosis |
|---|---|---|
| Encoding | Which OETF/encoding curve produced the samples? | “The file is gamma 2.2.” |
| Signal representation | RGB, Y′CbCr, constant-luminance, ICtCp? | “Y is luminance.” |
| Quantization | Full or narrow range; bit depth; headroom/footroom? | “The contrast/gamma changed.” |
| Metadata | What do CICP/VUI, colr, ICC, PNG chunks say? |
“The codec changed the pixels.” |
| Processing | Were operations done in code space or linear light? | “The scaler is darker.” |
| Rendering | Which EOTF, OOTF, tone map, display profile, surround? | “The monitor has the wrong gamma.” |
ITU-R BT.2100-3 defines the terms as follows [A-07, Annex 1]:
For a reference chain in which one OETF feeds one EOTF:
Conversely, an OETF designed to realize a desired OOTF on a specified EOTF can be written:
This is why an EOTF is not, in general, the inverse of the camera OETF. The inverse OETF reconstructs scene-referred linear values. The EOTF produces display-referred linear light. Those are different endpoints.
The phrase can mean at least six different things:
Levels.Only the first two unambiguously specify a mathematical function, and even then the black and white normalization, clipping behavior, sign handling, and domain must be stated. sRGB is not a pure 2.2 law. BT.1886 is not a pure 2.4 law when . BT.709 is an OETF with a linear toe and an offset power branch, not “gamma 0.45.” PQ is not meaningfully described by a single gamma. HLG has an OETF plus an explicit luminance-dependent OOTF.
For the special case of pure powers,
where is the camera/encoding exponent and is the display exponent. Then
and the system gamma is .
An alternate notation calls the encoding “gamma” . In that notation the same system exponent is . Mixing these two conventions is a common source of reversed corrections.
For real television curves, the composite is not exactly a power law, so “system gamma” is a rendering-intent characterization or a local/fitted slope, not an identity valid at every level.
BT.709-6 specifies the source OETF [A-03, Part 1, item 1.2]:
With an ideal-black BT.1886 display, the normalized EOTF is . Therefore the exact composite is
It is not , because the BT.709 power branch has an offset. Its local log-log slope is
| Scene-linear | BT.709 | BT.1886 relative output | Local log slope |
|---|---|---|---|
| 0.018 | 0.081248 | 0.002419 | 2.3960 |
| 0.020 | 0.090000 | 0.003092 | 2.2680 |
| 0.050 | 0.186453 | 0.017757 | 1.6534 |
| 0.100 | 0.290940 | 0.051657 | 1.4475 |
| 0.180 | 0.409008 | 0.116992 | 1.3414 |
| 0.250 | 0.489940 | 0.180444 | 1.2982 |
| 0.500 | 0.705515 | 0.432927 | 1.2315 |
| 0.750 | 0.866551 | 0.709098 | 1.2034 |
| 1.000 | 1.000000 | 1.000000 | 1.1869 |
Calculation: direct composition of the published BT.709 OETF and ideal-black BT.1886, followed by analytic differentiation. No fit was used.
At the top end, the effective OETF slope is about 0.495, not 0.45; multiplied by 2.4 it gives 1.187. Over the upper half of the signal, a constrained log-domain fit is about 1.202. This is the engineering basis for the familiar “about 1.2” description.
More importantly, ITU-R Report BT.2390-12 explicitly states that conventional SDR television has an OOTF/system gamma of 1.2 and explains it as rendering intent that compensates for viewing an emissive screen in a dark or dim surround [A-08, §6.2]. BBC engineering literature likewise describes an effective camera exponent near 0.5 and a display exponent near 2.4, giving approximately 1.2 [L-03]. It is therefore wrong to call the non-inverse pairing a standards mistake. The non-unity OOTF is deliberate.
The toe is not well represented by any single system exponent. It reflects camera-noise and legacy design constraints and should not be used to infer that the entire system has a 2.4 OOTF.
Most SDR RGB encodings are relative. A normalized value of 1 means reference white, but the number alone does not state whether white is 80, 100, 120, or 200 cd/m². The rendered appearance depends on the display white, black, surround, flare, adaptation, and OOTF.
PQ is different. SMPTE ST 2084 and BT.2100 define an absolute EOTF whose output is cd/m² up to 10,000 cd/m² [A-06, A-07]. A PQ code does not merely mean “a fraction of white.” HLG remains scene-relative at the OETF stage and obtains display behavior through an explicit system gamma and peak-luminance parameter. Consequently:
IEC 61966-2-1:1999 defines the following component encoding [A-01; B-01]. For linear :
The decoding function is
The standard was published in 1999; the pre-standard HP/Microsoft/W3C proposal dates to 1996, and the final IEC constants corrected a small rounding issue in the proposal [A-01; A-15]. sRGB uses BT.709 primaries and D65 but not the BT.709 OETF.
Define a pointwise effective decoding exponent
It is 1.739 at code 8, 1.901 at code 16, 2.042 at code 32, 2.149 at code 64, 2.224 at code 128, and 2.257 at code 192. Thus no constant 2.2 describes the toe and shadows.
A least-squares pure-power fit minimizing unweighted linear-light error over the entire normalized code interval gives approximately 2.2333. A log-domain fit restricted to gives approximately 2.1959. Therefore “2.2” is a useful mid/high-tone shorthand under a specified fitting criterion, not the sRGB formula.
For a pure 2.2 decoding law:
| 8-bit code | sRGB linear | Pure 2.2 linear | Pure 2.2 − sRGB | Relative error | Difference at 100-nit white |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | — | 0 cd/m² |
| 1 | 0.000303527 | 0.000005077 | −0.000298450 | −98.33% | −0.029845 cd/m² |
| 2 | 0.000607054 | 0.000023328 | −0.000583726 | −96.16% | −0.058373 cd/m² |
| 4 | 0.001214108 | 0.000107187 | −0.001106921 | −91.17% | −0.110692 cd/m² |
| 8 | 0.002428216 | 0.000492504 | −0.001935712 | −79.72% | −0.193571 cd/m² |
| 12 | 0.003676507 | 0.001201740 | −0.002474767 | −67.31% | −0.247477 cd/m² |
| 16 | 0.005181517 | 0.002262953 | −0.002918564 | −56.33% | −0.291856 cd/m² |
| 20 | 0.006995410 | 0.003697240 | −0.003298170 | −47.15% | −0.329817 cd/m² |
| 32 | 0.014443844 | 0.010397802 | −0.004046042 | −28.01% | −0.404604 cd/m² |
| 64 | 0.051269458 | 0.047775754 | −0.003493704 | −6.81% | −0.349370 cd/m² |
| 128 | 0.215860500 | 0.219519718 | +0.003659218 | +1.70% | +0.365922 cd/m² |
| 192 | 0.527115126 | 0.535641609 | +0.008526483 | +1.62% | +0.852648 cd/m² |
| 255 | 1 | 1 | 0 | 0% | 0 cd/m² |
Calculation: decode each integer code divided by 255 using the exact sRGB function and . Relative error is measured against exact sRGB linear light. The large shadow percentages coexist with small absolute luminance differences; both facts matter.
Across continuous , the maximum absolute linear-light difference is 0.00852769 near code value 0.7496. Across integer 8-bit samples, the practical consequence depends on later quantization, display black, flare, and spatial context.
ITU-R BT.1886-0 was approved in March 2011 to provide a reproducible reference EOTF for flat-panel HDTV production displays as CRT reference monitors disappeared [A-04, Scope and Annex 1]. Its full form is
with
For , this reduces to . For , it does not. For example, at cd/m²:
The max defines behavior below the effective black
offset. BT.1886 also gives a 10-bit narrow-range normalization
[A-04, Annex 1].
BT.1886 was not intended to assert that every CRT was exactly 2.4. Its motivation was to replace an undocumented, device-dependent appeal to “whatever a good CRT does” with a portable target that is reasonably equivalent to legacy reference behavior.
A CRT’s dominant nonlinearity is associated with electron-gun/cathode-grid drive and space-charge behavior, not with a nonlinear phosphor light output [L-01, L-04]. Over the useful range, beam current is well approximated by a power of drive voltage. Engineering literature commonly reports properly adjusted CRT exponents in approximately the 2.35–2.55 range and uses 2.5 as a representative value [L-01].
Three qualifications are essential:
Thus “CRT gamma 2.5” is a historically useful physical approximation, not a universal metadata value that should be attached to all legacy video.
The early Macintosh convention was an effective code-to-display-light response near 1.8, achieved through the video lookup table feeding an ordinary CRT; it did not mean that Macintosh CRT phosphors had a special physical exponent [L-01]. If the CRT itself was near 2.5, a LUT with an exponent near produced an overall response near 1.8.
Poynton documents a strong relationship to graphic arts and prepress: QuickDraw codes sent to an imagesetter gave a tolerable match to the approximately 1.75-power behavior of common halftone/offset-print workflows [L-01]. This supports a printing/prepress compatibility explanation. However, this research did not find a first-party Apple design memorandum proving the narrower popular story that “1.8 was chosen specifically to compensate a particular LaserWriter.” That narrower claim should be treated as unverified.
Mac OS X 10.6 Snow Leopard, released on 28 August 2009, changed the default display gamma from 1.8 to 2.2. Apple’s contemporaneous system-refinements text said the change better served digital-content producers and consumers; the original page is no longer live, but the wording survives in contemporaneous archives and quotations [B-21, C-06]. Apple’s release date is independently documented in its newsroom [B-20].
BT.601-7 and BT.709-6 use the same nominal piecewise OETF [A-02, §2.6.4; A-03, item 1.2]:
BT.2020-2 expresses the same shape as [A-05, Table 4]:
The continuous coefficients are
BT.2020’s practical tabulated values are for 10-bit systems and for 12-bit systems. These differences preserve continuity and code-level behavior at the stated precision. The three standards therefore have nearly identical transfer shapes, but their primaries, matrices, resolutions, and digital representations are not interchangeable.
PQ is an absolute display-referred EOTF. For normalized PQ code , BT.2100-3 gives [A-07, Table 4]:
where
The inverse is
Representative codes are approximately 0.50808 for 100 cd/m², 0.58069 for 203 cd/m², 0.75183 for 1000 cd/m², and 0.90257 for 4000 cd/m². These are not fractions of a content-defined white; they encode absolute luminance according to the reference EOTF.
BT.2100 additionally defines a reference PQ OOTF and constructs its reference PQ OETF as , making explicit that an HDR production OETF is not merely the inverse of PQ’s EOTF [A-07, Table 4 and Annex 1].
BT.2100-3 defines the HLG OETF [A-07, Table 5]:
with
The inverse is
HLG’s reference OOTF applies the system exponent to scene luminance rather than independently to each color component:
where and at cd/m². For nominal peaks from 400 to 2000 cd/m² [A-07, note 5f]:
| Nominal peak | System gamma | HLG HDR reference white in BT.2408-9 |
|---|---|---|
| 400 cd/m² | 1.03 | 101 cd/m² |
| 600 cd/m² | 1.11 | 138 cd/m² |
| 800 cd/m² | 1.16 | 172 cd/m² |
| 1000 cd/m² | 1.20 | 203 cd/m² |
| 1500 cd/m² | 1.27 | 276 cd/m² |
| 2000 cd/m² | 1.33 | 343 cd/m² |
The gamma values are the BT.2100 formula rounded to two decimals; reference-white values are from ITU-R BT.2408-9, Table 4 [A-09]. HLG therefore has no single display gamma independent of peak brightness.
scRGB-nl is a nonlinear encoding; it
must not be confused with linear scRGB.| Encoding/display function | Direction | Exact character | Reference |
|---|---|---|---|
| sRGB | linear ↔︎ code | piecewise linear + offset 2.4 power | IEC 61966-2-1:1999 |
| Adobe RGB (1998) | linear ↔︎ code | pure 563/256 decoding power | Adobe/ICC |
| BT.601/709 OETF | scene linear → code | 4.5 toe + offset 0.45 power | BT.601-7 / BT.709-6 |
| BT.2020 OETF | scene linear → code | same shape, precision-specific α/β | BT.2020-2 |
| BT.1886 | code → display light | 2.4 with black/white parameters | BT.1886-0:2011 |
| PQ | code → absolute display light | perceptual rational-power function | ST 2084 / BT.2100 |
| HLG OETF + OOTF | scene linear → code → display | hybrid log/sqrt plus peak-dependent system gamma | BT.2100 |
| scRGB | scene/display linear storage | linear extended-range RGB | IEC 61966-2-2:2003 |
| Display P3 | linear ↔︎ code | sRGB transfer, P3-D65 primaries | CSS Color 4 / Apple ecosystem |
Let decode source code into a common linear-light domain and encode that linear value into the destination representation. The correct conversion is
If primaries or white differ, insert a colorimetric transform in linear light:
where is a chromatic-adaptation transform when required. Range and matrix decoding occur at their own layers before this RGB-light conversion.
Assume all of the following:
Let
Then
The direction is determined by the decoding exponents. To convert code values that currently decode with 2.2 into code values that should decode with 2.4, use exponent . Reversing the conversion uses .
| Source decoding exponent | Destination decoding exponent | Code-domain exponent | Effect on mid-code values |
|---|---|---|---|
| 2.2 | 2.4 | 2.2/2.4 = 0.9166667 | raises destination codes so 2.4 decoding preserves light |
| 2.4 | 2.2 | 2.4/2.2 = 1.0909091 | lowers destination codes |
| 2.2 | 2.5 | 2.2/2.5 = 0.88 | raises destination codes |
| 2.5 | 2.2 | 2.5/2.2 = 1.1363636 | lowers destination codes |
| 1.8 | 2.2 | 1.8/2.2 = 0.8181818 | raises destination codes |
| 2.2 | 1.8 | 2.2/1.8 = 1.2222222 | lowers destination codes |
“Raises codes” does not mean “raises final luminance”: the destination’s steeper EOTF cancels the code change under the stated assumptions.
Consider exact conversion from sRGB code to a destination encoded for a pure 2.4 EOTF:
The shortcut is
| Input sRGB code | Exact destination code | 2.2/2.4 approximation | Approx − exact |
|---|---|---|---|
| 0 | 0.000 | 0.000 | 0.000 |
| 8 | 20.753 | 10.675 | −10.078 |
| 16 | 28.460 | 20.152 | −8.308 |
| 32 | 43.626 | 38.042 | −5.583 |
| 64 | 73.957 | 71.814 | −2.143 |
| 128 | 134.621 | 135.567 | +0.946 |
| 192 | 195.284 | 196.594 | +1.310 |
| 255 | 255.000 | 255.000 | 0.000 |
All 256 integer inputs: maximum absolute error 10.2078 code values at input code 6; mean absolute error 2.1610; RMSE 3.3063; 95th-percentile absolute error 8.5509. The destination was not requantized before measurement.
The large low-code difference is exactly where the sRGB linear segment matters. A power ratio is not a faithful sRGB converter even though its midtones may look close in many images.
The reverse conversion, pure-2.4 code to sRGB, has maximum absolute error 8.5471 codes and mean absolute error 2.1684 when approximated by .
Assume source code represents relative light . On a target display with white and black , the desired absolute luminance is
The exact destination drive is . Compare that with the black-ignorant shortcut :
| Contrast | Max abs error | Input at max | Mean abs error | RMSE | |
|---|---|---|---|---|---|
| 0 cd/m² | ∞ | 0.000 code | — | 0.000 | 0.000 |
| 0.001 cd/m² | 100000:1 | 1.945 | 9 | 0.992 | 1.152 |
| 0.01 cd/m² | 10000:1 | 4.800 | 17 | 2.546 | 2.940 |
| 0.1 cd/m² | 1000:1 | 11.373 | 31 | 6.366 | 7.284 |
| 1.0 cd/m² | 100:1 | 25.125 | 54 | 15.001 | 16.924 |
Calculation: all 256 8-bit input codes, normalized source power 2.2, absolute black-to-white interpolation as written above, exact inversion of BT.1886, no final quantization. The table is a sensitivity study, not a recommendation to master SDR at every listed black level.
A power-ratio shortcut is exact only in the ideal-black row. Once black is nonzero, the affine placement of relative image light between and , plus BT.1886’s terms, changes the required codes.
The following compares the exact sRGB decoding with pure 2.2 decoding, using BT.709/sRGB primaries, D65, CIE Lab, and CIEDE2000. No chromatic adaptation or post-conversion quantization was applied.
| Sample set | Mean ΔE00 | 95th percentile | Maximum | Fraction ≥ 1 |
|---|---|---|---|---|
| 256 neutral codes | 0.554 | 1.786 | 2.033 at code 27 | 18.36% |
| 17×17×17 RGB code cube | 0.523 | 1.273 | 4.799 at (0, 0.0625, 0) | 8.06% |
A ΔE00 near 1 is often used as a rough controlled-condition visibility scale, but it is not a universal threshold: adaptation, luminance, surround, spatial frequency, display black, and observer variability matter. For HDR, ITU’s ΔEITP is designed so that approximately 1 corresponds to one just-noticeable difference under its intended conditions [A-20]; that statement must not be transferred mechanically to ΔE00.
The table also demonstrates why “the average error is small” can coexist with visible seams, dark UI elements, or differences in low-level saturated colors.
For any named pair of transfer functions:
1. Parse/override source metadata explicitly.
2. Undo quantization range and Y'CbCr matrix to obtain encoded RGB.
3. Apply the exact source inverse transfer function.
4. Transform primaries/white in linear light, if needed.
5. Apply the intended OOTF or tone map, if and only if the task requires it.
6. Apply the exact destination encoding function.
7. Convert to destination matrix/range.
8. Quantize with adequate precision and, where appropriate, dither.
9. Write matching bitstream and container metadata.
10. Verify with decoded pixel values and a controlled reference display path.
A one-line pow() is correct only when the assumptions in
§4.2 have been proved, not merely guessed from visual similarity.
This chapter is the core diagnostic classification. Each class lists mechanism, characteristic symptom, reproducible condition, and discriminating test.
Modern video elementary streams can signal CICP-style values for:
colour_primaries;transfer_characteristics;matrix_coefficients;H.264/AVC and HEVC carry such declarations in SPS VUI; AV1 has
corresponding sequence-header fields. Containers may also carry color
information: ISO BMFF/QuickTime uses a colr box/atom,
commonly with nclc or nclx payloads. Matroska
has its own Colour elements. A transcoder can preserve
pixels while deleting, inventing, or changing these declarations.
colr says BT.709 while SPS VUI says BT.2020/PQ.There is no universal cross-application rule that “container always
wins” or “bitstream always wins.” Demuxers expose side data differently;
decoders may report VUI; applications may merge, override, or ignore one
source. FFmpeg issue reports demonstrate real mov
colr versus SPS-VUI disagreements [B-24]. Therefore
precedence is an empirical property of a particular
demuxer/decoder/application version, not a safe assumption.
ffprobe -v error -select_streams v:0 \
-show_entries stream=color_range,color_space,color_transfer,color_primaries \
-of default=nw=1 input.mp4
# Inspect codec headers rather than only container-level side data.
ffmpeg -i input.mp4 -map 0:v:0 -c copy -bsf:v trace_headers -f null - 2>&1 \
| rg 'colour_primaries|transfer_characteristics|matrix_coefficients|video_full_range'Inspect colr separately with an ISO-BMFF parser such as
Bento4 mp4dump. Then compare: (a) the file, (b) a raw
elementary stream extracted without re-encoding, and (c) a remux with
explicit tags. If appearance changes while decoded numeric RGB does not,
the difference is downstream color management, not codec
reconstruction.
For 8-bit luma/R′G′B′ in conventional narrow range:
For full range:
Chroma conventionally uses codes 16–240 with neutral 128. At 10-bit, nominal luma black/white are 64/940 and chroma extrema are 64/960.
| Code | Interpreted full | Interpreted narrow , unclamped |
|---|---|---|
| 0 | 0.000000 | −0.073059 |
| 16 | 0.062745 | 0.000000 |
| 32 | 0.125490 | 0.073059 |
| 64 | 0.250980 | 0.219178 |
| 128 | 0.501961 | 0.511416 |
| 192 | 0.752941 | 0.803653 |
| 235 | 0.921569 | 1.000000 |
| 255 | 1.000000 | 1.091324 |
If narrow data are treated as full, code 16 becomes gray 0.0627 and code 235 becomes 0.9216: blacks rise and whites dim. If full data are treated as narrow, sub-16 and over-235 values clip, crushing both ends.
A range mapping has form
A pure gamma mismatch has form
On a plot of corresponding samples, the first is a straight line in ordinary coordinates; the second is curved. In log-log coordinates a pure power is a straight line through the origin after normalization, whereas the affine offset bends strongly. This is the fastest mathematical distinction between the two.
Caveat: clipping, quantization, and subsequent tone curves can make a range error look less perfectly affine. Fit the central unsaturated samples first.
For nonlinear RGB, BT.601 and BT.709 form different luma-like signals:
A neutral survives either matrix unchanged, so a matrix mismatch does not normally produce a uniform neutral gamma curve. It changes saturated colors and color edges.
Example: encode as BT.601 full-range Y′CbCr, then decode with BT.709 coefficients. The result is approximately
Encoding the same RGB as BT.709 and decoding as BT.601 gives
Pure gray remains gray. Highly saturated red encoded as 601 and decoded as 709 can reconstruct to approximately before clipping. Clipping then makes the error more nonlinear and can create an apparent local brightness change, but the root cause is the matrix.
Apple’s 1999 Technical Note TN2162 documents a colr
image-description extension with nclc values for primaries,
transfer function, and Y′CbCr matrix [B-04]. It also states:
colr supersedes the older gama
extension;gama when colr is
present;colr.Those statements already show why “QuickTime gamma bug” is too
coarse. Possible causes included a legacy gamma-level atom, an
nclc interpretation, a codec-specific Y′CbCr-to-RGB path,
an application preview transform, ColorSync display conversion, range
expansion, and old assumptions about the Macintosh 1.8 graphics
system.
The legacy gama image-description extension stores a
gamma-level indication. The newer colr/nclc
mechanism separates primaries, transfer, and matrix. Current ISO-BMFF
ecosystems also use nclx, which can carry a full-range
flag. H.264/HEVC can simultaneously carry VUI values. Consequently, a
ProRes or H.264 file can traverse Apple and non-Apple software with
identical compressed samples but different color interpretation.
colr/nclc,
gama precedence, and legacy Mac graphics behavior
[B-04].nclc/nclx, ColorSync, codec tags, display
profiles, and application-specific Rec.709 interpretations continue to
produce differences, but they must be identified by exact version and
path rather than attributed to a timeless “QuickTime bug.”The reliable diagnostic method is to export a gray ramp, decode it to raw numeric samples using multiple independent paths, inspect all metadata layers, and compare application screenshots only after display color management is controlled.
Current After Effects color management distinguishes the input interpretation, project working space, working-space linearization, display transform, and output profile [B-06]. Two controls that are often confused are:
Adobe recommends adequate precision, especially 16- or 32-bpc, when linearizing. At 8-bpc, repeated decoding, processing, and re-encoding can expose banding.
With no project working space, After Effects can behave more like a pass-through/legacy numeric pipeline, but imported footage interpretation, viewer display color management, codec conversion, and export tags can still differ. “Color management off” does not prove “no transform anywhere.”
Current Premiere documents an explicit source/input color space → working color space → output color space architecture, with optional input and output tone mapping and gamut compression [B-07]. Tone mapping before effects and tone mapping after effects are not equivalent. A source misidentified as Rec.709 rather than HLG/PQ can be transformed incorrectly before the user applies a single grade.
For reproducibility, record:
A screenshot of the timeline and a screenshot of an external player are not a valid test unless both display paths are colorimetrically characterized.
A spatial average should average light when the operation models light integration. Averaging encoded values instead averages a perceptual/code representation.
For a 50% black/white checkerboard:
Thus a gamma-space 50% downscale produces only 42.8% of the correct light, or a −57.2% relative luminance error in this constructed case.
The same principle affects:
The qualifier is semantic: some authored UI/graphic operations are intentionally defined in sRGB-like code space to preserve familiar appearance rather than simulate radiometry. The engineering error is not “ever using nonlinear arithmetic”; it is using it unknowingly or inconsistently.
4:2:0 does not merely reduce chroma resolution. It also requires a geometric convention for where each chroma sample lies relative to luma samples. Common conventions include:
Bitstreams can signal chroma sample location, but metadata may be absent or ignored. Upsampling with the wrong phase shifts chroma by a fraction of a luma sample. On colored edges this produces fringes, color bleeding, and RGB overshoot/undershoot. Because reconstructed RGB contributes to perceived brightness and may clip, the artifact can be described as a brightness halo even though the Y′ plane was unchanged.
A diagnostic edge pattern should contain saturated red/blue transitions at one-pixel and two-pixel positions. Compare planes before RGB conversion, and test alternative siting explicitly. A true gamma error affects broad neutral ramps; a siting error is localized around color edges and reverses direction with edge polarity.
Conventional Y′CbCr is formed from nonlinear components. In BT.709:
Physical relative luminance is formed from linear components:
In general,
For linear pure red , . The nonlinear components are , so ; applying the BT.709 inverse OETF to that scalar gives only about 0.06075, not 0.2126. Equality holds for neutrals, not arbitrary colors.
This non-constant-luminance property means that chroma filtering, quantization, clipping, gamut conversion, or nonlinear RGB operations can change reconstructed physical luminance even if stored Y′ appears unchanged. The error becomes more consequential with BT.2020’s wider gamut and HDR’s larger luminance range [A-08].
BT.2020 defines both:
CL better isolates luminance from chroma but is more complex and less widely deployed. BT.2100 introduces ICtCp/constant-intensity options for HDR; NCL Y′CbCr remains common and must not be treated as physical luminance.
PNG Third Edition establishes this color-space precedence [A-12, §4.3]:
cICP;iCCP;sRGB;cHRM plus gAMA.A decoder should use the highest-priority color-space information it
understands. iCCP may coexist with approximate
gAMA/cHRM for older readers; a compliant
modern reader uses the ICC profile. iCCP and
sRGB should not both be present. The gAMA
integer stores image gamma multiplied by 100000; for a nominal 1/2.2
encoding it is about 45455. Interpreting that field as a display
exponent of 2.2 reverses its meaning.
The alpha channel is linear coverage/full-range and is not
gamma-corrected by gAMA. Treating alpha as if it had the
RGB transfer function creates edge errors.
Historical libraries do not all implement the current precedence identically. Conflicting chunks are therefore a portability hazard even when one interpretation is formally correct.
JPEG commonly embeds ICC profiles in APP2 markers. The ICC convention
uses an ICC_PROFILE\0 identifier, a sequence number, and
total-chunk count because one marker cannot hold a large profile; the
profile can span multiple APP2 segments [A-17]. Missing or misordered
segments invalidate the profile. JPEG may also contain EXIF color-space
hints, but an ICC profile is the stronger colorimetric description when
valid.
Whether a screenshot embeds the source display profile, converts pixels to sRGB, embeds Display P3, or writes no profile depends on OS, API, application, and format. The only reliable answer for a specific screenshot is to inspect the file. Outside a defined ecosystem there is no universal rule for an untagged image. On the modern web, browsers generally treat untagged image content as sRGB; legacy or non-color-managed applications may instead send raw numbers directly to the display.
On a wide-gamut Display-P3-like monitor, an untagged sRGB image must not be interpreted as native display RGB. Correct rendering conceptually does:
assumed/embedded source RGB
-> source linear/PCS
-> display profile
-> display device values
Sending sRGB numbers directly to a wider-gamut native framebuffer oversaturates colors. Assigning a Display P3 profile to unchanged sRGB numbers also changes their meaning; converting sRGB to P3 preserves color while changing numbers.
Operating systems differ in where color conversion occurs—application, graphics API, window compositor, or display pipeline—and in whether calibration curves are applied separately from ICC characterization. Double color management occurs when both the application and another layer apply a transform intended to be singular. Bypassing management produces the opposite failure.
WebKit documents color matching for wide-gamut web content and assumes untagged images are sRGB [B-18]. Firefox 89 changed macOS behavior so untagged images were treated as sRGB consistently with CSS colors [B-19]. Browser, OS, GPU, and display-profile versions remain relevant; a valid test page should include tagged sRGB, tagged Display P3, untagged copies, CSS colors, and canvas readbacks.
HDR-to-SDR must decide how an absolute or high-relative-range scene is rendered onto a lower peak and narrower gamut. Variables include:
Current ITU guidance uses 203 cd/m² as HDR reference white in key production/interchange contexts [A-09]. This does not redefine the conventional BT.1886 SDR reference display as 203 cd/m²; it specifies how reference/diffuse white is placed in an HDR system. BT.2446 documents multiple HDR/SDR conversion methods rather than one exponent [A-10].
Two implementations can both correctly decode PQ yet look different because one uses a hard clip, another uses a global knee, and a third uses dynamic/local tone mapping. Their pixel relationship is content-dependent and often spatially varying. Fitting one gamma to those results is a descriptive approximation, not an explanation.
SDR-to-HDR likewise requires an inverse tone-mapping/rendering decision: what remains diffuse white, how much highlight expansion is synthesized, whether colors expand, and what peak is targeted. Merely decoding SDR and re-encoding as PQ produces an HDR container, not an HDR rendering.
Early video systems could rely on the useful accident that a CRT’s electron-gun drive was nonlinear in a reasonably repeatable way. Camera-side pre-correction both improved the allocation of signal steps and approximately compensated the receiver. But television was never a perfectly inverse camera/display chain: viewing in a dim room called for a contrast-enhancing OOTF.
The shorthand history is:
physical CRT family: roughly 2.35–2.55 in useful operating ranges
camera/source pre-correction: roughly a 0.45–0.5 exponent plus toe/offset
end-to-end rendering: deliberately greater than unity
flat-panel era: no common natural CRT law
2011: BT.1886 defines a reproducible reference EOTF, gamma 2.4 with Lw/Lb
The value 2.5 came from measurement and electron-gun physics, not an instruction that every file be decoded with a 2.5 LUT. The later 2.4 reference is a standardization choice that reproduces accepted reference viewing behavior while accounting for display black.
The well-supported facts are:
The weaker claim is that Apple selected exactly 1.8 as a documented mathematical compensation for one named LaserWriter model. This research searched Apple support/developer archives, QuickDraw/ColorSync material, LaserWriter documentation indexes, Poynton’s publications, and contemporary discussions. It found no first-party design record establishing that narrow story. The defensible wording is print/prepress-chain compatibility, not proven LaserWriter-specific origin.
This distinction matters because a historical rationale does not create a valid modern file conversion. A 1.8 desktop LUT, a printing dot-gain curve, an ICC perceptual rendering, and a video OOTF are different transforms.
A compact evidence-weighted timeline:
| Date | Evidence | What it establishes |
|---|---|---|
| 1999-12-14 | Apple TN2162 [B-04] | nclc/colr semantics, colr
over gama, and a legacy Mac Y′CbCr transfer adjustment that
colr cannot describe |
| 2001-era QTFF | Apple file-format docs [B-05] | gamma and color image-description extensions existed in the container architecture |
| 2006 | Apple/community reports [C-07] | users observed cross-player gamma/black differences; mechanism not uniquely proved |
| 2007 | After Effects CS3 material [C-02] | QuickTime 7.2-era behavior changed enough for Adobe to expose a legacy-matching option |
| 2008 | Video-production guidance [C-03] | gamma-shift workarounds were already common, but often mixed player, codec, and range effects |
| 2009-08-28 | Snow Leopard [B-20, B-21] | default desktop gamma changed 1.8→2.2; QuickTime X changed the application stack |
| 2010-12-31 | controlled blog test [C-01] | one AE CS5/Windows/QuickTime H.264 path was empirically fit as ; QuickTime Player also showed a separate range/contrast remap |
| 2013 onward | encoding forums/guides [C-04, C-05] | Levels(..., gamma=0.88) became a portable community
recipe, often detached from its original conditions |
The reports conflict because they did not all test the same pipeline. Some compared decoded bytes; some compared screenshots. Some used H.264, some ProRes, some Animation. Some ran on Windows through Apple’s QuickTime components; others ran through ColorSync on macOS. Some errors were nonlinear; others were full/narrow range. A single folklore label absorbed all of them.
The earliest independently retrievable, date-stamped, exact 0.88 measurement found in this research is the 31 December 2010 article “The QuickTime Gamma Bug” [C-01]. It reports that After Effects CS5 on Windows, when encoding H.264 through the tested QuickTime path, transformed gray samples approximately as
with a maximum 8-bit increase of about 12 codes around input 88. It also reports a different QuickTime Player transformation that raised black and compressed contrast—an affine/range-like error, not the same gamma curve.
This is C-class empirical evidence, not an Apple or Adobe specification. It establishes that the tested path behaved that way. It does not establish that all QuickTime H.264 encoders did, that ProRes did, or that the implementation’s internal rationale was .
Numerically,
That makes a CRT/PC-gamma story tempting. However, no first-party source found here says that the implementation deliberately converted a 2.2-coded signal to a 2.5-coded signal. The 2010 author explicitly treated the cause as unknown. Therefore the theoretical-origin claim remains unproven; 0.88 should be described as an empirical fit/recipe whose numerical equality to may explain later folklore.
Search limitations: several old Doom9 links and mailing-list references are dead, blocked, or unindexed. Earlier exact uses may exist. The claim here is “earliest retrievable in this search,” not “first ever posted.”
Levels(gamma=0.88) does not apply
AviSynth documents Levels as [B-10]:
with input clamping and the selected range parameters. VapourSynth
std.Levels exposes matching semantics: values greater than
1 brighten, values less than 1 darken, and planes selects
which planes are processed [B-09]. In normalized full-range form:
Therefore:
gamma=0.88 applies exponent
and darkens;Levels(gamma=0.88) applies its inverse
,
which explains the recipe.This is different from saying “convert a correct pure-2.2 encoding to
a correct pure-2.5 encoding.” That forward conversion is
,
which would require Levels(gamma=1.13636), not 0.88. Much
community discussion fails to distinguish the correction parameter from
the mathematical exponent.
gamma=0.88 is defensible only when measurements show
that the source has been brightened by approximately
in the same normalized domain and that the desired correction is its
inverse. Conditions include:
It is harmful when:
The correct order is to fit the observed mapping first, then choose an inverse. Never begin with 0.88 because two images “look Apple-different.”
Take full-range BT.709 nonlinear RGB
Its Y′CbCr values are approximately
Apply Levels(gamma=0.88), i.e. exponent 1.13636:
| Method | Resulting | HSV hue | HSV saturation | sRGB-decoded relative luminance |
|---|---|---|---|---|
| Original | (0.800000, 0.400000, 0.200000) | 20.000° | 0.7500 | 0.22579 |
| Gamma on Y′ only; Cb/Cr copied | (0.754033, 0.354033, 0.154033) | 20.000° | 0.7957 | 0.18751 |
| Same gamma per R′/G′/B′ channel | (0.776024, 0.353017, 0.160589) | 18.760° | 0.7931 | 0.19466 |
Calculation: full-range BT.709 NCL matrix; no clipping; HSV is computed on nonlinear RGB; the last column decodes each channel with sRGB solely as a reproducible display-light proxy.
Changing Y′ with Cb/Cr fixed adds the same code-domain delta to R′, G′, and B′ before clipping. It preserves channel differences and this example’s HSV hue, but it changes saturation and does not preserve physical luminance or chromaticity. Per-channel gamma changes channel ratios and therefore hue and saturation. With clipping, Y′-only processing can also change hue abruptly.
For YUV clips, planes=0 means “process the Y plane,” not
“perform a colorimetrically correct transfer conversion.” For RGB clips,
selecting all three color planes applies the scalar law to each encoded
component. Neither operation automatically linearizes, changes
primaries, handles BT.1886 black, or updates metadata.
| Tool/filter | Domain and purpose | Linear-light/color-managed? | Main risk |
|---|---|---|---|
AviSynth Levels / VS std.Levels |
sample/range remap with power; selected planes | No automatic color management | parameter direction, Y-only default/workflow, range assumptions |
FFmpeg eq |
simple brightness/contrast/gamma adjustment, commonly per native plane | Not a standards conversion | applying “gamma” to YUV samples and mistaking it for TRC conversion |
FFmpeg lut, lutrgb,
lutyuv |
arbitrary sample-domain LUT | Only if user builds exact pipeline | wrong domain, clipping, low precision |
FFmpeg colorspace |
transfer/primaries/matrix/range conversion | Standards-aware; uses defined conversions | wrong/missing input declarations or unsupported edge case |
FFmpeg zscale |
zimg scaling and color conversion with dither/precision options | Can perform exact transfer/primaries/matrix conversions | metadata defaults and option direction still matter |
| mpv/libplacebo | playback rendering, ICC/output TRC, linear scaling, tone/gamut mapping | Designed around explicit color pipeline; options select behavior | using --gamma-factor as a metadata fix, wrong target
assumptions |
| madVR | proprietary playback renderer with calibration/tone controls | Exact internals not fully public | inferring mathematical order from UI labels or forum descriptions |
FFmpeg’s colorspace/zscale are the
appropriate families for declared color-space conversion;
eq and LUT filters are appropriate for deliberate sample
transforms. mpv’s --linear-downscaling,
--target-trc, ICC, --target-peak,
tone-mapping, and --treat-srgb-as-power22 options
demonstrate that “gamma” interacts with the full rendering pipeline
[B-11, B-12]. --gamma-factor is an appearance adjustment,
not a substitute for correct source metadata.
For madVR, this research found extensive community descriptions but insufficient public primary documentation to state a single exact internal operation order for every mode. Any exact claim should be verified against a particular build with ramps and readback/capture measurements.
Filter Effects Level 1 inherits a historically important split [A-13, §10]:
color-interpolation-filters property; its initial value is
linearRGB.brightness(),
contrast(), grayscale(), sepia(),
and saturate() operate in sRGB;
color-interpolation-filters does not change them.Thus these are not guaranteed to match:
.element-a { filter: brightness(0.5); } /* CSS function: sRGB */
.element-b { filter: url(#svgBrightness); } /* SVG primitive: linearRGB by default */An SVG equivalent must explicitly choose the intended domain:
<filter id="svgBrightness" color-interpolation-filters="sRGB">
<feComponentTransfer>
<feFuncR type="linear" slope="0.5"/>
<feFuncG type="linear" slope="0.5"/>
<feFuncB type="linear" slope="0.5"/>
</feComponentTransfer>
</filter>Without sRGB, halving a mid-gray in linear light and
halving its encoded sRGB code are different operations. The split exists
because SVG filter primitives were specified with linear-light color
processing as the default, while CSS filter-function behavior was
standardized around established sRGB-like compositing and web
compatibility. It is a normative inconsistency in default domains, not
necessarily a browser bug.
WebKit bug 185343 reported that filter:url(#id)
incorrectly forced sRGB instead of respecting SVG’s
linearRGB default; it was reported on 4 May 2018 and
resolved fixed on 7 May 2018 [B-13]. This is concrete evidence that a
browser could implement the two paths inconsistently even where the
specification was clear. Older Safari/WebKit Gaussian-blur reports
likewise involved linearRGB/sRGB differences and backend behavior
[B-14].
Do not infer current cross-browser equality from one historical fix.
Use a test page with explicit sRGB and
linearRGB, capture numeric output where possible, and
record browser build, OS, GPU, and color profile.
feComponentTransfer exact semanticsFilter Effects Level 1 defines per-component functions on unpremultiplied color values [A-13, §9.12]:
type="identity"
type="linear"
type="gamma"
type="table": piecewise-linear interpolation through
tableValues. For
entries
,
let
,
;
between entries,
with
selecting the last entry.type="discrete": step selection. For
values,
Defaults are amplitude=1, exponent=1,
offset=0, slope=1, and
intercept=0. Separate feFuncR,
feFuncG, feFuncB, and feFuncA
elements control the channels. A gamma primitive’s
parameter is the actual exponent—not the reciprocal convention used by
Levels—so parameter names alone do not establish direction
across APIs.
The transfer operates in the filter primitive’s selected
interpolation color space. A mathematically identical component function
gives different visual light output in sRGB and
linearRGB.
feComposite operator="arithmetic", clamping, and signed
intermediatesThe arithmetic composite is [A-13, §9.8]:
Each output component is clamped to . Filter primitive results are likewise clipped to the allowed premultiplied range before being passed onward. Therefore a nominal subtraction
<feComposite in="A" in2="B" operator="arithmetic"
k1="0" k2="1" k3="-1" k4="0"/>cannot preserve negative values: they become zero immediately.
A common engineering workaround is to bias and scale a signed difference into the legal interval:
implemented with . Later processing can interpret 0.5 as zero. The workaround only succeeds if every intermediate remains in range; a later “unpack” that generates negative values will again clamp. If exact signed arithmetic is required, WebGL/WebGPU, a float canvas, or an off-browser processing stage is usually more appropriate than chaining normative clamped SVG primitives.
The specification defines functional results and clamping but does not promise that every implementation uses the same intermediate storage precision. Engines may use 8-bit unorm surfaces, 10/16-bit formats, half-float textures, CPU float buffers, or mixed paths depending on primitive, GPU, color space, tiling, and compositing mode.
An 8-bit intermediate can expose banding when:
Higher bit depth reduces quantization but does not defeat normative clipping. Dither can decorrelate banding at the final quantization stage; it cannot recover values already clipped or rounded away in an earlier surface.
A reproducible browser test should use a 16-bit or generated float ramp as source, apply one primitive at a time, and compare screenshots/readbacks at 400% magnification. A visible smooth result is not proof of high precision because dithering can mask low bit depth; a histogram/readback is more informative.
Filter Effects specifies that filter images are generally represented
as premultiplied RGBA. feColorMatrix and
feComponentTransfer are exceptions that conceptually
operate on temporarily unpremultiplied color values and then return to
premultiplied form [A-13, §8].
If a pipeline mistakenly applies a nonlinear function directly to premultiplied channels, then for color and alpha :
unless or special values apply. Low-alpha edges change color and darkness. Correct handling is conceptually:
if alpha > 0: unpremultiply RGB
apply color operation in declared color space
premultiply RGB by alpha
if alpha == 0: define RGB safely, normally zero
Repeated unpremultiply/premultiply at low alpha can amplify quantization and hidden-color differences. Test transparent edges over both black and white backgrounds; an error invisible over one background may be obvious over the other.
Browser engines can route filters through Blink/WebCore/Gecko display lists, Skia/CoreGraphics/Moz2D, GPU shaders, or CPU fallbacks. Differences can arise from:
Public examples include:
linearRGB default; resolved fixed in May 2018
[B-13].feGaussianBlur banding [B-16]. The current issue
page requires authentication in this research environment, so its
present status is not asserted.feColorMatrix output between sRGB and Display
P3 contexts [B-17]. Its current status is likewise not
asserted because the tracker exposes only a sign-in page
here.These issue numbers are evidence that path-dependent differences have existed. They are not permission to claim that current Chrome, Safari, or Firefox always renders filters incorrectly. A current compatibility statement requires a build-specific test.
getImageDataThe Canvas 2D specification supports requesting a context color
space, and current WHATWG text includes srgb,
srgb-linear, display-p3, and
display-p3-linear concepts; deployed browser support is
narrower and should be feature-tested [A-14]. Common shipping syntax
includes:
const ctx = canvas.getContext("2d", { colorSpace: "display-p3" });getImageData() can request a color space and pixel
format in implementations that support the options:
const image = ctx.getImageData(0, 0, w, h, {
colorSpace: "display-p3",
pixelFormat: "rgba-float16"
});The important semantic point is that readback can involve a
color-space conversion into the requested ImageData
representation. It is not necessarily a dump of raw GPU framebuffer
bytes. rgba-unorm8 quantizes to 8-bit normalized channels;
rgba-float16 permits HDR/wide-range values where
implemented. Drawing an ImageData into a context of another
color space can trigger another conversion.
Canvas has at least four distinct spaces to keep separate:
ImageData readback/requested color space and
format;For conformance testing, fill patches using CSS color values, decoded
images, and direct ImageData; read them back in both sRGB
and P3; and compare against independently computed colorimetric values.
A screenshot alone conflates canvas conversion with display color
management.
A transfer fit is meaningful only after the images are registered and represent the same stage of the pipeline. Before fitting:
For same-resolution sources, phase correlation can estimate translation. For rescaled or slightly warped images, use robust feature matching followed by an affine/homography fit, but sample smooth interior regions for tone analysis. Edges are contaminated by kernel and chroma-siting differences.
START: Two images of the same frame look different
|
|-- Are decoded numeric pixels identical before display?
| |-- YES -> display/application color management, output profile,
| | HDR mode, OS compositor, monitor profile, or screenshot path.
| `-- NO -> continue.
|
|-- Do black and white endpoints map approximately by y = a*x + b?
| |-- YES -> full/narrow range, level scaling, pedestal, or contrast/offset.
| | Check 16/235 (or 64/940) and clipping.
| `-- NO -> continue.
|
|-- Are neutral corresponding pixels related by y ≈ c*x^p, with small residuals?
| |-- YES -> transfer-function/power-like mismatch. Inspect tags and fit p.
| `-- NO -> continue.
|
|-- Do errors depend strongly on hue while neutrals remain close?
| |-- YES -> 601/709/2020 matrix, primaries/profile, gamut mapping,
| | per-channel curve, or chroma processing.
| `-- NO -> continue.
|
|-- Are differences concentrated at colored edges or fine detail?
| |-- YES -> chroma siting/upsampling, scaling kernel, nonlinear filtering,
| | sharpening, alpha, or GPU precision.
| `-- NO -> continue.
|
|-- Are highlight differences content-dependent, spatially local, or capped in nits?
| |-- YES -> HDR/SDR tone mapping, local adaptation, dynamic metadata,
| | target peak/reference-white mismatch.
| `-- NO -> continue.
|
`-- Fit a combined model: per-channel 3x3 matrix + offset + monotonic curves;
inspect residuals and pipeline metadata. Do not label it “gamma” yet.
This tree deliberately checks numeric identity before appearance. If two applications display the same decoded RGB differently, changing the file’s pixels may “fix” one application while corrupting every correctly managed path.
For normalized, positive, unclipped corresponding neutral samples , fit
Taking logarithms:
A linear regression gives exponent and gain . The assumptions are strict:
A more flexible model is
but parameter coupling makes it easy to overfit. Prefer known range endpoints and standard transfer functions over an unconstrained curve when metadata suggests a standard.
For exactly the same frame but unknown spatial alignment, compare channel or luminance quantiles:
A monotonic transfer can be estimated from many quantile pairs. This is useful for exposure/range/gamma hypotheses but is not uniquely identifying: different spatial images can share a histogram, and matrix/gamut changes alter channel distributions. Use it to propose a curve, then verify with registered patches or a test chart.
Histogram clues:
Include exact values 0, 1, 2, 4, 8, 12, 16, 20, 32, 64, 128, 192, 235, 240, and 255 for 8-bit tests, plus equivalent 10-bit values. Use both:
This separates transfer interpretation from quantization spacing.
Include below-black, black, near-black, white, and above-white values. For 8-bit narrow video, explicitly mark 0, 16, 235, and 255; for chroma include 16, 128, and 240. A range error becomes immediately visible and measurable.
Use equal-area black/white checks at multiple spatial frequencies, then downscale or blur. Correct linear-light averaging tends toward 50% light, encoded as sRGB ≈0.735357. A nonlinear-domain average tends toward code 0.5. Include a zone plate to expose kernel and mipmap changes.
Use red, green, blue, cyan, magenta, yellow, skin-like colors, and neutrals. A 601/709 matrix mismatch leaves neutral gray close while moving saturated patches. Add near-gamut-boundary values to reveal clipping.
Use alternating red/blue, red/gray, and blue/gray one-pixel vertical and horizontal edges. Inspect Cb/Cr planes and reconstructed RGB separately. Shift the assumed chroma location by half a sample to reproduce the fringe.
Use colored shapes with alpha values 1, 0.75, 0.5, 0.25, 0.01, and 0 over black, white, and saturated backgrounds. Compare straight and premultiplied representations. Nonlinear and premultiplication errors appear at different edge positions.
For PQ, include patches encoded for known absolute values such as 0.005, 0.1, 1, 10, 100, 203, 1000, and 4000 cd/m². For HLG, include reference-white and peak-related levels under a stated and system gamma. Measure display output with an instrument if the question concerns actual nits rather than decoded values.
A test is incomplete if it records only what the viewer looked like.
If pixels are correct and tags are wrong, repair tags. If tags are correct and one application ignores them, configure or replace that application. Alter pixels only when the intended color representation truly changes or when a measured historical transform must be inverted for archival restoration.
A metadata-only repair is not harmless if the original pixels were actually created under different assumptions; verify against a known reference before relabeling.
Do not linearize merely because “linear is always correct.” A transfer function is part of a representation; the operation’s physical or perceptual meaning determines the correct domain.
Decode and transform in float or at least 16-bit integer where possible. Avoid repeated 8-bit round trips. Delay clipping until a defined gamut/range boundary. Dither at the final reduction to a lower integer bit depth; do not use dither to hide an incorrect curve.
Where the format permits both bitstream and container declarations, write consistent values. Preserve:
Automated CI should reject contradictory tags and compare a decode against golden patch values.
A reference output should have a known display EOTF, peak, black, surround, and signal range. GUI viewers are useful but are mediated by OS color management. Consumer previews are deliberately variable. Do not force all three to match by baking an undocumented correction into the master.
Use 203 cd/m² where the relevant HDR standard/workflow defines HDR reference white. Do not assume that every SDR master, desktop white, or display calibration should be 203 nits. Record whether a value is SDR reference-display white, HDR diffuse/reference white, graphics white, or display peak.
If a legacy application truly requires a corrective LUT, name it after the measured path, for example:
AE_CS5_WIN_QT_H264_measured_inverse_x_pow_0p88.cube
Document the source test, range, plane, precision, and expected hash.
Do not call it simply quicktime_gamma_fix.cube.
| Popular claim | Actual situation | Evidence strength |
|---|---|---|
| “sRGB is gamma 2.2.” | sRGB is a piecewise linear/offset-2.4 function; 2.2 is a fitting shorthand whose accuracy depends on range and metric. | A |
| “BT.709 gamma is 2.4.” | BT.709 specifies an OETF; BT.1886 specifies the reference display EOTF. | A |
| “0.45 × 2.4 = 1.08, so 1.2 is wrong.” | BT.709 has an offset and varying local slope; upper-tone effective camera exponent is near 0.5. ITU explicitly defines conventional SDR rendering intent around system gamma 1.2. | A/L |
| “The camera OETF and display EOTF should be inverses.” | Their composition includes the intended OOTF; unity is not required. | A |
| “BT.1886 is exactly .” | Only when ; otherwise change the curve. | A |
| “CRT gamma was exactly 2.5.” | 2.5 is a representative value; measured useful-range values vary, commonly about 2.35–2.55. | L |
| “Mac monitors physically had gamma 1.8.” | The effective system used LUT compensation plus an ordinary CRT. | L |
| “Apple chose 1.8 specifically for the LaserWriter.” | Broad print/prepress compatibility is supported; a first-party LaserWriter-specific design proof was not found. | L/C, limited |
| “Snow Leopard changed Mac gamma in 2010.” | Mac OS X 10.6 shipped 28 August 2009 and changed the default 1.8→2.2. | B |
| “QuickTime always adds 0.88 gamma.” | One tested 2010 AE CS5/Windows/QuickTime H.264 path was fit as ; other paths and versions differ. | C, path-specific |
“Levels(gamma=0.88) applies
.” |
It applies in normalized form and therefore darkens. | B |
| “A lifted black is a gamma error.” | Full/narrow mismatch is affine and should be tested first. | A/mathematical |
| “Y′ is luminance.” | Y′ is a weighted sum of nonlinear components; equality with physical luminance holds only for neutrals/special cases. | A |
| “A 601/709 mismatch changes only brightness.” | It primarily produces color-dependent RGB errors; neutrals remain neutral. | A/mathematical |
| “Linear-light processing is always visually correct.” | It is radiometrically appropriate for light integration; some standards deliberately define perceptual/code-space operations. | A/engineering |
“PNG gAMA beats an ICC profile.” |
Current PNG precedence places iCCP above
sRGB and cHRM+gAMA;
cICP is highest. |
A |
| “CSS and SVG brightness filters use the same space.” | CSS filter functions use sRGB; SVG primitives default to linearRGB. | A |
| “Canvas readback is raw device bytes.” | getImageData can convert to a requested color
space/pixel format. |
A |
| “HDR-to-SDR is a gamma conversion.” | It is a rendering/tone/gamut mapping problem with absolute/reference-white decisions. | A |
| “If two screenshots match, the pipeline is correct.” | Two wrong display paths can match; validate decoded values, metadata, and measured output. | Methodological |
| Term | Domain mapping | Common confusion |
|---|---|---|
| OETF | scene-linear light → nonlinear signal | called “display gamma” |
| inverse OETF | nonlinear signal → scene-linear representation | confused with EOTF |
| EOTF | nonlinear signal → display-linear light | assumed to invert camera OETF |
| OOTF | scene-linear light → display-linear light | omitted from simplistic gamma diagrams |
| TRC | generic tone-response curve, often ICC channel curve | treated as necessarily a pure gamma |
| system gamma | effective/OOTF exponent under stated conditions | treated as one exact exponent over a piecewise curve |
| Y′ | luma-like weighted sum of nonlinear RGB | called physical luminance Y |
| luminance Y | weighted sum of linear RGB in a colorimetric space | computed from primed components |
| narrow range | nominal legal video range with head/foot room | called “gamma-compressed range” |
| reference white | specified white level in a rendering system | confused with display peak |
| PQ | absolute display EOTF | treated as relative gamma |
| HLG | scene-relative OETF plus display OOTF | treated as one fixed display curve |
| Function | Key parameters |
|---|---|
| sRGB encode | threshold 0.0031308; slope 12.92; exponent 1/2.4; scale 1.055; offset 0.055 |
| sRGB decode | threshold 0.04045; reciprocal slope 1/12.92; exponent 2.4 |
| BT.601/709 OETF | threshold 0.018; slope 4.5; exponent 0.45; scale 1.099; offset 0.099 |
| BT.2020 OETF exact | α=1.09929682680944…; β=0.018053968510807…; exponent 0.45 |
| BT.2020 10-bit practical | α=1.099; β=0.018 |
| BT.2020 12-bit practical | α=1.0993; β=0.0181 |
| BT.1886 | γ=2.4; from |
| Adobe RGB (1998) | decode exponent 563/256=2.19921875 |
| PQ | ; ; ; ; |
| HLG | a=0.17883277; b=0.28466892; c=0.55991073; γ=1.2 at 1000 nits |
# Encoded source RGB -> encoded destination RGB.
# Source/destination functions must be exact named functions, not labels like "2.2-ish".
def convert(rgb_code, decode_source, encode_destination,
linear_rgb_matrix=None, rendering_transform=None):
linear = decode_source(rgb_code)
if linear_rgb_matrix is not None:
linear = linear_rgb_matrix @ linear
if rendering_transform is not None:
# OOTF, tone map, gamut map, exposure, or adaptation explicitly chosen.
linear = rendering_transform(linear)
return encode_destination(linear)For Y′CbCr input, range normalization and inverse matrix precede this function. For Y′CbCr output, destination encoding precedes the destination matrix and range quantization. For PQ, the decoded value is absolute luminance unless a specified production transform changes the domain.
[A-01] IEC 61966-2-1:1999, Multimedia systems and equipment — Colour measurement and management — Part 2-1: Colour management — Default RGB colour space — sRGB, including Amendment 1. IEC publication page: https://webstore.iec.ch/publication/6169. Public parameter registry: https://registry.color.org/rgb-registry/srgb.
[A-02] ITU-R BT.601-7 (03/2011), Studio encoding parameters of digital television for standard 4:3 and wide-screen 16:9 aspect ratios, §2.6.4 for OETF and digital-representation clauses. https://www.itu.int/dms_pubrec/itu-r/rec/bt/r-rec-bt.601-7-201103-i!!pdf-e.pdf.
[A-03] ITU-R BT.709-6 (06/2015), Parameter values for the HDTV standards for production and international programme exchange, Part 1, item 1.2 for OETF; Part 2 for current production parameters. https://www.itu.int/rec/R-REC-BT.709-6-201506-I/en.
[A-04] ITU-R BT.1886-0 (03/2011), Reference electro-optical transfer function for flat panel displays used in HDTV studio production, Scope and Annex 1, Eq. (1)–(4). https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.1886-0-201103-I!!PDF-E.pdf.
[A-05] ITU-R BT.2020-2 (10/2015), Parameter values for ultra-high definition television systems for production and international programme exchange, Table 4 for OETF and tables defining NCL/CL. https://www.itu.int/rec/R-REC-BT.2020-2-201510-I/en.
[A-06] SMPTE ST 2084:2014, High Dynamic Range Electro-Optical Transfer Function of Mastering Reference Displays. DOI: https://doi.org/10.5594/SMPTE.ST2084.2014.
[A-07] ITU-R BT.2100-3 (02/2025), Image parameter values for high dynamic range television for use in production and international programme exchange, Tables 4–5, notes 5e–5f, Annexes 1–2. https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.2100-3-202502-I!!PDF-E.pdf.
[A-08] ITU-R Report BT.2390-12 (03/2025), High dynamic range television for production and international programme exchange, especially §6.2 on OOTF/system gamma. https://www.itu.int/dms_pub/itu-r/opb/rep/R-REP-BT.2390-12-2025-PDF-E.pdf.
[A-09] ITU-R Report BT.2408-9 (03/2026), Operational practices in HDR television production, Table 4 for monitor luminance/reference white. https://www.itu.int/dms_pub/itu-r/opb/rep/R-REP-BT.2408-9-2026-PDF-E.pdf.
[A-10] ITU-R Report BT.2446-1 (2021), Methods for conversion of high dynamic range content to standard dynamic range content and vice-versa. https://www.itu.int/pub/R-REP-BT.2446.
[A-11] ITU-T H.273 (03/2024), Coding-independent code points for video signal type identification, tables for colour primaries, transfer characteristics, matrix coefficients, range, and chroma location. https://www.itu.int/rec/T-REC-H.273.
[A-12] W3C, Portable Network Graphics (PNG) Specification, Third Edition, Recommendation 24 June 2025, §4.3 and color-space chunk chapters. https://www.w3.org/TR/png-3/.
[A-13] W3C, Filter Effects Module Level 1,
18 December 2018, §§8–10 (feComposite,
feComponentTransfer,
color-interpolation-filters). https://www.w3.org/TR/filter-effects-1/.
[A-14] WHATWG, HTML Living Standard, Canvas
2D context, color spaces, ImageData, and
getImageData settings. https://html.spec.whatwg.org/multipage/canvas.html.
[A-15] W3C, CSS Color Module Level 4, predefined sRGB and Display-P3 spaces and transfer functions. https://www.w3.org/TR/css-color-4/.
[A-16] IEC 61966-2-2:2003, Extended RGB colour space — scRGB. Public ICC registry entry: https://registry.color.org/rgb-registry/scrgb.
[A-17] ICC, Embedding ICC Profiles in JPEG Files, technical note, APP2 chunk structure. https://www.color.org/technotes/ICC-Technote-ProfileEmbedding.pdf.
[A-18] ISO/CIE 11664-6:2014, Colorimetry — Part 6: CIEDE2000 colour-difference formula. https://www.iso.org/standard/63731.html.
[A-19] ITU-R BT.2035, A reference viewing environment for evaluation of HDTV programme material or completed programmes. https://www.itu.int/rec/R-REC-BT.2035.
[A-20] ITU-R BT.2124-0 (01/2019), Objective metric for the assessment of the potential visibility of colour differences in television. https://www.itu.int/rec/R-REC-BT.2124.
[A-21] ISO/IEC 14496-12, ISO base media file
format, colr/colour-information box family; current
edition should be consulted for nclx syntax. ISO catalogue:
https://www.iso.org/standard/83102.html.
[A-22] ITU-T H Supplement 15 (2017), Conversion and coding practices for HDR/WCG Y′CbCr 4:2:0 video with PQ transfer characteristics, and H Supplement 19 for HDR/WCG signalling and chroma location. https://www.itu.int/rec/T-REC-H.Sup15.
[B-01] ICC Three Component Color Encoding Registry, sRGB, exact public encoding characteristics. https://registry.color.org/rgb-registry/srgb.
[B-02] Adobe, Adobe RGB (1998) Color Image Encoding, May 2005; ICC registry entry gives gamma 2.19921875. https://www.adobe.com/digitalimag/pdfs/AdobeRGB1998.pdf; https://registry.color.org/rgb-registry/adobe-rgb-1998.
[B-03] ICC registry, scRGB and
scRGB-nl. https://registry.color.org/rgb-registry/scrgb.
[B-04] Apple Technical Note TN2162, Uncompressed
Y′CbCr Video in QuickTime Files, published 14 December 1999;
sections on colr, nclc, gama, and
legacy Macintosh graphics conversion. https://developer.apple.com/library/archive/technotes/tn2162/_index.html.
[B-05] Apple, QuickTime File Format Specification (archived), image-description extensions including gamma/color information. https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/.
[B-06] Adobe, Color management — After Effects, updated 27 March 2026; working space, linearization, and “Blend Colors Using 1.0 Gamma.” https://helpx.adobe.com/after-effects/desktop/adjust-colors/color-management/color-management.html.
[B-07] Adobe, How Color Management works — Premiere, updated 7 January 2026; input, working, output, tone mapping, and gamut compression. https://helpx.adobe.com/premiere/desktop/correct-color/set-up-color-management/how-color-management-works.html.
[B-08] FFmpeg project, FFmpeg Filters
Documentation, eq, lut,
colorspace, and zscale; consult the source
corresponding to the deployed release. https://ffmpeg.org/ffmpeg-filters.html.
[B-09] VapourSynth R76 documentation,
std.Levels, gamma direction and planes. https://www.vapoursynth.com/doc/functions/video/levels.html.
[B-10] AviSynth+ documentation, Levels,
exact
formula and YUV behavior. https://avisynthplus.readthedocs.io/en/latest/avisynthdoc/corefilters/levels.html.
[B-11] mpv stable manual, color management, linear downscaling, target TRC/peak, ICC, gamma factor, and tone mapping. https://mpv.io/manual/stable/.
[B-12] libplacebo documentation/source, GPU color-management and tone-mapping pipeline. https://libplacebo.org/; https://code.videolan.org/videolan/libplacebo.
[B-13] WebKit Bug 185343, “CSS filters which
reference SVG filters fail to respect the
color-interpolation-filters of the filter,” reported/fixed
May 2018. https://bugs.webkit.org/show_bug.cgi?id=185343.
[B-14] WebKit Bug 136418 and related Bug 212649, historical linearRGB Gaussian-blur/filter behavior. https://bugs.webkit.org/show_bug.cgi?id=136418.
[B-15] Mozilla Bug 948265, “Render CSS Filters using Moz2D,” verified fixed, milestone Firefox 34. https://bugzilla.mozilla.org/show_bug.cgi?id=948265.
[B-16] Chromium Issue 41007781, public-indexed
feGaussianBlur banding report; current tracker status
unavailable without sign-in during this research. https://issues.chromium.org/issues/41007781.
[B-17] Skia Issue 362116527, public-indexed
sRGB/Display-P3 SVG feColorMatrix difference; current
status unavailable without sign-in during this research. https://issues.skia.org/issues/362116527.
[B-18] WebKit, Wide Gamut Color in CSS with Display-P3, browser color matching and untagged-image assumptions. https://webkit.org/blog/10042/wide-gamut-color-in-css-with-display-p3/.
[B-19] Mozilla, Firefox 89 release notes for developers / color management changes on macOS, untagged images treated as sRGB. https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/89.
[B-20] Apple Newsroom, “Apple to Ship Mac OS X Snow Leopard on August 28,” 24 August 2009. https://www.apple.com/newsroom/2009/08/24Apple-to-Ship-Mac-OS-X-Snow-Leopard-on-August-28/.
[B-21] Apple, archived Mac OS X 10.6 refinements text: “The default gamma has been changed from 1.8 to 2.2…” The original page is retired; contemporaneous quotation is preserved in [C-06]. Treat the wording as an archived vendor statement, not a live support page.
[B-22] Microsoft, Use DirectX with Advanced Color on high/standard dynamic range displays, scRGB linear values and SDR white convention. https://learn.microsoft.com/windows/win32/direct3darticles/high-dynamic-range.
[B-23] Adobe, Color basics — After Effects, linear-light compositing guidance. https://helpx.adobe.com/after-effects/desktop/adjust-colors/color-basics/color-basics.html.
[B-24] FFmpeg Trac #9637, container
colr versus SPS VUI color metadata behavior. https://trac.ffmpeg.org/ticket/9637.
[C-01] Vitrolite, “The QuickTime Gamma Bug,” 31 December 2010. Empirical AE CS5/Windows/QuickTime tests, including and a separate QuickTime Player contrast/range result. https://vitrolite.wordpress.com/2010/12/31/quicktime_gamma_bug/.
[C-02] ProVideo Coalition, After Effects CS3/QuickTime gamma-adjustment discussion, 31 October 2007. Used only as evidence of an Adobe-exposed legacy compatibility option and contemporary behavior reports.
[C-03] Video Copilot, QuickTime gamma-shift workflow discussion, 17 June 2008. Used as evidence that the workaround class was circulating, not as a normative mechanism.
[C-04] AnimeMusicVideos forum, 2013 discussion using
Levels(0,0.88,255,0,255) and linking the 2010 empirical
article. Community propagation evidence.
[C-05] SilentAperture, Advanced Encoding Guide — The 0.88 gamma bug. https://silentaperture.gitlab.io/mdbook-guide/filtering/detinting.html. Contemporary recipe; mechanism claims remain C-class.
[C-06] Creative COW, “Great news: default gamma in Snow Leopard will be 2.2,” 9 June 2009, quoting Apple’s then-live refinements page. https://creativecow.net/forums/thread/great-news-default-gamma-in-snow-leopard-will-be-2/.
[C-07] Apple Support Communities and production forums, 2006–2009 QuickTime/H.264/ProRes gamma-shift reports. Used only to establish chronology and variability; individual posts do not prove a universal transform.
[L-01] Charles Poynton, Frequently Asked Questions about Gamma, 1998 revision; CRT exponent range, Macintosh effective 1.8 behavior, and printing/imagesetter discussion. https://www.poynton.ca/pdf/GammaFAQ.pdf.
[L-02] Charles Poynton, “The rehabilitation of gamma,” SPIE/IS&T Electronic Imaging era technical paper; terminology and system-rendering discussion. https://www.poynton.ca/PDFs/Rehabilitation_of_gamma.pdf.
[L-03] BBC Research & Development, White Paper WHP 283, High Dynamic Range Television and Hybrid Log-Gamma / transfer-function engineering discussion; effective SDR camera exponent near 0.5, display 2.4, system about 1.2. https://downloads.bbc.co.uk/rd/pubs/whp/whp-pdf-files/WHP283.pdf.
[L-04] GTE Laboratories, US Patent 3,934,171 (1976), electron-gun drive/beam-current power-law behavior. https://patents.google.com/patent/US3934171A/en.
The companion gamma-analysis-repro.py implements sRGB,
BT.709, BT.1886, PQ, HLG, CIE Lab/CIEDE2000, matrix examples, and every
numerical error table in this article. It writes machine-readable
gamma-analysis-results.json. Recompute before adapting the
figures to a different bit depth, black level, white level, code range,
transfer pair, or perceptual metric.