App 如何获取不同 ev 输入

需求

YUV 夜景这类 HDR 效果的 SDK,需要不同 EV (Exposure Value)的输入。

踩坑

之前经验不够,获取不同 EV 是通过设置 iso 和 曝光时间来做的。ev0 的 iso 和曝光时间直接从预览拿到,然后在此基础上计算出 ev+ 和 ev- 的 iso 和曝光时间。计算方式是将 iso 和曝光时间乘以(或除以) 2。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
private List<CaptureRequest> getCaptureRequestBuilder(String id, Surface surface) {
List<CaptureRequest> requests = new ArrayList<>();
CaptureRequest.Builder stillBuilder = createBuilder(id, CameraDevice.TEMPLATE_STILL_CAPTURE, surface);
if (null != stillBuilder) {
stillBuilder.set(CaptureRequest.CONTROL_CAPTURE_INTENT, CaptureRequest.CONTROL_CAPTURE_INTENT_STILL_CAPTURE);
stillBuilder.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_OFF);
stillBuilder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_OFF);
}
int ev0CaptureIso = mBaseIso;
long ev0CaptureExposureTime = mBaseExpoTime;
int evP1CaptureIso = ev0CaptureIso * 2;
long evP1CaptureExposureTime = ev0CaptureExposureTime * 2;
int evN1CaptureIso = ev0CaptureIso / 2;
long evN1CaptureExposureTime = ev0CaptureExposureTime / 2;
int evN2CaptureIso = evN1CaptureIso / 2;
long evN2CaptureExposureTime = evN1CaptureExposureTime / 2;
int evN3CaptureIso = evN2CaptureIso / 2;
long evN3CaptureExposureTime = evN2CaptureExposureTime / 2;
int evN4CaptureIso = evN3CaptureIso / 2;
long evN4CaptureExposureTime = evN3CaptureExposureTime / 2;
if (stillBuilder != null) {
for (int i = 0; i < EVP1_SIZE; ++i) {
stillBuilder.set(CaptureRequest.SENSOR_SENSITIVITY, evP1CaptureIso);
stillBuilder.set(CaptureRequest.SENSOR_EXPOSURE_TIME, evP1CaptureExposureTime);
requests.add(stillBuilder.build());
}
for (int i = 0; i < EV0_SIZE; ++i) {
stillBuilder.set(CaptureRequest.SENSOR_SENSITIVITY, ev0CaptureIso);
stillBuilder.set(CaptureRequest.SENSOR_EXPOSURE_TIME, ev0CaptureExposureTime);
requests.add(stillBuilder.build());
}
for (int i = 0; i < EVN1_SIZE; ++i) {
stillBuilder.set(CaptureRequest.SENSOR_SENSITIVITY, evN1CaptureIso);
stillBuilder.set(CaptureRequest.SENSOR_EXPOSURE_TIME, evN1CaptureExposureTime);
requests.add(stillBuilder.build());
}
for (int i = 0; i < EVN2_SIZE; ++i) {
stillBuilder.set(CaptureRequest.SENSOR_SENSITIVITY, evN2CaptureIso);
stillBuilder.set(CaptureRequest.SENSOR_EXPOSURE_TIME, evN2CaptureExposureTime);
requests.add(stillBuilder.build());
}
for (int i = 0; i < EVN3_SIZE; ++i) {
stillBuilder.set(CaptureRequest.SENSOR_SENSITIVITY, evN3CaptureIso);
stillBuilder.set(CaptureRequest.SENSOR_EXPOSURE_TIME, evN3CaptureExposureTime);
requests.add(stillBuilder.build());
}
for (int i = 0; i < EVN4_SIZE; ++i) {
stillBuilder.set(CaptureRequest.SENSOR_SENSITIVITY, evN4CaptureIso);
stillBuilder.set(CaptureRequest.SENSOR_EXPOSURE_TIME, evN4CaptureExposureTime);
requests.add(stillBuilder.build());
}
}
return requests;
}

这种方式的缺点:

  • App 上拿到的 ev0 的 iso 可能不是最终拍照时使用的 iso,经过平台代码处理后,可能还会乘以 sensor gain 值。这会导致我们计算出来的所有 ev 值都不准确。
  • 不知道手机支持的 ev 范围。
  • 直接设置 iso 和曝光时间来获取不同 ev 的输入也不是推荐的做法。
  • 代码很难看。

更优雅的方式

更好的方式是直接使用设置 ev 的方式来获取不同 ev 的输入,而且可以通过相关接口来得知当前手机 APP 层支持的 ev 范围。(这个范围不是死的,客户可以扩展。)

当前 ROM 支持的 ev 范围

手机支持的 ev 范围可以通过下面这两个接口计算出:

CameraCharacteristics#CONTROL_AE_COMPENSATION_RANGE

CameraCharacteristics#CONTROL_AE_COMPENSATION_STEP

上面两个接口的含义可以直接看下官方文档的解释,解释的很清楚。简单来说手机支持的 ev 范围就是 CONTROL_AE_COMPENSATION_RANGE 给出的范围乘以 CONTROL_AE_COMPENSATION_STEP。

比如 RANGE 是 [-18, 18],STEP 是 1/6,那当前支持的 ev 范围是 [-3, 3]。

PS:如果最暗帧(比如上面例子中的 ev-3)还是不满足算法要求,比如高光细节不够等,就可以请客户协助扩展 RANGE,也就是扩展支持的 ev。

设置 ev

设置 ev 是通过设置 CaptureRequest#CONTROL_AE_EXPOSURE_COMPENSATION 的值,需要注意的是,我们设置的不是手机支持的 ev 值,而是上面 CONTROL_AE_COMPENSATION_RANGE 给出的范围里面的值。还是上面的例子,如果我们要拍 ev+1,那么我们设置的值就应该是 1/(1/6) = 6,以此类推 ev+2 就是设置 12,所以当你发现 ev+1 不够亮而 ev+2 又太亮时,其实还可以把 ev 设置为 6 ~ 12 间的任意值,都是支持的。

来看下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
private List<CaptureRequest> getCaptureRequestBuilder() throws CameraAccessException {
CaptureRequest.Builder captureBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
captureBuilder.setTag(getCameraId());
captureBuilder.addTarget(mImageReader.getSurface());
captureBuilder.set(CaptureRequest.CONTROL_CAPTURE_INTENT, CaptureRequest.CONTROL_CAPTURE_INTENT_STILL_CAPTURE);
captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_ON);
captureBuilder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_OFF);
List<CaptureRequest> requests = new ArrayList<>(EV_REQUIRED_SIZE);
for (int i = 1; i <= EV_REQUIRED_SIZE; ++i) {
if (EXCLUDE_INDEX.contains(i)) {
int realEvValue = calculateAECompensation(EV_INPUT_PARAMS[i-1]);
captureBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, realEvValue);
}
requests.add(captureBuilder.build());
}
return requests;
}
private int calculateAECompensation(int evValue) {
int result = 0;
CameraCharacteristics characteristics = getManager().getCharacteristics(getCameraId());
Range<Integer> range = characteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE);
if (range == null) {
return result;
}
int max = range.getUpper();
int min = range.getLower();
Log.d("TAG", "CONTROL_AE_COMPENSATION_RANGE Lower: " + min + ", Upper: " + max); // eg: -18 ~ 18
if (min == 0 || max == 0) {
return result;
}
Rational rational = characteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP);
if (rational == null) {
return result;
}
double step = rational.doubleValue();
Log.d("TAG", "CONTROL_AE_COMPENSATION_STEP: " + step); // eg: 1/6
double aeCompensation = evValue / step;
result = range.clamp((int) Math.rint(aeCompensation));
Log.d("TAG", "calculateAECompensation required ev: " + evValue + ", result: " + result);
return result;
}

利用 calculateAECompensation 函数我们将平时我们说的 ev+1,ev0,ev-1 转换成对应需要设置的值:

1
2
int realEvValue = calculateAECompensation(EV_INPUT_PARAMS[i-1]);
captureBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, realEvValue);

隔帧取

在我们设置了 ev 之后,并不是立马就会生效的,它有一个收敛过程,所以我们需要隔帧取。

比如我们需要 3 张 ev+1,3 张 ev0,3 张 ev-2, 3 张 ev-4 共 12 张。我们不能只下发 12 个 request(这样的话我们拿到的输入图 ev 可能不对),而是需要下发 16 个 request。具体做法是每个 ev 档位我们都多拍一张,图片来的时候,又丢弃掉每个 ev 档位的第一张。在这个例子也就是下发 4 张 ev+1,4 张 ev0,4 张 ev-2, 4 张 ev-4 共 16 张。然后当图片到来时,丢弃掉第 1 张 ev+1,第 1 张 ev0,第 1 张 ev-1,第 1 张 ev-2。

1
2
3
4
if(!EXCLUDE_INDEX.contains(mImageIndex)) {
byte[] nv21 = getNV21DataFromImage(capImage);
mBurstImages.add(nv21);
}

高通 camx 和非 camx 架构的设置 ev 值的差异

另外在 camx 和非 camx 架构上设置 ev 时也有一个差异。

细心的同学可能已经发现了,上面的代码中有一个 if 判断:

1
2
3
4
5
6
7
8
9
List<CaptureRequest> requests = new ArrayList<>(EV_REQUIRED_SIZE);
for (int i = 1; i <= EV_REQUIRED_SIZE; ++i) {
if (EXCLUDE_INDEX.contains(i)) {
int realEvValue = calculateAECompensation(EV_INPUT_PARAMS[i-1]);
captureBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, realEvValue);
}
requests.add(captureBuilder.build());
}
return requests;

这个是非 camx 架构设置 ev 的方式,即我们只设置每组 ev 第一张的 ev 值。还是上面的例子,我们在第一张 ev+1 的 request 中设置它的 ev 为 +1,后续三张 ev+1 不需要再设置 ev 值。接着我们在第一张 ev0 的 request 中设置它的 ev 为 0,后续三张 ev0 不需要再设置 ev 值。以此类推。原因是在非 camx 框架上,每一次设置 ev 值都会有收敛过程,所以我们只需要设置每组 ev 第一张的 ev 值。如果每张都设置它的 ev 值,拍出来的图会发现同一组 ev 亮度会不一样。

在 camx 新架构的平台上,设置 ev 是直接就生效的,所以需要设置每一张的 ev 值,不再需要判断是否是每组 ev 的第一张:

1
2
3
4
5
6
7
List<CaptureRequest> requests = new ArrayList<>(EV_REQUIRED_SIZE);
for (int i = 1; i <= EV_REQUIRED_SIZE; ++i) {
int realEvValue = calculateAECompensation(EV_INPUT_PARAMS[i-1]);
captureBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, realEvValue);
requests.add(captureBuilder.build());
}
return requests;

注意:经过项目上验证,camx 和非 camx 架构都需要上一节提到过的隔帧取。

全文完

感谢阅读