前言

虽然网上有很多CameraX的教程,但是每次用CameraX的时候总要在翻一翻官方的文档或者网上的教程之类的,这次就自己记一下吧,翻自己的总比翻别人的好吧🤪

声明依赖项

要添加 CameraX 的依赖项,您必须将 Google Maven 代码库添加到项目中。

打开项目的 settings.gradle 文件并添加 google() 代码库,如下所示:

1
2
3
4
5
6
7
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
1
2
3
4
5
6
7
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}

将以下内容添加到 Android 代码块的末尾:

1
2
3
4
5
6
7
8
9
10
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
// For Kotlin projects
kotlinOptions {
jvmTarget = "1.8"
}
}
1
2
3
4
5
6
7
8
9
10
android {
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
// For Kotlin projects
kotlinOptions {
jvmTarget = "1.8"
}
}

将以下内容添加到应用的每个模块的 build.gradle 文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dependencies {
// CameraX核心库使用camera2实现
def camerax_version = "1.2.3"
// 以下行是可选的,因为核心库是由camera-camera2间接包含的
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
// 如果您想另外使用 CameraX Lifecycle 库
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
// 如果您想另外使用 CameraX VideoCapture 库
implementation "androidx.camera:camera-video:${camerax_version}"
// 如果您想另外使用 CameraX View 类
implementation "androidx.camera:camera-view:${camerax_version}"
// 如果您想另外添加 CameraX ML Kit Vision Integration
implementation "androidx.camera:camera-mlkit-vision:${camerax_version}"
// 如果您想另外使用 CameraX Extensions 库
implementation "androidx.camera:camera-extensions:${camerax_version}"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dependencies {
// CameraX核心库使用camera2实现
val camerax_version = "1.2.3"
// 以下行是可选的,因为核心库是由camera-camera2间接包含的
implementation("androidx.camera:camera-core:${camerax_version}")
implementation("androidx.camera:camera-camera2:${camerax_version}")
// 如果您想另外使用 CameraX Lifecycle 库
implementation("androidx.camera:camera-lifecycle:${camerax_version}")
// 如果您想另外使用 CameraX VideoCapture 库
implementation("androidx.camera:camera-video:${camerax_version}")
// 如果您想另外使用 CameraX View 类
implementation("androidx.camera:camera-view:${camerax_version}")
// 如果您想另外添加 CameraX ML Kit Vision Integration
implementation("androidx.camera:camera-mlkit-vision:${camerax_version}")
// 如果您想另外使用 CameraX Extensions 库
implementation("androidx.camera:camera-extensions:${camerax_version}")
}

如需详细了解如何配置应用以满足上述要求或查看CameraX最新版本号,请参阅声明依赖项

实现预览

在向应用添加预览时,请使用 PreviewView,这是一种可以剪裁、缩放和旋转以确保正确显示的 View

当相机处于活动状态时,图片预览会流式传输到 PreviewView 中的 Surface。

将 PreviewView 添加到布局

以下示例显示了布局中的 PreviewView

1
2
3
4
5
<FrameLayout
android:id="@+id/container">
<androidx.camera.view.PreviewView
android:id="@+id/previewView" />
</FrameLayout>

初始化相机

以下代码展示了如何初始化预览需要 CameraProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import androidx.camera.lifecycle.ProcessCameraProvider
import com.google.common.util.concurrent.ListenableFuture

class CameraActivity : AppCompatActivity() {
private lateinit var cameraProviderFuture : ListenableFuture<ProcessCameraProvider>
private var cameraProvider: ProcessCameraProvider? = null

/**
* 初始化相机,可在onCreate()内调用
*/
private fun initCamera() {
// 检查权限
checkPermission() {
cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
cameraProvider = cameraProviderFuture.get()
// 检查摄像头
lensFacing = when {
hasBackCamera() -> CameraSelector.LENS_FACING_BACK
hasFrontCamera() -> CameraSelector.LENS_FACING_FRONT
else -> {
toast("前后摄像头不可用")
return@addListener
}
}
// (可忽略) 将PreviewView的预览View改为TextureView
binding.previewView.implementationMode = PreviewView.ImplementationMode.COMPATIBLE
// 绑定相机
bindPreview(cameraProvider ?: return@addListener)
}, ContextCompat.getMainExecutor(requireContext()))
}
}
}

检查摄像头

1
2
3
4
5
6
7
8
9
/** 如果设备有可用的后置摄像头,则返回 true。否则为假 */
private fun hasBackCamera(): Boolean {
return cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ?: false
}

/** 如果设备有可用的前置摄像头,则返回 true。否则为假 */
private fun hasFrontCamera(): Boolean {
return cameraProvider?.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) ?: false
}

绑定相机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Camera
private var camera: Camera? = null

// 创建Preview
private val preview by lazy {
Preview.Builder().build()
}

// 当前摄像头
private var lensFacing: Int = CameraSelector.LENS_FACING_BACK

/**
* 绑定相机
*/
private fun bindPreview(cameraProvider: ProcessCameraProvider) {
// 选择摄像头
val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build()
// 解除所有绑定
cameraProvider.unbindAll()
// 将所选相机和任意用例绑定到生命周期
camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview)
// 将 Preview 连接到 PreviewView
preview.setSurfaceProvider(binding.previewView.surfaceProvider)
}

请注意,bindToLifecycle() 会返回一个 Camera 对象。如需详细了解如何控制相机输出(例如变焦和曝光),请参阅相机输出

图片拍摄

1
2
3
4
5
6
7
8
9
10
private val imageCapture by lazy {
ImageCapture.Builder().setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
.setTargetAspectRatio(AspectRatio.RATIO_4_3).build()
}

/**
* 方向
*/
private var orientationEventListener: OrientationEventListener? = null

cameraProviderFuture.addListener内部添加 调用bindPreview() 方法之前

1
2
3
4
5
6
7
8
9
10
11
orientationEventListener = object : OrientationEventListener(this) {
override fun onOrientationChanged(orientation: Int) { // Monitors orientation values to determine the target rotation value
val rotation = when (orientation) {
in 45..134 -> Surface.ROTATION_270
in 135..224 -> Surface.ROTATION_180
in 225..314 -> Surface.ROTATION_90
else -> Surface.ROTATION_0
}
imageCapture.targetRotation = rotation
}
}

在bindPreview()方法内启用
orientationEventListener?.enable()

及时暂停

1
2
3
4
override fun onPause() {
orientationEventListener?.disable()
super.onPause()
}

bindToLifecycle内加入UseCase

1
camera = cameraProvider.bindToLifecycle(viewLifecycleOwner, cameraSelector, preview, imageCapture)

拍照

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 拍照
*/
@SuppressLint("RestrictedApi")
override fun takePicture(save: (uri: Uri?) -> Unit) {
// 存储照片的路径
val dir = File(ConfigConstant.PATH)
if (!dir.exists()) dir.mkdirs()
val srcFile = File(dir, ConfigConstant.BITMAP_NAME)
val outputFileOptions = ImageCapture.OutputFileOptions.Builder(srcFile).build()
imageCapture.takePicture(outputFileOptions, cameraExecutor, object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
save(outputFileResults?.savedUri)
// outputFileResults可以拿到图片的Uri
}

override fun onError(exception: ImageCaptureException) {
save(null)
toast("拍照异常")
}
})
}

到这里基本上就能实现正常的拍摄,再来介绍一些小功能吧

部分功能

切换摄像头

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 切换摄像头
*/
override fun switchCamera() {
orientationEventListener?.disable()
lensFacing = if (CameraSelector.LENS_FACING_FRONT == lensFacing) {
CameraSelector.LENS_FACING_BACK
} else {
CameraSelector.LENS_FACING_FRONT
}
bindPreview(cameraProvider ?: return)
}

手电筒

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 切换手电筒,可能打开失败
*/
override fun flashlight(): Boolean {
camera?.let {
val mode = if (imageCapture.flashMode == ImageCapture.FLASH_MODE_OFF) ImageCapture.FLASH_MODE_ON
else ImageCapture.FLASH_MODE_OFF
imageCapture.flashMode = mode
return imageCapture.flashMode == ImageCapture.FLASH_MODE_ON
} ?: toast("闪光灯开启失败,请检查相机能否正常使用")
return false
}

曝光补偿

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 设置曝光补偿, 可能设置失败
*/
override fun setExposureCompensationIndex(value: Int) {
camera?.let { camera ->
val cameraInfo = camera.cameraInfo
if (cameraInfo.exposureState.exposureCompensationRange.upper == 0 && cameraInfo.exposureState.exposureCompensationRange.lower == 0) {
toast("当前设备不支持设置曝光补偿")
} else if (cameraInfo.exposureState.exposureCompensationRange.contains(value)) {
val index = (value / camera.cameraInfo.exposureState.exposureCompensationStep.toFloat()).roundToInt()
camera.cameraControl.setExposureCompensationIndex(index)
}
} ?: toast("设置曝光补偿失败,请检查相机能否正常使用")
}

下面的内容来源于Android官网文档

PreviewView 的其他API

CameraX PreviewView 提供了一些其他 API 来用于配置属性,例如:

实现模式

PreviewView 可以使用以下模式之一将预览流渲染到目标 View 上:

  • PERFORMANCE 是默认模式。PreviewView 会使用 SurfaceView 显示视频串流,但在某些情况下会回退为使用 TextureViewSurfaceView 具有专用的绘图界面,该对象更有可能通过内部硬件合成器实现硬件叠加层,尤其是当预览视频上面没有其他界面元素(如按钮)时。通过使用硬件叠加层进行渲染,视频帧会避开 GPU 路径,从而能降低平台功耗并缩短延迟时间。
  • COMPATIBLE 模式。在此模式下,PreviewView 会使用 TextureView;不同于 SurfaceView,该对象没有专用的绘图表面。因此,视频要通过混合渲染,才能显示。在这个额外的步骤中,应用可以执行额外的处理工作,例如不受限制地缩放和旋转视频。

您可以使用 PreviewView.setImplementationMode() 选择适合具体应用的实现模式。如果默认的 PERFORMANCE 模式不适合您的应用,请参阅以下代码示例,了解如何设置 COMPATIBLE 模式:

1
2
// viewFinder 是一个 PreviewView 实例
viewFinder.implementationMode = PreviewView.ImplementationMode.COMPATIBLE

缩放类型

当预览视频分辨率与目标 PreviewView 的尺寸不同时,视频内容需要通过剪裁操作或添加遮幅式黑边来适应视图(保持原始宽高比)。为此,PreviewView 提供了以下 ScaleTypes

  • FIT_CENTERFIT_STARTFIT_END,用于添加遮幅式黑边。整个视频内容会调整(放大或缩小)为可在目标 PreviewView 中显示的最大尺寸。不过,虽然整个视频帧会完整显示,但屏幕画面中可能会出现空白部分。视频帧会与目标视图的中心、起始或结束位置对齐,具体取决于您在上述三种缩放类型中选择了哪一种。
  • FILL_CENTERFILL_STARTFILL_END,用于进行剪裁。如果视频的宽高比与 PreviewView 不匹配,画面中只会显示部分内容,但视频仍会填满整个 PreviewView

CameraX 使用的默认缩放类型是 FILL_CENTER。您可以使用 PreviewView.setScaleType() 设置最适合具体应用的缩放类型。下面的代码示例设置了 FIT_CENTER 缩放类型:

1
2
// viewFinder 是一个 PreviewView 实例
viewFinder.scaleType = PreviewView.ScaleType.FIT_CENTER

视频显示过程包括以下步骤:

  1. 缩放视频:
    • 如果您选择的缩放类型是 FIT_*,请使用 min(dst.width/src.width, dst.height/src.height) 缩放视频。
    • 如果您选择的缩放类型是 FILL_*,请使用 max(dst.width/src.width, dst.height/src.height) 缩放视频。
  2. 将经过缩放的视频与目标PreviewView对齐:
    • 对于 FIT_CENTER/FILL_CENTER,请将经过缩放的视频与目标 PreviewView 居中对齐。
    • 对于 FIT_START/FILL_START,请以每个视频的左上角为准,将经过缩放的视频与目标 PreviewView 对齐。
    • 对于 FIT_END/FILL_END,请以每个视频的右下角为准,将经过缩放的视频与目标 PreviewView 对齐。

配置选项

您可以配置每个 CameraX 用例,以控制用例操作的不同方面。

例如,对于图片拍摄用例,您可以设置目标宽高比和闪光灯模式。以下代码显示了一个示例:

1
2
3
4
val imageCapture = ImageCapture.Builder()
.setFlashMode(...)
.setTargetAspectRatio(...)
.build()

除配置选项之外,一些用例会公开 API 以便在创建后动态更改设置。如需了解各个用例的专属配置,请参阅实现预览分析图片图片拍摄

CameraXConfig

为简单起见,CameraX 具有适合大多数使用场景的默认配置(例如内部执行器和处理程序)。但是,如果您的应用有特殊要求或希望自定义这些配置,可使用 CameraXConfig 接口实现此目的。

借助 CameraXConfig,应用可以执行以下操作:

使用模式

以下程序说明了如何使用 CameraXConfig

  1. 使用您的自定义配置创建一个 CameraXConfig 对象。
  2. Application 中实现 CameraXConfig.Provider 接口,并在 getCameraXConfig() 中返回 CameraXConfig 对象。
  3. 按照此处的说明,将您的 Application 类添加到 AndroidManifest.xml 文件中。

例如,以下代码示例将 CameraX 日志记录限制为仅记录错误消息:

1
2
3
4
5
6
class CameraApplication : Application(), CameraXConfig.Provider {
override fun getCameraXConfig(): CameraXConfig {
return CameraXConfig.Builder.fromConfig(Camera2Config.defaultConfig())
.setMinimumLoggingLevel(Log.ERROR).build()
}
}

如果您的应用需要在设置 CameraX 配置后了解该配置,请保留 CameraXConfig 对象的本地副本。

摄像头限制器

在第一次调用 ProcessCameraProvider.getInstance() 期间,CameraX 会枚举和查询设备上可用摄像头的特性。由于 CameraX 需要与硬件组件通信,因此对每个摄像头执行此过程可能需要较长时间,尤其是在低端设备上。如果您的应用仅使用设备上的特定摄像头(例如默认前置摄像头),您可以将 CameraX 设置为忽略其他摄像头,从而缩短应用所用摄像头的启动延迟时间。

如果传递给 CameraXConfig.Builder.setAvailableCamerasLimiter()CameraSelector 过滤掉了某个摄像头,则 CameraX 在运行时会假定该摄像头不存在。例如,以下代码会限制应用只能使用设备的默认后置摄像头:

1
2
3
4
5
6
7
class MainApplication : Application(), CameraXConfig.Provider {
override fun getCameraXConfig(): CameraXConfig {
return CameraXConfig.Builder.fromConfig(Camera2Config.defaultConfig())
.setAvailableCamerasLimiter(CameraSelector.DEFAULT_BACK_CAMERA)
.build()
}
}

线程

构建 CameraX 时所采用的很多平台 API 都要求阻塞与硬件之间的进程间通信 (IPC),此类通信有时可能需要数百毫秒的响应时间。因此,CameraX 仅从后台线程调用这些 API,从而避免主线程发生阻塞,使界面保持流畅。CameraX 会在内部管理这些后台线程,因此这类行为显得比较透明。但是,某些应用需要严格控制线程。CameraXConfig 允许应用设置通过 CameraXConfig.Builder.setCameraExecutor()CameraXConfig.Builder.setSchedulerHandler() 使用的后台线程。

注意:提供自定义执行器或调度器处理程序时,请使用不会在主线程上执行代码的处理程序。

摄像头执行器

摄像头执行器用于所有内部摄像头平台 API 调用,以及来自这些 API 的回调。CameraX 会分配和管理内部 Executor 来执行这些任务。 但是,如果您的应用需要更严格的线程控制,请使用 CameraXConfig.Builder.setCameraExecutor()

调度器处理程序

调度器处理程序用于按固定的时间间隔调度内部任务,例如在摄像头不可用时再次尝试打开该摄像头。该处理程序不执行作业,而是仅将作业分派给摄像头执行器。有时,该处理程序还用于需要使用 Handler 进行回调的旧版 API 平台。在这些情况下,回调仍仅直接分派给摄像头执行器。CameraX 会分配和管理内部 HandlerThread 来执行这些任务,但您可以将其替换为 CameraXConfig.Builder.setSchedulerHandler()

日志记录

借助 CameraX 日志记录,应用可以过滤 logcat 消息,因为在正式版代码中应尽量避免包含详细消息。CameraX 支持四种日志记录级别(从最详细到最严重):

  • Log.DEBUG(默认)
  • Log.INFO
  • Log.WARN
  • Log.ERROR

如需详细了解这些日志级别,请参阅 Android 日志文档。您可以使用 CameraXConfig.Builder.setMinimumLoggingLevel(int) 为您的应用设置适当的日志记录级别。

自动选择

CameraX 会根据运行您的应用的设备自动提供专用的功能。例如,如果您未指定分辨率或您指定的分辨率不受支持,CameraX 会自动确定要使用的最佳分辨率。所有这些操作均由库进行处理,无需您编写设备专属代码。

CameraX 的目标是成功初始化摄像头会话。这意味着,CameraX 会根据设备功能降低分辨率和宽高比。发生这种情况的原因如下:

  • 设备不支持请求的分辨率。
  • 设备存在兼容性问题,例如需要特定分辨率才能正常运行的旧设备。
  • 在某些设备上,某些格式仅在某些宽高比下可用。
  • 对于 JPEG 或视频编码,设备首选“最近的 mod16”。如需了解详情,请参阅 SCALER_STREAM_CONFIGURATION_MAP

尽管 CameraX 会创建并管理会话,您也应始终在代码中检查用例输出所返回的图片大小,并进行相应调整。

旋转

默认情况下,在用例创建期间,摄像头的旋转角度会设置为与默认的显示屏旋转角度保持一致。在此默认情况下,CameraX 会生成输出,确保应用与您预期在预览中看到的内容保持一致。通过在配置用例对象时传入当前显示屏方向或在创建用例对象之后动态传入显示屏方向,您可以将旋转角度更改为自定义值以支持多显示屏设备。

您的应用可以使用配置设置来设置目标旋转角度。然后,即使生命周期处于运行状态,应用也可以通过使用用例 API 中的方法(例如 ImageAnalysis.setTargetRotation())更新旋转设置。您可以在应用锁定为纵向模式时执行上述操作,这样就无需重新配置旋转角度,但是照片或分析用例需要了解设备当前的旋转角度。例如,用例可能需要了解旋转角度才能以正确的方向进行人脸检测,或者将照片设置为横向或纵向。

存储所拍摄图片的数据时可能不会包含旋转信息。Exif 数据包含旋转信息,以便图库应用在保存后以正确的屏幕方向显示图片。

如需以正确的屏幕方向显示预览数据,您可以使用 Preview.PreviewOutput() 的元数据输出创建转换。

以下代码示例展示了如何为屏幕方向事件设置旋转角度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
override fun onCreate() {
val imageCapture = ImageCapture.Builder().build()

val orientationEventListener = object : OrientationEventListener(this as Context) {
override fun onOrientationChanged(orientation : Int) {
// Monitors orientation values to determine the target rotation value
val rotation : Int = when (orientation) {
in 45..134 -> Surface.ROTATION_270
in 135..224 -> Surface.ROTATION_180
in 225..314 -> Surface.ROTATION_90
else -> Surface.ROTATION_0
}

imageCapture.targetRotation = rotation
}
}
orientationEventListener.enable()
}

每个用例都会根据设定的旋转角度直接旋转图片数据,或者向用户提供未旋转图片数据的旋转元数据。

  • Preview:提供元数据输出,以便使用 Preview.getTargetRotation() 了解目标分辨率的旋转设置。
  • ImageAnalysis:提供元数据输出,以便了解图片缓冲区坐标相对于显示坐标的位置。
  • ImageCapture:更改图片 Exif 元数据、缓冲区或同时更改两者,从而反映旋转设置。更改的值取决于 HAL 实现。

剪裁矩形

默认情况下,剪裁矩形是完整的缓冲区矩形,您可通过 ViewPortUseCaseGroup 对其进行自定义。通过对用例进行分组并设置视口,CameraX 可保证一个组中的所有用例的剪裁矩形都指向摄像头传感器中的同一个区域。

以下代码段展示了这两个类的使用方法:

1
2
3
4
5
6
7
8
val viewPort =  ViewPort.Builder(Rational(width, height), display.rotation).build()
val useCaseGroup = UseCaseGroup.Builder()
.addUseCase(preview)
.addUseCase(imageAnalysis)
.addUseCase(imageCapture)
.setViewPort(viewPort)
.build()
cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, useCaseGroup)

ViewPort 用于指定最终用户可看到的缓冲区矩形。CameraX 会根据视口的属性及附加的用例计算出可能的最大剪裁矩形。一般情况下,为了达到 WYSIWYG 效果,您应根据预览用例来配置视口。获取视口的一种简单方法是使用 PreviewView

以下代码段展示了如何获取 ViewPort 对象:

1
val viewport = findViewById<PreviewView>(R.id.preview_view).viewPort

在前面的示例中,应用通过 ImageAnalysisImageCapture 获取的内容与最终用户在 PreviewView 中看到的内容相同(假定 PreviewView 的缩放类型设为默认值 FILL_CENTER)。将剪裁矩形和旋转角度应用到输出缓冲区后,图片将在所有用例中保持一致,但分辨率可能会有所不同。如需详细了解如何应用转换信息,请参阅转换输出

摄像头分辨率

您可以选择让 CameraX 根据设备功能、设备支持的硬件级别、用例和所提供的宽高比组合设置图片分辨率。或者,您也可以在支持相应配置的用例中设置特定目标分辨率或特定宽高比。

自动分辨率

CameraX 可以根据 cameraProcessProvider.bindToLifecycle() 中指定的用例自动确定最佳分辨率设置。请尽可能在单个 bindToLifecycle() 调用的单个会话中指定需要同时运行的所有用例。CameraX 会考虑设备支持的硬件级别以及设备专属变化(设备超出或不满足可用的信息流配置),根据绑定的成组用例确定分辨率。 这样做是为了确保应用在各种设备上运行时,能够最大限度地减少设备专属代码路径。

图片拍摄和图片分析用例的默认宽高比为 4:3。

对于具有可配置宽高比的用例,可让应用根据界面设计来指定所需的宽高比。CameraX 会按照请求的宽高比生成输出,并尽可能匹配设备支持的宽高比。如果没有任何支持的完全匹配分辨率,则选择满足最多条件的分辨率。也就是说,应用会决定摄像头在应用中的显示方式,CameraX 则会决定最佳摄像头分辨率设置,以满足不同设备的具体要求。

例如,应用可以执行以下任一操作:

  • 为用例指定 4:3 或 16:9 的目标分辨率
  • 指定自定义分辨率,CameraX 会尝试查找与该分辨率最接近的分辨率
  • ImageCapture 指定剪裁宽高比

CameraX 会自动选择内部 Camera2 界面的分辨率。下表显示了这些分辨率:

用例 内部界面分辨率 输出数据分辨率
预览 宽高比:使目标与设置最相符的分辨率。 内部界面分辨率。通过提供元数据,可让视图针对目标宽高比进行剪裁、缩放和旋转。
默认分辨率:最高的预览分辨率,或与预览宽高比匹配的最高设备首选分辨率。
最大分辨率:预览大小,指的是与设备的屏幕分辨率或 1080p (1920x1080)(以较低者为准)匹配的最佳尺寸。
图片分析 宽高比:使目标与设置最相符的分辨率。 内部界面分辨率。
默认分辨率:默认目标分辨率设置为 640x480。同时调整目标分辨率和相应的宽高比可获得支持的最佳分辨率。
最大分辨率:从 StreamConfigurationMap.getOutputSizes() 中检索到的 YUV_420_888 格式的摄像头设备最大输出分辨率。 目标分辨率默认设置为 640x480。因此,如果您希望分辨率大于 640x480,必须使用 setTargetResolution()setTargetAspectRatio() 从支持的分辨率中选择最接近的一个。
图片拍摄 宽高比:最适合设置的宽高比。 内部界面分辨率。
默认分辨率:最高可用分辨率,或与 ImageCapture 的宽高比匹配的最高设备首选分辨率。
最大分辨率:JPEG 格式的摄像头设备最大输出分辨率。请使用 StreamConfigurationMap.getOutputSizes() 检索此分辨率。

指定分辨率

使用 setTargetResolution(Size resolution) 方法构建用例时,您可以设置特定分辨率,如以下代码示例所示:

1
2
3
val imageAnalysis = ImageAnalysis.Builder()
.setTargetResolution(Size(1280, 720))
.build()

您无法针对同一个用例设置目标宽高比和目标分辨率。如果这样做,则会在构建配置对象时抛出 IllegalArgumentException

按照目标旋转角度旋转支持的大小后,请在坐标系中表示分辨率 Size。例如,自然屏幕方向为纵向并采用自然目标旋转角度的设备如果请求纵向图片,可指定 480x640;而同一设备旋转 90 度并以横向屏幕方向为目标后,可指定 640x480。

目标分辨率会尝试制定图片分辨率的下限。实际的图片分辨率是最接近的可用分辨率,其大小不小于由摄像头实现所决定的目标分辨率。

但是,如果不存在等于或大于目标分辨率的分辨率,就会从小于目标分辨率的可用分辨率中选择最接近的一个。与提供的 Size 具有相同宽高比的分辨率,其优先级高于具有不同宽高比的分辨率。

CameraX 会根据请求应用最合适的分辨率。如果主要需求是满足宽高比要求,则仅指定 setTargetAspectRatio,CameraX 会根据设备确定合适的特定分辨率。 如果应用的主要需求是指定分辨率以提高图片处理效率(例如根据设备处理能力处理较小或中等大小的图片),请使用 setTargetResolution(Size resolution)

注意:如果使用 setTargetResolution(),可能会得到宽高比与其他用例不匹配的缓冲区。如果宽高比必须匹配,请检查两个用例返回的缓冲区尺寸,然后剪裁或缩放其中一个以与另一个匹配。

如果您的应用需要精确的分辨率,请参阅 createCaptureSession() 内的表格,以确定每个硬件级别支持的最大分辨率。如需查看当前设备支持的特定分辨率,请参阅 StreamConfigurationMap.getOutputSizes(int)

如果您的应用在 Android 10 或更高版本上运行,您可以使用 isSessionConfigurationSupported() 验证特定的 SessionConfiguration

控制摄像头输出

CameraX 不仅让您可以视需要为每个单独的用例配置摄像头输出,还实现了以下接口,从而支持所有绑定用例中通用的摄像头操作:

  • 利用 CameraControl,您可以配置通用摄像头功能。
  • 利用 CameraInfo,您可以查询这些通用摄像头功能的状态。

以下是 CameraControl 支持的摄像头功能:

  • 变焦
  • 手电筒
  • 对焦和测光(点按即可对焦)
  • 曝光补偿

获取 CameraControl 和 CameraInfo 的实例

使用 ProcessCameraProvider.bindToLifecycle() 返回的 Camera 对象检索 CameraControlCameraInfo 的实例。 以下代码展示了一个示例:

1
2
3
4
5
6
val camera = processCameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview)

// For performing operations that affect all outputs.
val cameraControl = camera.cameraControl
// For querying information and states.
val cameraInfo = camera.cameraInfo

例如,您可以在调用 bindToLifecycle() 后提交变焦操作及其他 CameraControl 操作。如果您停止或销毁用于绑定摄像头实例的 activity,CameraControl 无法再执行操作,并且会返回失败的 ListenableFuture

注意:如果 LifecycleOwner 被停止或销毁,Camera 就会关闭,之后变焦、手电筒、对焦和测光以及曝光补偿控件的所有状态更改均会还原成默认值。

对焦和测光

CameraControl.startFocusAndMetering() 可根据指定的 FocusMeteringAction 设置 AF/AE/AWB 测光区域,以触发自动对焦和曝光测光。有许多摄像头应用通过这种方式实现“点按对焦”功能。

MeteringPoint

首先,使用 MeteringPointFactory.createPoint(float x, float y, float size) 创建 MeteringPointMeteringPoint 表示摄像头 Surface 上的单个点。它以标准化形式存储,所以能轻松转换为传感器坐标,从而用于指定 AF/AE/AWB 区域。

MeteringPoint 的大小介于 0 到 1 之间,默认大小为 0.15f。调用 MeteringPointFactory.createPoint(float x, float y, float size) 时,CameraX 会为提供的 size 创建以 (x, y) 为中心的矩形区域。

下面的代码演示了如何创建 MeteringPoint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Use PreviewView.getMeteringPointFactory if PreviewView is used for preview.
previewView.setOnTouchListener((view, motionEvent) -> {
val meteringPoint = previewView.meteringPointFactory
.createPoint(motionEvent.x, motionEvent.y)

}

// Use DisplayOrientedMeteringPointFactory if SurfaceView / TextureView is used for
// preview. Please note that if the preview is scaled or cropped in the View,
// it’s the application's responsibility to transform the coordinates properly
// so that the width and height of this factory represents the full Preview FOV.
// And the (x,y) passed to create MeteringPoint might need to be adjusted with
// the offsets.
val meteringPointFactory = DisplayOrientedMeteringPointFactory(
surfaceView.display,
camera.cameraInfo,
surfaceView.width,
surfaceView.height
)

// Use SurfaceOrientedMeteringPointFactory if the point is specified in
// ImageAnalysis ImageProxy.
val meteringPointFactory = SurfaceOrientedMeteringPointFactory(
imageWidth,
imageHeight,
imageAnalysis)

startFocusAndMetering 和 FocusMeteringAction

如需调用 startFocusAndMetering(),应用必须构建 FocusMeteringAction,其中包含一个或多个 MeteringPoints,后者由 FLAG_AFFLAG_AEFLAG_AWB 这些可选测光模式组合而成。下面的代码演示了这一用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
val meteringPoint1 = meteringPointFactory.createPoint(x1, x1)
val meteringPoint2 = meteringPointFactory.createPoint(x2, y2)
val action = FocusMeteringAction.Builder(meteringPoint1) // default AF|AE|AWB
// Optionally add meteringPoint2 for AF/AE.
.addPoint(meteringPoint2, FLAG_AF | FLAG_AE)
// The action is canceled in 3 seconds (if not set, default is 5s).
.setAutoCancelDuration(3, TimeUnit.SECONDS)
.build()

val result = cameraControl.startFocusAndMetering(action)
// Adds listener to the ListenableFuture if you need to know the focusMetering result.
result.addListener({
// result.get().isFocusSuccessful returns if the auto focus is successful or not.
}, ContextCompat.getMainExecutor(this)

如上面的代码所示,startFocusAndMetering() 接受一个 FocusMeteringAction,后者包含一个用于 AF/AE/AWB 测光区域的 MeteringPoint,以及另一个仅用于 AF 和 AE 的 MeteringPoint。

在内部,CameraX 会将其转换为 Camera2 MeteringRectangles,并将相应的 CONTROL_AF_REGIONS/CONTROL_AE_REGIONS/CONTROL_AWB_REGIONS 参数设置为拍摄请求。

由于并非所有设备都支持 AF/AE/AWB 和多个区域,CameraX 会尽最大努力执行 FocusMeteringAction。CameraX 会使用所支持的最大数量的 MeteringPoint,并按测光点的添加顺序依次使用。对于在超出支持的最大数量之外添加的所有 MeteringPoint,CameraX 会一律忽略。例如,如果您在仅支持 2 个 MeteringPoint 的平台上为 FocusMeteringAction 提供 3 个 MeteringPoint,那么 CameraX 只会使用前 2 个 MeteringPoint,并忽略最后一个 MeteringPoint

图像分析

图像分析用例为您的应用提供可供 CPU 访问的图像,您可以对这些图像执行图像处理、计算机视觉或机器学习推断。应用会实现对每个帧运行的 analyze() 方法。

如需了解如何将 Google 的机器学习套件与 CameraX 应用集成,请参阅机器学习套件分析器

操作模式

当应用的分析流水线无法满足 CameraX 的帧速率要求时,您可以将 CameraX 配置为通过以下其中一种方式丢帧:

  • 非阻塞(默认):在该模式下,执行器始终会将最新的图像缓存到图像缓冲区(与深度为 1 的队列相似),与此同时,应用会分析上一个图像。如果 CameraX 在应用完成处理之前收到新图像,则新图像会保存到同一缓冲区,并覆盖上一个图像。 请注意,在这种情况下,ImageAnalysis.Builder.setImageQueueDepth() 不起任何作用,缓冲区内容始终会被覆盖。您可以通过使用 STRATEGY_KEEP_ONLY_LATEST 调用 setBackpressureStrategy() 来启用该非阻塞模式。如需详细了解执行器的相关影响,请参阅 STRATEGY_KEEP_ONLY_LATEST 的参考文档。
  • 阻塞:在该模式下,内部执行器可以向内部图像队列添加多个图像,并仅在队列已满时才开始丢帧。系统会在整个相机设备上进行屏蔽:如果相机设备具有多个绑定用例,那么在 CameraX 处理这些图像时,系统会屏蔽所有这些用例。例如,如果预览和图像分析都已绑定到某个相机设备,那么在 CameraX 处理图像时,系统也会屏蔽相应预览。您可以通过将 STRATEGY_BLOCK_PRODUCER 传递到 setBackpressureStrategy() 来启用阻塞模式。此外,您还可以通过使用 ImageAnalysis.Builder.setImageQueueDepth() 来配置图像队列深度。

如果分析器延迟低且性能高,在这种情况下用于分析图像的总时间低于 CameraX 帧的时长(例如,60fps 用时 16 毫秒),那么上述两种操作模式均可提供顺畅的总体体验。在某些情况下,阻塞模式仍非常有用,例如在处理非常短暂的系统抖动时。

如果分析器延迟高且性能高,则需要结合使用阻塞模式和较长的队列来抵补延迟。但请注意,在这种情况下,应用仍可以处理所有帧。

如果分析器延迟高且耗时长(分析器无法处理所有帧),非阻塞模式可能更为适用,因为在这种情况下,系统必须针对分析路径进行丢帧,但要让其他同时绑定的用例仍能看到所有帧。

实现

如需在您的应用中使用图像分析,请按以下步骤操作:

绑定后,CameraX 会立即将图像发送到已注册的分析器。 完成分析后,调用 ImageAnalysis.clearAnalyzer() 或解除绑定 ImageAnalysis 用例以停止分析。

构建 ImageAnalysis 用例

ImageAnalysis 可将分析器(图像使用方)连接到 CameraX(图像生成方)。应用可以使用 ImageAnalysis.Builder 来构建 ImageAnalysis 对象。借助 ImageAnalysis.Builder,应用可以进行以下配置:

应用可以设置分辨率或宽高比,但不能同时设置这两个值。确切的输出分辨率取决于应用请求的大小(或宽高比)和硬件功能,并可能与请求的大小或宽高比不同。如需了解分辨率匹配算法,请参阅有关 setTargetResolution() 的文档

应用可以将输出图像像素配置为采用 YUV(默认)或 RGBA 颜色空间。设置 RGBA 输出格式时,CameraX 会在内部将图像从 YUV 颜色空间转换为 RGBA 颜色空间,并将图像位打包到 ImageProxy 第一个平面(其他两个平面未使用)的 ByteBuffer 中,序列如下:

1
2
3
4
5
ImageProxy.getPlanes()[0].buffer[0]: alpha
ImageProxy.getPlanes()[0].buffer[1]: red
ImageProxy.getPlanes()[0].buffer[2]: green
ImageProxy.getPlanes()[0].buffer[3]: blue
...

在执行设备无法满足帧速率要求的复杂图像分析时,您可以使用本主题的操作模式部分所述的策略将 CameraX 配置为丢帧。

创建分析器

应用可以通过实现 ImageAnalysis.Analyzer 接口并替换 analyze(ImageProxy image) 来创建分析器。 在每个分析器中,应用都会收到一个 ImageProxy,它是 Media.Image 的封装容器。可以使用 ImageProxy.getFormat() 来查询图像格式。该格式使用应用通过 ImageAnalysis.Builder 提供的以下值之一表示:

  • 如果应用请求了 OUTPUT_IMAGE_FORMAT_RGBA_8888,则为 ImageFormat.RGBA_8888
  • 如果应用请求了 OUTPUT_IMAGE_FORMAT_YUV_420_888,则为 ImageFormat.YUV_420_888

如需了解颜色空间配置以及可检索像素字节的位置,请参阅构建 ImageAnalysis 用例

在分析器中,应用应执行以下操作:

  1. 尽快分析给定的帧,最好在给定的帧速率时间限制内进行分析(例如,如果帧速率为 30 fps,则用时应低于 32 毫秒)。如果应用无法足够快地分析帧,请考虑采用一种受支持的丢帧机制
  2. 通过调用 ImageProxy.close()ImageProxy 发布到 CameraX。请注意,您不应调用已封装 Media.Image 的 close 函数 (Media.Image.close())。

应用可以直接使用 ImageProxy 中的已封装 Media.Image。 请不要对已封装的图像调用 Media.Image.close(),因为这会破坏 CameraX 中的图像分享机制;请改为使用 ImageProxy.close() 将底层 Media.Image 发布到 CameraX。

针对 ImageAnalysis 配置分析器

创建分析器后,使用 ImageAnalysis.setAnalyzer() 注册该分析器以开始分析。完成分析后,使用 ImageAnalysis.clearAnalyzer() 移除已注册的分析器。

您只能将一个分析器配置为活动状态,用于分析图像。调用 ImageAnalysis.setAnalyzer() 会替换已注册的分析器(如果已存在该分析器)。应用可以在绑定用例之前或之后随时设置新的分析器。

将 ImageAnalysis 绑定到生命周期

注意:该步骤适用于所有 CameraX 用例。如需详细了解绑定和生命周期自定义,请参阅 CameraX API 模型

强烈建议您使用 ProcessCameraProvider.bindToLifecycle() 函数将 ImageAnalysis 绑定到现有的 AndroidX 生命周期。请注意,bindToLifecycle() 函数会返回选定的 Camera 设备,该函数可用于微调曝光等高级设置。如需详细了解如何控制相机输出,请参阅此指南

以下示例结合了上述步骤中的所有操作,将 CameraX ImageAnalysisPreview 用例绑定到了 lifeCycle 所有者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val imageAnalysis = ImageAnalysis.Builder()
// enable the following line if RGBA output is needed.
// .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
.setTargetResolution(Size(1280, 720))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
imageAnalysis.setAnalyzer(executor, ImageAnalysis.Analyzer { imageProxy ->
val rotationDegrees = imageProxy.imageInfo.rotationDegrees
// insert your code here.
...
// after done, release the ImageProxy object
imageProxy.close()
})

cameraProvider.bindToLifecycle(this as LifecycleOwner, cameraSelector, imageAnalysis, preview)

参考

Android CameraX