操作說明

使用 CameraX 和 Jetpack Compose 建立聚光燈效果

8 分鐘閱讀
Jolanda Verhoef
開發人員關係工程師

你好啊!歡迎繼續收看 CameraX 和 Jetpack Compose 系列影片。在先前的文章中,我們介紹了設定相機預覽的基礎知識,並新增了輕觸對焦功能。

🧱 第 1 部分使用新的 camera-compose 構件,建構基本攝影機預覽畫面。我們已介紹權限處理和基本整合。

👆 第 2 部分使用 Compose 手勢系統、圖像和協同程式,實作輕觸對焦的視覺效果。

🔦 第 3 部分 (本文):瞭解如何在相機預覽畫面中疊加 Compose UI 元素,提供更豐富的使用者體驗。

📂 第 4 部分:使用自適應 API 和 Compose 動畫架構,在摺疊式手機上順暢地切換桌面模式。

在這篇文章中,我們將深入探討更具視覺吸引力的內容,也就是在攝影機預覽畫面中實作聚光燈效果,並以臉部偵測做為效果的基礎。你可能會問為什麼?不確定。但看起來很酷 🙂。更重要的是,這項功能示範了如何輕鬆將感應器座標轉換為 UI 座標,以便在 Compose 中使用!

face-detection.gif

啟用臉部偵測功能

首先,請修改 CameraPreviewViewModel,啟用臉部偵測功能。我們將使用 Camera2Interop API,透過 CameraX 與基礎 Camera2 API 互動。這讓我們有機會使用 CameraX 未直接公開的相機功能。我們需要進行下列變更:

  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 物件清單,代表感應器座標中偵測到的臉部周框。

將感應器座標轉換為 UI 座標

我們在上一個部分儲存的偵測到臉部的邊界框,會使用感應器座標系統中的座標。如要在 UI 中繪製邊界方塊,我們需要轉換這些座標,確保座標在 Compose 座標系統中正確無誤。我們需要:

  • 感應器座標轉換為預覽緩衝區座標
  • 預覽緩衝區座標轉換為 Compose UI 座標

這些轉換作業是透過轉換矩陣完成。每項轉換都有自己的矩陣:

我們可以建立輔助方法,為我們執行轉換:

  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 會將座標從 UI 轉換為緩衝區座標。在本例中,我們希望矩陣能反向運作,將緩衝區座標轉換為 UI 座標。因此,我們使用 invert() 方法反轉矩陣。
  • 我們首先使用 sensorToBufferTransformMatrix 將臉部從感應器座標轉換為緩衝區座標,然後使用 bufferToUiTransformMatrix 將這些緩衝區座標轉換為 UI 座標。

實作聚光燈效果

現在,請更新 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 函式在介面要求中設定監聽器,並在可組合函式離開組合樹狀結構時清除這個監聽器。
  • 我們使用 Canvas 繪製覆蓋整個畫面的半透明粉紅色矩形。
  • 我們會延後讀取 sensorFaceRects 變數,直到進入 Canvas 繪製區塊為止。接著,我們會將座標轉換為 UI 座標。
  • 我們會疊代偵測到的臉部,並為每個臉部繪製放射狀漸層,讓臉部矩形內部呈現透明。
  • 我們使用 BlendMode.DstOut,確保從粉紅色矩形剪下漸層,建立聚光燈效果。

注意:將攝影機變更為 DEFAULT_FRONT_CAMERA 時,你會發現聚光燈畫面會鏡像翻轉!這是已知問題,已在 Google Issue Tracker中追蹤。

結果

有了這段程式碼,我們就能完全正常運作聚光燈效果,醒目顯示偵測到的臉部。如要查看完整程式碼片段,請按這裡

這只是開始,您可以使用 Compose 的強大功能,打造各種令人驚豔的攝影機體驗。能夠將感應器和緩衝區座標轉換為 Compose UI 座標,反之亦然,代表我們可以運用所有 Compose UI 功能,並與基礎攝影機系統無縫整合。有了動畫、進階 UI 圖像、簡單的 UI 狀態管理和完整的手勢控制功能,您的想像力就是極限!

在本系列的最後一篇文章中,我們將深入探討如何使用自適應 API 和 Compose 動畫架構,在摺疊式裝置上順暢切換不同的相機 UI。敬請拭目以待!


本網誌中的程式碼片段採用下列授權:

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

感謝 Nick ButcherAlex VanyoTrevor McGuireDon Turner 和 Lauren Ward 審查並提供意見回饋。感謝 Yasith Vidanaarachch 的辛勤付出。

 

撰寫者:

繼續閱讀