方法指南

使用 CameraX 和 Jetpack Compose 创建聚光灯效果

阅读用时:8 分钟
Jolanda Verhoef
开发者关系工程师

您好!欢迎回来继续学习我们的 CameraX 和 Jetpack Compose 系列教程。在之前的博文中,我们介绍了设置相机预览的基本知识,并添加了点按对焦功能。

🧱 第 1 部分:使用新的 camera-compose 制品构建基本的相机预览。我们介绍了权限处理和基本集成。

👆 第 2 部分:使用 Compose 手势系统、图形和协程来实现点按即聚焦的视觉效果。

🔦 第 3 部分(本文):探索如何将 Compose 界面元素叠加在相机预览之上,以获得更丰富的用户体验。

📂 第 4 部分:使用自适应 API 和 Compose 动画框架在可折叠手机上顺畅地切换到桌面模式和从桌面模式切换。

在这篇博文中,我们将深入探讨一些更具视觉吸引力的内容,即在相机预览上实现聚光效果,并使用人脸检测作为效果的基础。您可能会问,为什么?我不确定。但它看起来确实很酷炫 🙂。更重要的是,它展示了如何轻松将传感器坐标转换为界面坐标,从而让我们可以在 Compose 中使用它们!

face-detection.gif

启用人脸检测功能

首先,我们修改 CameraPreviewViewModel 以启用人脸检测。我们将使用 Camera2Interop API,以便从 CameraX 与底层 Camera2 API 进行交互。这样一来,我们就有机会使用 CameraX 未直接公开的相机功能。我们需要进行以下更改:

  • 创建一个 StateFlow,其中包含面部边界(以 Rect 列表的形式)。
  • STATISTICS_FACE_DETECT_MODE 捕获请求选项设置为 FULL,以启用人脸检测。
  • 设置 CaptureCallback 以从拍摄结果中获取人脸信息。
  class CameraPreviewViewModel : ViewModel() {
    ...
    private val _sensorFaceRects = MutableStateFlow(listOf<Rect>())
    val sensorFaceRects: StateFlow<List<Rect>> = _sensorFaceRects.asStateFlow()

    private val cameraPreviewUseCase = Preview.Builder()
        .apply {
            Camera2Interop.Extender(this)
                .setCaptureRequestOption(
                    CaptureRequest.STATISTICS_FACE_DETECT_MODE,
                    CaptureRequest.STATISTICS_FACE_DETECT_MODE_FULL
                )
                .setSessionCaptureCallback(object : CameraCaptureSession.CaptureCallback() {
                    override fun onCaptureCompleted(
                        session: CameraCaptureSession,
                        request: CaptureRequest,
                        result: TotalCaptureResult
                    ) {
                        super.onCaptureCompleted(session, request, result)
                        result.get(CaptureResult.STATISTICS_FACES)
                            ?.map { face -> face.bounds.toComposeRect() }
                            ?.toList()
                            ?.let { faces -> _sensorFaceRects.update { faces } }
                    }
                })
        }
        .build().apply {
    ...
}

完成这些更改后,我们的视图模型现在会发出一个 Rect 对象列表,表示以传感器坐标表示的检测到的人脸的边界框。

将传感器坐标转换为界面坐标

我们在上一部分中存储的检测到的人脸的边界框使用传感器坐标系中的坐标。为了在界面中绘制边界框,我们需要转换这些坐标,以便它们在 Compose 坐标系中正确无误。我们需要:

  • 传感器坐标转换为预览缓冲区坐标
  • 预览缓冲区坐标转换为 Compose 界面坐标

这些转换通过转换矩阵完成。每种转换都有自己的矩阵:

我们可以创建一个辅助方法来为我们执行转换:

  private fun List<Rect>.transformToUiCoords(
    transformationInfo: SurfaceRequest.TransformationInfo?,
    uiToBufferCoordinateTransformer: MutableCoordinateTransformer
): List<Rect> = this.map { sensorRect ->
    val bufferToUiTransformMatrix = Matrix().apply {
        setFrom(uiToBufferCoordinateTransformer.transformMatrix)
        invert()
    }

    val sensorToBufferTransformMatrix = Matrix().apply {
        transformationInfo?.let {
            setFrom(it.sensorToBufferTransform)
        }
    }

    val bufferRect = sensorToBufferTransformMatrix.map(sensorRect)
    val uiRect = bufferToUiTransformMatrix.map(bufferRect)

    uiRect
}
  • 我们遍历检测到的人脸列表,并针对每张人脸执行转换。
  • 我们从 CameraXViewfinder 获取的 CoordinateTransformer.transformMatrix 默认情况下会将坐标从界面坐标转换为缓冲区坐标。在本例中,我们希望矩阵以相反的方式工作,将缓冲区坐标转换为界面坐标。因此,我们使用 invert() 方法来求矩阵的逆。
  • 我们首先使用 sensorToBufferTransformMatrix 将面部从传感器坐标转换为缓冲区坐标,然后使用 bufferToUiTransformMatrix 将这些缓冲区坐标转换为界面坐标。

实现聚光灯效果

现在,我们来更新 CameraPreviewContent 可组合项以绘制聚光灯效果。我们将使用 Canvas 可组合函数在预览上绘制渐变遮罩,使检测到的人脸可见:

  @Composable
fun CameraPreviewContent(
    viewModel: CameraPreviewViewModel,
    modifier: Modifier = Modifier,
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current
) {
    val surfaceRequest by viewModel.surfaceRequest.collectAsStateWithLifecycle()
    val sensorFaceRects by viewModel.sensorFaceRects.collectAsStateWithLifecycle()
    val transformationInfo by
        produceState<SurfaceRequest.TransformationInfo?>(null, surfaceRequest) {
            try {
                surfaceRequest?.setTransformationInfoListener(Runnable::run) { transformationInfo ->
                    value = transformationInfo
                }
                awaitCancellation()
            } finally {
                surfaceRequest?.clearTransformationInfoListener()
            }
        }
    val shouldSpotlightFaces by remember {
        derivedStateOf { sensorFaceRects.isNotEmpty() && transformationInfo != null} 
    }
    val spotlightColor = Color(0xDDE60991)
    ..

    surfaceRequest?.let { request ->
        val coordinateTransformer = remember { MutableCoordinateTransformer() }
        CameraXViewfinder(
            surfaceRequest = request,
            coordinateTransformer = coordinateTransformer,
            modifier = ..
        )

        AnimatedVisibility(shouldSpotlightFaces, enter = fadeIn(), exit = fadeOut()) {
            Canvas(Modifier.fillMaxSize()) {
                val uiFaceRects = sensorFaceRects.transformToUiCoords(
                    transformationInfo = transformationInfo,
                    uiToBufferCoordinateTransformer = coordinateTransformer
                )

                // Fill the whole space with the color
                drawRect(spotlightColor)
                // Then extract each face and make it transparent

                uiFaceRects.forEach { faceRect ->
                    drawRect(
                        Brush.radialGradient(
                            0.4f to Color.Black, 1f to Color.Transparent,
                            center = faceRect.center,
                            radius = faceRect.minDimension * 2f,
                        ),
                        blendMode = BlendMode.DstOut
                    )
                }
            }
        }
    }
}

其运作方式如下:

  • 我们从视图模型中收集人脸列表。
  • 为了确保我们不会在每次检测到的人脸列表发生变化时重新合成整个屏幕,我们使用 derivedStateOf 来跟踪是否检测到任何人脸。然后,您可以将此值与 AnimatedVisibility 搭配使用,以实现彩色叠加层的淡入和淡出动画效果。
  • surfaceRequest 包含将传感器坐标转换为 SurfaceRequest.TransformationInfo 中的缓冲区坐标所需的信息。我们使用 produceState 函数在 surface 请求中设置监听器,并在可组合函数离开组合树时清除此监听器。
  • 我们使用 Canvas 绘制一个覆盖整个屏幕的半透明粉色矩形。
  • 我们会延迟读取 sensorFaceRects 变量,直到进入 Canvas 绘制块。然后,我们将坐标转换为界面坐标。
  • 我们遍历检测到的人脸,并为每个人脸绘制一个径向渐变,使人脸矩形的内部透明。
  • 我们使用 BlendMode.DstOut 来确保从粉色矩形中剪切出渐变,从而创建聚光灯效果。

注意:当您将摄像头更改为 DEFAULT_FRONT_CAMERA 时,您会发现聚光灯效果是镜像的!这是一个已知问题,已在 Google 问题跟踪器中进行跟踪。

结果

有了这段代码,我们就实现了可突出显示检测到的人脸的聚光灯效果。您可以点击此处查看完整的代码段。

这只是一个开始,借助 Compose 的强大功能,您可以打造出无数令人惊艳的相机体验。能够将传感器和缓冲区坐标转换为 Compose 界面坐标,反之亦然,这意味着我们可以利用所有 Compose 界面功能,并将其与底层相机系统无缝集成。借助动画、高级界面图形、简单的界面状态管理和全面的手势控制,您可以尽情发挥想象力!

在本系列的最后一篇博文中,我们将深入探讨如何使用自适应 API 和 Compose 动画框架在可折叠设备上无缝切换不同的相机界面。敬请期待!


本博文中的代码段采用以下许可:

// Copyright 2024 Google LLC. SPDX-License-Identifier: Apache-2.0

非常感谢 Nick ButcherAlex VanyoTrevor McGuireDon Turner 和 Lauren Ward 的审核和反馈。感谢 Yasith Vidanaarachch 的辛勤付出。

 

作者:

继续阅读