CameraX 入门指南

1. 准备工作

在本 Codelab 中,您将学习如何使用 CameraX 创建相机应用,以显示取景器、拍照并分析相机的图像流。

为了实现此目标,我们将引入 CameraX 中的用例概念,您可在多个操作中应用此概念,包括显示取景器和实时分析画面帧等。

先决条件

  • 基本的 Android 开发体验。

您需要执行的操作

  • 了解如何添加 CameraX 依赖项。
  • 了解如何在 Activity 中显示相机预览内容。(Preview 用例)
  • 构建可以拍照并将照片保存在存储空间中的应用。(ImageCapture 用例)
  • 了解如何实时分析相机中的画面帧。(ImageAnalysis 用例)

您需要用到的工具

  • 一台 Android 设备,也可以使用 Android Studio 模拟器。我们建议使用搭载 Android 11 或更高版本的 AVD。
  • 受支持的最低 API 级别为 21。
  • Android Studio 3.6 或更高版本。

2. 创建项目

  1. 使用 Android Studio 菜单,新建项目并在收到系统提示时选择*"Empty Activity"* (空 Activity)。

ddc817a03892e44.png

  1. 下一步,将应用命名为"CameraX App"。确保将语言设置为 Kotlin、将最低 API 级别设为 21(对于 CameraX,这是所需的最低级别),并确保您使用 AndroidX 工件。

2383e490b6550aed.png

添加 Gradle 依赖项

  1. 打开 build.gradle(Module: app) 文件并将 CameraX 依赖项添加到应用 Gradle 文件中的"依赖项"部分内:
def camerax_version = "1.0.0-beta07"
// CameraX core library using camera2 implementation
implementation "androidx.camera:camera-camera2:$camerax_version"
// CameraX Lifecycle Library
implementation "androidx.camera:camera-lifecycle:$camerax_version"
// CameraX View class
implementation "androidx.camera:camera-view:1.0.0-alpha14"
  1. CameraX 需要用到 Java 8 中的一些方法,因此我们需要对编译选项进行相应设置。 在 android 块末尾,紧跟 buildTypes 的位置添加以下内容:
compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}
  1. 如果尚未添加此插件,请在最上方进行添加。
apply plugin: 'kotlin-android-extensions'

收到系统提示时,点击 Sync Now(立即同步),这样我们便做好了在应用中使用 CameraX 的准备。

创建取景器布局

使用以下代码替换默认布局:

  1. 打开 activity_main 布局文件并将其替换为以下代码。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <Button
       android:id="@+id/camera_capture_button"
       android:layout_width="100dp"
       android:layout_height="100dp"
       android:layout_marginBottom="50dp"
       android:scaleType="fitCenter"
       android:text="Take Photo"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintBottom_toBottomOf="parent"
       android:elevation="2dp" />

   <androidx.camera.view.PreviewView
       android:id="@+id/viewFinder"
       android:layout_width="match_parent"
       android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

设置 MainActivity.kt

  1. MainActivity.kt 中的代码替换为此内容。其中将包含输入语句、将会实例化的变量、将实现的函数以及常量。

系统已实现 onCreate(),可便于您检查相机权限、启动相机、为拍照按钮设置 onClickListener(),并实现 outputDirectorycameraExecutor。即使您已实现了 onCreate(),相机也不会工作,直到您在文件中实施了这些方法为止。

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.Manifest
import android.content.pm.PackageManager
import android.net.Uri
import android.util.Log
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import java.util.concurrent.Executors
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import kotlinx.android.synthetic.main.activity_main.*
import java.io.File
import java.nio.ByteBuffer
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
typealias LumaListener = (luma: Double) -> Unit

class MainActivity : AppCompatActivity() {
   private var imageCapture: ImageCapture? = null

   private lateinit var outputDirectory: File
   private lateinit var cameraExecutor: ExecutorService

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       // Request camera permissions
       if (allPermissionsGranted()) {
           startCamera()
       } else {
           ActivityCompat.requestPermissions(
               this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
       }

       // Set up the listener for take photo button
       camera_capture_button.setOnClickListener { takePhoto() }

       outputDirectory = getOutputDirectory()

       cameraExecutor = Executors.newSingleThreadExecutor()
   }

   private fun takePhoto() {}

   private fun startCamera() {}

   private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
       ContextCompat.checkSelfPermission(
           baseContext, it) == PackageManager.PERMISSION_GRANTED
   }

   private fun getOutputDirectory(): File {
       val mediaDir = externalMediaDirs.firstOrNull()?.let {
           File(it, resources.getString(R.string.app_name)).apply { mkdirs() } }
       return if (mediaDir != null && mediaDir.exists())
           mediaDir else filesDir
   }

   override fun onDestroy() {
       super.onDestroy()
       cameraExecutor.shutdown()
   }

   companion object {
       private const val TAG = "CameraXBasic"
       private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
       private const val REQUEST_CODE_PERMISSIONS = 10
       private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
   }
}
  1. 运行代码。应用的界面应如下所示:

86b7a9ccc8e13fb0.png

3. 请求相机权限

在应用打开相机前,其需要用户授予执行此操作的权限。在此步骤中,您将用到相机权限。

  1. 打开 AndroidManifest.xml 并在 application 标记之前添加以下行。
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />

添加用于确保设备配备有相机的 android.hardware.camera.any。指定 .any,用以表示相机可以是前置摄像头或后置摄像头。

  1. 将此代码复制到 MainActivity.kt. 中。下方的项目符号点将把您刚才复制的代码分为几个部分。
override fun onRequestPermissionsResult(
   requestCode: Int, permissions: Array<String>, grantResults:
   IntArray) {
   if (requestCode == REQUEST_CODE_PERMISSIONS) {
       if (allPermissionsGranted()) {
           startCamera()
       } else {
           Toast.makeText(this,
               "Permissions not granted by the user.",
               Toast.LENGTH_SHORT).show()
           finish()
       }
   }
}
  • 检查请求代码是否正确;如果此代码不正确,请将其忽略。
if (requestCode == REQUEST_CODE_PERMISSIONS) {

}
  • 如果已授予权限,系统则会调用 startCamera()
if (allPermissionsGranted()) {
   startCamera()
}
  • 如果未授予权限,系统则会显示一个消息框,告知用户未授予权限。
else {
   Toast.makeText(this,
       "Permissions not granted by the user.",
       Toast.LENGTH_SHORT).show()
   finish()
}
  1. 运行应用。

应用现在应该会请求相机使用权限:

885896e144926d4e.png

4. 实现预览用例

在相机应用中,用户可借助取景器预览他们要拍摄的照片。您可以使用 CameraX Preview 类实现取景器功能。

如要使用 Preview,您首先需要定义配置,然后使用该配置创建用例的实例。所生成的实例是您要绑定到 CameraX 生命周期的内容。

  1. 将此代码复制到 startCamera() 函数中。

下方的项目符号点将把您刚才复制的代码分为几个部分。

private fun startCamera() {
   val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

   cameraProviderFuture.addListener(Runnable {
       // Used to bind the lifecycle of cameras to the lifecycle owner
       val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

       // Preview
       val preview = Preview.Builder()
          .build()
          .also {
              it.setSurfaceProvider(viewFinder.createSurfaceProvider())
          }

       // Select back camera as a default
       val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

       try {
           // Unbind use cases before rebinding
           cameraProvider.unbindAll()

           // Bind use cases to camera
           cameraProvider.bindToLifecycle(
               this, cameraSelector, preview)

       } catch(exc: Exception) {
           Log.e(TAG, "Use case binding failed", exc)
       }

   }, ContextCompat.getMainExecutor(this))
}
  • 创建 ProcessCameraProvider 的实例。此实例用于将相机的生命周期绑定到生命周期所有者。由于 CameraX 具有生命周期感知能力,所以这样可以省去打开和关闭相机的任务。
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
  • cameraProviderFuture 中添加监听器。添加 Runnable 作为参数。我们将稍后为其填入数值。添加 ContextCompat.getMainExecutor() 作为第二个参数。这将返回在主线程上运行的 Executor
cameraProviderFuture.addListener(Runnable {}, ContextCompat.getMainExecutor(this))
  • Runnable 中,添加 ProcessCameraProvider。此类用于将相机的生命周期绑定到应用进程内的 LifecycleOwner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
  • 初始化您的 Preview 对象,在该对象上调用 build,从取景器中获取表面提供程序,然后在预览中进行设置。
val preview = Preview.Builder()
   .build()
   .also {
       it.setSurfaceProvider(viewFinder.createSurfaceProvider())
   }
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
  • 创建 try 块。在该块中,确保任何内容都未绑定到您的 cameraProvider,然后将您的 cameraSelector 和预览对象绑定到 cameraProvider
try {
   cameraProvider.unbindAll()
   cameraProvider.bindToLifecycle(
       this, cameraSelector, preview)
} 
  • 在少数情况下,此代码会失败,例如应用不再处于焦点中。将此代码放入 catch 块中,以记录是否存在失败情况。
catch(exc: Exception) {
      Log.e(TAG, "Use case binding failed", exc)
}
  1. 运行应用。您应会看到相机预览!

392602bf3da0a336.png

5. 实现 ImageCapture 用例

其他用例的运行方式非常类似于 Preview。首先,定义用于对实际用例对象进行实例化的配置对象。为了拍摄照片,您需要采用 takePhoto() 方法,当用户按下 **Take photo(拍摄照片)**按钮时,系统会调用此方法。

将此代码复制到 takePhoto() 方法中。

下方的项目符号点将把您刚才复制的代码分为几个部分。

private fun takePhoto() {
   // Get a stable reference of the modifiable image capture use case
   val imageCapture = imageCapture ?: return

   // Create time-stamped output file to hold the image
   val photoFile = File(
       outputDirectory,
       SimpleDateFormat(FILENAME_FORMAT, Locale.US
       ).format(System.currentTimeMillis()) + ".jpg")

   // Create output options object which contains file + metadata
   val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()

   // Set up image capture listener, which is triggered after photo has
   // been taken
   imageCapture.takePicture(
       outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback {
           override fun onError(exc: ImageCaptureException) {
               Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
           }

           override fun onImageSaved(output: ImageCapture.OutputFileResults) {
               val savedUri = Uri.fromFile(photoFile)
               val msg = "Photo capture succeeded: $savedUri"
               Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
               Log.d(TAG, msg)
           }
       })
}
  • 首先,获取对 ImageCapture 用例的引用。如果用例为 null,则退出函数。如果您在设置拍摄图像之前点按拍照按钮,则这将为 null。如果没有 return 语句,则在用例为 null 的情况下,应用会崩溃。
val imageCapture = imageCapture ?: return
  • 接下来,创建一个容纳图像的文件。添加时间戳,以避免文件名重复。
val photoFile = File(
   outputDirectory,
   SimpleDateFormat(FILENAME_FORMAT, Locale.US
   ).format(System.currentTimeMillis()) + ".jpg")
  • 创建 OutputFileOptions 对象。您可以在此对象中指定有关输出方式的设置。如果您希望将输出内容保存在刚创建的文件中,则添加您的 photoFile
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
  • imageCapture 对象调用 takePicture()。传入执行程序 outputOptions 以及在保存图像时使用的回调。接下来,您将填写回调。
imageCapture.takePicture(
   outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback {} 
)
  • 在图像拍摄失败或图像拍摄结果保存失败的情况下,添加一个错误示例,以记录失败情况。
override fun onError(exc: ImageCaptureException) {
   Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
}
  • 如果拍摄未失败,则表示拍照成功!将照片保存到您先前创建的文件中,显示一个消息框以告知用户操作成功,然后输出日志语句。
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
   val savedUri = Uri.fromFile(photoFile)
   val msg = "Photo capture succeeded: $savedUri"
   Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
   Log.d(TAG, msg)
}
  1. 使用 startCamera() 方法并将此代码复制到用于预览的代码之下。
imageCapture = ImageCapture.Builder()
   .build()
  1. 最后,更新对 try 块中对 bindToLifecycle() 的调用以加入新的用例:
cameraProvider.bindToLifecycle(
   this, cameraSelector, preview, imageCapture)

此时,该方法将如下所示:

private fun startCamera() {
   val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

   cameraProviderFuture.addListener(Runnable {
       // Used to bind the lifecycle of cameras to the lifecycle owner
       val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

       // Preview
       val preview = Preview.Builder()
           .build()
           .also {
                 it.setSurfaceProvider(viewFinder.createSurfaceProvider())
           }

       imageCapture = ImageCapture.Builder()
           .build()

       // Select back camera as a default
       val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

       try {
           // Unbind use cases before rebinding
           cameraProvider.unbindAll()

           // Bind use cases to camera
           cameraProvider.bindToLifecycle(
               this, cameraSelector, preview, imageCapture)

       } catch(exc: Exception) {
           Log.e(TAG, "Use case binding failed", exc)
       }

   }, ContextCompat.getMainExecutor(this))
}
  1. 重新运行应用并按 Take Photo(拍照)。 屏幕上会出现一个消息框,其中会显示一条日志消息。

d59fc4ad1241154f.png

查看照片

  1. 检查日志语句。您将看到一条日志消息,其会通知您照片已拍摄成功。
2020-04-24 15:13:26.146 11981-11981/com.example.cameraxapp D/CameraXBasic: Photo capture succeeded: file:///storage/emulated/0/Android/media/com.example.cameraxapp/CameraXApp/2020-04-24-15-13-25-746.jpg
  1. 复制用于存储照片的文件,并省略 file:// prefix
/storage/emulated/0/Android/media/com.example.cameraxapp/CameraXApp/2020-04-24-15-13-25-746.jpg
  1. 在 Android Studio 终端中,运行以下命令:
adb shell
cp [INSERT THE FILE FROM STEP 2 HERE] /sdcard/Download/photo.jpg
  1. 运行此 ADB 命令,然后退出 shell:
adb pull /sdcard/Download/photo.jpg
  1. 您可以查看保存在当前文件夹的 photo.jpg 文件内的照片。

如果您需要实现简单的相机应用,则已达到目的。就是这么简单!如果您想要实现图像分析程序,请继续阅读!

6. 使用 ImageAnalysis 用例

如要让您的相机应用变得更加有趣,使用 ImageAnalysis 功能不妨是一种好方法。 通过此功能,您可以定义用于实现 ImageAnalysis.Analyzer 接口的自定义类,该接口将使用传入的相机帧来进行调用。您不必管理相机会话状态,甚至无须处理图像,只需将其绑定到应用的预期生命周期即可,就像其他 具有生命周期感知能力的组件一样。

  1. 添加此分析程序,将其作为 MainActivity.kt 中的内部类。 此分析程序可用于记录图像的平均亮度。如要创建分析程序,您需要替换类中的 analyze 函数,该函数可用于负责实现 ImageAnalysis.Analyzer 界面。
private class LuminosityAnalyzer(private val listener: LumaListener) : ImageAnalysis.Analyzer {

   private fun ByteBuffer.toByteArray(): ByteArray {
       rewind()    // Rewind the buffer to zero
       val data = ByteArray(remaining())
       get(data)   // Copy the buffer into a byte array
       return data // Return the byte array
   }

   override fun analyze(image: ImageProxy) {

       val buffer = image.planes[0].buffer
       val data = buffer.toByteArray()
       val pixels = data.map { it.toInt() and 0xFF }
       val luma = pixels.average()

       listener(luma)

       image.close()
   }
}

通过使用我们的类实现 ImageAnalysis.Analyzer 界面,我们只需在 ImageAnalysis, 中对 LuminosityAnalyzer 的实例进行实例化(类似于其他用例),并再次更新 startCamera() 函数,然后再调用 CameraX.bindToLifecycle() 即可:

  1. startCamera() 方法中,将此代码添加到 imageCapture() 代码下。
val imageAnalyzer = ImageAnalysis.Builder()
   .build()
   .also {
       it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
           Log.d(TAG, "Average luminosity: $luma")
       })
   }
  1. 更新 cameraProvider 中的 bindToLifecycle() 调用,以加入 imageAnalyzer
cameraProvider.bindToLifecycle(
   this, cameraSelector, preview, imageCapture, imageAnalyzer)

完整方法现如下所示:

private fun startCamera() {
   val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

   cameraProviderFuture.addListener(Runnable {
       // Used to bind the lifecycle of cameras to the lifecycle owner
       val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

       // Preview
       val preview = Preview.Builder()
           .build()
           .also {
               it.setSurfaceProvider(viewFinder.createSurfaceProvider())
           }

       imageCapture = ImageCapture.Builder()
           .build()

       val imageAnalyzer = ImageAnalysis.Builder()
           .build()
           .also {
               it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
                   Log.d(TAG, "Average luminosity: $luma")
               })
           }

       // Select back camera as a default
       val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

       try {
           // Unbind use cases before rebinding
           cameraProvider.unbindAll()

           // Bind use cases to camera
           cameraProvider.bindToLifecycle(
               this, cameraSelector, preview, imageCapture, imageAnalyzer)

       } catch(exc: Exception) {
           Log.e(TAG, "Use case binding failed", exc)
       }

   }, ContextCompat.getMainExecutor(this))
}
  1. 现在运行应用!应用大约每秒会在 logcat 中生成一条类似于以下内容的消息。
D/CameraXApp: Average luminosity: ...

7. 恭喜!

您已成功地从零开始,在新的 Android 应用中实现了以下内容:

  • 已将 CameraX 依赖项加入到您的项目中。
  • 已显示相机取景器(使用 Preview 用例)
  • 已能够拍摄照片,并可将图像保存到存储空间(使用 ImageCapture 用例)
  • 已实现对来自相机的画面帧进行实时分析(使用 ImageAnalysis 用例)

如果您有兴趣了解更多有关 CameraX 的内容以及您可以用其完成的事情,请查看 文档或克隆 官方示例