稀有猿诉

十年磨一剑,历炼出锋芒,说话千百句,不如码二行。

Camera2拍照之3A处理

前面一篇文章介如了如何进行拍照,但那是最为基本的操作,还不够,作为相机还需要处理3A相关的参数和状态,以得到更好的拍照效果,这篇文章就来详细的学习一下如何处理最基础的3A。

基础知识

3A,也即是AF – Auto Focus(自动对焦), AE – Auto Exposure(自动爆光)和 AWB – Auto White Balance(自动白平衡),是拍照里面最为基础的三参数,对拍照的效果有直接影响。

大多数情况下,是不需要做特别的处理的,只要是一款合格的相机(无论是单反,卡片机,还是智能手机)都有自动模式,也就是说相机硬件系统会运行在一组自动的参数之下,也是默认的,基本的和常用的模式,用户不需要特别调整。只有在手动模式,或者称为高级模式的时候,才需要用户去设置特别的参数,以达到更为惊艳的拍照效果,或者应对更为复杂的拍摄条件,当然手动模式需要用户具备专业的知识,否则得到的拍摄效果会比auto模式还要差。

常规的参数设置

旧的API中只需要从Camera对象中拿到Parameters对象,它像一个Map,更改其中的值就可以了。Camera 2略有变化 ,但总体思想是一样的,仍是类似于Map,键-值式的设置具体的参数,我们来看一下。

关键的对象

基类是CameraMetadata,它里面以键-值形式定义了参数,类似于一个Map,但它的键并不是单纯的String,值也并不是简单的数据类型如整数或者字串,而是全部都搞成了对象,以一种比较复杂的泛型的方式搞出来的,但就具体使用上面与Map一样,就get/set就好了,虽然都是对象,但都能autobox。

classDiagram CameraMetadata <|-- CaptureRequest CameraMetadata <|-- CaptureResult CaptureRequest *-- Builder class CameraMetadata { } class CaptureRequest { } class Builder { } class CaptureResult { }

更为具体的,把下发参数都用CaptureRequest来封装,也就是说上层对底层下发的参数请求,放于此对象中;而从底层读参数的状态,或者说读取某些参数的值则放在了CaptureResult里面。

CaptureRequest通常是在下发请求时要去从其Builder中生成的;而CaptureResult则是在CaptureCallback中由底层传上来的。

参数设置的流程

根据不同的参数需要可以在预览时,或者拍照时下发参数,方法基本上是一样的,都是通过请求下去的:CameraCaptureSession#setRepeatingRequest,CameraCaptureSession#capture。

在下发请求前需要通过CaptureRequest#Builder创建请求,而参数就是在此时以键-值方式设置下去的。CaptureRequest是一个Immutable的对象,只能过构建者模式Builder来改变和生成。

sequenceDiagram CameraAgent ->> CameraDevice: createCaptureRequest CameraDevice -->> CameraAgent: CaptureRequest.Builder CameraAgent ->> CameraCaptureSession: setRepeatingRequest CameraAgent ->> CameraCaptureSession: capture CameraCaptureSession -->> CameraAgent: CaptureCallback.onCaptureStarted CameraCaptureSession -->> CameraAgent: CaptureCallback.onCaptureProgressed CameraCaptureSession -->> CameraAgent: CaptureCallback.onCaptureCompleted

常见的预览参数设置

把3A都设置为Auto即可,能满足需求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void applyCommonParameters(CaptureRequest.Builder requestBuilder) {
    requestBuilder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO);
    requestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
    requestBuilder.set(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_AUTO);
}

public void applyForPreview(CaptureRequest.Builder previewBuilder) {
    applyCommonParameters(previewBuilder);

    previewBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
    previewBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE);
    previewBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false);

    previewBuilder.set(CaptureRequest.CONTROL_CAPTURE_INTENT, CaptureRequest.CONTROL_CAPTURE_INTENT_PREVIEW);

    applyFlashMode(previewBuilder, ApplyType.PREVIEW);
}

常见的拍照参数设置

相较于preview无明显变化,只不过需要添加一些成片相关的设置,如图片质量,旋转等:

1
2
3
4
5
6
7
8
9
10
11
public void applyForStillCapture(CaptureRequest.Builder requestBuilder) {
    applyCommonParameters(requestBuilder);

    requestBuilder.set(CaptureRequest.JPEG_QUALITY, (byte) 92);
    requestBuilder.set(CaptureRequest.JPEG_ORIENTATION, jpegRotation);

    requestBuilder.set(CaptureRequest.CONTROL_CAPTURE_INTENT, CaptureRequest.CONTROL_CAPTURE_INTENT_STILL_CAPTURE);
    requestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE);

    applyFlashMode(requestBuilder, ApplyType.CAPTURE);
}

如果就是普通的拍照需求,以上这次常规的参数设置就足够了。

闪光灯

闪光灯是拍照过程中非常重要的参数,也会显著的影响拍照效果,但与旧的Camera不同,Camera 2中打闪的过程比较复杂,并不是把四种闪光灯模式设置下去就能完事儿的,还涉及AE状态的处理,需要仔细学习和研究。

闪光灯模式,现代的闪光灯共有四种模式:关闭,自动,打开和常亮:

  • 关闭 – 也就是关闭闪光灯,无论什么条件下都不打闪
  • 自动 – 拍照时,依据AE自动爆光时的光线情况,决定是否需要打闪
  • 打开 – 拍照时,打闪
  • 常亮 – 像手电筒一样一直打开闪光灯,预览和拍照时都可生效。注意不能常亮时间太长,否则会引起手机温度过高,耗电过快,甚至可能此起主板烧断。

通过在CaptureRequest中设置,键为CaptureRequest.FLASH_MODE,但与旧的不一样,它的值只有三个:

与预期的四种模式并不匹配,并且如果你下发上面的模式会发现,OFF和TORCH还是符合预期的,分别对应着关闭和常亮。但是即使是SINGLE也并不是打开,它打闪的时间很短,拍照还没有完成就一闪而过,并且具体的打闪的时机,似乎与AE过程并不匹配。前面提到了闪光灯是与自动爆光(AE)过程关联特别大的,只有当AE收敛后打闪才是最合适的时机,并且打闪过程要持续到拍照结束。这里涉及AE状态的处理,并且真实下发的FLASH_MODE并不是SINGLE。

注意:闪光灯的自动和打开状态仅对拍照过程生效,预览过程是不生效的,所以设置什么值都一样。下面的讨论也是针对拍照过程中的。

闪光灯打开模式实现方法

仔细查阅文档可以发现,自动和打开,其实与AE有关,并且AE有两种模式疑似与闪光灯有关系:

  1. ON_AUTO_FLASH
  2. ON_ALWAYS_FLASH
  3. ON_AUTO_FLASH_REDEYE

但直接把AE设置为以上的方式,也不能实现自动打闪和打闪,这两种情况与AE强相关,受AE状态控制,需要处理AE的状态,依据状态的不同然后换思路去实现。

一种实现方式是,真实下发的闪光灯状态是TORCH,但要受AE状态来控制,先触发AE_PRECAPTURE_TRIGGER,然后监听AE状态变化,当其从SEARCHING变为收敛时,或者变为FLASH_REQUIRED时,就直接下发TORCH,直到拍照结束onCaptureCompleted,恢复为正常的状态。

flowchart TD capturePhoto --> flashMode flashMode{FlashMode is ON or AUTO} flashMode == No ==> doCapture flashMode == Yes ==> preCapture preCapture[capture with request
AE_PRECAPTURE_TRIGGER to START] preCapture --> aeSearching aeSearching[AE state is searching] aeSearching --> aeConverged aeConverged[AE state is converged or flashRequired] aeConverged --> flashTorch flashTorch[set flashMode to TORCH] flashTorch --> doCapture doCapture --> onCaptureCompleted onCaptureCompleted --> flashSingle flashSingle[set flashMode to SINGLE]

需要注意的是,预拍照请求是通过单次请求capture下发的,其CaptureCallback可以与预览使用同一个。另外就是AE收敛过程的处理是在onCaptureProgressed中处理的,而其最终的状态是在onCaptureCompleted中的。

闪灯光自动模式实现方式

自动模式,与打开时类似,也是当需要打闪的时候直接下发TORCH,只不过,需要在预拍照时,看到AE的状态为FLASH_REQUIRED时,再去下发TORCH,否则就是OFF。

sequenceDiagram CameraAgent ->> CameraCaptureSession: capture AE_PRECAPTURE START CameraCaptureSession -->> CameraAgent: onCaptureProgressed AE PRECAPTURE CameraCaptureSession -->> CameraAgent: onCaptureProgressed AE SEARCHING CameraCaptureSession -->> CameraAgent: onCaptureProgressed AE CONVERGED CameraCaptureSession -->> CameraAgent: onCaptureProgressed AE FLASH_REQUIRED CameraAgent ->> CameraCaptureSession: capture FLASH TORCH CameraCaptureSession -->> CameraAgent: onCaptureCompleted CameraAgent ->> CameraCaptureSession: setRepeatRequest FLASH SINGLE or OFF


总结以上,那么下发flash mode需要做一些修改:

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
private void applyFlashMode(CaptureRequest.Builder requestBuilder, ApplyType type) {
    Log.d(LOG_TAG, "applyFlashMode mode " + flashMode + ", applyType " + type);
    FlashMode applyFlashMode = flashMode;

    switch (type) {
        case PRECAPTURE:
        case CAPTURE:
            if ((flashMode == FlashMode.AUTO || flashMode == FlashMode.ON) && flashRequired)  {
                applyFlashMode = FlashMode.TORCH;
            }
            break;
        default:
            break;
    }
    Log.d(LOG_TAG, "applyFlashMode apply flash mode " + applyFlashMode);
    requestBuilder.set(CaptureRequest.FLASH_MODE, applyFlashMode.value);

    if (applyFlashMode == FlashMode.AUTO) {
        requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
    } else if (applyFlashMode == FlashMode.ON) {
        requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH);
    } else {
        requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
    }
}

由于打闪需要,现在capturePhoto方法已变成了三个方法,或者分为三个主要的步骤,一是预拍照,二是拍照,三是拍照后的流程:

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
58
59
60
61
62
63
64
void capturePhoto(Consumer<PhotoCaptureStatus> consumer, PhotoSaveAgent imageSaveAgent, int orientation) {
    Log.d(LOG_TAG, "takeSnapshot");
    if (!cameraDevice.isPresent() || !captureSession.isPresent()) {
        Log.d(LOG_TAG, "takeSnapshot, cannot snap");
        consumer.accept(null);
        return;
    }

    parameters.setFlashRequired(previewCallback.flashRequired());
    final boolean needPreCapture = parameters.needPreCapture();

    Runnable actionPostCapture = null;
    if (needPreCapture) {
        actionPostCapture = () -> {
            Log.d(LOG_TAG, "postCapture reset flash status for post capture.");
            parameters.applyForPreview(requestBuilder);
            previewCallback.setAEState(CameraParameters.Preview3AState.PICTURE_TAKEN);
            try {
                captureSession.get().setRepeatingRequest(requestBuilder.build(), previewCallback, cameraHandler);
            } catch (CameraAccessException e) {
                e.printStackTrace();
            }
        };
    }

    final int jpegOrientation = CameraUtils.calculateRelativeRotation(characteristics, orientation);
    parameters.setJpegRotation(jpegOrientation);
    final PhotoStillCapture stillCapture = new PhotoStillCapture(consumer, imageSaveAgent, actionPostCapture);

    final Runnable action = () -> {
        Log.d(LOG_TAG, "capture doCapture");
        try {
            captureSession.get().capture(
                    stillCapture.generateRequest(cameraDevice.get(), imageSaveAgent.getOutputTarget(), parameters),
                    stillCapture.getCaptureCallback(),
                    cameraHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    };

    if (needPreCapture) {
        pendingCaptureAction = action;
        preCapture();
    } else {
        action.run();
        pendingCaptureAction = null;
    }
}

private void preCapture() {
    Log.d(LOG_TAG, "preCapture");
    try {
        CaptureRequest.Builder builder = cameraDevice.get().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
        for (Surface surface : previewSurfaces) {
            builder.addTarget(surface);
        }
        parameters.applyForPreCapture(builder);
        previewCallback.setAEState(CameraParameters.Preview3AState.WAITING_PRECAPTURE);
        captureSession.get().capture(builder.build(), previewCallback, cameraHandler);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

CameraParameters对象

为了方便3A以及拍照参数的管理,创建一个新的对象CameraParameters专门用于预览和拍照相关的参数和配置管理。此对象仅与CameraAgent交互,并且由CameraAgent持有。

classDiagram PhotoStillCapture *-- CameraParameters CameraAgent *-- CameraParameters class CameraParameters { } class PhotoStillCapture { } class CameraAgent { }

Comments