HDR Tone Mapping and Color Gamut Conversion in Practice 2025
Published: Sep 26, 2025 · Reading time: 7 min · By Unified Image Tools Editorial
With the proliferation of HDR (High Dynamic Range) images, tone mapping and color gamut conversion techniques that achieve consistent color representation across different display environments have become increasingly important. This article provides detailed implementation-level explanations of conversion from HDR formats like PQ (Perceptual Quantizer) and HLG (Hybrid Log-Gamma) to sRGB and Display P3.
HDR Tone Mapping Fundamentals
Characteristics of Major HDR Standards
PQ (Perceptual Quantizer / SMPTE ST 2084)
- Expresses luminance up to 10,000 nits
- Absolute representation within fixed luminance range
- Widely adopted in film and broadcasting industry
- Enables more precise gradation representation
HLG (Hybrid Log-Gamma / ITU-R BT.2100)
- Emphasizes backward compatibility with SDR
- Relative luminance representation
- Designed for live broadcasting
- Device-dependent display adjustment
Internal Links: P3→sRGB Color Consistency Practical Guide 2025, HDR→sRGB Tonemapping Workflow 2025 — Reliable Distribution Pipeline
Color Gamut Conversion Theory
Processing Gamut Boundaries
In converting from wide gamut to narrow gamut, how to handle unreproducible colors is crucial:
// Gamut boundary check
function isInGamut(color, gamut) {
const [L, a, b] = rgbToLab(color);
return checkGamutBoundary(L, a, b, gamut);
}
// Clipping vs compression
function gamutMapping(color, sourceGamut, targetGamut) {
if (isInGamut(color, targetGamut)) {
return color; // No conversion needed
}
// Perceptual compression
return perceptualCompress(color, sourceGamut, targetGamut);
}
Conversion Matrix Selection
Rec.2020 → sRGB Conversion
[R'] [3.2406 -1.5372 -0.4986] [R]
[G'] = [-0.9689 1.8758 0.0415] × [G]
[B'] [0.0557 -0.2040 1.0570] [B]
Practical Tone Mapping Methods
ACES Tone Mapping
The film industry standard ACES tone mapping curve performs HDR to SDR conversion while maintaining natural appearance:
// ACES Tone Mapping
vec3 acesToneMapping(vec3 color) {
float a = 2.51;
float b = 0.03;
float c = 2.43;
float d = 0.59;
float e = 0.14;
return clamp((color * (a * color + b)) /
(color * (c * color + d) + e), 0.0, 1.0);
}
Reinhard Tone Mapping
Simple and effective Reinhard operator:
function reinhardToneMapping(hdrColor, whitePoint = 1.0) {
return hdrColor.map(channel =>
channel * (1 + channel / (whitePoint * whitePoint)) / (1 + channel)
);
}
Filmic Tone Mapping
Film-texture-focused approach:
vec3 filmicToneMapping(vec3 x) {
float A = 0.15; // Shoulder Strength
float B = 0.50; // Linear Strength
float C = 0.10; // Linear Angle
float D = 0.20; // Toe Strength
float E = 0.02; // Toe Numerator
float F = 0.30; // Toe Denominator
return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F))-E/F;
}
Color Gamut Conversion Implementation
Conversion via Lab Color Space
Using Lab color space as intermediate representation for most accurate color conversion:
import numpy as np
from colorspacious import cspace_convert
def convert_color_gamut(image, source_space, target_space):
"""
Color gamut conversion implementation
"""
# Linear RGB → Lab conversion
lab_image = cspace_convert(image, source_space, "CIELab")
# Gamut compression (if needed)
compressed_lab = apply_gamut_compression(lab_image, target_space)
# Lab → target color space conversion
result = cspace_convert(compressed_lab, "CIELab", target_space)
return np.clip(result, 0, 1)
def apply_gamut_compression(lab_color, target_gamut):
"""
Perceptual gamut compression
"""
L, a, b = lab_color[..., 0], lab_color[..., 1], lab_color[..., 2]
# Chroma calculation
chroma = np.sqrt(a**2 + b**2)
# Gamut boundary calculation
max_chroma = calculate_max_chroma(L, target_gamut)
# Compression ratio calculation
compression_ratio = np.where(chroma > max_chroma,
max_chroma / chroma, 1.0)
# Compressed color calculation
compressed_lab = np.stack([
L,
a * compression_ratio,
b * compression_ratio
], axis=-1)
return compressed_lab
Optimization Considering Perceptual Color Difference
Quality evaluation using CIEDE2000 color difference formula:
from colorspacious import deltaE
def evaluate_conversion_quality(original, converted):
"""
Conversion quality evaluation
"""
# Calculate color difference in Lab color space
original_lab = cspace_convert(original, "sRGB1", "CIELab")
converted_lab = cspace_convert(converted, "sRGB1", "CIELab")
# CIEDE2000 color difference
delta_e = deltaE(original_lab, converted_lab, input_space="CIELab")
# Acceptable range: ΔE < 2.3 (perceptually equivalent)
acceptable_ratio = np.mean(delta_e < 2.3)
return {
'mean_delta_e': np.mean(delta_e),
'max_delta_e': np.max(delta_e),
'acceptable_ratio': acceptable_ratio
}
Device Support and Profile Management
Utilizing ICC Profiles
// ICC profile application in WebGL
const iccProfileTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_3D, iccProfileTexture);
// Load profile as 3D LUT
function loadICCProfile(profileData) {
const lutSize = 64;
const lutData = new Uint8Array(lutSize * lutSize * lutSize * 4);
// Generate 3D LUT from profile
generateLUT(profileData, lutData, lutSize);
gl.texImage3D(gl.TEXTURE_3D, 0, gl.RGBA8,
lutSize, lutSize, lutSize, 0,
gl.RGBA, gl.UNSIGNED_BYTE, lutData);
}
Adaptive Tone Mapping
Parameter adjustment according to display device characteristics:
function getAdaptiveTonemapParams(displayInfo) {
const {
maxLuminance,
gamut,
gamma,
ambientLight
} = displayInfo;
// Adjustment based on ambient light
const adaptationFactor = calculateAdaptation(ambientLight);
// Mapping based on display gamut
const gamutCompression = calculateGamutCompression(gamut);
return {
exposure: adaptationFactor * 0.8,
whitePoint: maxLuminance / 100,
gamutCompression: gamutCompression,
gamma: gamma || 2.2
};
}
Performance Optimization
Utilizing GPU Processing
// Vertex shader
attribute vec4 position;
attribute vec2 texCoord;
varying vec2 vTexCoord;
void main() {
gl_Position = position;
vTexCoord = texCoord;
}
// Fragment shader
precision highp float;
varying vec2 vTexCoord;
uniform sampler2D hdrTexture;
uniform sampler3D lutTexture;
uniform float exposure;
uniform float gamma;
vec3 ACESFilmic(vec3 x) {
float a = 2.51;
float b = 0.03;
float c = 2.43;
float d = 0.59;
float e = 0.14;
return clamp((x*(a*x+b))/(x*(c*x+d)+e), 0.0, 1.0);
}
void main() {
vec4 hdrColor = texture2D(hdrTexture, vTexCoord);
// Exposure adjustment
vec3 exposedColor = hdrColor.rgb * exposure;
// Tone mapping
vec3 toneMapped = ACESFilmic(exposedColor);
// Gamma correction
vec3 gammaCorrected = pow(toneMapped, vec3(1.0 / gamma));
// 3D LUT application (color gamut conversion)
vec3 lutColor = texture3D(lutTexture, gammaCorrected).rgb;
gl_FragColor = vec4(lutColor, hdrColor.a);
}
Parallel Processing Implementation
import multiprocessing as mp
from functools import partial
def process_hdr_batch(image_paths, source_space, target_space):
"""
Parallel execution of HDR conversion in batch processing
"""
pool_size = mp.cpu_count()
with mp.Pool(pool_size) as pool:
convert_func = partial(
convert_single_image,
source_space=source_space,
target_space=target_space
)
results = pool.map(convert_func, image_paths)
return results
def convert_single_image(image_path, source_space, target_space):
"""
Single image HDR conversion processing
"""
# Image loading
image = load_hdr_image(image_path)
# Tone mapping
tone_mapped = apply_tone_mapping(image)
# Color gamut conversion
converted = convert_color_gamut(tone_mapped, source_space, target_space)
# Save
output_path = get_output_path(image_path, target_space)
save_image(converted, output_path)
return output_path
Quality Evaluation and Testing
Automated Quality Assessment
def evaluate_hdr_conversion(original_hdr, converted_sdr, reference_sdr=None):
"""
HDR→SDR conversion quality evaluation
"""
metrics = {}
# Structural Similarity (SSIM)
metrics['ssim'] = calculate_ssim(converted_sdr, reference_sdr)
# Perceptual Image Quality Assessment (LPIPS)
metrics['lpips'] = calculate_lpips(converted_sdr, reference_sdr)
# Color histogram comparison
metrics['histogram_correlation'] = compare_histograms(
converted_sdr, reference_sdr)
# Dynamic range preservation rate
metrics['dynamic_range_preservation'] = calculate_dr_preservation(
original_hdr, converted_sdr)
return metrics
def calculate_dr_preservation(hdr_image, sdr_image):
"""
Dynamic range preservation rate calculation
"""
# Effective dynamic range of HDR
hdr_range = np.log10(np.max(hdr_image) / np.min(hdr_image[hdr_image > 0]))
# Effective dynamic range of SDR
sdr_range = np.log10(np.max(sdr_image) / np.min(sdr_image[sdr_image > 0]))
# Preservation rate
preservation_ratio = sdr_range / hdr_range
return preservation_ratio
A/B Testing Framework
class HDRConversionTester {
constructor(originalHDR, methods) {
this.originalHDR = originalHDR;
this.methods = methods;
this.results = {};
}
async runAllTests() {
for (const [methodName, method] of Object.entries(this.methods)) {
console.log(`Testing ${methodName}...`);
const startTime = performance.now();
const converted = await method.convert(this.originalHDR);
const endTime = performance.now();
this.results[methodName] = {
image: converted,
processingTime: endTime - startTime,
quality: await this.evaluateQuality(converted),
fileSize: this.calculateFileSize(converted)
};
}
return this.generateReport();
}
generateReport() {
const sortedResults = Object.entries(this.results)
.sort((a, b) => b[1].quality.overall - a[1].quality.overall);
return {
bestMethod: sortedResults[0][0],
rankings: sortedResults,
recommendations: this.generateRecommendations(sortedResults)
};
}
}
Practical Operational Considerations
Workflow Integration
# CI/CD pipeline example
hdr_processing:
stage: process
script:
- python scripts/batch_hdr_convert.py
--input-dir assets/hdr/
--output-dir dist/images/
--source-space rec2020
--target-space srgb
--tone-mapping aces
--quality-check
artifacts:
paths:
- dist/images/
reports:
- quality_report.json
Monitoring and Alerts
def setup_quality_monitoring():
"""
Quality monitoring setup
"""
quality_thresholds = {
'min_ssim': 0.85,
'max_lpips': 0.1,
'min_dynamic_range_preservation': 0.7
}
def quality_check_callback(metrics):
for metric, value in metrics.items():
if metric.startswith('min_') and value < quality_thresholds[metric]:
send_alert(f"Quality degradation: {metric} = {value}")
elif metric.startswith('max_') and value > quality_thresholds[metric]:
send_alert(f"Quality degradation: {metric} = {value}")
return quality_check_callback
Summary
HDR tone mapping and color gamut conversion are important technical domains in modern image processing. Through appropriate method selection and implementation, consistent color representation across different display environments can be achieved.
Key Points:
- Understanding Theory: Characteristics of PQ/HLG and principles of gamut conversion
- Method Selection: Proper use of ACES, Reinhard, and Filmic tone mapping
- Implementation Optimization: Performance improvement through GPU processing and parallel processing
- Quality Management: Continuous improvement through automated evaluation and A/B testing
Internal Links: P3→sRGB Color Consistency Practical Guide 2025, HDR→sRGB Tonemapping Workflow 2025 — Reliable Distribution Pipeline, Proper Color Management and ICC Profile Strategy 2025 — Practical Guide to Stabilize Web Image Color Reproduction
Related tools
Related Articles
HDR→sRGB Tonemapping Workflow 2025 — Reliable Distribution Pipeline
PQ/HLG→sRGB conversion strategies for highlight compression, saturation shifts, and banding prevention. Complete guide for 10bit→8bit and P3→sRGB transitions.
HDR / Display-P3 Image Delivery Design 2025 — Balancing Color Fidelity and Performance
Implementation guide for safely handling color gamuts beyond sRGB on the web. Practical color management considering ICC profiles, metadata, fallbacks, and viewer differences.
Display-P3 Utilization and sRGB Integration for Web 2025 — Practical Workflow
Practical workflow for safely distributing Display-P3 while ensuring color reproduction in sRGB environments. Comprehensive explanation from ICC/color space tags, conversion, to accessibility.
Proper Color Management and ICC Profile Strategy 2025 — Practical Guide to Stabilize Web Image Color Reproduction
Systematize ICC profile/color space/embedding policies and optimization procedures for WebP/AVIF/JPEG/PNG formats to prevent color shifts across devices and browsers.
CMYK Conversion and Gamut Check 2025 — Safe Handoff from sRGB/Display P3
Practical guide for transferring web materials to print. ICC profile selection, out-of-gamut detection and correction, black design, and vendor agreement formation.
Color Management and ICC Operations sRGB/Display-P3/CMYK Handoff 2025
Organize color profile operations from web to print. Explains sRGB and Display-P3 selection, CMYK handoff procedures, and practical points for embedding/conversion.