第四章:全局色调映射(Global Tone Mapping)

全局色调映射,说白了就是把高动态范围的图像,压缩到普通显示器能显示的范围里。你想想看,现实世界的亮度范围动辄十几档光圈,而我们的屏幕只有可怜的6-8档。怎么把大象塞进冰箱?这就是色调映射要解决的问题。

我个人习惯把全局映射比作「一刀切」的策略。整张图像用同一个映射函数,每个像素都走同样的变换曲线。好处是简单、高效、适合硬件流水线;坏处嘛,局部细节可能会丢失。我在项目中遇到过不少次,全局映射做得好,画面通透自然;做得不好,暗部一片死黑,亮部一片惨白。

4.1 对数映射(Logarithmic Mapping)

对数映射是最经典的方法。它模仿人眼对亮度的感知——人眼对暗部变化敏感,对亮部变化迟钝。对数函数正好符合这个特性。

公式很简单:

L_out = log(1 + L_in) / log(1 + L_max)

其中 L_in 是输入亮度,L_max 是场景最大亮度。输出范围被压缩到 [0, 1]。

嗯,这里要注意:对数映射对暗部细节保留得不错,但亮部压缩太狠。我曾经在一个夜景项目里用对数映射,结果路灯周围的光晕全被压没了,画面看起来像阴天。后来我加了一个可调参数 k:

L_out = log(1 + k * L_in) / log(1 + k * L_max)

调大 k 值,亮部细节会好一些。但别调太大,否则暗部又不行了。这是个 trade-off。

核心要点:对数映射适合暗部细节丰富的场景,比如星空、夜景。亮部细节要求高的场景慎用。

4.2 指数映射(Exponential Mapping)

指数映射正好反过来。它用指数函数做压缩,对亮部更友好。

L_out = 1 - exp(-L_in / L_max)

你想想看,指数函数在输入值较小时增长缓慢,输入值较大时迅速饱和。所以暗部会被压得更暗,亮部反而保留得更好。

我记得有一次做室内场景的 HDR 处理,窗外阳光强烈,室内光线昏暗。用对数映射,窗外一片白;换成指数映射,窗外的云朵纹理出来了,但室内暗部几乎看不见。没办法,全局映射就是这样,顾此失彼。

我的经验:指数映射适合高对比度场景,比如逆光人像、窗口风景。但暗部细节会丢失,需要配合局部映射或后期提亮。

4.3 Sigmoid 映射(Sigmoid Mapping)

Sigmoid 映射是我个人比较喜欢的一种。它结合了对数和指数的优点,中间段线性,两端平滑压缩。

公式长这样:

L_out = 1 / (1 + exp(-a * (L_in - b)))

参数 a 控制曲线斜率,b 控制中心点位置。调参就像调 EQ,a 越大对比度越高,b 往左移画面变亮,往右移变暗。

我曾经在一个医疗影像项目里用过 Sigmoid 映射。医学图像要求不能丢失任何细节,对数映射太软,指数映射太硬。Sigmoid 映射的 S 形曲线正好在中间段保留了线性响应,两端又做了平滑压缩。嗯,效果还不错。

避坑指南:我曾经把 a 值设得太大,结果画面出现明显的「断层」——亮度过渡不自然,像 posterization 效果。Sigmoid 的 a 值建议控制在 2-8 之间,具体看场景。

4.4 自适应全局映射(Adaptive Global Mapping)

自适应全局映射,说白了就是让映射函数根据图像内容自动调整参数。不需要手动调参,芯片自己算。

常见的做法是统计图像的亮度直方图,然后根据直方图分布决定映射曲线。比如:

1. 计算亮度直方图
2. 找到中位数亮度 L_median
3. 根据 L_median 调整映射曲线的中心点
4. 根据直方图宽度调整曲线斜率

我在一个手机 ISP 芯片里实现过自适应映射。流程是这样的:

// 伪代码示例
float compute_adaptive_curve(float L_in, float L_max, float L_median) {
    float a = 4.0;  // 基础斜率
    float b = L_median / L_max;  // 中心点自适应
    
    // 如果直方图偏暗,降低斜率保护暗部
    if (L_median < 0.3 * L_max) {
        a = 2.5;
    }
    // 如果直方图偏亮,提高斜率增强对比度
    else if (L_median > 0.7 * L_max) {
        a = 6.0;
    }
    
    return 1.0 / (1.0 + exp(-a * (L_in / L_max - b)));
}

你想想看,这个逻辑其实很直观:画面暗的时候,曲线缓一点,别把暗部压死;画面亮的时候,曲线陡一点,把亮部细节拉出来。

核心要点:自适应映射是全局映射里最实用的方案。它不需要人工干预,适合消费电子产品的实时处理。但算法复杂度比固定映射高,需要硬件加速。

4.5 四种映射对比

我把四种映射的特点整理成了一张表,方便你对比:

映射类型 暗部细节 亮部细节 计算复杂度 适用场景
对数映射 优秀 较差 夜景、星空
指数映射 较差 优秀 逆光、窗口
Sigmoid 映射 良好 良好 通用场景
自适应映射 自适应 自适应 消费电子

我个人建议:如果是做芯片 IP,优先考虑自适应映射。虽然硬件资源多花一点,但效果稳定,不用调参。如果是做算法验证,可以先从 Sigmoid 映射入手,调参方便,效果也够用。

下面这张图展示了四种映射的曲线对比,你可以直观感受一下它们的差异:

0 1.0 0 1.0 输入亮度 L_in 输出亮度 L_out 对数 指数 Sigmoid 自适应

从图中可以看出:对数曲线在暗部(左侧)斜率大,亮部(右侧)斜率小;指数曲线正好相反;Sigmoid 曲线在中间段接近线性,两端平滑;自适应曲线则根据图像内容动态调整。

我的建议:在实际芯片设计中,不要只依赖一种映射。可以设计一个映射选择器,根据场景检测结果自动切换。比如检测到夜景用对数,检测到逆光用指数,普通场景用 Sigmoid。这样既保证了效果,又控制了硬件开销。

好了,全局色调映射就讲到这里。四种方法各有千秋,没有绝对的好坏。关键是根据你的应用场景和硬件资源,选择最合适的方案。下一章我们会聊局部色调映射,那才是真正考验算法功底的地方。


公众号:蓝海资料掘金营,微信deep3321