Camera2 三预览

2023-05-16

1 获取预览尺寸

CameraCharacteristics 是一个只读的相机信息提供者,其内部携带大量的相机信息,包括代表相机朝向的 LENS_FACING;判断闪光灯是否可用的 FLASH_INFO_AVAILABLE;获取所有可用 AE 模式的 CONTROL_AE_AVAILABLE_MODES 等等。如果你对 Camera1 比较熟悉,那么 CameraCharacteristics 有点像 Camera1 的 Camera.CameraInfo 或者 Camera.Parameters。CameraCharacteristics 以键值对的方式提供相机信息,你可以通过 CameraCharacteristics.get() 方法获取相机信息,该方法要求你传递一个 Key 以确定你要获取哪方面的相机信息,例如下面的代码展示了如何获取摄像头方向信息:

val cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId)
val lensFacing = cameraCharacteristics[CameraCharacteristics.LENS_FACING]
when(lensFacing) {
    CameraCharacteristics.LENS_FACING_FRONT -> { // 前置摄像头 }
    CameraCharacteristics.LENS_FACING_BACK -> { // 后置摄像头 }
    CameraCharacteristics.LENS_FACING_EXTERNAL -> { // 外置摄像头 }
}

CameraCharacteristics 有大量的 Key 定义,这里就不一一阐述,当你在开发过程中需要获取某些相机信息的时候再去查阅 API文档即可。

由于不同厂商对相机的实现都会有差异,所以很多参数在不同的手机上支持的情况也不一样,相机的预览尺寸也是,所以接下来我们就要通过 CameraCharacteristics 获取相机支持的预览尺寸列表。所谓的预览尺寸,指的就是相机把画面输出到手机屏幕上供用户预览的尺寸,通常来说我们希望预览尺寸在不超过手机屏幕分辨率的情况下,越大越好。另外,出于业务需求,我们的相机可能需要支持多种不同的预览比例供用户选择,例如 4:3 和 16:9 的比例。由于不同厂商对相机的实现都会有差异,所以很多参数在不同的手机上支持的情况也不一样,相机的预览尺寸也是。所以在设置相机预览尺寸之前,我们先通过 CameraCharacteristics 获取该设备支持的所有预览尺寸:

val streamConfigurationMap = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
val supportedSizes = streamConfigurationMap?.getOutputSizes(SurfaceTexture::class.java)

从上面的代码可以看出预览尺寸列表并不是直接从 CameraCharacteristics 获取的,而是先通过 SCALER_STREAM_CONFIGURATION_MAP 获取 StreamConfigurationMap 对象,然后通过 StreamConfigurationMap.getOutputSizes() 方法获取尺寸列表,该方法会要求你传递一个 Class 类型,然后根据这个类型返回对应的尺寸列表,如果给定的类型不支持,则返回 null,你可以通过 StreamConfigurationMap.isOutputSupportedFor() 方法判断某一个类型是否被支持,常见的类型有:

  • ImageReader:常用来拍照或接收 YUV 数据。
  • MediaRecorder:常用来录制视频。
  • MediaCodec:常用来录制视频。
  • SurfaceHolder:常用来显示预览画面。
  • SurfaceTexture:常用来显示预览画面。

由于我们使用的是 SurfaceTexture,所以显然这里我们就要传递 SurfaceTexture.class 获取支持的尺寸列表。如果我们把所有的预览尺寸都打印出来看时,会发现一个比较特别的情况,就是预览尺寸的宽是长边,高是短边,例如 1920x1080,而不是 1080x1920,这是因为相机 Sensor 的宽是长边,而高是短边。

在获取到预览尺寸列表之后,我们要根据自己的实际需求过滤出其中一个最符合要求的尺寸,并且把它设置给相机,在我们的 Demo 里,只有当预览尺寸的比例和大小都满足要求时才能被设置给相机,如下所示:

@WorkerThread
private fun getOptimalSize(cameraCharacteristics: CameraCharacteristics, clazz: Class<*>, maxWidth: Int, maxHeight: Int): Size? {
    val aspectRatio = maxWidth.toFloat() / maxHeight
    val streamConfigurationMap = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
    val supportedSizes = streamConfigurationMap?.getOutputSizes(clazz)
    if (supportedSizes != null) {
        for (size in supportedSizes) {
            if (size.width.toFloat() / size.height == aspectRatio && size.height <= maxHeight && size.width <= maxWidth) {
                return size
            }
        }
    }
    return null
}

2 配置预览尺寸

在获取适合的预览尺寸之后,接下来就是配置预览尺寸使其生效了。在配置尺寸方面,Camera2 和 Camera1 有着很大的不同,Camera1 是将所有的尺寸信息都设置给相机,而 Camera2 则是把尺寸信息设置给 Surface,例如接收预览画面的 SurfaceTexture,或者是接收拍照图片的 ImageReader,相机在输出图像数据的时候会根据 Surface 配置的 Buffer 大小输出对应尺寸的画面。

获取 Surface 的方式有很多种,可以通过 TextureView、SurfaceView、ImageReader 甚至是通过 OpenGL 创建,这里我们要将预览画面显示在屏幕上,所以我们选择了 TextureView,并且通过 TextureView.SurfaceTextureListener 回调接口监听 SurfaceTexture 的状态,在获取可用的 SurfaceTexture 对象之后通过 SurfaceTexture.setDefaultBufferSize() 设置预览画面的尺寸,最后使用 Surface(SurfaceTexture) 构造方法创建出预览的 Surface 对象:

首先,我们在布局文件中添加一个 TextureView,并给它取个 ID 叫 camera_preview:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextureView
        android:id="@+id/camera_preview"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

然后我们在 Activity 里获取 TextureView 对象,并且注册一个 TextureView.SurfaceTextureListener 用于监听 SurfaceTexture 的状态:

private inner class PreviewSurfaceTextureListener : TextureView.SurfaceTextureListener {
    @MainThread
    override fun onSurfaceTextureSizeChanged(surfaceTexture: SurfaceTexture, width: Int, height: Int) = Unit

    @MainThread
    override fun onSurfaceTextureUpdated(surfaceTexture: SurfaceTexture) = Unit

    @MainThread
    override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture): Boolean = false

    @MainThread
    override fun onSurfaceTextureAvailable(surfaceTexture: SurfaceTexture, width: Int, height: Int) {
        previewSurfaceTexture = surfaceTexture
    }
}
cameraPreview = findViewById<CameraPreview>(R.id.camera_preview)
cameraPreview.surfaceTextureListener = PreviewSurfaceTextureListener()

当 SurfaceTexture 可用的时候会回调 onSurfaceTextureAvailable() 方法并且把 SurfaceTexture 对象和尺寸传递给我们,此时我们要做的就是通过 SurfaceTexture.setDefaultBufferSize() 设置预览画面的尺寸并且创建 Surface 对象:

val previewSize = getOptimalSize(cameraCharacteristics, SurfaceTexture::class.java, width, height)!!
previewSurfaceTexture.setDefaultBufferSize(previewSize.width, previewSize.height)
previewSurface = Surface(previewSurfaceTexture)

到这里,用于预览的 Surface 就准备好了,接下来我们来看下如何创建 CameraCaptureSession。

3 创建 CameraCaptureSession

用于接收预览画面的 Surface 准备就绪了,接了下来我们要使用这个 Surface 创建一个 CameraCaptureSession 实例,涉及的方法是 CameraDevice.createCaptureSession(),该方法要求你传递以下三个参数:

  • outputs:所有用于接收图像数据的 Surface,例如本章用于接收预览画面的 Surface,后续还会有用于拍照的 Surface,这些 Surface 必须在创建 Session 之前就准备好,并且在创建 Session 的时候传递给底层用于配置 Pipeline。
  • callback:用于监听 Session 状态的 CameraCaptureSession.StateCallback 对象,就如同开关相机一样,创建和销毁 Session 也需要我们注册一个状态监听器。
  • handler:用于执行 CameraCaptureSession.StateCallback 的 Handler 对象,可以是异步线程的 Handler,也可以是主线程的 Handler,在我们的 Demo 里使用的是主线程 Handler。
private inner class SessionStateCallback : CameraCaptureSession.StateCallback() {
    @MainThread
    override fun onConfigureFailed(session: CameraCaptureSession) {

    }

    @MainThread
    override fun onConfigured(session: CameraCaptureSession) {
       
    }

    @MainThread
    override fun onClosed(session: CameraCaptureSession) {
        
    }
}
val sessionStateCallback = SessionStateCallback()
val outputs = listOf(previewSurface)
cameraDevice.createCaptureSession(outputs, sessionStateCallback, mainHandler)

4 创建 CaptureRequest

在介绍如何开启和关闭预览之前,我们有必要先介绍下 CaptureRequest,因为它是我们执行任何相机操作都绕不开的核心类,因为 CaptureRequest 是向 CameraCaptureSession 提交 Capture 请求时的信息载体,其内部包括了本次 Capture 的参数配置和接收图像数据的 Surface。CaptureRequest 可以配置的信息非常多,包括图像格式、图像分辨率、传感器控制、闪光灯控制、3A 控制等等,可以说绝大部分的相机参数都是通过 CaptureRequest 配置的。我们可以通过 CameraDevice.createCaptureRequest() 方法创建一个 CaptureRequest.Builder 对象,该方法只有一个参数 templateType 用于指定使用何种模板创建 CaptureRequest.Builder 对象。因为 CaptureRequest 可以配置的参数实在是太多了,如果每一个参数都要我们手动去配置,那真的是既复杂又费时,所以 Camera2 根据使用场景的不同,为我们事先配置好了一些常用的参数模板:

  • TEMPLATE_PREVIEW:适用于配置预览的模板。
  • TEMPLATE_RECORD:适用于视频录制的模板。
  • TEMPLATE_STILL_CAPTURE:适用于拍照的模板。
  • TEMPLATE_VIDEO_SNAPSHOT:适用于在录制视频过程中支持拍照的模板。
  • TEMPLATE_MANUAL:适用于希望自己手动配置大部分参数的模板。

这里我们要创建一个用于预览的 CaptureRequest,所以传递了 TEMPLATE_PREVIEW 作为参数:

val requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)

一个 CaptureRequest 除了需要配置很多参数之外,还要求至少配置一个 Surface(任何相机操作的本质都是为了捕获图像),并且配置的 Surface 必须属于创建 Session 时添加的那些 Surface,涉及的方法是 CaptureRequest.Builder.addTarget(),你可以多次调用该方法添加多个 Surface。

requestBuilder.addTarget(previewSurface)

最后,我们通过 CaptureRequest.Builder.build() 方法创建出一个只读的 CaptureRequest 实例:

val request = requestBuilder.build()

5 开启和停止预览

在 Camera2 里,预览本质上是不断重复执行的 Capture 操作,每一次 Capture 都会把预览画面输出到对应的 Surface 上,涉及的方法是 CameraCaptureSession.setRepeatingRequest(),该方法有三个参数:

  • request:在不断重复执行 Capture 时使用的 CaptureRequest 对象。
  • callback:监听每一次 Capture 状态的 CameraCaptureSession.CaptureCallback 对象,例如 onCaptureStarted() 意味着一次 Capture 的开始,而 onCaptureCompleted() 意味着一次 Capture 的结束。
  • hander:用于执行 CameraCaptureSession.CaptureCallback 的 Handler 对象,可以是异步线程的 Handler,也可以是主线程的 Handler,在我们的 Demo 里使用的是主线程 Handler。

了解了核心方法之后,开启预览的操作就很显而易见了:

val requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
requestBuilder.addTarget(previewSurface)
val request = requestBuilder.build()
captureSession.setRepeatingRequest(request, RepeatingCaptureStateCallback(), mainHandler)

如果要关闭预览的话,可以通过 CameraCaptureSession.stopRepeating() 停止不断重复执行的 Capture 操作:

captureSession.stopRepeating()

到目前为止,如果一切正常的话,预览画面应该就已经显示出来了。

6 适配预览比例

前面我们使用了一个占满屏幕的 TextureView 来显示预览画面,并且预览尺寸我们选择了 4:3 的比例,你很可能会看到预览画面变形的情况,这因为 Surface 的比例和 TextureView 的比例不一致导致的,你可以想象 Surface 就是一张图片,TextureView 就是 ImageView,将 4:3 的图片显示在 16:9 的 ImageView 上必然会出现画面拉伸变形的情况:

预览画面变形

所以接下来我们要学习的是如何适配不同的预览比例。预览比例的适配有多种方式:

  1. 根据预览比例修改 TextureView 的宽高,比如用户选择了 4:3 的预览比例,这个时候我们会选取 4:3 的预览尺寸并且把 TextureView 修改成 4:3 的比例,从而让画面不会变形。
  2. 使用固定的预览比例,然后根据比例去选取适合的预览尺寸,例如固定 4:3 的比例,选择 1440x1080 的尺寸,并且把 TextureView 的宽高也设置成 4:3。
  3. 固定 TextureView 的宽高,然后根据预览比例使用 TextureView.setTransform() 方法修改预览画面绘制在 TextureView 上的方式,从而让预览画面不变形,这跟 ImageView.setImageMatrix() 如出一辙。

简单来说,解决预览画面变形的问题,本质上就是解决画面和画布比例不一致的问题。在我们的 Demo 中,出于简化的目的,我们选择了第二种方式适配比例,因为这种方式实现起来比较简单,所以我们会写一个自定义的 TextureView,让它的比例固定是 4:3,它的宽度固定填满父布局,高度根据比例动态计算:

class CameraPreview @JvmOverloads constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int = 0) : TextureView(context, attrs, defStyleAttr) {
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val width = MeasureSpec.getSize(widthMeasureSpec)
        setMeasuredDimension(width, width / 3 * 4)
    }
}

7 认识 ImageReader

在 Camera2 里,ImageReader 是获取图像数据的一个重要途径,我们可以通过它获取各种各样格式的图像数据,例如 JPEG、YUV 和 RAW 等等。我们可以通过 ImageReader.newInstance() 方法创建一个 ImageReader 对象,该方法要求我们传递以下四个参数:

  • width:图像数据的宽度。
  • height:图像数据的高度。
  • format:图像数据的格式,定义在 ImageFormat 里,例如 ImageFormat.YUV_420_888
  • maxImages:最大 Image 个数,可以理解成 Image 对象池的大小。

当有图像数据生成的时候,ImageReader 会通过通过 ImageReader.OnImageAvailableListener.onImageAvailable() 方法通知我们,然后我们可以调用 ImageReader.acquireNextImage() 方法获取存有最新数据的 Image 对象,而在 Image 对象里图像数据又根据不同格式被划分多个部分分别存储在单独的 Plane 对象里,我们可以通过调用 Image.getPlanes() 方法获取所有的 Plane 对象的数组,最后通过 Plane.getBuffer() 获取每一个 Plane 里存储的图像数据。以 YUV 数据为例,当有 YUV 数据生成的时候,数据会被分成 Y、U、V 三部分分别存储到 Plane 里,如下图所示:

override fun onImageAvailable(imageReader: ImageReader) {
    val image = imageReader.acquireNextImage()
    if (image != null) {
        val planes = image.planes
        val yPlane = planes[0]
        val uPlane = planes[1]
        val vPlane = planes[2]
        val yBuffer = yPlane.buffer // Data from Y channel
        val uBuffer = uPlane.buffer // Data from U channel
        val vBuffer = vPlane.buffer // Data from V channel
    }
    image?.close()
}

上面的代码是获取 YUV 数据的流程,特别要注意的是最后一步调用 Image.close() 方法十分重要,当我们不再需要使用某一个 Image 对象的时候记得通过该方法释放资源,因为 Image 对象实际上来自于一个创建 ImageReader 时就确定大小的对象池,如果我们不释放它的话就会导致对象池很快就被耗光,并且抛出一个异常。类似的的当我们不再需要使用 某一个 ImageReader 对象的时候,也要记得调用 ImageReader.close() 方法释放资源。

8 获取预览数据

介绍完 ImageReader 之后,接下来我们就来创建一个接收每一帧预览数据的 ImageReader,并且数据格式为 YUV_420_888。首先,我们要先判断 YUV_420_888 数据格式是否支持,所以会有如下的代码:

val imageFormat = ImageFormat.YUV_420_888
val streamConfigurationMap = cameraCharacteristics[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]
if (streamConfigurationMap?.isOutputSupportedFor(imageFormat) == true) {
    // YUV_420_888 is supported
}

接着,我们使用前面已经确定好的预览尺寸创建一个 ImageReader,并且注册一个 ImageReader.OnImageAvailableListener 用于监听数据的更新,最后通过 ImageReader.getSurface() 方法获取接收预览数据的 Surface:

val imageFormat = ImageFormat.YUV_420_888
val streamConfigurationMap = cameraCharacteristics[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]
if (streamConfigurationMap?.isOutputSupportedFor(imageFormat) == true) {
    previewDataImageReader = ImageReader.newInstance(previewSize.width, previewSize.height, imageFormat, 3)
    previewDataImageReader?.setOnImageAvailableListener(OnPreviewDataAvailableListener(), cameraHandler)
    previewDataSurface = previewDataImageReader?.surface
}

创建完 ImageReader,并且获取它的 Surface 之后,我们就可以在创建 Session 的时候添加这个 Surface 告诉 Pipeline 我们有一个专门接收 YUV_420_888 的 Surface:

val sessionStateCallback = SessionStateCallback()
val outputs = mutableListOf<Surface>()
val previewSurface = previewSurface
val previewDataSurface = previewDataSurface
outputs.add(previewSurface!!)
if (previewDataSurface != null) {
    outputs.add(previewDataSurface)
}
cameraDevice.createCaptureSession(outputs, sessionStateCallback, mainHandler)

获取预览数据和显示预览画面一样都是不断重复执行的 Capture 操作,所以我们只需要在开始预览的时候通过 CaptureRequest.Builder.addTarget() 方法添加接收预览数据的 Surface 即可,所以一个 CaptureRequest
会有两个 Surface,一个现实预览画面的 Surface,一个接收预览数据的 Surface:

val requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
val previewSurface = previewSurface
val previewDataSurface = previewDataSurface
requestBuilder.addTarget(previewSurface!!)
if (previewDataSurface != null) {
    requestBuilder.addTarget(previewDataSurface)
}
val request = requestBuilder.build()
captureSession.setRepeatingRequest(request, RepeatingCaptureStateCallback(), mainHandler)

在开始预览之后,每一次刷新预览画面的时候,都会通过 ImageReader.OnImageAvailableListener.onImageAvailable() 方法通知我们:

private inner class OnPreviewDataAvailableListener : ImageReader.OnImageAvailableListener {

    /**
     * Called every time the preview frame data is available.
     */
    override fun onImageAvailable(imageReader: ImageReader) {
        val image = imageReader.acquireNextImage()
        if (image != null) {
            val planes = image.planes
            val yPlane = planes[0]
            val uPlane = planes[1]
            val vPlane = planes[2]
            val yBuffer = yPlane.buffer // Data from Y channel
            val uBuffer = uPlane.buffer // Data from U channel
            val vBuffer = vPlane.buffer // Data from V channel
        }
        image?.close()
    }
}

9 如何矫正图像数据的方向

如果你熟悉 Camera1 的话,也许已经发现了一个问题,就是 Camera2 不需要经过任何预览画面方向的矫正,就可以正确现实画面,而 Camera1 则需要根据摄像头传感器的方向进行预览画面的方向矫正。其实,Camera2 也需要进行预览画面的矫正,只不过系统帮我们做了而已,当我们使用 TextureView 或者 SurfaceView 进行画面预览的时候,系统会根据【设备自然方向】、【摄像传感器方向】和【显示方向】自动矫正预览画面的方向,并且该矫正规则只适用于显示方向和和设备自然方向一致的情况下,举个例子,当我们把手机横放并且允许自动旋转屏幕的时候,看到的预览画面的方向就是错误的。此外,当我们使用一个 GLSurfaceView 显示预览画面或者使用 ImageReader 接收图像数据的时候,系统都不会进行画面的自动矫正,因为它不知道我们要如何显示预览画面,所以我们还是有必要学习下如何矫正图像数据的方向,在介绍如何矫正图像数据方向之前,我们需要先了解几个概念,它们分别是【设备自然方向】、【局部坐标系】、【显示方向】和【摄像头传感器方向】。

9.1 设备方向

当我们谈论方向的时候,实际上都是相对于某一个 0° 方向的角度,这个 0° 方向被称作自然方向,例如人站立的时候就是自然方向,你总不会认为一个人要倒立的时候才是自然方向吧,而接下来我们要谈论的设备方向就有的自然方向的定义。

设备方向指的是硬件设备在空间中的方向与其自然方向的顺时针夹角。这里提到的自然方向指的就是我们手持一个设备的时候最习惯的方向,比如手机我们习惯竖着拿,而平板我们则习惯横着拿,所以通常情况下手机的自然方向就是竖着的时候,平板的自然方向就是横着的时候。

以手机为例,我们可以有以下四个比较常见的设备方向:

  • 当我们把手机垂直放置且屏幕朝向我们的时候,设备方向为 0°,即设备自然方向
  • 当我们把手机向右横放且屏幕朝向我们的时候,设备方向为 90°
  • 当我们把手机倒着放置且屏幕朝向我们的时候,设备方向为 180°
  • 当我们把手机向左横放且屏幕朝向我们的时候,设备方向为 270°

了解了设备方向的概念之后,我们可以通过 OrientationEventListener 监听设备的方向,进而判断设备当前是否处于自然方向,当设备的方向发生变化的时候会回调 OrientationEventListener.onOrientationChanged(int) 方法,传给我们一个 0° 到 359° 的方向值,其中 0° 就代表设备处于自然方向。

9.2 局部坐标系

所谓的局部坐标系指的是当设备处于自然方向时,相对于设备屏幕的坐标系,该坐标系是固定不变的,不会因为设备方向的变化而改变,下图是基于手机的局部坐标系示意图:

 

局部坐标系

  • x 轴是当手机处于自然方向时,和手机屏幕平行且指向右边的坐标轴。
  • y 轴是当手机处于自然方向时,和手机屏幕平行且指向上方的坐标轴。
  • z 轴是当手机处于自然方向时,和手机屏幕垂直且指向屏幕外面的坐标轴。

为了进一步解释【坐标系是固定不变的,不会因为设备方向的变化而改变】的概念,这里举个例子,当我们把手机向右横放且屏幕朝向我们的时候,此时设备方向为 90°,局部坐标系相对于手机屏幕是保持不变的,所以 y 轴正方向指向右边,x 轴正方向指向下方,z 轴正方向还是指向屏幕外面,如下图所示:

 设备方向 90°

9.3 显示方向

显示方向指的是屏幕上显示画面与局部坐标系 y 轴的顺时针夹角。

为了更清楚的说明这个概念,我们举一个例子,假设我们将手机向右横放看电影,此时画面是朝上的,如下图所示:

从上图来看,手机向右横放会导致设备方向变成了 90°,但是显示方向却是 270°,因为它是相对局部坐标系 y 轴的顺时针夹角,所以跟设备方向没有任何关系。如果把图中的设备换成是平板,结果就不一样了,因为平板横放的时候就是它的设备自然方向,y 轴朝上,屏幕画面显示的方向和 y 轴的夹角是 0°,设备方向也是 0°。总结一下,设备方向是相对于其现实空间中自然方向的角度,而显示方向是相对局部坐标系的角度。

9.4 摄像头传感器方向

摄像头传感器方向指的是传感器采集到的画面方向经过顺时针旋转多少度之后才能和局部坐标系的 y 轴正方向一致,也就是通过 CameraCharacteristics.SENSOR_ORIENTATION 获取到的值。

例如 orientation 为 90° 时,意味我们将摄像头采集到的画面顺时针旋转 90° 之后,画面的方向就和局部坐标系的 y 轴正方向一致,换个说法就是原始画面的方向和 y 轴的夹角是逆时针 90°。

最后我们要考虑一个特殊情况,就是前置摄像头的画面是做了镜像处理的,也就是所谓的前置镜像操作,这个情况下, orientation 的值并不是实际我们要旋转的角度,我们需要取它的镜像值才是我们真正要旋转的角度,例如 orientation 为 270°,实际我们要旋转的角度是 90°。

注意:摄像头传感器方向在不同的手机上可能不一样,大部分手机都是 90°,也有小部分是 0° 的,所以我们要通过 CameraCharacteristics.SENSOR_ORIENTATION 去判断方向,而不是假设所有设备的摄像头传感器方向都是 90°。

9.5 矫正图像数据的方向

介绍完几个方向的概念之后,我们就来说下如何校正相机的预览画面。我们会举几个例子,由简到繁逐步说明预览画面校正过程中要注意的事项。

首先我们要知道的是摄像头传感器方向只有 0°、90°、180°、270° 四个可选值,并且这些值是相对于局部坐标系 的 y 轴定义出来的,现在假设一个相机 APP 的画面在手机上是竖屏显示,也就是显示方向是 0° ,并且假设摄像头传感器的方向是 90°,如果我们没有校正画面的话,则显示的画面如下图所示(忽略画面变形):

 很明显,上面显示的画面内容方向是错误的,里面的人物应该是垂直向上显示才对,所以我们应该吧摄像头采集到的画面顺时针旋转 90°,才能得到正确的显示结果,如下图所示:

 上面的例子是建立在我们的显示方向是 0° 的时候,如果我们要求显示方向是 90°,也就是手机向左横放的时候画面才是正的,并且假设摄像头传感器的方向还是 90°,如果我们没有校正画面的话,则显示的画面如下图所示(忽略画面变形):

 此时,我们知道传感器的方向是 90°,如果我们将传感器采集到的画面顺时针旋转 90° 显然是无法得到正确的画面,因为它是相对于局部坐标系 y 轴的角度,而不是实际显示方向,所以在做画面校正的时候我们还要把实际显示方向也考虑进去,这里实际显示方向是 90°,所以我们应该把传感器采集到的画面顺时针旋转 180°(摄像头传感器方向 + 实际显示方向) 才能得到正确的画面,显示的画面如下图所示(忽略画面变形):

 总结一下,在校正画面方向的时候要同时考虑两个因素,即摄像头传感器方向和显示方向。接下来我们要回到我们的相机应用里,看看通过代码是如何实现预览画面方向校正的。

如果你有自己看过 Camera 的官方 API 文档,你会发现官方已经给我们写好了一个同时考虑显示方向和摄像头传感器方向的方法,我把它翻译成 Kotlin 语法:

private fun getDisplayRotation(cameraCharacteristics: CameraCharacteristics): Int {
    val rotation = windowManager.defaultDisplay.rotation
    val degrees = when (rotation) {
        Surface.ROTATION_0 -> 0
        Surface.ROTATION_90 -> 90
        Surface.ROTATION_180 -> 180
        Surface.ROTATION_270 -> 270
        else -> 0
    }
    val sensorOrientation = cameraCharacteristics[CameraCharacteristics.SENSOR_ORIENTATION]!!
    return if (cameraCharacteristics[CameraCharacteristics.LENS_FACING] == CameraCharacteristics.LENS_FACING_FRONT) {
        (360 - (sensorOrientation + degrees) % 360) % 360
    } else {
        (sensorOrientation - degrees + 360) % 360
    }
}

如果你已经完全理解前面介绍的那些角度的概念,那你应该很容易就能理解上面这段代码,实际上就是通过 WindowManager 获取当前的显示方向,然后再参照摄像头传感器方向以及是否是前后置,最后计算出我们实际要旋转的角度。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Camera2 三预览 的相关文章

  • YUV420P旋转

    YUV420与YUV420P YUV 和我们熟知的 RGB 类似 xff0c 是一种颜色编码格式 它主要用于电视系统和模拟视频邻域 xff08 如 Camera 系统 xff09 YUV 包含三个分量 xff0c 其中 Y 表示明亮度 xf
  • android-camera方向

    1 概念解释 自然方向 xff1a 指当宽比高短时 xff0c 我们看到的手机的方向 xff08 竖屏 xff09 xff0c 就是自然方向 2 相机图像传感器采集图像的方向 由于手机Camera拍摄到的图片来自相机的图像传感器 xff0c
  • clion创建第一个C项目

    点击new project 选择C Executable 输入路径 Language standard C99 main c include lt stdio h gt void main char string 61 34 I love

随机推荐

  • VMware Ubuntu虚拟机忘记密码

    前言 xff1a 在VMware运行Ubuntu虚拟机时 xff0c 开机之后忘记密码怎么办 xff1f 环境 xff1a Ubuntu版本 xff1a ubuntu 16 04 6 server amd64 xff1b VMware版本
  • Ubuntu18.04安装matlabR2019A

    载安装包和破解文件 链接 https pan baidu com s 1X09GAchToEqyMRol3msGAA 密码 wak6 下载完成后解压 右击 iso镜像文件 xff0c 选择使用其他程序打开 选择磁盘映像挂载器 打开后会在桌面
  • 03-对抗样本攻击

    对抗样本攻击 Github xff1a https github com Gary11111 03 GAN 研究背景 尽管深度学习在很多计算机视觉领域的任务上表现出色 xff0c Szegedy第一次发现了深度神经网络在图像分类领域存在有意
  • 冒泡排序算法

    冒泡排序是非常好理解的 xff0c 以从小到大排序为例 xff0c 每一轮排序就找出未排序序列中最大值放在最后 设数组的长度为N xff1a xff08 1 xff09 比较前后相邻的二个数据 xff0c 如果前面数据大于后面的数据 xff
  • 将一个分支上的commit 转移到另一个分支上git cherry-pick <commit id>

    使用 cherry pick 根据git 文档 xff1a Apply the changes introduced by some existing commits 就是对已经存在的commit 进行apply 可以理解为再次提交 xff
  • Git 如何查看和修改用户名、邮箱

    用户名和邮箱地址是本地git客户端的一个变量 xff0c 不随git库而改变 每次commit都会用用户名和邮箱纪录 1 查看用户名和地址 git config user name git config user email 2 修改用户名
  • Android PendingIntent

    在Android中 xff0c 我们常常使用PendingIntent来表达一种 留待日后处理 的意思 从这个角度来说 xff0c PendingIntent可以被理解为一种特殊的异步处理机制 不过 xff0c 单就命名而言 xff0c P
  • .CR2格式文件怎么快速批量转换成JPG等格式

    打开需要转换的CR2文件夹 用FSViewer exe看图软件打开CR2文件 xff0c 然后再双击打开已打开的图片并选中所有需要转换的CR2文件 点击工具 选择批量转换选中的图像 若是右边窗口中没有文件 xff0c 则需要手动点击 全部添
  • 利用AlarmManager完成精准的轮询

    问题分析 想起轮询我们一般会想起利用Handler和Timer xff0c 然而AlarmManager相比于Handler和Timer有优势 xff0c 具体的分析我参考了一个大神的博客 xff1a 最近在做一个需求 xff1a 客户端按
  • Intellij IDEA junit 使用之org.junit不存在

    1 同理打开 xff1a File gt project Structure gt librabries gt 点击左上角 43 号 gt From Maven 2 输入 junit 4 12 3 确定即可 在build gradle文件中
  • Android studio显示你的主机中的软件中止了一个已建立的连接

    Android studio在run的时候显示你的主机中的软件中止了一个已建立的连接 出现的场景 是在run项目的时候出现了这样的 如下图 当我把热点关了就好了 再也不会出现类似的情况了
  • 设计模式、重构、编程规范等的经典书籍书籍推荐

    有关设计模式 重构 编程规范等的经典书籍很多 xff0c 有很多你应该已经听说过 甚至看过 今天 xff0c 我就结合我的经验 xff0c 对这些书籍进行一个整理和点评 你可以据此来选择适合你的书籍 xff0c 结合着专栏一块儿来学习 xf
  • Android打开或者关闭GPS

    打开和关闭gps 需要系统权限 android permission WRITE SECURE SETTINGS 打开或者关闭gps public void openGPS boolean open if Build VERSION SDK
  • 【Erro】安装homebrew报错curl: (7) Failed to connect to raw.githubusercontent.com port 443: Operation

    下载brew bin bash c 34 curl fsSL https raw githubusercontent com Homebrew install master install sh 34 出现这样的错误 xff1f curl
  • ChatGPT火爆科研圈,登上《Nature》《Science》正刊

    ChatGPT火出圈了 xff0c 几乎涉及到各行各业的每个领域 xff0c 科研圈更甚 Science 期刊主编H HOLDEN THORP发表关于ChatGPT的社论 xff1a ChatGPT is fun but not an au
  • 解决 Unable to determine application id: com.android.tools.idea.run.ApkProvisionException

    问题 xff1a Unable to determine application id com android tools idea run ApkProvisionException Error loading build artifac
  • Android SurfaceView预览变形完美解决方法

    这个问题百度上一搜一大把 xff0c 基本上都是说找到和SurfaceView的比例相近的camera预览尺寸 xff0c 但是发现预览时候还是差了点意思 xff0c 具体看下面这个回调就知道是为什么了 64 Override public
  • Camera2 教程 一概览

    从 Android 5 0 开始 xff0c Google 引入了一套全新的相机框架 Camera2 xff08 android hardware camera2 xff09 并且废弃了旧的相机框架 Camera1 xff08 androi
  • Camera2 二开关相机

    1 创建相机项目 正如前所说的 xff0c 我们会开发一个具有完整相机功能的应用程序 xff0c 所以第一步要做的就是创建一个相机项目 xff0c 这里我用 AS 创建了一个叫 Camera2Sample 的项目 xff0c 并且有一个 A
  • Camera2 三预览

    1 获取预览尺寸 CameraCharacteristics 是一个只读的相机信息提供者 xff0c 其内部携带大量的相机信息 xff0c 包括代表相机朝向的 LENS FACING xff1b 判断闪光灯是否可用的 FLASH INFO