上个月接了一个三方相机兼容性问题,客户的相机 HAL 适配到自家芯片上后,有人像 AE 需求。问题描述很简短:开启人脸 AE 后,只要用户在预览画面里点一下屏幕,人脸测光就失效,画面过曝,脸部亮度跟着点击区域跑。这个现象系统相机不复现,只在特定三方相机 apk 上出现。我本以为是个偶发调度问题,结果用 apk 一复现,几乎 100%。拉到 HAL 的 3A log 后,怀疑对象从“芯片 AE 算法不稳”慢慢转移到“touch AE 与 face AE 的权重冲突”上。最后花了一个多星期定位和验证,根因比想象中简单:三方相机在下发 touch AE region 时,底层策略直接把 face AE 的权重清零了。
这类问题在 Camera 适配项目里非常典型,尤其是涉及三方相机、美颜算法、人像模式的时候。整条链路涉及 app 层、camera framework、HAL 3A 引擎,每一层都可能因为一个小小的优先级处理不到位而最终导致人脸曝光异常。写这篇文章不是单纯记录一个 bug,而是把问题拆分到可复现、可定位、可修复三个环节,给正在做三方相机兼容性或者 HAL tuning 的兄弟一个参考路径。
如果你不太了解 Camera 底层的 AE 策略,或者你正准备在同一套相机能力上接入第二、第三方应用,那么重点看第 2 节和第 4 节的策略设计,应该能帮你避开类似坑。
1. 问题现场:三方相机下的 touch ae 与 face ae 异常
1.1 三方相机生态的特殊性
三方相机,我指的是非系统预装相机、通过 Camera2/CameraX 或私有 SDK 反复调用相机能力的第三方应用。它们和系统相机有非常大的差异:系统相机通常由相机模组厂商或整机厂一起联合调试,AE、AF、AWB 的策略跟 HAL 的预期是一致的;三方相机则不同,它一般只关心自己在 UI 层的交互逻辑,不会关心底层 3A 策略里 “touch region” 和 “face region” 应该谁优先。
在这个案例里,客户的验证 apk 是一个具备人像美颜模式的三方相机。它的预览 UI 很标准:顶部有美颜开关,右侧有滤镜选择,底部快门。人脸检测打开后,画面里会出现一个矩形框,除此之外没有任何明显的 touch 图标。但问题恰恰就出现在用户点击屏幕的一瞬间——不管点在人脸上还是点在人脸外的空白区域,人脸 AE 都会失效。如果点在人脸框内,画面可能还稍微好一点;一旦点在人脸旁边,脸部立刻过曝。
为什么系统相机没有这个问题?因为系统相机的交互设计里,点击屏幕和人脸检测是两套独立的模块。点击屏幕默认是触发对被点击点的 AF+AE,但如果同时存在人脸框,系统相机会判断两者的关系,甚至会让 AF 跟随人脸。也就是说,系统相机在 framework 层面对 AE regions 做了更严格的策略控制。三方相机则不然,大多数三方相机都直接使用 Camera2 API 的CONTROL_AE_REGIONS,你传什么,HAL 就执行什么,几乎不存在“智能判断”。
那么“自动触发 touch AE”是怎么回事?其实不一定是用户主动点击,有些三方相机的界面会有悬浮控件、手势抓拍、甚至广告浮层,都有可能触发触摸事件。此外还有更隐蔽的情况:app 为了实现“人脸追焦”,在每一次 preview 回调里自动下发一个 AE region,这个 region 是跟着人脸的中心点生成的。从用户视角看,没有人点击屏幕,但底层每个 frame 都在更新 touch AE 区域,一样会触发同样的问题。我们这个项目里,后面拉 app 的 trace 发现,它同时存在这两种路径。
1.2 复现步骤与现象记录
为了把问题描述清楚,我整理了一份复现步骤,方便大家对照检查。这里只讲我们验证过的路径。
- 开启前置摄像头(后置也可以,但前置人像场景更明显),切换到人像模式,打开人脸检测/人脸 AE。
- 让人脸位于画面中央偏右侧,等待 2~3 秒,让 AE 收敛。此时画面亮度正常,log 里应能看到
face_rects有值,且faceWeight > 0。 - 在屏幕右侧空白区域点一下,或者让 app 自动触发一次
CONTROL_AE_REGIONS下发。 - 观察画面亮度和 HAL log。典型结果是:下一步 frame 开始,
touchWeight=1.0,faceWeight=0.0,AE 统计区域完全变为点按区域,人脸区域不再参与测光。 - 后续即使不再点击屏幕,face AE 也不会自动恢复,因为 HAL 的 AE 策略没有设计“touch 退出后恢复 face”的逻辑,除非 lock/unlock AE 或重启 preview。
现象记录里还有两个值得注意的点:一是故障不会马上表现为一张全白的脸,有时候是先轻微过曝,再过 1~2 秒才完全爆掉,因为 AE 收敛有个时间过程;二是如果把点击位置恰好点在人脸框的中心,故障现象会被掩盖,画面可能依旧正常,因为 touch 区域与人脸区域重叠,AE 的测光结果其实仍在人脸区域。但一旦点偏离,问题立刻暴露。这两个特点对后续定位非常有帮助,能避免我们误判故障范围。
这些现象印证了一个判断:问题不在人脸检测本身,而在于 AE 策略的优先级。Face detection 的返回值一直存在,但底层 AE 没有使用它。
2. 从底层看 Touch AE 和 Face AE 的完整链路
2.1 Camera 框架层的 AE 区域是怎么传下来的
Android Camera 从 API 1 到 API 2,尽管调用方式不同,但本质都是把一组控制参数通过 request 传给 HAL。API 1 里是Camera.Parameters#setFocusAreas和setMeteringAreas;API 2 里是CaptureRequest.CONTROL_AF_REGIONS和CONTROL_AE_REGIONS。
CONTROL_AE_REGIONS的类型是MeteringRectangle[],每个矩形由 x, y, width, height 和 weight 组成。坐标范围是 1000x1000 坐标系,左上角为 (0,0),右下角为 (1000,1000)。而人脸的检测结果通过STATISTICS_FACE_DETECT_MODE开启后,在CaptureResult.STATISTICS_FACES里返回一组Face对象,包含 bounds 和 score,坐标也是同一个 1000 坐标系。
当三方相机点击屏幕后,点击坐标会被转换为对应的MeteringRectangle,作为 AE regions 放进下一次 repeating request。此时 HAL 收到的 request 中同时包含两个信息:a) AE regions 非空;b) face detect mode 开启且上一帧检测到人脸。正常情况下,HAL 应该先判断检测到的人脸区域,再决定这些 touch region 是加入测光权重还是直接覆盖。但很多 HAL 的默认实现逻辑非常粗暴,只要 AE regions 非空就进入“用户自定义测光”模式,不管 face detect 开没开。
这就是框架层传参的常见形态。如果你在 app 侧把CONTROL_AE_REGIONS每帧都更新,那么每次 request 都会让 HAL 以为有新的 touch 意图,所以也会反复触发这个问题。很多三方相机 App 的“自动对焦”实现就是这种思路。
2.2 3A 引擎里两种 AE 策略的博弈
进入 HAL 的 3A 引擎后,AE 策略通常是一个状态机或者多分支选择。伪代码大概长这样:
void AeStrategy::process(const AeRequest& req, AeParams* params) { // 1. 基础曝光计算 computeBaseAe(params); // 2. 处理 face detected if (req.hasFaces && req.faceCount > 0) { params->faceWeight = 0.8f; updateAeRegion(params, req.faceRects, params->faceWeight); } // 3. 处理 touch ae region if (!req.touchRegion.isEmpty()) { params->faceWeight = 0.0f; // 这里把 face 权重清零 params->touchWeight = 1.0f; updateAeRegion(params, req.touchRegion, params->touchWeight); } // 4. 收敛权重并计算最终 target ... }问题就出现在第 3 步。很多 HAL 在实现了“点击屏幕测光”功能以后,就把 touch 分支当作品一优先级,并且直接用faceWeight = 0来清掉 face 的影响,甚至不判断req.hasFaces。少数实现会判断req.touchRegion的 weight 是否为 0,但如果 app 传的默认 weight 是 1,无论如何都会进 touch 分支。
从 AE 色调统计的角度看,最终参与曝光的目标亮度是各个 region 的加权平均值。如果 touch 权重为 1、face 权重为 0,那么人脸区域对 AE target 几乎没有贡献。人脸过曝是因为人脸区域在传感器上通常比背景亮,测光中心又被人为移到了暗背景上,AE 为了让背景平均亮度正常,就会把人脸拉爆。所以最终我们看到的现象是:点哪里,哪里正常,人脸跟着完蛋。
2.3 由 log 反推问题触发点
要快速定位这一类问题,不能只凭 app 表现,必须打开 HAL 的 3A log。以我们的平台为例,在以下位置加上 log:
processRequest入口:打印CONTROL_AE_REGIONS、CONTROL_AF_REGIONS、face_detect_mode、face rects。AeStrategy::updateWeights:打印 touch region、face rect、touchWeight、faceWeight。AeController::computeFinalTarget:打印最终参与 AE 统计的 region 合并结果。
故障帧的 log 类似:
[AE] touchRegion=(520,400,200,200), touchWeight=1.000000 [AE] faceRects=[(50,300,300,400)], faceWeight=0.000000 [AE] finalRegions=(520,400,200,200), finalTargetWeight=1.000000把这份 log 给 app 开发看,他们也很无语,因为从 app 侧来看,自己明明开启了人脸检测,却不知道底层已经把 face 权重清零了。为了进一步验证,我在 HAL 里临时加了一个 patch:如果检测到 face,则强制跳过 touch 分支。测试打出来,故障立刻消失,face AE 一直生效,点击屏幕只改变了 AF 区域,对 AE 影响很小。这更加确定了根因范围。
所以整个链路其实并不复杂:app 下发 touch AE -> HAL 策略一刀切 -> face AE 权重清零 -> 人脸区域对曝光没有贡献。
3. 根因定位与代码级验证
3.1 最可疑的“自动触发 touch ae”场景
前面提到,我们这个案例里最隐蔽的部分不是“用户点击”本身,而是“自动触发”。所谓自动触发,有两类:
第一类是用户在 UI 上确实点了屏幕,随即 app 调用builder.set(CaptureRequest.CONTROL_AE_REGIONS, meteringRect)。这个是正常路径,不隐蔽。
第二类是 app 在预览循环里每帧自动设置 AE regions。比如打开人脸追踪后,为了让人脸始终成为测光中心,app 把人脸矩形转成 MeteringRectangle 下发。这种情况下,从用户角度没有任何点击动作,但底层每一帧都收到非空的 AE region,HAL 自然把它当作 touch AE 处理。这个路径在问题排查中最容易被忽略,因为你会觉得“我没点屏幕,为什么 touch ae 被触发”。如果你也遇到这种诡异现象,建议重点检查 app 的 CaptureCallback 里有没有对CONTROL_AE_REGIONS做持续赋值。
我们最后直接抓拍了 app 的 binder 调用:在 HAL 的processCaptureRequest里统计了 5 秒内的 request,发现 150 帧中有 148 帧的CONTROL_AE_REGIONS不为空。换句话说,HAL 的 face AE 根本没有机会被真正打开,因为每一次 request 过来都会走进 touch 覆盖分支。app 开发者自己都没意识到,他们以为自己在做“人脸测光”,其实是在做“模拟 touch AE”。
3.2 加 log 与 patch 验证的要点
在 3A 这类问题上,光看 log 不足以完全确认,还需要做几个对照实验。我当时的验证顺序是:
- 修改 app 侧:把每帧自动下发 AE regions 的代码暂时注释掉,只保留手动点击时才下发。测试结果:人脸 AE 恢复,点击屏幕后仍会触发故障,但不再点击后基本能保持人脸测光。这证明了 app 的持续下发是“自动触发”的主要来源之一。
- 修改 HAL 侧:在 touch 分支里增加
if (!hasFace)条件,也就是说,检测到人脸时不走 touch 覆盖逻辑。测试结果:无论 app 怎么下发,人脸 AE 都正常。 - 在两个 patch 都去掉的情况下,恢复原样,故障复现。
这三个实验基本锁定了问题方向:app 的行为有问题,但 HAL 的防御策略也缺失。作为底层厂商,我们不能要求所有三方 app 都按标准实现,HAL 需要有一定的自适应能力。这就是为什么最终修复没有完全依赖 app 侧修改,而是放在 HAL 里做兼容。
3.3 根因确认
根因可以总结成一句:HAL 的 AE 策略在 AE regions 非空时,无条件把 AE 模式切换成 touch 优先,忽略了同时存在的人脸检测结果,导致 Face AE 失效。对三方相机来说,app 侧为了追求跟手的人脸跟随测光,又高频下发 AE regions,于是问题被放大成 100% 复现。
代码层面,问题出在 HAL 3A 引擎里对 touch region 和 face region 的优先级管理上,不是底层 AE 统计公式的错误,也不是 sensor 硬件的问题。定位清楚之后,修复方向就非常明确了。
4. 修复方案设计
4.1 方案一:框架层修正 region 合并逻辑
第一种思路是在 framework 的 CameraMetadata 处理层“打补丁”。比如在 camera service 的 processCaptureRequest 处,如果发现 request 同时带有CONTROL_AE_REGIONS和开启的人脸检测,就自动把人脸 rect 合并进 AE regions。这样做的好处是 HAL 侧完全不用改,对已有 HAL 版本也能通过 framework 更新覆盖。
坏处也很明显:第一,framework 改动属于系统级改动,要过 CTS、VTS 以及各家 app 兼容性测试,回归成本非常大。第二,framework 拿到的 face rect 可能是上一帧的检测结果,拿它去 merge 到当前帧 request 可能会增加额外延迟。第三,不是所有三方 app 都希望你的 framework 帮他决定优先级,有些直播 app 就是想让用户点哪测光哪里,强行合并反而打乱产品预期。
所以这个方案我一般只在单机型快速验证时使用,不作为正式发布方案。
4.2 方案二:HAL 内实现对 touch 与 face 区域加权合并
这是我们最终选择的方案。核心思路是在 AE 策略里改变“单一胜出”的模型,改为“区域融合 + 权重分配”:
- 当 face 未检测到,且 touch region 非空:保持原有 touch AE 行为,touchWeight = 1.0。
- 当 face 检测到,且 touch region 为空:保持原有 face AE 行为,faceWeight = 1.0。
- 当 face 检测到,且 touch region 非空:将 touch region 与所有 face rect 做并集,按比例分配权重,默认 faceWeight = 0.7,touchWeight = 0.3。
- 如果 touch region 与某一 face rect 重叠面积超过 50%,则忽略 touch region,完全按 face 处理。
伪代码:
void AeStrategy::updateAeWeights(AeRequest& req, AeParams* params) { bool hasFace = (!req.faceRects.empty() && req.faceDetectEnabled); bool hasTouch = !req.touchRegion.isEmpty(); if (hasFace && hasTouch) { // 计算 overlap 比例 float overlap = calcOverlapRate(req.touchRegion, req.faceRects); if (overlap > 0.5f) { params->faceWeight = 1.0f; params->touchWeight = 0.0f; } else { params->faceWeight = 0.7f; params->touchWeight = 0.3f; mergeRegions(params->aeRegions, req.touchRegion, req.faceRects); } return; } if (hasFace) { params->faceWeight = 1.0f; params->touchWeight = 0.0f; setRegions(params->aeRegions, req.faceRects); return; } if (hasTouch) { params->faceWeight = 0.0f; params->touchWeight = 1.0f; setRegions(params->aeRegions, req.touchRegion); return; } }这个 patch 落地后,需要重点回归三类场景:只点人脸、点人脸外、人脸移动过程中点屏幕。实际测试下来,前两类都能保持人脸不过曝,第三类偶发会出现 1~2 帧亮度摆动,但很快会被 AE 收敛回来。
4.3 方案三:通过 vendor tag 让三方相机显式声明优先级
如果 HAL 的自动判断在特定产品上不能满足,还可以增加一个 vendor tag,类似com.xxx.camera.CONTROL_AE_PRIORITY,取值FACE_PRIOR、TOUCH_PRIOR、AUTO。三方相机通过CaptureRequest.Builder.getTag()或专用接口设置。这个方案适合对三方相机有控制权的项目,比如你们和自己的合作伙伴联合调试 app 时。
不过 vendor tag 的缺点是需要 app 配合。如果 app 不写这个 tag,默认还是要走 AUTO 逻辑。所以我在项目里把它做成一个可选项,配合方案二一起工作:默认 AUTO 由 HAL 判断;如果 app 有特殊需求,再显式指定。App 侧的调用方式类似:
CaptureRequest.Builder builder = session.getDevice().createCaptureRequest(TEMPLATE_PREVIEW); builder.set(CaptureRequest.CONTROL_AE_REGIONS, meteringRect); // 自定义 tag,告诉 HAL 人脸优先 builder.set(new VendorTagKey<>("com.xxx.camera.CONTROL_AE_PRIORITY", Integer.class), 1);4.4 三种方案的取舍与最终落地
最后我们选定方案二为正式方案,在 HAL 中增加一个兼容开关persist.vendor.camera.ae_face_touch_compat,默认值为 1。当它关闭时,走原始策略,便于现场对比;开启时,走加权合并。另外还附带了以下特性:
- 每次 AE 策略切换时,在 log 里打印
compat_mode=on/off,方便确认功能是否生效。 - 对 touch region 与 face rect 的 overlap 计算做了阈值可调处理,用 property 控制。
- 对 app 高频下发 AE region 的情况做了防抖:如果连续 10 帧 touch region 坐标变化小于某个门限,就优先保持 face AE 权重,直到用户再次点击。
这个防抖逻辑在实际测试中很管用,因为三方相机的“自动 touch AE”往往是同一位置反复下发,并不是有意改变测光点。它虽然不是必须,但对用户体感提升明显。
4.5 验证结果
修复后,我用同一套三方相机 apk 重新执行了复现步骤,结果如下:
| 测试场景 | 修复前现象 | 修复后现象 |
|---|---|---|
| 人脸静止,点击人脸外 | 人脸 1~2 秒内过曝,不再恢复 | 人脸亮度轻微波动,1~2 帧后恢复 |
| 人脸静止,点击人脸内 | 视点按位置而定,有时正常 | 始终正常,AE 跟脸 |
| 人脸移动中持续点击屏幕 | 面部频繁闪烁,AE log 显示 touch 权重占满 | 偶发抖动,很快稳定,face 权重保持在 0.7 |
| 不点击屏幕,app 自动下发 touch AE | 一进入预览就出现 face AE 失效 | 正常,防抖逻辑生效 |
回归测试还覆盖了普通静物场景、风景场景、低光场景,touch AE 功能没有被破坏。最终方案顺利通过客户验证。
5. 踩坑与实战心得
5.1 容易被误判为芯片 AE bug 的两种现象
第一个是“人脸移动时亮度闪烁”。这个问题如果只测系统相机,你会发现系统相机内部已经把 touch region 和 face 做了融合,所以不会闪;但一上三方相机,人脸移动时 app 自动更新 AE region,导致每个 region 权重变化频繁,AE 目标忽高忽低。肉眼看到的就是人脸忽明忽暗。如果你不抓 log,很容易觉得是 AE 收敛太慢,然后陷入 tuning 死循环。
第二个是“开启美颜滤镜后画面亮度异常”。滤镜本身会改变图像亮度,但很多三方相机在开启滤镜后会自动把 AE region 设置到人眼区域,假如底层策略还是 touch 优先,就可能出现开启滤镜的瞬间亮度跳变。排查时要把滤镜效果和 AE 策略分开验证,不然会把两个问题混为一谈。
5.2 给三方相机开发者的排障清单
如果你是在 app 层做三方相机,不想踩这个坑,请对照下面几点检查:
- 不要每帧都设置
CONTROL_AE_REGIONS,人脸测光不等于设置 metering region。尽量用STATISTICS_FACE_DETECT_MODE和CONTROL_MODE配合系统人脸 AE 权重。 - 如果确实需要自定义测光点,请记录用户上一次手动点击的位置。当用户没有点击动作时,不要自动把某个 region 塞进 request,除非你有明确的产品设计。
- 点击屏幕的交互里,建议把人脸框检测结果纳入判断:点击点落在人脸框内时,只更新 AF 区域,不更新 AE 区域;点击空白区域时才更新 AE 区域。
- 如果想对多个区域做加权测光,Camera2 的
MeteringRectangle自带 weight 字段,不要把两块区域合成一个矩形丢下去,而是把不同矩形保留,HAL 才能正确处理。 - 在三方相机适配过程中,尽可能用多个不同芯片平台做交叉验证,因为各家 HAL 对 touch/face 优先级的处理差异很大。
5.3 从这次故障里学到的 3A 调试经验
一个工程上的经验:调试 3A 问题,永远先把“谁在控制权重”这个问题理清楚。AE 的最终目标就是一堆 region 的加权统计,谁占的权重大,谁就是当前曝光依据。你只需要把每个时间点的 region 和 weight 打出来,再和用户操作对应起来,问题基本就能浮出水面。
第二个经验是:三方相机兼容性项目,永远不要假设 app 是按 Android 最佳实践写的。很多 app 用的是很早期的 Camera1 接口迁移过来的代码,甚至会在同一个 request 里同时设置 focusAreas 和 meteringAreas,而且 weight 随手填。作为 HAL 侧,一定要有“防御”意识,对不合理的参数做收敛处理。
第三个经验是:任何修复都要加开关,不要直接替换原始策略。因为你不知道下一个 app 会有什么怪异用法,开关让你能在现场快速对比,也能在出现新回归时一键回退。
5.4 一个隐蔽变种:Face AE 时好时坏
除了上面那种 100% 复现的情况,还有一种更隐蔽的变种:人脸 AE 时好时坏,亮度偶尔跳一下,抓 log 时又恢复正常。这种问题往往不是单一 region 覆盖,而是多个 region 叠加后的归一化异常。
举个例子,三方相机同时下发了一个 touch region 和两个 face rect,HAL 把它们简单合并成三个区域后,没有对重叠部分做去重或归一化,结果总权重超过 1,AE 在计算 target 时出现跳变。很多开发会在 app 层看到人脸框还在,就觉得 face AE 在工作,其实底层只把 face rect 当成了背景噪声,根本没有分配有效权重。
遇到这种变种,建议把 HAL 里最终送入统计模块的 region 数量、坐标、weight 全部打印出来,肉眼核一遍。我遇到过最离谱的情况是五个 region 里有三个坐标一样,权重还各不相同,计算出来的人脸亮度自然对不上。
6. 写在最后的个人体会
这次三方相机的问题虽然花费了不少时间,但价值挺大。它让我重新理解了 AE 策略的优先级设计:一个成熟的 3A 引擎不应该用简单的开关模型去处理“用户意图”和“算法意图”的冲突。Face AE 不是一定要被 touch AE 压制的,二者可以共存,关键是权重怎么分配。
在实际代码里,我现在更倾向于把“touch region”和“face region”看作相同地位的两个观察者,让 AE 统计模块根据当前场景动态调配权重。能做到这一点,不仅三方相机的问题少了,后续系统相机的人像模式、多区域测光也会好调很多。如果你也正在处理类似问题,建议先检查 log 里 touchWeight 和 faceWeight 的变化,大概率能一针见血。