こんにちは。CameraX と Jetpack Compose に関するシリーズへようこそ。前回の投稿では、カメラ プレビューの設定の基本と、タップしてフォーカスする機能の追加について説明しました。
🧱 パート 1: 新しい camera-compose アーティファクトを使用して基本的なカメラ プレビューを構築します。権限の処理と基本的な統合について説明しました。
👆 パート 2: Compose のジェスチャー システム、グラフィック、コルーチンを使用して、タップしてフォーカスを合わせる機能を実装します。
🔦 パート 3(この投稿): Compose UI 要素をカメラ プレビューの上にオーバーレイして、ユーザー エクスペリエンスを向上させる方法について説明します。
📂 パート 4: アダプティブ API と Compose アニメーション フレームワークを使用して、折りたたみ式スマートフォンのテーブルトップ モードへのスムーズな切り替えを実現します。
今回の投稿では、カメラ プレビューの上にスポットライト効果を実装し、顔検出を効果のベースとして使用するという、視覚的に少し魅力的なものについて詳しく説明します。なぜでしょうか?わからない見た目はかっこいいですね。さらに重要なのは、センサーの座標を UI の座標に簡単に変換して、Compose で使用できることを示していることです。
顔検出を有効にする
まず、顔検出を有効にするために 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 Butcher、Alex Vanyo、Trevor McGuire、Don Turner、Lauren Ward に感謝いたします。Yasith Vidanaarachch の尽力により実現しました。
続きを読む
-
ハウツー
この記事では、Compose の waitUntil テスト API を使用して、特定の条件が満たされるまで待機する方法について説明します。
Jose Alcérreca • 所要時間: 3 分
-
ハウツー
Android Studio の Gemini、Gemini CLI、Antigravity、または Claude Code や Codex などのサードパーティ エージェントを使用しているかどうかにかかわらず、高品質の Android 開発をどこでも実現できるようにすることが私たちの使命です。
Adarsh Fernando, Esteban de la Canal • 所要時間: 4 分
-
ハウツー
バッテリーの消耗が Android ユーザーにとって最も重要な問題であると認識し、Google はデベロッパーがより省電力なアプリを構築できるよう、さまざまな取り組みを行ってきました。
Alice Yuan • 所要時間: 8 分
メールを受け取る
Android 開発に関する最新の分析情報を毎週メールでお届けします。