曝光控制:让画面亮暗由你说了算

大家好,我是你们的Android摄像头开发课讲师。今天我们来聊聊曝光控制——说白了,就是怎么让相机拍出你想要的亮度。

我记得刚入行那会儿,总觉得曝光是相机自动搞定的事。直到有一次做扫码项目,在强光下二维码死活扫不出来,我才意识到:曝光控制,其实是摄像头开发里最容易被忽视、却又最关键的一环。

测光模式:相机怎么判断「亮不亮」?

相机要决定曝光,首先得知道场景有多亮。这个「知道」的过程,就是测光。

Android Camera2 API 提供了三种测光模式,我一个个说。

1. 矩阵测光(MeteringMode.MATRIX)

这是最常用的模式。相机把画面分成几十个区域,分别测光,然后加权平均。说白了,就是「大家投票决定曝光值」。

适合风景、日常拍照。我自己的习惯是,拍大场景时优先用矩阵测光,省心。

2. 中心权重测光(MeteringMode.CENTER_WEIGHTED)

画面中心区域权重更高,边缘区域权重低。适合主体在画面中央的场景,比如人像。

嗯,这里要注意:如果你的主体不在中央,用这个模式容易翻车。我曾经拍一张侧脸人像,背景太亮,结果人脸全黑了——就是因为用了中心权重测光。

3. 点测光(MeteringMode.SPOT)

只取画面中一个很小的点(通常1%-5%的区域)来测光。适合逆光、舞台、月亮等极端场景。

为什么会有这种模式?你想想看,拍月亮时如果还用矩阵测光,整个画面会被平均成灰蒙蒙一片,月亮根本拍不清楚。点测光只盯着月亮测,就能把月亮拍得又亮又清晰。

核心要点:测光模式决定了「相机看哪里」,但不决定「最终曝光值」。最终曝光值还要结合曝光补偿、ISO、快门速度一起算。

曝光补偿:手动微调亮度

自动测光再聪明,也有犯傻的时候。比如雪景,相机觉得太亮会自动压暗,结果拍出来雪是灰色的。这时候就需要曝光补偿。

曝光补偿的单位是EV(Exposure Value)。每+1EV,亮度翻倍;每-1EV,亮度减半。

场景 建议补偿值 原因
雪景/白色背景 +1.0 ~ +2.0 EV 相机容易过暗,需要提亮
黑色物体/夜景 -0.5 ~ -1.0 EV 相机容易过亮,需要压暗
逆光人像 +0.7 ~ +1.3 EV 人脸需要补光

在Camera2 API里,设置曝光补偿的代码很简单:

// 获取曝光补偿范围
Range<Integer> compensationRange = characteristics.get(
    CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE);
int min = compensationRange.getLower();
int max = compensationRange.getUpper();

// 设置曝光补偿(单位:EV的1/8步进)
CaptureRequest.Builder builder = cameraDevice.createCaptureRequest(
    CameraDevice.TEMPLATE_PREVIEW);
builder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, 6); // +0.75 EV
小技巧:曝光补偿的步进值通常是1/3 EV或1/2 EV。我习惯用1/3 EV步进,调节更精细。在强光下做扫码项目时,我经常把补偿调到-1.0 EV,能有效减少反光。

AE Bracketing:多拍几张,总有一张对的

AE Bracketing,全称是自动曝光包围。说白了就是:按一次快门,相机自动拍多张不同曝光的照片。

为什么要这么做?

因为有些场景太复杂,一张照片根本搞不定。比如拍日落,天空很亮、地面很暗。你曝光调亮了,天空过曝;调暗了,地面死黑。这时候AE Bracketing就派上用场了。

典型的Bracketing设置:

  • 张数:3张或5张
  • 步进:±0.7 EV 或 ±1.0 EV
  • 顺序:欠曝 → 正常 → 过曝

在Camera2 API里,实现Bracketing需要用到CaptureRequest的重复请求:

// 创建三个不同曝光的请求
List<CaptureRequest> bracketRequests = new ArrayList<>();

// 欠曝 -1.0 EV
CaptureRequest.Builder builder1 = cameraDevice.createCaptureRequest(
    CameraDevice.TEMPLATE_STILL_CAPTURE);
builder1.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, -8);
bracketRequests.add(builder1.build());

// 正常曝光
CaptureRequest.Builder builder2 = cameraDevice.createCaptureRequest(
    CameraDevice.TEMPLATE_STILL_CAPTURE);
builder2.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, 0);
bracketRequests.add(builder2.build());

// 过曝 +1.0 EV
CaptureRequest.Builder builder3 = cameraDevice.createCaptureRequest(
    CameraDevice.TEMPLATE_STILL_CAPTURE);
builder3.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, 8);
bracketRequests.add(builder3.build());

// 发送Burst请求
captureSession.captureBurst(bracketRequests, captureCallback, null);
注意:不是所有设备都支持Burst capture。建议先检查REQUEST_AVAILABLE_CAPABILITIES中是否包含BURST_CAPTURE。我曾经在低端设备上踩过坑,Burst请求发出去后只返回了一张照片。

HDR原理:把多张照片合成一张

AE Bracketing拍完多张照片后,下一步就是合成HDR。

HDR的全称是High Dynamic Range,高动态范围。它的核心思想很简单:

  • 从欠曝照片里提取高光细节(天空、灯光等)
  • 从过曝照片里提取暗部细节(阴影、地面等)
  • 把两者合成到一张照片里

说白了,就是「取长补短」。

Android从4.0开始就内置了HDR模式。在Camera2 API里,开启HDR的方式有两种:

  1. 场景模式CONTROL_SCENE_MODE_HDR,系统自动处理Bracketing和合成
  2. 手动Bracketing + 第三方合成:自己拍多张,然后用OpenCV或RenderScript合成

第一种最简单,但可控性差。第二种灵活,但需要自己写合成算法。

我个人建议:如果只是做普通拍照App,用系统自带的HDR就够了。但如果你在做专业相机App,比如拍星空、拍产品,那手动Bracketing + 自定义合成才是王道。

避坑指南:我曾经在HDR合成时遇到过鬼影问题——因为手持拍摄,三张照片之间有位移,合成后边缘出现重影。解决方案是:要么用三脚架,要么在合成前做图像对齐(Image Alignment)。OpenCV的findTransformECC函数可以搞定。

知识体系总览

下面这张图,把今天讲的内容串起来了:

曝光控制知识体系 曝光控制 测光模式:矩阵 / 中心权重 / 点测光 曝光补偿:±EV 手动微调亮度 AE Bracketing:多张不同曝光照片 HDR合成:取长补短,一张搞定

从这张图可以看得很清楚:测光模式决定了「看哪里」,曝光补偿决定了「调多少」,AE Bracketing决定了「拍几张」,HDR决定了「怎么合」。四者环环相扣,缺一不可。

写在最后

曝光控制这块,说难不难,说简单也不简单。我见过很多开发者,花大量时间研究图像算法,却忽略了最基础的曝光控制。结果拍出来的照片,要么过曝、要么欠曝,算法再好也救不回来。

记住一句话:曝光是照片的骨架,算法只是血肉。骨架歪了,血肉再丰满也没用。

好了,今天的内容就到这里。下一章我们聊聊白平衡——为什么有时候拍出来的照片偏黄、偏蓝?怎么手动校正?到时候见。


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