画面のフラッシュ(フロント フラッシュ、自撮り写真のフラッシュとも呼ばれる)は、暗い場所で前面カメラを使用して画像をキャプチャする際に、スマートフォンの画面の明るさを利用して被写体を照らします。多くのネイティブ カメラアプリやソーシャル メディア アプリで使用できます。ほとんどの人は、自撮り写真をフレームに収める際はスマートフォンを十分近づけて撮影するため、この方法が効果的です。
しかし、デベロッパーがこの機能を適切に実装し、デバイス間で一貫した良好なキャプチャ品質を維持するのは困難です。このガイドでは、低レベルの Android カメラ フレームワーク API である Camera2 を使用して、この機能を適切に実装する方法を説明します。
全般的なワークフロー
この機能を適切に実装するには、プリキャプチャ測定シーケンス(自動露出プリキャプチャ)の使用方法と処理のタイミングの 2 つが重要な要素となります。一般的なワークフローを図 1 に示します。
画面のフラッシュ機能を使用して画像をキャプチャする必要がある場合は、次の手順を実施します。
- 画面のフラッシュに必要な UI の変更を適用します。これにより、デバイスの画面を使用して写真を撮影するのに十分な明るさを確保できます。一般的なユースケースでは、テストで使用されている次の UI の変更をおすすめします。
- アプリ画面が白色のオーバーレイで覆われている。
- 画面の明るさは最大になります。
- 自動露出(AE)モードを
CONTROL_AE_MODE_ON_EXTERNAL_FLASH
に設定します(サポートされている場合)。 CONTROL_AE_PRECAPTURE_TRIGGER
を使用して、プリキャプチャ測定シーケンスをトリガーします。自動露出(AE)と自動ホワイト バランス(AWB)が収束するまで待ちます。
収束すると、アプリの通常の写真キャプチャ フローが使用されます。
フレームワークにキャプチャ リクエストを送信します。
キャプチャ結果の受信を待ちます。
CONTROL_AE_MODE_ON_EXTERNAL_FLASH
が設定されている場合は AE モードをリセット。画面の点滅に関する UI の変更を消去します。
Camera2 のサンプルコード
アプリ画面を白色のオーバーレイで覆う
アプリケーションのレイアウト XML ファイルにビューを追加します。画面のフラッシュ キャプチャ中に、他のすべての UI 要素の上に配置できる十分なエレベーションを持つビュー。デフォルトでは非表示で、画面の Flash UI の変更が適用された場合にのみ表示されます。
次のコードサンプルでは、ビューの例として白色(#FFFFFF
)を使用しています。アプリケーションは、要件に基づいて色を選択することも、複数の色をユーザーに提供することもできます。
<View android:id="@+id/white_color_overlay" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#FFFFFF" android:visibility="invisible" android:elevation="8dp" />
画面の明るさを最大にする
Android アプリの画面の明るさを変更する方法は複数あります。直接的な方法の一つは、アクティビティ ウィンドウ リファレンスの screenBrightness WindowManager パラメータを変更することです。
Kotlin
private var previousBrightness: Float = -1.0f private fun maximizeScreenBrightness() { activity?.window?.let { window -> window.attributes?.apply { previousBrightness = screenBrightness screenBrightness = 1f window.attributes = this } } } private fun restoreScreenBrightness() { activity?.window?.let { window -> window.attributes?.apply { screenBrightness = previousBrightness window.attributes = this } } }
Java
private float mPreviousBrightness = -1.0f; private void maximizeScreenBrightness() { if (getActivity() == null || getActivity().getWindow() == null) { return; } Window window = getActivity().getWindow(); WindowManager.LayoutParams attributes = window.getAttributes(); mPreviousBrightness = attributes.screenBrightness; attributes.screenBrightness = 1f; window.setAttributes(attributes); } private void restoreScreenBrightness() { if (getActivity() == null || getActivity().getWindow() == null) { return; } Window window = getActivity().getWindow(); WindowManager.LayoutParams attributes = window.getAttributes(); attributes.screenBrightness = mPreviousBrightness; window.setAttributes(attributes); }
AE モードを CONTROL_AE_MODE_ON_EXTERNAL_FLASH
に設定
CONTROL_AE_MODE_ON_EXTERNAL_FLASH
は API レベル 28 以降で使用できます。ただし、この AE モードは一部のデバイスで利用できないため、AE モードを使用できるかどうかを確認し、それに応じて値を設定してください。可用性を確認するには、CameraCharacteristics#CONTROL_AE_AVAILABLE_MODES
を使用します。
Kotlin
private val characteristics: CameraCharacteristics by lazy { cameraManager.getCameraCharacteristics(cameraId) } @RequiresApi(Build.VERSION_CODES.P) private fun isExternalFlashAeModeAvailable() = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES) ?.contains(CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH) ?: false
Java
try { mCharacteristics = mCameraManager.getCameraCharacteristics(mCameraId); } catch (CameraAccessException e) { e.printStackTrace(); } @RequiresApi(Build.VERSION_CODES.P) private boolean isExternalFlashAeModeAvailable() { int[] availableAeModes = mCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES); for (int aeMode : availableAeModes) { if (aeMode == CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH) { return true; } } return false; }
アプリに繰り返しキャプチャ リクエストが設定されている場合(プレビューに必要です)、AE モードを繰り返しリクエストに設定する必要があります。そうしないと、次回の撮影を繰り返す際に、デフォルトまたは他のユーザーが設定した AE モードによってオーバーライドされる可能性があります。その場合、カメラが通常の外部フラッシュ AE モードに対して行うすべてのオペレーションを完了できない可能性があります。
カメラが AE モードの更新リクエストを完全に処理できるようにするには、反復キャプチャ コールバックでキャプチャ結果を確認し、その結果で AE モードが更新されるまで待ちます。
AE モードの更新を待機できるキャプチャ コールバック
次のコード スニペットは、この方法を示しています。
Kotlin
private val repeatingCaptureCallback = object : CameraCaptureSession.CaptureCallback() { private var targetAeMode: Int? = null private var aeModeUpdateDeferred: CompletableDeferred? = null suspend fun awaitAeModeUpdate(targetAeMode: Int) { this.targetAeMode = targetAeMode aeModeUpdateDeferred = CompletableDeferred() // Makes the current coroutine wait until aeModeUpdateDeferred is completed. It is // completed once targetAeMode is found in the following capture callbacks aeModeUpdateDeferred?.await() } private fun process(result: CaptureResult) { // Checks if AE mode is updated and completes any awaiting Deferred aeModeUpdateDeferred?.let { val aeMode = result[CaptureResult.CONTROL_AE_MODE] if (aeMode == targetAeMode) { it.complete(Unit) } } } override fun onCaptureCompleted( session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult ) { super.onCaptureCompleted(session, request, result) process(result) } }
Java
static class AwaitingCaptureCallback extends CameraCaptureSession.CaptureCallback { private int mTargetAeMode; private CountDownLatch mAeModeUpdateLatch = null; public void awaitAeModeUpdate(int targetAeMode) { mTargetAeMode = targetAeMode; mAeModeUpdateLatch = new CountDownLatch(1); // Makes the current thread wait until mAeModeUpdateLatch is released, it will be // released once targetAeMode is found in the capture callbacks below try { mAeModeUpdateLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } } private void process(CaptureResult result) { // Checks if AE mode is updated and decrements the count of any awaiting latch if (mAeModeUpdateLatch != null) { int aeMode = result.get(CaptureResult.CONTROL_AE_MODE); if (aeMode == mTargetAeMode) { mAeModeUpdateLatch.countDown(); } } } @Override public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { super.onCaptureCompleted(session, request, result); process(result); } } private final AwaitingCaptureCallback mRepeatingCaptureCallback = new AwaitingCaptureCallback();
AE モードを有効または無効にする繰り返しリクエストを設定します
以下のコードサンプルは、キャプチャ コールバックを実装した繰り返しリクエストの設定方法を示しています。
Kotlin
/** [HandlerThread] where all camera operations run */ private val cameraThread = HandlerThread("CameraThread").apply { start() } /** [Handler] corresponding to [cameraThread] */ private val cameraHandler = Handler(cameraThread.looper) private suspend fun enableExternalFlashAeMode() { if (Build.VERSION.SDK_INT >= 28 && isExternalFlashAeModeAvailable()) { session.setRepeatingRequest( camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply { addTarget(previewSurface) set( CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH ) }.build(), repeatingCaptureCallback, cameraHandler ) // Wait for the request to be processed by camera repeatingCaptureCallback.awaitAeModeUpdate(CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH) } } private fun disableExternalFlashAeMode() { if (Build.VERSION.SDK_INT >= 28 && isExternalFlashAeModeAvailable()) { session.setRepeatingRequest( camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply { addTarget(previewSurface) }.build(), repeatingCaptureCallback, cameraHandler ) } }
Java
private void setupCameraThread() { // HandlerThread where all camera operations run HandlerThread cameraThread = new HandlerThread("CameraThread"); cameraThread.start(); // Handler corresponding to cameraThread mCameraHandler = new Handler(cameraThread.getLooper()); } private void enableExternalFlashAeMode() { if (Build.VERSION.SDK_INT >= 28 && isExternalFlashAeModeAvailable()) { try { CaptureRequest.Builder requestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); requestBuilder.addTarget(mPreviewSurface); requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH); mSession.setRepeatingRequest(requestBuilder.build(), mRepeatingCaptureCallback, mCameraHandler); } catch (CameraAccessException e) { e.printStackTrace(); } // Wait for the request to be processed by camera mRepeatingCaptureCallback.awaitAeModeUpdate(CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH); } } private void disableExternalFlashAeMode() { if (Build.VERSION.SDK_INT >= 28 && isExternalFlashAeModeAvailable()) { try { CaptureRequest.Builder requestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); requestBuilder.addTarget(mPreviewSurface); mSession.setRepeatingRequest(requestBuilder.build(), mRepeatingCaptureCallback, mCameraHandler); } catch (CameraAccessException e) { e.printStackTrace(); } } }
プリキャプチャ シーケンスをトリガーする
プリキャプチャ測定シーケンスをトリガーするには、CONTROL_AE_PRECAPTURE_TRIGGER_START
値をリクエストに設定して CaptureRequest
を送信します。リクエストが処理されるのを待ってから、AE と AWB が収束するのを待つ必要があります。
プリキャプチャ トリガーは単一のキャプチャ リクエストで行いますが、AE と AWB のコンバージェンスを待機するには、より複雑な作業が必要になります。反復リクエストに設定されたキャプチャ コールバックを使用して、AE 状態と AWB 状態を追跡できます。
同じ繰り返しコールバックを更新することで、コードをシンプルにできます。多くの場合、アプリには、カメラのセットアップ時に繰り返しリクエストを設定するプレビューが必要になります。そのため、繰り返しキャプチャ コールバックをその最初の繰り返しリクエストに 1 回設定すれば、結果の確認や待機のために再利用できます。
コールバック コードの更新をキャプチャして収束を待つ
反復キャプチャ コールバックを更新するには、次のコード スニペットを使用します。
Kotlin
private val repeatingCaptureCallback = object : CameraCaptureSession.CaptureCallback() { private var targetAeMode: Int? = null private var aeModeUpdateDeferred: CompletableDeferred? = null private var convergenceDeferred: CompletableDeferred ? = null suspend fun awaitAeModeUpdate(targetAeMode: Int) { this.targetAeMode = targetAeMode aeModeUpdateDeferred = CompletableDeferred() // Makes the current coroutine wait until aeModeUpdateDeferred is completed. It is // completed once targetAeMode is found in the following capture callbacks aeModeUpdateDeferred?.await() } suspend fun awaitAeAwbConvergence() { convergenceDeferred = CompletableDeferred() // Makes the current coroutine wait until convergenceDeferred is completed, it will be // completed once both AE & AWB are reported as converged in the capture callbacks below convergenceDeferred?.await() } private fun process(result: CaptureResult) { // Checks if AE mode is updated and completes any awaiting Deferred aeModeUpdateDeferred?.let { val aeMode = result[CaptureResult.CONTROL_AE_MODE] if (aeMode == targetAeMode) { it.complete(Unit) } } // Checks for convergence and completes any awaiting Deferred convergenceDeferred?.let { val aeState = result[CaptureResult.CONTROL_AE_STATE] val awbState = result[CaptureResult.CONTROL_AWB_STATE] val isAeReady = ( aeState == null // May be null in some devices (e.g. legacy camera HW level) || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED || aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED ) val isAwbReady = ( awbState == null // May be null in some devices (e.g. legacy camera HW level) || awbState == CaptureResult.CONTROL_AWB_STATE_CONVERGED ) if (isAeReady && isAwbReady) { // if any non-null convergenceDeferred is set, complete it it.complete(Unit) } } } override fun onCaptureCompleted( session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult ) { super.onCaptureCompleted(session, request, result) process(result) } }
Java
static class AwaitingCaptureCallback extends CameraCaptureSession.CaptureCallback { private int mTargetAeMode; private CountDownLatch mAeModeUpdateLatch = null; private CountDownLatch mConvergenceLatch = null; public void awaitAeModeUpdate(int targetAeMode) { mTargetAeMode = targetAeMode; mAeModeUpdateLatch = new CountDownLatch(1); // Makes the current thread wait until mAeModeUpdateLatch is released, it will be // released once targetAeMode is found in the capture callbacks below try { mAeModeUpdateLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } } public void awaitAeAwbConvergence() { mConvergenceLatch = new CountDownLatch(1); // Makes the current coroutine wait until mConvergenceLatch is released, it will be // released once both AE & AWB are reported as converged in the capture callbacks below try { mConvergenceLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } } private void process(CaptureResult result) { // Checks if AE mode is updated and decrements the count of any awaiting latch if (mAeModeUpdateLatch != null) { int aeMode = result.get(CaptureResult.CONTROL_AE_MODE); if (aeMode == mTargetAeMode) { mAeModeUpdateLatch.countDown(); } } // Checks for convergence and decrements the count of any awaiting latch if (mConvergenceLatch != null) { Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); Integer awbState = result.get(CaptureResult.CONTROL_AWB_STATE); boolean isAeReady = ( aeState == null // May be null in some devices (e.g. legacy camera HW level) || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED || aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED ); boolean isAwbReady = ( awbState == null // May be null in some devices (e.g. legacy camera HW level) || awbState == CaptureResult.CONTROL_AWB_STATE_CONVERGED ); if (isAeReady && isAwbReady) { mConvergenceLatch.countDown(); mConvergenceLatch = null; } } } @Override public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { super.onCaptureCompleted(session, request, result); process(result); } }
カメラのセットアップ中にリクエストを繰り返し行うようにコールバックを設定する
次のコードサンプルを使用すると、初期化中にコールバックを反復リクエストに設定できます。
Kotlin
// Open the selected camera camera = openCamera(cameraManager, cameraId, cameraHandler) // Creates list of Surfaces where the camera will output frames val targets = listOf(previewSurface, imageReaderSurface) // Start a capture session using our open camera and list of Surfaces where frames will go session = createCameraCaptureSession(camera, targets, cameraHandler) val captureRequest = camera.createCaptureRequest( CameraDevice.TEMPLATE_PREVIEW).apply { addTarget(previewSurface) } // This will keep sending the capture request as frequently as possible until the // session is torn down or session.stopRepeating() is called session.setRepeatingRequest(captureRequest.build(), repeatingCaptureCallback, cameraHandler)
Java
// Open the selected camera mCamera = openCamera(mCameraManager, mCameraId, mCameraHandler); // Creates list of Surfaces where the camera will output frames Listtargets = new ArrayList<>(Arrays.asList(mPreviewSurface, mImageReaderSurface)); // Start a capture session using our open camera and list of Surfaces where frames will go mSession = createCaptureSession(mCamera, targets, mCameraHandler); try { CaptureRequest.Builder requestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); requestBuilder.addTarget(mPreviewSurface); // This will keep sending the capture request as frequently as possible until the // session is torn down or session.stopRepeating() is called mSession.setRepeatingRequest(requestBuilder.build(), mRepeatingCaptureCallback, mCameraHandler); } catch (CameraAccessException e) { e.printStackTrace(); }
プリキャプチャ シーケンスのトリガーと待機
コールバックを設定すると、次のコードサンプルを使用して、プリキャプチャ シーケンスのトリガーと待機を行うことができます。
Kotlin
private suspend fun runPrecaptureSequence() { // Creates a new capture request with CONTROL_AE_PRECAPTURE_TRIGGER_START val captureRequest = session.device.createCaptureRequest( CameraDevice.TEMPLATE_PREVIEW ).apply { addTarget(previewSurface) set( CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START ) } val precaptureDeferred = CompletableDeferred() session.capture(captureRequest.build(), object: CameraCaptureSession.CaptureCallback() { override fun onCaptureCompleted( session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult ) { // Waiting for this callback ensures the precapture request has been processed precaptureDeferred.complete(Unit) } }, cameraHandler) precaptureDeferred.await() // Precapture trigger request has been processed, we can wait for AE & AWB convergence now repeatingCaptureCallback.awaitAeAwbConvergence() }
Java
private void runPrecaptureSequence() { // Creates a new capture request with CONTROL_AE_PRECAPTURE_TRIGGER_START try { CaptureRequest.Builder requestBuilder = mSession.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); requestBuilder.addTarget(mPreviewSurface); requestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); CountDownLatch precaptureLatch = new CountDownLatch(1); mSession.capture(requestBuilder.build(), new CameraCaptureSession.CaptureCallback() { @Override public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { Log.d(TAG, "CONTROL_AE_PRECAPTURE_TRIGGER_START processed"); // Waiting for this callback ensures the precapture request has been processed precaptureLatch.countDown(); } }, mCameraHandler); precaptureLatch.await(); // Precapture trigger request has been processed, we can wait for AE & AWB convergence now mRepeatingCaptureCallback.awaitAeAwbConvergence(); } catch (CameraAccessException | InterruptedException e) { e.printStackTrace(); } }
すべてをつなぎ合わせる
すべての主要コンポーネントの準備が整い、写真を撮る必要がある場合(ユーザーがキャプチャ ボタンをクリックして写真を撮る場合など)に、すべてのステップを前述の説明とコードサンプルで示された順序で実行できます。
Kotlin
// User clicks captureButton to take picture captureButton.setOnClickListener { v -> // Apply the screen flash related UI changes whiteColorOverlayView.visibility = View.VISIBLE maximizeScreenBrightness() // Perform I/O heavy operations in a different scope lifecycleScope.launch(Dispatchers.IO) { // Enable external flash AE mode and wait for it to be processed enableExternalFlashAeMode() // Run precapture sequence and wait for it to complete runPrecaptureSequence() // Start taking picture and wait for it to complete takePhoto() disableExternalFlashAeMode() v.post { // Clear the screen flash related UI changes restoreScreenBrightness() whiteColorOverlayView.visibility = View.INVISIBLE } } }
Java
// User clicks captureButton to take picture mCaptureButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // Apply the screen flash related UI changes mWhiteColorOverlayView.setVisibility(View.VISIBLE); maximizeScreenBrightness(); // Perform heavy operations in a different thread Executors.newSingleThreadExecutor().execute(() -> { // Enable external flash AE mode and wait for it to be processed enableExternalFlashAeMode(); // Run precapture sequence and wait for it to complete runPrecaptureSequence(); // Start taking picture and wait for it to complete takePhoto(); disableExternalFlashAeMode(); v.post(() -> { // Clear the screen flash related UI changes restoreScreenBrightness(); mWhiteColorOverlayView.setVisibility(View.INVISIBLE); }); }); } });
サンプル画像
画面のフラッシュが正しく実装されなかった場合と正しく実装された場合の影響について、次の例で確認できます。
間違ったとき
画面のフラッシュが適切に実装されていない場合、複数のキャプチャ、デバイス、照明条件で一貫性のない結果が得られます。多くの場合、キャプチャした画像に不適切な露出や色合いの問題があります。一部のデバイスでは、この種のバグは、完全に暗い環境ではなく、暗い環境など、特定の照明条件下でより顕著になります。
次の表に、このような問題の例を示します。CameraX ラボのインフラストラクチャで撮影され、光源は暖かい白色のままです。この暖かい白色の光源により、青色の色合いが光源の副作用ではなく、実際の問題であることを確認できます。
環境 | 露出不足 | 露出過多 | 色合い |
---|---|---|---|
暗い環境(スマートフォン以外に光源がない) |
|
|
|
低光量(追加の約 3 ルクスの光源) |
|
|
|
うまく活用すれば
次の表に、同じデバイスと条件で標準実装を使用した場合の結果を示します。
環境 | 露出不足(固定) | 露出過多(固定) | 色合い(固定) |
---|---|---|---|
暗い環境(スマートフォン以外に光源がない) |
|
|
|
低光量(追加の約 3 ルクスの光源) |
|
|
|
ご覧のとおり、標準実装のほうが画質が大幅に向上しています。