Android相机的部分工作原理。

预览流程

相机预览是Android Camera最常用的功能之一,它是很多功能重要的输入,例如扫码、AR等。

一般而言,相机预览的整体流程,可以通过下图表示:

Android 相机拍照预览和拍出来不一致 手机相机预览界面在哪_android

其中,本文主要针对于camera 1的预览API进行总结。

启动相机

在android camera 1之中,是通过以下代码来打开Android设备上的相机:

Camera.open(int cameraId);

其中camerId, 表示Android设备上的某个相机,一般为以下两个值之一:

  • CameraInfo#CAMERA_FACING_BACK : 后置相机
  • CameraInfo#CAMERA_FACING_FRONT : 前置相机

需注意,在以下两种情况下,该API可能会抛出异常:

  • 当前应用没有相机权限;
  • 存在其他应用占用了当前cameraId的相机;

具体是返回空还是抛出异常,在不同的机型和不同版本的Android系统上,会有不同的体现,具体如下表所示:

机型

Android系统

没有相机权限

相机被占用

小米 mix3

9.0

抛出RuntimException异常

无感知抢占过来

华为 navo 3i

8.1

抛出RuntimException异常

无感知抢占过来

华为 navo

7.0

抛出RuntimException异常

无感知抢占过来

OPPO R9s

6.0

抛出RuntimException异常

无感知抢占过来

VIVO X7+

5.1

API调用成功,但是预览无画面

无感知抢占过来

荣耀3C

4.4

抛出RuntimException异常

抛出RuntimException异常

OPPO R7

4.4

抛出RuntimException异常

抛出RuntimException异常

预览参数配置

可以分别用以下两个API获取、设置当前相机的参数:

获取参数:

Camera#getParameters()

设置参数:

Camera#setParameters(Camera.Parameters params)

和预览相关的相机参数主要有三个:预览尺寸、预览格式以及自动聚焦。

▐  预览尺寸

预览尺寸,表示每一预览帧的高度与宽度。在Android设备上,设置的预览尺寸必须是相机支持的尺寸,否则在调用Camera.setParameters方法时,会抛出RuntimeException异常。

不同的设备,所支持的预览尺寸不同,可以通过下面的API获取当前设备支持的预览尺寸列表:

android.hardware.Camera.Parameters#getSupportedPreviewSizes()

一般,如果对预览尺寸没有很强制的需求时,可以不用设置该值,直接走当前设备默认的预览尺寸。

▐  预览格式

大部分Android设备,只支持NV21和YV12两种预览格式;虽然两种格式都属于YUV格式,但是依旧存在一些区别;简单而言,两者的区别是:

  • YV12:存储顺序是先存Y,再存V,最后存U。YYYVVVUUU;
  • NV21:存储顺序是先存Y,再存U,最后存V。YYYVUVUVU

当前设备所支持的预览格式列表的API,如下:

Camera.Parameters#getSupportedPreviewFormats()

与预览尺寸一致,在Android设备上,设置的预览格式必须是相机支持的格式,否则在调用Camera.setParameters方法时,会抛出RuntimeException异常。

▐  自动聚焦

如果不设置自动聚焦,那么在预览状态下,随着手机设备的前后、左右移动,会出现预览界面模糊的现象。

一般而言,有两种自动聚焦的方法:

  • 使用相机自带的自动聚焦模式;
  • 使用传感器监听设备的运动情况(静止或移动),然后以此时机,执行相机自带的触摸聚焦API,可参考详情页面(回复页面);

目前,Android Camera 1 只支持以下四种自动聚焦模式:

  • FOCUS_MODE_AUTO
  • FOCUS_MODE_CONTINUOUS_PICTURE
  • 一般适用于拍照的场景
  • FOCUS_MODE_CONTINUOUS_VIDEO
  • 一般适用于录屏的场景
  • FOCUS_MODE_MACRO
  • 一般适用于特写镜头场景

一般而言,会使用FOCUS_MODE_AUTO作为自动聚焦的聚焦模式;原因是因为FOCUS_MODE_CONTINUOUS_PICTURE与FOCUS_MODE_CONTINUOUS_VIDEO模式在部分机型中无法聚焦;至于FOCUS_MODE_MACRO模式,目前暂未发现与FOCUS_MODE_AUTO有什么比较明显的区别。

Android Camera 1通过以下API进行聚焦模式的设置:

Camera.Parameters#setFocusMode(String focusMode)

除了上述四种支持自动聚焦的聚焦模式之外,Android Camera 1还有一些其他场景使用的聚焦模式。

再设置了自动聚焦的聚焦模式后,还需要以下API来完成最终的自动聚焦效果:

// 先取消其他的自动聚焦操作
mCamera.cancelAutoFocus();
mCamera.autoFocus(this);

autoFocus方法的参数是Camera.AutoFocusCallback接口的实现者;该接口提供了一个自动聚焦的回调方法:

public interface AutoFocusCallback{
   void onAutoFocus(boolean success, Camera camera);
}

回调方法中的success参数表示聚焦是否成功(成功表示当前预览帧界面比较清晰);

需要注意的是,autoFocus方法的内部并不会循环聚焦;所以,如果要一直保持自动聚焦,则需要在回调方法中再次调用autoFocus方法;如下所示:

@override
void onAutoFocus(boolean success, final Camera camera){
   Handler handler = new Handler(Looper.getMainLooper());
   
   // 一般而言,需要延迟1秒再次执行自动聚焦;
   // 之所以不马上执行,是因为在部分机型上,马上执行自动聚焦,会引起预览界面闪烁(尤其是后置摄像头)
   handler.postDelay(new Runnable(){
      @override
      public void run(){
         camera.autoFocuse(this);
      }
   }, 1000)
}

还需要注意的一点是,autoFocus方法一定要在Camera#startPreview()方法之后执行,否则autoFocus方法会抛出RuntimeException异常。

▐  相机显示角度

原始图片

Android 相机拍照预览和拍出来不一致 手机相机预览界面在哪_安卓_02

后置摄像头某一帧预览图片

Android 相机拍照预览和拍出来不一致 手机相机预览界面在哪_redis_03

前置摄像头某一帧预览图片

Android 相机拍照预览和拍出来不一致 手机相机预览界面在哪_java_04

从三张图片可以对不得出,以下两个结论:

  • 后置摄像头,返回的预览帧图片相较于原始图片,顺时针旋转90度;
  • 前置摄像头,返回的预览帧图片相较于原始图片,逆时针旋转90度;

因此,在处理预览帧需要保证预览帧的方向与屏幕(界面)方向一致;具体而言,可以分为以下两个场景:

  • 对于系统渲染预览帧的场景(SurfaceView或TextureView),需要调用Camera#setDisplayOrientation(int degrees)方法来设置相机的显示角度;
  • 对于使用GPU来渲染预览帧的场景(GLSurfaceView),则在采样预览帧对应的纹理的时候,需要考虑旋转的兼容;具体的旋转角度可参考Camera.CameraInfo#orientation属性(该属性的含义表示帧图片需要顺时针旋转多少度才能恢复成原始图片);

对于第一种场景,setDisplayOrientation方法的参数值,可通过以下标准方法来准确的获取:

public static int setCameraDisplayOrientation(Activity activity, int cameraId) {
    android.hardware.Camera.CameraInfo info =
            new android.hardware.Camera.CameraInfo();
    android.hardware.Camera.getCameraInfo(cameraId, info);
    int rotation = activity.getWindowManager().getDefaultDisplay()
            .getRotation();
    // 一般而言,当前Activity为竖屏模式,degress为0;
    // 当前Activity为横屏模式,degress为90;
    int degrees;
    switch (rotation) {
        case Surface.ROTATION_0:
            degrees = 0;
            break;
        case Surface.ROTATION_90:
            degrees = 90;
            break;
        case Surface.ROTATION_180:
            degrees = 180;
            break;
        case Surface.ROTATION_270:
            degrees = 270;
            break;
        default:
            degrees = 0;
            break;
    }


    // info.orientation表示预览帧图片为了与设备的自然方向对齐,所需的顺时针旋转的角度
    int result;
    if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
        result = (info.orientation + degrees) % 360;
        // compensate the mirror
        // 前置摄像头,在顺时针旋转之前,会水平翻转
        result = (360 - result) % 360;
    } else {  // back-facing
        result = (info.orientation + (360 - degrees)) % 360;
    }
    return result;
}

开启预览

当预览相关的参数设置完毕,需要调用以下API开启相机的预览功能:

Camera.startPreview()

该API有三点需要注意的:

  • 在调用startPreview方法之前,需确保调用过Camera.setPreviewTexture方法或者Camera.setPreviewDisplay方法;否则,不仅没有预览画面,而且预览回调接口也不会回调。
  • 如果调用startPreview方法的Camera对象已经调用了release方法,则会抛出以下异常:
java.lang.RuntimeException: Camera is being used after Camera.release() was called
        at android.hardware.Camera.startPreview(Native Method)
  • 如果调用startPreview方法之前,有其他应用调用了openCamera,则会抛出以下异常:
java.lang.RuntimeException: startPreview failed
        at android.hardware.Camera.startPreview(Native Method)

预览回调

当调用Camera.startPreview方法之后,Android Camera1会回调以下接口:

public interface PreviewCallback
{
    void onPreviewFrame(byte[] data, Camera camera);
}

Android Camera 1通常有以下几种方式来设置预览回调接口:

方法名

入参

说明

setOneShotPreviewCallback

PreviewCallback

  • 设置的预览回调只会回调一次;
  • 可在任意时刻调用该方法,即使在预览过程之中;
  • 该方法设置的预览回调会覆盖已有的回调接口对象

setPreviewCallbackAllocation

Allocation

  • 将预览帧数据作为一个Allocation对象使用
  • 该方法主要结合RenderScript使

setPreviewCallbackWithBuffer

PreviewCallback

  • 设置的预览回调会回调多次;
  • 该方法与Camera.addCallbackBuffer方法结合使用,以便复用预览帧数据数组;减少预览帧数据数组的频繁创建;
  • Camera.addCallbackBuffer方法将添加一个预览帧数组到预览帧数组Buffer之中,当预览帧来临,且buffer中有可用的数组,则回调预览回调接口;否则擦除此次来临的预览帧;
  • 在日常开发中,使用最多

setPreviewCallback

PreviewCallback

  • 设置的预览回调会回调多次;
  • 不会复用预览帧数组

预览数据显示

将预览数据显示出来,目前有以下两种方式:

  • 向Android Camera 1提供一个用来显示的本地窗口(SurfaceHolder或SurfaceTexture);
  • 利用OpenGL ES环境来渲染预览数据;

这两种方式之中,前者的实现比较方便,但是无法个性化渲染预览数据;后者的实现较为复杂,但是可以灵活的处理、渲染预览帧数据。

▐  SurfaceHolder&SurfaceTexture

Android Camera 1使用SurfaceHolder对象与SurfaceTexture对象,将预览帧数据分别发送给SurfaceView与TextrueView进行显示。

虽然,两者的开发流程相似,但是背后的原理,还是差异较大。

对于SurfaceHolder(SurfaceView),可以通过下图简略的说明其流程:

Android 相机拍照预览和拍出来不一致 手机相机预览界面在哪_android_05

首先,SurfaceHolder(或者说SurfaceView)会分配好一块单独的GraphicBuffer用以单独渲染;这块单独的GraphicBuffer,与SurfaceView所在的视图树对应的GraphicBuffer不同,前者在SurfaceFlinger端,也有单独的记录用以管理、合成。

然后,使用相机的应用,会将SurfaceHolder传递给相机服务进程,相机服务进程便以此创建一个Surface对象;

当相机服务进程开始预览,相机服务进程会将预览的YUV数据,绘制至此Surface对象之上,最后通过此Surface对象与SurfaceFlinger进程,跨进程通信,将内容渲染到屏幕之上。

此方式在预览期间,无法使用Surface.lockCanvas方法,会抛出IllegalArgumentException异常;所以,无法额外的在预览界面上进行个性化的渲染。

对于SurfaceTexture(TextureView),可以通过下图简略的说明流程:

Android 相机拍照预览和拍出来不一致 手机相机预览界面在哪_webgl_06

首先,TextureView会创建好一个SurfaceTexture对象;这个SurfaceTexture对象,会与一个硬件加速相关的Layer关联起来;这个Layer对象属于TextureView所在的视图树。

然后,使用相机的应用,会将SurfaceTexture传递给相机服务进程,相机服务进程便以此创建一个Surface对象;

当相机服务进程开始预览,相机服务进程会将预览的YUV数据,绘制至此Surface对象之上,最后通过此Surface对象与使用相机的应用,跨进程通信,将内容渲染到TextureView所在的视图树上;可以将SurfaceTexture看做是一个跨进程写入的纹理,相机服务进程持有此纹理的写权限。

最终,通过视图树与SurfaceFlinger进程,通信,将内容渲染到屏幕之上。

此方式在预览期间,无法使用Surface.lockCanvas方法,会抛出IllegalArgumentException异常;所以,无法额外的在预览界面上进行个性化的绘制。

此外该方式还有一个地方需要注意:Camera.setPreviewTexture方法并不会全局持有SurfaceTexture,而SurfaceTexture对象一旦失去引用,被GC掉,那么预览效果同样会失效。

▐  openGL ES渲染预览数据

在openGL环境之中,可以通过以下两种方式渲染预览数据:

  • 直接通过预览帧的byte数组渲染;
  • 通过预览帧对应的SurfaceTexture对象进行渲染;

两种方式,最大的区别是:是否由android camera自动将预览数据填充至纹理之中。

直接通过预览帧byte数组渲染

根据上面的叙述,预览帧byte数组的格式是yuv格式;因此该种方式的核心思路是,在GPU中将yuv数据转换成RGB格式。

相应的转换脚本,在网上很容易找到,大体上的计算方式是一致的,具体如下代码所示:

precision mediump float;
uniform sampler2D tex_y;
uniform sampler2D tex_uv;
// 纹理坐标
varying vec2 texture_coordinate;


void main(){
    float r, g, b, y, u, v;
    // 提取yuv颜色信息
    y = 1.1643 * (texture2D(tex_y, texture_coordinate).r - 0.0625);
    u = texture2D(tex_uv, texture_coordinate).a - 0.5;
    v = texture2D(tex_uv, texture_coordinate).r - 0.5;
    // 将yuv格式转换成rgb格式
    r = y + 1.13983 * v;
    g = y - 0.39465 * u - 0.58060 * v;
    b = y + 2.03211 * u;
    // 片元上色
    gl_FragColor = vec4(r, g, b ,1.0);
}

如代码所示,yuv格式的byte数组,是填充到两个纹理之中:y纹理与uv纹理;之所以用两个纹理,是因为纹理yuv格式中的y数据与uv数据尺寸不一致,前者等于一张帧图片的分辨率,后者则只等于一张帧图片分辨率的1/4;所以无法在一个纹理中复用。

接下来,就是很常规的open gl es绘制流程:

  1. 清空屏幕(FrameBuffer);
  2. 使用指定的脚本程序;
  3. 如果需要,分别创建y纹理与uv纹理;
  4. 根据onPreviewCallback返回的yuv byte数组填充第三步两个纹理;其中y纹理一个值表示一个像素,uv纹理两个值表示一个像素。
  5. 更新mvp矩阵;
  6. 更新绘制屏幕的顶点坐标;
  7. 更新y纹理与uv纹理共用的纹理坐标;其中纹理坐标的顺序与第6步顶点坐标的顺序保持一致;
  8. 通过glDrawArrays方法,使用GLES20.GL_TRIANGLE模式绘制;
  9. 一些收尾工作;

该方式还需要注意的一点是,yuv格式的byte数组与屏幕存在一定的旋转角度(详情参看3.4小节),所以第七步更新纹理坐标时,需要考虑相应的旋转角度。

SurfaceTexture对象进行渲染

用SurfaceTexture对象进行渲染,则相机的配置、打开及预览相关API的调用与TextureView预览流程基本一致。

为了简化EGL环境的创建,demo之中使用GLSurfaceView(也可以使用TextureView,但是TextureView需要手动创建EGL环境)来演示。

与纯粹的使用TextureView预览相机帧不同的一点是,我们需要在GL环境中,手动的创建一张纹理,并根据此纹理创建出一个SurfaceTexture对象,然后把这个SurfaceTexture对象设置给Camera。同时,这一步骤需在打开相机之前完成(具体而言,在调用Camera.setPreviewTexture方法之前完成)。

具体的open gl es绘制流程如下所示:

  1. 清空屏幕(FrameBuffer);
  2. 使用指定的脚本程序;
  3. 调用SurfaceTextureView.updateTexImage方法来将相机服务线程传递来的最新帧数据,填充至该对象对应的纹理之中;可以通过控制该方法的调用频率,来控制渲染相机预览帧的速率;
  4. 更新mvp矩阵;
  5. 更新绘制屏幕的顶点坐标;
  6. 更新SurfaceTexture对应纹理的纹理坐标(采样坐标);其中纹理坐标的顺序与第5步顶点坐标的顺序保持一致;
  7. 将SurfaceTexture.getTransformMatrix方法返回的矩阵传递给GPU,该矩阵可将一般的纹理坐标,转换成SurfaceTexture对象对应纹理的正确采用坐标;
  8. 通过glDrawArrays方法,使用GLES20.GL_TRIANGLE模式绘制;
  9. 一些收尾工作;

因为第七步的缘故,所以无需考虑纹理与屏幕的旋转角度。

预览数据显示

虽然,业界对Android Camera 1相关的API存在一定的诟病,但是,如果使用得当,对于一般性的相机预览需求,还是能够“胜任”。

而在整个相机预览流程中,对于相机的操作(例如打开、预览等)主要还是依赖于系统提供的API,而对于预览画面的展示,Android系统还是提供了较为丰富的可选技术方案;每一种技术方案各有优缺点,可以覆盖较大范围的使用场景。

作者|书意

编辑|橙子君