15. 对焦与测光:触摸对焦实现、测光区域设置、对焦模式选择(AF/AE/AWB)

拍照这件事,说白了就是「对得准、曝得对、颜色正」。这三个东西在 Android 摄像头里分别叫 AF(自动对焦)、AE(自动曝光)、AWB(自动白平衡)。

我刚开始做相机开发时,觉得这三个东西系统都帮你搞定了,我调什么?结果一上手就发现——默认的自动模式在复杂场景下根本不够用。用户想点哪里对焦,你就得让他点;他想锁定曝光,你就得给他锁。今天我们就来把这套逻辑彻底捋清楚。

核心知识点速览

  • 触摸对焦:用户点击屏幕 → 映射到传感器坐标 → 触发 AF/AE 区域
  • 测光区域:MeteringRectangle 控制曝光权重
  • 对焦模式:AF 模式(自动/连续/手动)、AE 模式(锁定/解锁)、AWB 模式(锁定/解锁)
对焦与测光 触摸对焦 测光区域设置 对焦模式选择 坐标映射 AF 区域 AE 区域联动 MeteringRect 权重设置 AE/AWB 分离 AF 模式 AE 锁定 AWB 锁定 三者协同工作:触摸对焦触发 AF 区域 + AE 测光区域联动

15.1 触摸对焦的实现

触摸对焦,听起来高大上,其实就三步:

  1. 用户点击屏幕,拿到 (x, y) 坐标
  2. 把屏幕坐标映射到传感器坐标
  3. 设置 AF 区域和 AE 区域,触发对焦

嗯,这里要注意第二步。屏幕坐标和传感器坐标不是一回事。屏幕是 1080×2400,传感器可能是 4000×3000,而且还有裁剪和旋转。我早期踩过这个坑——直接拿屏幕坐标去设区域,结果对焦对到天边去了。

正确的做法是用 CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE 拿到传感器的有效区域,然后做归一化映射。来看代码:

// 将屏幕坐标映射到传感器坐标
fun mapToSensorCoordinates(
    screenX: Float,
    screenY: Float,
    screenWidth: Int,
    screenHeight: Int,
    sensorRect: Rect
): Point {
    val sensorW = sensorRect.width()
    val sensorH = sensorRect.height()
    val sensorX = (screenX / screenWidth) * sensorW + sensorRect.left
    val sensorY = (screenY / screenHeight) * sensorH + sensorRect.top
    return Point(sensorX.toInt(), sensorY.toInt())
}

拿到坐标后,构建 AF 区域。我个人习惯用 200×200 的矩形,太大不够精准,太小容易对不上。然后通过 CaptureRequest.Builder 设置:

val afRect = MeteringRectangle(
    sensorPoint.x - 100, sensorPoint.y - 100,
    200, 200,
    MeteringRectangle.METERING_WEIGHT_MAX
)

val aeRect = MeteringRectangle(
    sensorPoint.x - 150, sensorPoint.y - 150,
    300, 300,
    MeteringRectangle.METERING_WEIGHT_MAX
)

captureRequestBuilder.set(CaptureRequest.CONTROL_AF_REGIONS, arrayOf(afRect))
captureRequestBuilder.set(CaptureRequest.CONTROL_AE_REGIONS, arrayOf(aeRect))
captureRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START)
captureSession.capture(captureRequestBuilder.build(), callback, handler)

小技巧:AF 区域和 AE 区域可以不一样大。我通常让 AE 区域比 AF 区域大一圈,这样曝光更平滑,不会因为对焦框太小导致画面忽明忽暗。

15.2 测光区域设置

测光区域,说白了就是告诉相机「你重点看哪里来算曝光」。默认是整个画面平均测光,但用户想拍逆光人像时,你得让相机只看人脸区域。

Android Camera2 用 MeteringRectangle 来表示测光区域。你可以设置多个区域,每个区域带一个权重值。权重范围是 0 到 METERING_WEIGHT_MAX(通常是 255)。

这里有个坑——不是所有设备都支持自定义测光区域。我曾在某款低端机上发现,设置了 AE 区域后完全没效果,画面还是全局测光。所以一定要先检查特性:

val characteristics = cameraManager.getCameraCharacteristics(cameraId)
val aeRegionsSupported = characteristics.get(
    CameraCharacteristics.CONTROL_MAX_REGIONS_AE
) ?: 0

if (aeRegionsSupported > 0) {
    // 支持自定义测光区域
    builder.set(CaptureRequest.CONTROL_AE_REGIONS, regions)
} else {
    // 不支持,只能全局测光
    Log.w("Camera", "设备不支持自定义测光区域")
}

我曾经遇到过一台设备,CONTROL_MAX_REGIONS_AE 返回 1,但设置了区域后画面反而过曝了。后来发现是 HAL 层的 bug,需要把区域稍微缩小一点才能正常工作。嗯,这种问题只能靠实机测试。

15.3 对焦模式选择

对焦模式这块,Android 提供了好几种,我挑最常用的三个说:

模式 常量 适用场景
自动单次对焦 CONTROL_AF_MODE_AUTO 拍照场景,对焦一次后锁定
连续自动对焦 CONTROL_AF_MODE_CONTINUOUS_PICTURE 视频录制,持续跟踪画面
手动对焦 CONTROL_AF_MODE_OFF 专业模式,用户自己调焦

我个人最常用的是 CONTINUOUS_PICTURE 模式。你想想看,用户拿起手机拍照,哪有时间等你对焦?连续模式让相机一直在找焦点,用户按下快门时基本已经对好了。

但连续模式有个问题——它不会自动锁定。比如用户想拍一个固定场景,相机却因为画面微动反复对焦,画面就会「呼吸」。这时候需要手动触发一次单次对焦并锁定:

// 先切换到自动模式
builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO)
// 触发对焦
builder.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START)
captureSession.capture(builder.build(), object : CameraCaptureSession.CaptureCallback() {
    override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
        val afState = result.get(CaptureResult.CONTROL_AF_STATE)
        if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED ||
            afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) {
            // 对焦完成,锁定
            // 此时可以拍照了
        }
    }
}, handler)

注意:对焦锁定后,如果用户移动了手机,记得重新触发对焦。我见过有些 App 锁定后就不管了,结果拍出来的照片全是糊的。

15.4 AE/AWB 锁定

AE 和 AWB 锁定,是专业相机 App 的标配。用户对好焦、调好曝光后,不希望相机再自动变化。

锁定很简单:

// 锁定 AE
builder.set(CaptureRequest.CONTROL_AE_LOCK, true)
// 锁定 AWB
builder.set(CaptureRequest.CONTROL_AWB_LOCK, true)

但要注意,不是所有设备都支持锁定。我建议在 UI 上显示锁定按钮前,先检查:

val aeLockSupported = characteristics.get(
    CameraCharacteristics.CONTROL_AE_LOCK_AVAILABLE
) ?: false

val awbLockSupported = characteristics.get(
    CameraCharacteristics.CONTROL_AWB_LOCK_AVAILABLE
) ?: false

解锁时,把 CONTROL_AE_LOCKCONTROL_AWB_LOCK 设回 false 就行。但这里有个细节——解锁后相机不会立刻重新测光,需要等几帧才会稳定。我一般会在解锁后延迟 200ms 再更新 UI 上的曝光指示。

15.5 实战中的避坑指南

最后分享几个我踩过的坑:

  • 区域坐标越界:构建 MeteringRectangle 时,一定要确保矩形完全在 activeArraySize 内。我曾经因为没做边界检查,导致某些设备上直接崩溃。
  • 对焦回调不及时:有些设备对焦完成后不会立刻回调 CONTROL_AF_STATE_FOCUSED_LOCKED,而是先经过 CONTROL_AF_STATE_ACTIVE_SCAN。建议加一个超时机制,比如 3 秒后强制认为对焦完成。
  • 多摄像头切换:切换摄像头后,之前设置的 AF/AE 区域会失效。记得在切换后重新设置。
  • 预览流和拍照流分离:如果你用 repeatingRequest 做预览,用 capture 做拍照,注意拍照请求也要带上 AF/AE 区域设置,否则拍照时可能会用默认参数。

对焦和测光这块,说白了就是「用户指哪,相机打哪」。实现起来不复杂,但细节很多。多在不同设备上测试,你会发现每个厂商的 HAL 实现都有点小脾气。习惯了就好。


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