Lomont.org

(Also lomonster.com and clomont.com)

Color and HDR Notes: Best Practices and Gotchas

Published

Color and HDR: Best Practices, Gotchas and Error Recognition

Chris Lomont, with Claude Fable 5.2

What this article is. The symptom-first guide in a set of articles on color and HDR: what an error looks like, its likely cause, a quick test, the beliefs that cause most of the trouble, and the practices that prevent it.

Related articles. Expert notes: Color and HDR Notes . General-audience summary: Color and HDR Notes Summary . Lookup tables: Reference Tables . Companion topics: image resampling, dithering and quality metrics (summary , checklist ); sampling theory (summary , checklist ).

Contents

Each entry gives what you see, the likely cause, and a quick test.

Error gallery

1. What it looks like, and what it usually is

You see Usual cause Quick test
Milky blacks, dull whites, low contrast Narrow-range (16–235) data shown as full range Black measures code 16, white 235
Crushed shadows, clipped highlights Full-range data treated as narrow range, or range expanded twice Histogram has gaps and piles at both ends
Gray, flat, desaturated, dim PQ (HDR10) signal shown without its transfer function and gamut Mid-gray sits near code 50% and faces look putty-colored
Slightly flat and desaturated but watchable HLG shown as SDR; or BT.2020 SDR shown as BT.709 Saturated reds look orange-brown
Desaturated, hue of greens and reds off Wide-gamut pixels (P3, BT.2020) read as sRGB A pure P3 red reads as dull red; profile missing or ignored
Neon, oversaturated, sunburned skin sRGB pixels sent unmanaged to a wide-gamut panel, or tagged as P3/BT.2020 Compare to a color-managed viewer
Too dark, too contrasty, oversaturated Decoded twice (gamma applied to already-linear data) A value of 50% becomes about 21%
Pale, lifted shadows, low contrast Encoded twice, or linear data shown as if encoded Mid-gray 50% becomes about 73%
Shadows a little too light or too dark, the rest fine sRGB piecewise against pure 2.2; or BT.709 inverse OETF against BT.1886 Compare codes 1–20 only
Skin slightly too red or too green, greens off BT.601 and BT.709 matrices swapped Color bars: green and red luma change; common on SD/HD rescale
Strong green image, or green and purple Chroma offset missing (128 treated as 0), or zeroed chroma planes Gray areas turn green
Blue and red swapped (orange sky, blue faces) Cb/Cr swapped, or RGB/BGR order Color bars
Colored fringes on one side of sharp colored edges Chroma siting mismatch Red text on blue, zoomed
Thin colored lines and small colored text dim and smeared 4:2:0 or 4:2:2 chroma subsampling Compare with 4:4:4; see cs12_chroma_subsampling.png
Steps or contours in skies and dark gradients Too few bits for the transfer function (8-bit HDR, 10-bit clean graphics), lost dither, or processing after quantization Contours move when you add a small offset before rounding
Blue and yellow gradients band, others do not R11G11B10F: blue has one bit less Switch the render target to half float
Dark saturated colors noisy or blocky next to bright ones Shared-exponent format (RGB9E5, RGBE)
Dark fringes where saturated colors meet (red against green) Blending or resizing on encoded values Redo in linear; see cs14_mixing_spaces.png
Dark or light halos at soft alpha edges Premultiplied and straight alpha mixed, or premultiply done on encoded values
Orange sun ring turns yellow, sky near the sun cyan, colors snap to white Per-channel clipping of highlights Reduce exposure 2 stops and compare hue
Highlights desaturate and drift in hue (red to orange, blue to cyan) Per-channel tone curve cs15_tonemap_hue_paths.png
Bright saturated lights look flat and poster-like Luminance-only tone mapping with gamut clip
Blue gradients turn purple Interpolation or gamut mapping in CIELAB Use Oklab or ICtCp
HDR photo looks fine in one app, plain SDR in another Gain map stripped or not understood File size dropped; no second image after the first JPEG end marker
Halos or ghost outlines around bright objects in an HDR photo Gain map out of step with an edited, cropped or resized base cs18_gain_maps.png
HDR photo changes look as screen brightness changes Headroom-dependent gain-map weight; working as designed
HDR video looks dim in a bright room PQ is absolute and was graded for a dim room; display applies no ambient compensation
SDR content washed out when the desktop is in HDR mode SDR mapped with a different curve or white level than the SDR display used
Two calibrated displays do not match in white Observer metamerism between different emission spectra Not fixable by 1931 colorimetry
Screenshot of HDR content looks wrong Capture stored linear or PQ values without tags, or clipped at SDR white
Colors change after saving a “lossless” file Profile dropped, range flag changed, or premultiply round trip Diff the pixels, then the metadata
One pixel in four differs by one code between two “identical” paths One path encodes with round(255 f), the other with floor(256 f) Sweep a float ramp through both and count mismatches (expect 24.9%)
Tones creep toward black and white over many save or process cycles Encoder and decoder use different rounding conventions Run a mid-gray through 100 cycles
Bright or infinite values are black on one CPU and white on another Out-of-range float converted to integer without a clamp Encode +∞ on both
White is 1020 instead of 1023, or 248 instead of 255 Bit depth changed by shifting on full-range data Convert pure white
Edges of transparent sprites too dark Color premultiplied by alpha on encoded values A white pixel at 50% alpha should hold 50% of the light
Saturated blues and reds go dark, sometimes nearly black, after subsampling or chroma filtering in HDR Chroma filtered in non-constant-luminance $Y'C_bC_r$ with PQ One-pixel saturated blue line on white
An object changes hue as it gets dimmer Wide-gamut color converted to a smaller gamut by per-channel clipping Fade a saturated orange
HDR chroma about twice too strong in an analysis tool PQ ICtCp matrix applied to an HLG signal
HLG picture a factor of 12 off, or mid-gray wildly wrong The two HLG normalizations mixed Feed scene value 1.0 through the library
Image looks right in one viewer and washed out in another, file “tagged sRGB” Range flag left at narrow, or several contradictory color chunks in a PNG Dump the tags
Lab values of HDR content all dark, ΔE tolerances useless CIELAB normalized to display peak instead of diffuse white $L^{\ast}$ of the reference white should be 100
Banding along one axis of a 3-D lookup table Table stored as a 2-D strip and sampled with a single bilinear fetch Blend two slices
Red and blue swapped after converting a lookup-table file Axis order differs between formats
Shadows crushed or banded only on phones Shader running at low or medium precision Count distinct outputs of a ramp at each precision

2. Thirty things that are commonly believed and wrong

  1. “Gamma is for CRTs.” It is perceptual coding: it spends codes evenly in visual terms. Linear 8-bit is unusable for images.
  2. “sRGB is gamma 2.2.” The encoding is piecewise with a linear toe. Whether the display should be 2.2 is disputed.
  3. “BT.709 is gamma 2.2” or “the inverse of the 709 camera curve.” The display curve is BT.1886, a 2.4 power with a black offset.
  4. “Luma is luminance.” Luma is a weighted sum of encoded values. Part of the luminance is in the chroma channels.
  5. “YUV is a color space.” It is a family of encodings of some RGB space; without primaries, transfer, matrix and range it is undefined.
  6. “4:2:0 only loses color detail.” It loses luminance on saturated detail.
  7. “10-bit means HDR.” Bit depth, transfer function and gamut are independent. 10-bit SDR and 8-bit HDR both exist.
  8. “HDR means brighter.” It means more range: deeper shadows and small bright highlights. Average picture level is meant to stay near SDR.
  9. “PQ is absolute, so every display shows the same thing.” Displays tone-map above their peak and dim large bright areas.
  10. “HDR10 is 1000 nits.” HDR10 is a 10,000 cd/m² container. Mastering peaks of 1000 and 4000 are common.
  11. “203 nits is where white goes on every HDR display.” It is a signal level; HLG displays show it at different luminances by design.
  12. “Nits tell you how bright it looks.” Saturated colors look brighter than whites of the same luminance.
  13. “Wider gamut means more saturated pictures.” Correctly managed, in-gamut colors look the same; only colors outside the old gamut change.
  14. “P3 is P3.” DCI-P3, P3-D65 and Display P3 differ in white point and transfer function.
  15. “Covers 100% of BT.2020.” Check: coverage or ratio, xy or u′v′, at what luminance.
  16. “A gamut is a triangle.” It is a volume that narrows toward white, faster on displays with a white sub-pixel.
  17. “XYZ is device independent, so equal XYZ looks equal.” Only for the standard observer, at one adaptation state, in one surround.
  18. “The CIE 1931 observer was replaced.” It is still the basis of every imaging standard, known blue error and all.
  19. “N bits is N stops.” Only for linear integers, and only by the weakest definition.
  20. “This camera has 15 stops.” At what SNR, per pixel or normalized, measured by whom?
  21. “Film has 14 stops, digital has less.” No source for 14 was found. Kodak’s own figure for a current negative stock is at least 11 stops of exposure latitude, which is not a noise-based measure. Good sensors reach 13 to 14 stops by the engineering definition.
  22. “The eye sees 20-plus stops.” About 11 to 12 at one adaptation state; far more over time.
  23. “OLED has infinite contrast.” In a dark room with a black screen. In a lit room, reflections set the black.
  24. “Raw is the sensor data.” Phone raw files may be demosaiced, tone-mapped or multi-frame merged.
  25. “Lossless formats preserve the image.” Only if profile, range, alpha convention and bit depth survive every step.
  26. “Linear light is always right.” For physical operations, yes. Gradients, quantization, gamut mapping and sharpening are better in perceptual spaces.
  27. “Half float is overkill.” It is the smallest common format that is below the visibility threshold at every level with headroom for arithmetic.
  28. “A LUT is a look.” A LUT is a function with an undeclared input and output space. Applied to the wrong encoding it is noise.
  29. “Gain maps are just metadata.” They are a second image that can drift from, or contradict, the first.
  30. “6P projectors have six primaries.” Two RGB sets, one per eye.

3. Practices that prevent most of it

  1. Carry the tags (primaries, transfer, matrix, range) with every buffer, and assert on them at module boundaries. Treat “unspecified” as an error.
  2. Convert once, to a linear working space with a declared reference white, and encode once at the end.
  3. Keep a parameter for reference white. Do not hard-code 80, 100 or 203.
  4. Use half float or better for working buffers. Dither when reducing bit depth.
  5. Do tone mapping and gamut mapping together, not as independent steps.
  6. After any geometric or tonal edit to a gain-map image, regenerate or transform the map, or drop it deliberately.
  7. Test with synthetic patterns, not photographs: color bars in each range, a gray ramp with codes 0–20 expanded, one-pixel red/blue lines, a saturated-color exposure sweep, a P3-only red patch.
  8. Check the tail of the pipeline: what the operating system, browser or player does after your code is often the largest error.
  9. When reporting a number, attach its definition: stops (which quantity, which criterion), gamut (which diagram, coverage or ratio), ΔE (which formula, observer, white), nits (window size, duration).
  10. For color-critical matching across display technologies, verify by eye with more than one person.
  11. Use one rounding rule everywhere: round(255 f) to encode, i/255 to decode, with the clamp done in floating point. Never pair floor(256 f) with i/255.
  12. Change bit depth by exact scaling for full-range data and by shifting only for narrow range.
  13. Premultiply in linear light, and remember that 8-bit premultiplied color has lost precision that cannot be recovered.
  14. For CIELAB and ΔE on display light, normalize to diffuse white, and state the formula, observer and white with every number.
  15. Copy constants from the standard, not from slides or code, and keep a unit test with a published test vector for each transfer function and matrix.
  16. Insert graphics after tone mapping, at the same reference white as the program.
  17. When making a gain map, compute it against the base image as a decoder will reconstruct it, keep the default offsets, and regenerate it after any edit.

4. Numeric and rounding traps

  • floor(256 f) paired with i/255 loses half a bit and drifts. It is the most common float-to-byte code in the wild. §7.4
  • (a*b) >> 8 is wrong for 72% of inputs. Use t = a*b + 128; (t + (t >> 8)) >> 8. §7.4
  • v >> 8 is the wrong way to make 8 bits from 16 on a quarter of all values. §7.4
  • Two correct-looking float-to-byte routines differ on 128 inputs because of single-precision rounding. Do the multiply in double. §7.4
  • An 8-bit image survives a half float only with the right decode rule. §7.4
  • Linear data needs four more bits than the encoded data it came from for a lossless round trip. §7.4
  • Clamping noise at zero after black subtraction lifts and tints the shadows. §7.4
  • RGBE files have two valid reader and writer pairings, and the most copied reference code mixes them, doubling the error.
  • Formats without a sign bit cannot hold wide-gamut colors expressed in a smaller space. The negatives clip silently. §7.1
  • What a half float can represent in the blacks depends on what 1.0 means. With 1.0 as 203 cd/m² the smallest normal value is coarser than 12-bit PQ’s darkest codes. §5.4
  • Dither goes before the quantizer, in the space being quantized. Noise added afterwards does nothing for banding. §7.3

5. Tag, standard and tool traps

  • Tagging a stream is not converting it. §5.3
  • The range flag defaults to narrow. §12.1
  • PQ landmarks are not round numbers: signal 0.5 is 92 cd/m², not 100. §5.2
  • “HLG gamma is 1.2” is true at 1000 cd/m² only, and the formula changes outside 400 to 2000. §5.3
  • The same tone curve applied twice compresses twice. Static tone mapping is not stable under repeated conversion between PQ and HLG; a hard clip is. §10.1
  • Reference monitors should clip, not tone-map, and should show which pixels are out of range.
  • “Peak nits” without a window size is meaningless. Some panels reach their peak only on a few percent of the screen. §4
  • Gamut area depends on the diagram. P3 is 36% larger than sRGB in xy and 26% larger in u′v′. §3
  • A gain map’s headroom depends on which white you divide by. §11.3
  • A color temperature is reproducible to a kelvin or two at best, and should not be quoted at all far from the Planckian locus.
  • Narrow-band spectra must not be interpolated. Interpolate the color-matching functions to the measured wavelengths instead. §2
  • ICC version 2 and version 4 encode Lab differently, and ICC calibration has no HDR model. §12.1

Comments