ハウツー

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 を変更しましょう。ここでは、CameraX から基盤となる Camera2 API とやり取りできる Camera2Interop API を使用します。これにより、CameraX で直接公開されていないカメラ機能を使用できるようになります。次の変更を行う必要があります。

  • 顔の境界を Rect のリストとして含む StateFlow を作成します。
  • 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 オブジェクトのリストを生成するようになりました。

センサーの座標を UI の座標に変換する

前のセクションで保存した検出された顔のバウンディング ボックスは、センサー座標系の座標を使用します。UI でバウンディング ボックスを描画するには、これらの座標を Compose 座標系で正しくなるように変換する必要があります。必要な対応:

  • センサー座標プレビュー バッファ座標に変換する
  • プレビュー バッファの座標Compose UI の座標に変換する

これらの変換は、変換行列を使用して行われます。各変換には独自の行列があります。

  • SurfaceRequest は、sensorToBufferTranform マトリックスを含む TransformationInfo インスタンスを保持します。
  • CameraXViewfinder には CoordinateTransformer が関連付けられています。この変換ツールは、前回のブログ投稿でタップしてフォーカスする座標を変換するために使用したことを覚えているかもしれません。

変換を行うヘルパー メソッドを作成できます。

  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 を使用して、画面全体を覆う半透明のピンク色の長方形を描画します。
  • Canvas 描画ブロック内に入るまで、sensorFaceRects 変数の読み取りを遅延させます。次に、座標を 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 氏の尽力により実現しました。

 

作成者:

続きを読む