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:

  1. Understanding Theory: Characteristics of PQ/HLG and principles of gamut conversion
  2. Method Selection: Proper use of ACES, Reinhard, and Filmic tone mapping
  3. Implementation Optimization: Performance improvement through GPU processing and parallel processing
  4. 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 Articles

Color

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.

Color

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.

Color

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.

Color

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.

Printing

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

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.