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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容