4. 色调映射(Tone Mapping)入门:从全局到局部

HDR合成之后,你手里拿到的是一张32位浮点图像。动态范围可能高达20档甚至更多。但你的显示器、手机屏幕、打印纸,统统只能显示8位(0-255)或10位。怎么办?

硬生生把高动态范围压缩到低动态范围,同时还要保留亮部细节和暗部层次——这就是色调映射干的事。我最早接触这个领域时,以为就是个简单的「亮度压缩」,后来踩了不少坑才明白:色调映射的本质,是在动态范围和信息量之间做取舍。

核心观点:没有完美的色调映射算法,只有最适合你场景的映射策略。全局映射快但容易丢失局部对比度,局部映射保细节但容易产生光晕伪影。

色调映射(Tone Mapping)知识体系 HDR 32位浮点图像 全局映射 局部映射 Retinex理论 Gamma校正 对数映射 自适应对数映射 Dodging & Burning 双边滤波分解 引导滤波分解 SSR / MSR MSRCR 照明-反射分离 LDR 8位/10位输出 速度:全局 > 局部 > Retinex 质量:局部 ≈ Retinex > 全局

4.1 全局映射:简单粗暴但有效

全局映射的思路很直接:对图像中每个像素,用同一个映射函数把亮度压下来。不管像素在亮部还是暗部,映射曲线都一样。好处是计算快、实现简单;坏处是亮部细节和暗部细节往往不能兼顾。

4.1.1 Gamma校正

Gamma校正可能是你最早接触的色调映射方法。它的数学形式很简单:

// Gamma校正核心公式
output = pow(input, 1.0 / gamma)

// 实际应用中,gamma值通常在2.2左右
float gamma = 2.2f;
float corrected = pow(linear_hdr_value, 1.0f / gamma);

为什么是2.2?因为CRT显示器的物理响应曲线大致是gamma 2.2。虽然现在都是液晶屏了,但这个标准保留了下来。我刚开始做HDR项目时,直接套了个gamma 2.2,结果暗部一片漆黑——后来才意识到,HDR图像的亮度范围远大于普通图像,单纯gamma校正会把中间调压得太暗。

实用建议:对于HDR图像,我习惯先用一个简单的全局缩放把亮度拉到合理范围,再做gamma校正。比如:output = pow(input / max_luminance, 1.0/2.2)。这样至少能保证亮部不溢出。

4.1.2 对数映射

对数映射比gamma更「温柔」一些。它模仿人眼对亮度的对数响应特性:

// 对数映射
float L = luminance(hdr_pixel);
float L_max = max_luminance_of_scene;
float L_d = log(1.0 + L) / log(1.0 + L_max);

// 自适应对数映射(更实用)
float bias = 0.85f;  // 控制压缩强度
float L_d = log(1.0 + L * bias) / log(1.0 + L_max * bias);

对数映射的优点是能保留更多的暗部细节。我在处理夜景HDR时特别喜欢用这个方法——霓虹灯的高光不会过曝,同时街道阴影里的细节也能看清。但要注意,对数映射会让整体画面偏灰,对比度不够。所以通常后面还要加一个对比度拉伸。

方法 优点 缺点 适用场景
Gamma校正 计算极快,硬件友好 暗部细节丢失严重 实时预览、移动端
对数映射 暗部细节保留好 整体对比度偏低 夜景、低光照HDR
自适应对数 可调节压缩强度 需要调参 通用场景

4.2 局部映射:Dodging & Burning

全局映射最大的问题是什么?它用同一把尺子量所有区域。亮部细节和暗部细节,你只能保一头。

局部映射的思路就聪明多了:把图像分解成「基础层」(大尺度亮度变化)和「细节层」(小尺度纹理),然后对基础层做压缩,细节层保留或增强。这其实就是传统摄影中「遮挡-加光」(Dodging & Burning)的数字版本。

4.2.1 基于双边滤波的分解

双边滤波是局部映射的经典工具。它能在平滑图像的同时保留边缘:

// 双边滤波分解
// 输入:HDR亮度图 L
// 输出:基础层 B,细节层 D

// 1. 双边滤波提取基础层
Mat B = bilateralFilter(L, d=9, sigmaColor=50, sigmaSpace=50);

// 2. 细节层 = 原始亮度 / 基础层
Mat D = L / (B + 1e-6);

// 3. 压缩基础层,保留细节层
float compression = 0.3f;
Mat L_out = pow(B, compression) * D;

// 4. 还原颜色
// 用 L_out / L 作为比例因子乘到RGB通道上

我在项目中遇到过一个问题:双边滤波的参数很难调。sigmaColor太小,基础层里还有纹理,压缩后出现「浮雕」效果;sigmaColor太大,边缘被模糊,产生光晕(halo)。后来我改用引导滤波(Guided Filter),光晕问题好了很多。

避坑指南:我曾经在手机HDR上直接套用双边滤波分解,结果每帧处理时间超过200ms。后来发现,对降采样后的低分辨率图做分解,再上采样回原尺寸,速度提升5倍以上,质量损失肉眼几乎不可见。

4.2.2 多尺度分解

单尺度的双边滤波分解,细节层里既包含小纹理也包含中等尺度的边缘。更好的做法是多尺度分解:

  1. 第一层:大尺度基础层(大核滤波)
  2. 第二层:中等尺度细节层
  3. 第三层:小尺度纹理层

然后对不同尺度做不同的压缩/增强策略。大尺度压得狠一点,小尺度甚至可以稍微增强。这样出来的效果更自然,不会出现「塑料感」。

4.3 基于Retinex理论的方法

Retinex理论是Land提出的视觉模型。它认为人眼感知的颜色取决于物体的反射率,而不是入射光的强度。换句话说,一张白纸在阳光下和阴影里,人眼看起来都是白的——这就是「颜色恒常性」。

数学上,Retinex把图像分解为:

// Retinex分解
// S = R * L
// S: 传感器捕获的图像
// R: 反射分量(物体固有属性)
// L: 照明分量(光照条件)

// 取对数后变成加法:
// log(S) = log(R) + log(L)

色调映射的目标就是:去掉或压缩照明分量L,保留反射分量R。这样不管光照多强,物体的颜色和细节都能还原出来。

4.3.1 单尺度Retinex(SSR)

SSR用高斯滤波估计照明分量:

// 单尺度Retinex
Mat img = imread("hdr_image.exr", IMREAD_UNCHANGED);
Mat img_log;
log(img + 1.0, img_log);  // 避免log(0)

Mat L_log;
GaussianBlur(img_log, L_log, Size(31, 31), 15);

Mat R_log = img_log - L_log;  // 反射分量

Mat R;
exp(R_log, R);
// R就是色调映射后的结果

SSR的问题在于高斯核大小很难选。核小了,照明估计不准,出现光晕;核大了,反射分量里混入照明信息,颜色失真。我试过用不同尺度做融合,效果会好很多。

4.3.2 多尺度Retinex(MSR)

MSR把多个尺度的SSR结果加权平均:

// 多尺度Retinex
vector<Mat> scales = {15, 80, 250};  // 三个尺度
vector<float> weights = {1.0/3, 1.0/3, 1.0/3};

Mat R_msr = Mat::zeros(img.size(), CV_32F);
for (int i = 0; i < scales.size(); i++) {
    Mat L_log;
    GaussianBlur(img_log, L_log, Size(scales[i], scales[i]), scales[i]/2);
    Mat R_log = img_log - L_log;
    R_msr += weights[i] * R_log;
}

// 加上颜色恢复(MSRCR)
// 防止颜色饱和度下降
Mat color_restoration;
// ... 颜色恢复逻辑

MSRCR(带颜色恢复的多尺度Retinex)是目前工业界用得比较多的版本。它解决了MSR颜色饱和度下降的问题。我在做航拍图像增强时用过MSRCR,效果确实惊艳——雾霾天的图像能还原出清晰的纹理和真实的颜色。

经验之谈:Retinex方法计算量大,不适合实时处理。但如果你做的是单帧HDR后处理,或者离线渲染,MSRCR是非常好的选择。我一般会在SSR和MSR之间做个折中:用2个尺度(小尺度+大尺度),权重各0.5,速度比3尺度快30%,质量差距不大。

4.4 三种方法的对比与选择

维度 全局映射 局部映射 Retinex
计算速度 极快(<1ms) 中等(10-50ms) 较慢(50-200ms)
局部对比度 差 好 很好
颜色保真度 好 中等 需额外处理
光晕伪影 无 可能有 可能有
实现复杂度 低 中 高
适用场景 实时预览、移动端 高质量渲染 逆光、雾霾增强

选哪种方法?我的建议是:

  • 实时场景(视频、相机预览):用全局映射,最多加一个自适应参数调节
  • 高质量静态图像:用局部映射,引导滤波分解比双边滤波更稳定
  • 极端光照条件(大逆光、雾霾):用Retinex,MSRCR是首选

嗯,色调映射入门就聊到这儿。记住一句话:没有银弹。每种方法都有它的脾气,多试、多调参,才能找到最适合你场景的方案。