Android Camera2 实现触摸对焦功能(Touch to Focus)

之前在 Android Camera2 简介 这篇文章中简单介绍了下 Camera2 中 AF/AE 对焦区域如何进行设置, 之前是通过手动计算对应关系实现的, 但这种方式需要考虑到前后摄的区别, 前摄和后摄坐标映射有区别, 通用性不好, 本文讲一下如何通过矩阵(Matrix)来实现这个过程.

为什么要进行坐标映射

由于我们预览界面通常都是竖屏, 而对于 Camera 底层的坐标来说, 一般预览竖屏方向和后摄有90度夹角, 和前摄有270度夹角, 并且预览大小和底层图片实际大小也不是对应的, 所以我们点击预览界面某个位置后, 需要进行坐标转换, 这样才能根据点击位置进行正确的对焦和测光操作.

另外 Camera API 1 中的底层坐标区域和 Camera API 2 中的区域也有区别, 具体和预览坐标对应关系如下图(以后摄为例):

Camera_Coordination.PNG

图片中蓝色框表示手机预览界面, 紫色线条坐标为Android View坐标系, 绿色为 Camera 坐标系, 旧的Camera底层坐标范围大小是固定的, 宽高都为2000, 而Camera2中的 大小要根据查询出来的 SENSOR_INFO_ACTIVE_ARRAY_SIZE 来进行确定.

使用Matrix进行坐标映射

  • Camera API 1
    关于API 1的坐标映射, 可以参考Android源码中Camera代码, 路径:
    packages/apps/Camera2/src/com/android/camera/ui/focus/CameraCoordinateTransformer.java

核心代码如下:

private Matrix cameraToPreviewTransform(boolean mirrorX, int displayOrientation,
      RectF previewRect) {
    Matrix transform = new Matrix();

    // 缩放, (1, 1) 无改变, (-1, 1) x轴反向缩放, 即表示沿y轴镜像翻转
    // 如果是前置摄像头需翻转, 后置不需要.
    transform.setScale(mirrorX ? -1 : 1, 1);

    // 旋转, 从上面的坐标图可以看出, 预览和底层坐标有夹角
    transform.postRotate(displayOrientation);

    // 使用矩阵进行坐标映射, 将大小为 2000 x 2000矩形映射到
   // 预览大小, 比如 1920 x 1080  
    Matrix fill = new Matrix();
    fill.setRectToRect(CAMERA_DRIVER_RECT,
          previewRect,
          Matrix.ScaleToFit.FILL);

    // Concat the previous transform on top of the fill behavior.
    transform.setConcat(fill, transform);

    return transform;
}

上面是Android源码里面的代码, 是先求的Camera Driver坐标映射到Preview坐标的Matrix, 然后通过 Matrix.invert() 得到 Preview坐标到Camera Driver坐标的映射关系.
得到有映射关系的Matrix后, 坐标转换只需调用 mapRect(result, source);即可.

  • Camera API 2
    上面 API 1 的代码是不能直接用在 API 2中的, 主要原因是 Camera2 中底层的坐标和Camera中的区别比较大, Matrix.setRectToRect()的调用和API 1 中逻辑稍有差别,
    完整的映射关系代码如下:
    CoordinateTransformer.java
package com.smewise.camera2.utils;

import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.hardware.camera2.CameraCharacteristics;

/**
 * Transform coordinates to and from preview coordinate space and camera driver
 * coordinate space.
 */
public class CoordinateTransformer {

    private final Matrix mPreviewToCameraTransform;
    private RectF mDriverRectF;

    /**
     * Convert rectangles to / from camera coordinate and preview coordinate space.
     * @param chr camera characteristics
     * @param previewRect the preview rectangle size and position.
     */
    public CoordinateTransformer(CameraCharacteristics chr, RectF previewRect) {
        if (!hasNonZeroArea(previewRect)) {
            throw new IllegalArgumentException("previewRect");
        }
        Rect rect = chr.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
        Integer sensorOrientation = chr.get(CameraCharacteristics.SENSOR_ORIENTATION);
        int rotation = sensorOrientation == null ? 90 : sensorOrientation;
        mDriverRectF = new RectF(rect);
        Integer face = chr.get(CameraCharacteristics.LENS_FACING);
        boolean mirrorX = face != null && face == CameraCharacteristics.LENS_FACING_FRONT;
        mPreviewToCameraTransform = previewToCameraTransform(mirrorX, rotation, previewRect);
    }

    /**
     * Transform a rectangle in preview view space into a new rectangle in
     * camera view space.
     * @param source the rectangle in preview view space
     * @return the rectangle in camera view space.
     */
    public RectF toCameraSpace(RectF source) {
        RectF result = new RectF();
        mPreviewToCameraTransform.mapRect(result, source);
        return result;
    }

    private Matrix previewToCameraTransform(boolean mirrorX, int sensorOrientation,
          RectF previewRect) {
        Matrix transform = new Matrix();
        // Need mirror for front camera.
        transform.setScale(mirrorX ? -1 : 1, 1);
        // Because preview orientation is different  form sensor orientation,
        // rotate to same orientation, Counterclockwise.
        transform.postRotate(-sensorOrientation);
        // Map rotated matrix to preview rect
        transform.mapRect(previewRect);
        // Map  preview coordinates to driver coordinates
        Matrix fill = new Matrix();
        fill.setRectToRect(previewRect, mDriverRectF, Matrix.ScaleToFit.FILL);
        // Concat the previous transform on top of the fill behavior.
        transform.setConcat(fill, transform);
        // finally get transform matrix
        return transform;
    }

    private boolean hasNonZeroArea(RectF rect) {
        return rect.width() != 0 && rect.height() != 0;
    }
}

转换逻辑都在 previewToCameraTransform() 函数中, 直接求Preview到Camera Driver的坐标转换, 而不是像Android源码里面先反向求矩阵然后反转. 步骤为:

  1. 判读是否是前摄, 是否需要镜像翻转 transform.setScale(mirrorX ? -1 : 1, 1);
  2. 将预览坐标旋转对应角度, 使之和Camera Driver坐标长宽对应 transform.postRotate(-sensorOrientation);
  3. 将当前的Matrix操作作用于预览对应的矩阵上,transform.mapRect(previewRect); 此时得到的 previewRect逻辑上和 mDriverRectF已经对应了
  4. 通过 fill.setRectToRect() 转换后, 坐标已经完整映射到 mDriverRectF坐标系中了, 最后将之前两种变换的Matrix结合起来, transform.setConcat(fill, transform); ,得到最终坐标变换的Matrix.

得到想要的Matrix后, 击屏幕后, 根据屏幕坐标构建一个Rect, 通过调用 RectF toCameraSpace(RectF source);, 就得到了我们可以直接构造MeteringRectangle(Rect rect, int meteringWeight)Rect

注意: 构造函数 public CoordinateTransformer(CameraCharacteristics chr, RectF previewRect)中的 CameraCharacteristics chr, 要区分不同Camera ID, 前后摄不能弄错了.

触发对焦操作

这个之前已经讲过了, 再重新贴下代码:

public void startControlAFRequest(MeteringRectangle rect,
                                        CameraCaptureSession.CaptureCallback captureCallback) {

    MeteringRectangle[] rectangle = new MeteringRectangle[]{rect};
    // 对焦模式必须设置为AUTO
    mPreviewBuilder.set(CaptureRequest.CONTROL_AF_MODE,CaptureRequest.CONTROL_AF_MODE_AUTO);
    //AE
    mPreviewBuilder.set(CaptureRequest.CONTROL_AE_REGIONS,rectangle);
    //AF 此处AF和AE用的同一个rect, 实际AE矩形面积比AF稍大, 这样测光效果更好
    mPreviewBuilder.set(CaptureRequest.CONTROL_AF_REGIONS,rectangle);
    try {
        // AE/AF区域设置通过setRepeatingRequest不断发请求
        mSession.setRepeatingRequest(mPreviewBuilder.build(), null, mHandler);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
    //触发对焦
    mPreviewBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,CaptureRequest.CONTROL_AF_TRIGGER_START);
    try {
        //触发对焦通过capture发送请求, 因为用户点击屏幕后只需触发一次对焦
        mSession.capture(mPreviewBuilder.build(), captureCallback, mHandler);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

上面有一点需要注意, 当设置触发对焦的Request:
mPreviewBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,CaptureRequest.CONTROL_AF_TRIGGER_START);
我们是通过 mSession.capture() 触发一次对焦操作的, 但在下次进行 mSession.setRepeatingRequest() 之前, 需要将之前的触发对焦的Request给清除掉, 即设置:
mPreviewBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,CaptureRequest.CONTROL_AF_TRIGGER_IDLE);
如果不设置的话, 会造成连续不断的对焦.

完整Demo

如果想看完整的可运行的Demo App和源码, 可以看下我写的Camera2 Demo:
https://github.com/smewise/Camera2

你可能感兴趣的:(Android Camera2 实现触摸对焦功能(Touch to Focus))