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トーンマッピングとカラーガマット変換は、現代の画像処理における重要な技術領域です。適切な手法の選択と実装により、異なるディスプレイ環境での一貫した色表現を実現できます。
主要なポイント:
- 理論の理解: PQ/HLGの特性とガマット変換の原理
- 手法の選択: ACES、Reinhard、Filmicトーンマッピングの使い分け
- 実装の最適化: GPU処理と並列処理によるパフォーマンス向上
- 品質管理: 自動評価と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 へのハンドオフ手順、埋め込み/変換の実務ポイントを解説します。