HDRトーンマッピングとカラーガマット変換の実践 2025

公開: 2025年9月26日 · 読了目安: 8 · 著者: Unified Image Tools 編集部

HDR(High Dynamic Range)画像の普及に伴い、異なるディスプレイ環境で一貫した色表現を実現するトーンマッピングとカラーガマット変換技術が重要になってきました。本記事では、PQ(Perceptual Quantizer)、HLG(Hybrid Log-Gamma)といったHDR方式から、sRGBやDisplay P3への変換を実装レベルで詳しく解説します。

HDRトーンマッピングの基礎

主要なHDR規格の特徴

PQ (Perceptual Quantizer / SMPTE ST 2084)

  • 最大10,000 nitsまでの輝度を表現
  • 固定輝度範囲での絶対的な表現
  • 映画・放送業界で広く採用
  • より精密な階調表現が可能

HLG (Hybrid Log-Gamma / ITU-R BT.2100)

  • SDRとの後方互換性を重視
  • 相対的な輝度表現
  • 生放送に適した設計
  • デバイス依存の表示調整

内部リンク: P3→sRGB 変換で崩れない色管理 実務ガイド 2025, HDR→sRGBトーンマッピング実務 2025 — 破綻させない配信フロー

カラーガマット変換の理論

ガマット境界の処理

広色域から狭色域への変換では、再現不可能な色をどう扱うかが重要です:

// ガマット境界チェック
function isInGamut(color, gamut) {
  const [L, a, b] = rgbToLab(color);
  return checkGamutBoundary(L, a, b, gamut);
}

// クリッピング vs 圧縮
function gamutMapping(color, sourceGamut, targetGamut) {
  if (isInGamut(color, targetGamut)) {
    return color; // 変換不要
  }
  
  // 知覚的圧縮
  return perceptualCompress(color, sourceGamut, targetGamut);
}

変換マトリックスの選択

Rec.2020 → sRGB変換

[R']   [3.2406 -1.5372 -0.4986]   [R]
[G'] = [-0.9689  1.8758  0.0415] × [G]
[B']   [0.0557 -0.2040  1.0570]   [B]

実践的なトーンマッピング手法

ACES トーンマッピング

映画業界標準のACESトーンマッピングカーブは、自然な見た目を保ちながらHDRからSDRへの変換を行います:

// 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 トーンマッピング

シンプルで効果的なReinhardオペレーター:

function reinhardToneMapping(hdrColor, whitePoint = 1.0) {
  return hdrColor.map(channel => 
    channel * (1 + channel / (whitePoint * whitePoint)) / (1 + channel)
  );
}

Filmic トーンマッピング

映画的な質感を重視したアプローチ:

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;
}

カラーガマット変換の実装

Lab色空間を経由した変換

最も正確な色変換を行うため、Lab色空間を中間表現として使用:

import numpy as np
from colorspacious import cspace_convert

def convert_color_gamut(image, source_space, target_space):
    """
    カラーガマット変換の実装
    """
    # 線形RGB→Lab変換
    lab_image = cspace_convert(image, source_space, "CIELab")
    
    # ガマット圧縮(必要に応じて)
    compressed_lab = apply_gamut_compression(lab_image, target_space)
    
    # Lab→目標色空間変換
    result = cspace_convert(compressed_lab, "CIELab", target_space)
    
    return np.clip(result, 0, 1)

def apply_gamut_compression(lab_color, target_gamut):
    """
    知覚的ガマット圧縮
    """
    L, a, b = lab_color[..., 0], lab_color[..., 1], lab_color[..., 2]
    
    # 彩度の計算
    chroma = np.sqrt(a**2 + b**2)
    
    # ガマット境界の計算
    max_chroma = calculate_max_chroma(L, target_gamut)
    
    # 圧縮比の計算
    compression_ratio = np.where(chroma > max_chroma,
                                 max_chroma / chroma, 1.0)
    
    # 圧縮された色の計算
    compressed_lab = np.stack([
        L,
        a * compression_ratio,
        b * compression_ratio
    ], axis=-1)
    
    return compressed_lab

知覚的色差を考慮した最適化

CIEDE2000色差式を使用した品質評価:

from colorspacious import deltaE

def evaluate_conversion_quality(original, converted):
    """
    変換品質の評価
    """
    # Lab色空間で色差を計算
    original_lab = cspace_convert(original, "sRGB1", "CIELab")
    converted_lab = cspace_convert(converted, "sRGB1", "CIELab")
    
    # CIEDE2000色差
    delta_e = deltaE(original_lab, converted_lab, input_space="CIELab")
    
    # 許容範囲:ΔE < 2.3(知覚的に同等)
    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
    }

デバイス対応とプロファイル管理

ICC プロファイルの活用

// WebGL での ICC プロファイル適用
const iccProfileTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_3D, iccProfileTexture);

// 3D LUT としてプロファイルをロード
function loadICCProfile(profileData) {
  const lutSize = 64;
  const lutData = new Uint8Array(lutSize * lutSize * lutSize * 4);
  
  // プロファイルから3D LUTを生成
  generateLUT(profileData, lutData, lutSize);
  
  gl.texImage3D(gl.TEXTURE_3D, 0, gl.RGBA8,
                lutSize, lutSize, lutSize, 0,
                gl.RGBA, gl.UNSIGNED_BYTE, lutData);
}

適応的トーンマッピング

表示デバイスの特性に応じたパラメーター調整:

function getAdaptiveTonemapParams(displayInfo) {
  const {
    maxLuminance,
    gamut,
    gamma,
    ambientLight
  } = displayInfo;
  
  // 環境光に応じた調整
  const adaptationFactor = calculateAdaptation(ambientLight);
  
  // ディスプレイガマットに応じたマッピング
  const gamutCompression = calculateGamutCompression(gamut);
  
  return {
    exposure: adaptationFactor * 0.8,
    whitePoint: maxLuminance / 100,
    gamutCompression: gamutCompression,
    gamma: gamma || 2.2
  };
}

パフォーマンス最適化

GPU 処理の活用

// バーテックスシェーダー
attribute vec4 position;
attribute vec2 texCoord;
varying vec2 vTexCoord;

void main() {
  gl_Position = position;
  vTexCoord = texCoord;
}

// フラグメントシェーダー
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);
  
  // 露出調整
  vec3 exposedColor = hdrColor.rgb * exposure;
  
  // トーンマッピング
  vec3 toneMapped = ACESFilmic(exposedColor);
  
  // ガンマ補正
  vec3 gammaCorrected = pow(toneMapped, vec3(1.0 / gamma));
  
  // 3D LUT適用(カラーガマット変換)
  vec3 lutColor = texture3D(lutTexture, gammaCorrected).rgb;
  
  gl_FragColor = vec4(lutColor, hdrColor.a);
}

並列処理の実装

import multiprocessing as mp
from functools import partial

def process_hdr_batch(image_paths, source_space, target_space):
    """
    バッチ処理でHDR変換を並列実行
    """
    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):
    """
    単一画像のHDR変換処理
    """
    # 画像読み込み
    image = load_hdr_image(image_path)
    
    # トーンマッピング
    tone_mapped = apply_tone_mapping(image)
    
    # カラーガマット変換
    converted = convert_color_gamut(tone_mapped, source_space, target_space)
    
    # 保存
    output_path = get_output_path(image_path, target_space)
    save_image(converted, output_path)
    
    return output_path

品質評価とテスト

自動品質評価

def evaluate_hdr_conversion(original_hdr, converted_sdr, reference_sdr=None):
    """
    HDR→SDR変換の品質評価
    """
    metrics = {}
    
    # 構造的類似度(SSIM)
    metrics['ssim'] = calculate_ssim(converted_sdr, reference_sdr)
    
    # 知覚的画質評価(LPIPS)
    metrics['lpips'] = calculate_lpips(converted_sdr, reference_sdr)
    
    # カラーヒストグラム比較
    metrics['histogram_correlation'] = compare_histograms(
        converted_sdr, reference_sdr)
    
    # ダイナミックレンジ保持率
    metrics['dynamic_range_preservation'] = calculate_dr_preservation(
        original_hdr, converted_sdr)
    
    return metrics

def calculate_dr_preservation(hdr_image, sdr_image):
    """
    ダイナミックレンジ保持率の計算
    """
    # HDRの有効ダイナミックレンジ
    hdr_range = np.log10(np.max(hdr_image) / np.min(hdr_image[hdr_image > 0]))
    
    # SDRの有効ダイナミックレンジ  
    sdr_range = np.log10(np.max(sdr_image) / np.min(sdr_image[sdr_image > 0]))
    
    # 保持率
    preservation_ratio = sdr_range / hdr_range
    
    return preservation_ratio

A/Bテスト フレームワーク

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)
    };
  }
}

実運用での考慮点

ワークフロー統合

# CI/CDパイプライン例
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

モニタリングとアラート

def setup_quality_monitoring():
    """
    品質監視の設定
    """
    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

まとめ

HDRトーンマッピングとカラーガマット変換は、現代の画像処理における重要な技術領域です。適切な手法の選択と実装により、異なるディスプレイ環境での一貫した色表現を実現できます。

主要なポイント:

  1. 理論の理解: PQ/HLGの特性とガマット変換の原理
  2. 手法の選択: ACES、Reinhard、Filmicトーンマッピングの使い分け
  3. 実装の最適化: GPU処理と並列処理によるパフォーマンス向上
  4. 品質管理: 自動評価とA/Bテストによる継続的改善

内部リンク: P3→sRGB 変換で崩れない色管理 実務ガイド 2025, HDR→sRGBトーンマッピング実務 2025 — 破綻させない配信フロー, 正しいカラー管理とICCプロファイル戦略 2025 ─ Web画像の色再現を安定させる実践ガイド

関連記事

HDR→sRGBトーンマッピング実務 2025 — 破綻させない配信フロー

PQ/HLG→sRGB 変換時のハイライト圧縮、彩度シフト、バンディング回避。10bit→8bit、P3→sRGBの落とし穴をまとめて解説。

WebでのDisplay-P3活用とsRGB落とし込み 2025 — 実務ワークフロー

Display-P3 を安全に配信しつつ、sRGB 環境での色再現を担保するための実務フロー。ICC/色空間タグ、変換、アクセシビリティまで包括的に解説します。

HDR / Display-P3 画像の配信設計 2025 — 色忠実度とパフォーマンスの両立

sRGB を超える色域をウェブで安全に扱う実装ガイド。ICC プロファイル、メタデータ、フォールバック、ビュワー差分を踏まえた実戦のカラー管理。

正しいカラー管理とICCプロファイル戦略 2025 ─ Web画像の色再現を安定させる実践ガイド

デバイスやブラウザ間で色ズレを起こさないためのICCプロファイル/カラースペース/埋め込み方針と、WebP/AVIF/JPEG/PNG各形式における最適化手順を体系化。

印刷

CMYK変換とガモットチェック 2025 — sRGB/Display P3 から安全にハンドオフ

Web原稿を印刷へ渡すための実務ガイド。ICCプロファイルの選定、ガモット外の検出と補正、黒設計、ベンダーとの合意形成まで。

色管理と ICC 運用 sRGB/Display-P3/CMYK ハンドオフ 2025

Web から印刷までのカラープロファイル運用を整理。sRGB と Display-P3 の選択、CMYK へのハンドオフ手順、埋め込み/変換の実務ポイントを解説します。