15. 对焦与测光:触摸对焦实现、测光区域设置、对焦模式选择(AF/AE/AWB)
拍照这件事,说白了就是「对得准、曝得对、颜色正」。这三个东西在 Android 摄像头里分别叫 AF(自动对焦)、AE(自动曝光)、AWB(自动白平衡)。
我刚开始做相机开发时,觉得这三个东西系统都帮你搞定了,我调什么?结果一上手就发现——默认的自动模式在复杂场景下根本不够用。用户想点哪里对焦,你就得让他点;他想锁定曝光,你就得给他锁。今天我们就来把这套逻辑彻底捋清楚。
核心知识点速览
- 触摸对焦:用户点击屏幕 → 映射到传感器坐标 → 触发 AF/AE 区域
- 测光区域:MeteringRectangle 控制曝光权重
- 对焦模式:AF 模式(自动/连续/手动)、AE 模式(锁定/解锁)、AWB 模式(锁定/解锁)
15.1 触摸对焦的实现
触摸对焦,听起来高大上,其实就三步:
- 用户点击屏幕,拿到 (x, y) 坐标
- 把屏幕坐标映射到传感器坐标
- 设置 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_LOCK 和 CONTROL_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