3、亮度调节核心算法:基于Lux的线性映射、对数映射与指数映射、Gamma校正与视觉感知

好,咱们进入正题。

上一章我们聊了环境光传感器怎么采集数据,怎么滤波。但拿到一个干净的Lux值之后,怎么把它变成屏幕亮度?

这就是算法要做的事。

说白了,就是把一个物理量(环境光)映射到另一个物理量(屏幕亮度)。这个映射关系,决定了用户的眼睛舒不舒服。

3.1 线性映射:最直接,但最不好用

线性映射,公式很简单:

ScreenLux = k * AmbientLux + b

k是增益,b是偏移量。

举个例子:环境光从0变到10000 Lux,屏幕亮度从10 nit线性增加到500 nit。听起来很合理对吧?

但我在项目中踩过这个坑。

有一次,客户反馈说:“车子从地库开出来,屏幕突然亮得刺眼。” 我一看日志,环境光从50 Lux瞬间跳到8000 Lux,屏幕亮度直接从20 nit跳到400 nit。线性映射嘛,一步到位。

用户的眼睛根本来不及适应。

注意: 人眼对亮度的感知是非线性的。线性映射在低光区变化太快,在高光区变化又太慢。说白了,就是“低光刺眼,高光不够亮”。

线性映射适合做基准测试,但不适合直接用在产品里。除非你的屏幕亮度范围特别窄,比如只有0-100 nit,那还能凑合用。

3.2 对数映射与指数映射:更接近人眼

人眼对亮度的响应,其实是对数关系。你想想看,在暗处,一点点光变化就很明显;在亮处,几百Lux的变化你可能都感觉不到。

对数映射的公式:

ScreenLux = a * log(AmbientLux + 1) + b

加1是为了防止AmbientLux为0时取对数出错。

这个映射的好处是:低光区变化平缓,高光区变化也平缓。整体曲线像是一个“S”形的前半段。

我个人的习惯是,在车载场景下,优先用对数映射。为什么呢?

  • 地库到地面:亮度变化不会太突兀
  • 隧道出口:屏幕不会突然亮瞎眼
  • 夜间行车:屏幕亮度足够低,不干扰驾驶

指数映射则相反:

ScreenLux = a * exp(AmbientLux / c) + b

这个映射会让亮度变化在低光区很慢,高光区很快。说实话,在车载场景下用得不多。除非你想做“强光下屏幕特别亮”的效果,但那样功耗也上去了。

小技巧: 我建议把对数映射和线性映射结合起来。比如环境光低于100 Lux时用对数,高于100 Lux时用线性。这样既能保证低光舒适,又能保证高光够亮。

3.3 Gamma校正:让屏幕“说人话”

这里有个容易混淆的点。

屏幕本身的亮度响应也是非线性的。你给屏幕一个50%的电压,它不会输出50%的亮度,而是大概22%左右。这就是Gamma特性。

Gamma校正,就是提前把亮度值“扭曲”一下,让最终输出符合预期。

公式:

Output = Input ^ (1 / Gamma)

标准Gamma值通常是2.2。所以校正公式就是:

Output = Input ^ (1 / 2.2)

举个例子:

输入亮度(归一化) 未校正输出 Gamma校正后输出
0.5 0.22 0.73
0.8 0.55 0.90

你看,校正后输出值变大了。这样屏幕才能显示出正确的亮度。

核心要点: 亮度调节算法输出的目标亮度值,必须经过Gamma校正,再送给屏幕驱动。否则你算得再准,屏幕显示出来也是错的。

我曾经遇到过一个Bug:算法算出来目标亮度是200 nit,但屏幕显示出来只有150 nit。查了半天,发现是Gamma校正没做。嗯,从那以后,我把Gamma校正写进了算法流程的固定步骤里。

3.4 视觉感知:算法最终要服务眼睛

算法再复杂,最终是给人看的。所以必须考虑人眼的视觉特性。

几个关键点:

  • 亮度适应时间: 人眼从暗到亮需要几秒,从亮到暗需要几十秒。算法不能瞬间跳变,要加平滑过渡。我一般用一阶低通滤波,时间常数设1-3秒。
  • 对比度敏感度: 人眼对中频对比度最敏感。所以屏幕上的文字、图标,在中等亮度下最清晰。太暗或太亮都会降低可读性。
  • 色温影响: 亮度变化时,色温也要跟着变。这个我们下一章细讲。

我个人习惯在算法里加一个“视觉舒适度”指标。比如:

舒适度 = 1 - |目标亮度 - 当前亮度| / 最大亮度范围

这个值越接近1,说明亮度变化越平缓,用户越舒服。

避坑指南: 我曾经把平滑时间设得太短(0.5秒),结果用户反馈“屏幕闪烁”。后来改成2秒,问题解决。记住:车载场景下,宁慢勿快。

3.5 算法流程总结

好,我们把整个流程串一下:

  1. 获取滤波后的环境光Lux值
  2. 用对数映射(或混合映射)计算目标亮度
  3. 对目标亮度做平滑滤波(一阶低通)
  4. 对平滑后的亮度做Gamma校正
  5. 输出到屏幕驱动

代码示例(伪代码):

float ambientLux = getFilteredLux();
float targetBrightness = logMapping(ambientLux);
float smoothedBrightness = lowPassFilter(targetBrightness, 0.3);
float gammaCorrected = pow(smoothedBrightness, 1.0 / 2.2);
setScreenBrightness(gammaCorrected);

嗯,就这么简单。但每一步都有坑,每一步都需要调参。

下一章,我们聊聊色温调节。那个更麻烦,因为涉及到白平衡和人的主观感受。

课后思考: 如果你在强光下(比如10000 Lux)开车,屏幕亮度应该调到多少?试试用对数映射算一下,看看结果是否合理。