From a7701c8c9c2b5eaefb75190ca09ed28f143994f8 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 14 Feb 2024 17:04:25 +0100 Subject: [PATCH] perf: Allow skipping pre-capture sequence if already focused (#2561) This PR speeds up photo capture on Android by skipping pre-capture sequences on modes that are already focused (either AF, AE or AWB) --- .../core/PersistentCameraCaptureSession.kt | 11 ++++- .../CameraCaptureSession+precapture.kt | 47 +++++++++++++++---- ...setRepeatingRequestAndWaitForPrecapture.kt | 23 ++++++--- 3 files changed, 64 insertions(+), 17 deletions(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt index f953b23..df580b5 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt @@ -172,7 +172,14 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p // 1. Run precapture sequence val precaptureRequest = repeatingRequest.createCaptureRequest(device, deviceDetails, repeatingOutputs) - val options = PrecaptureOptions(listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE, PrecaptureTrigger.AWB), flash, emptyList()) + val skipIfPassivelyFocused = flash == Flash.OFF + val options = + PrecaptureOptions( + listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE, PrecaptureTrigger.AWB), + flash, + emptyList(), + skipIfPassivelyFocused + ) val result = session.precapture(precaptureRequest, deviceDetails, options) try { @@ -208,7 +215,7 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p // 1. Run a precapture sequence for AF, AE and AWB. val request = repeatingRequest.createCaptureRequest(device, deviceDetails, outputs) - val options = PrecaptureOptions(listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE), Flash.OFF, listOf(point)) + val options = PrecaptureOptions(listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE), Flash.OFF, listOf(point), false) session.precapture(request, deviceDetails, options) // 2. Wait 3 seconds diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+precapture.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+precapture.kt index 0372e05..89af52c 100644 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+precapture.kt +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+precapture.kt @@ -11,7 +11,12 @@ import com.mrousavy.camera.core.CameraDeviceDetails import com.mrousavy.camera.types.Flash import com.mrousavy.camera.types.HardwareLevel -data class PrecaptureOptions(val modes: List, val flash: Flash = Flash.OFF, val pointsOfInterest: List) +data class PrecaptureOptions( + val modes: List, + val flash: Flash = Flash.OFF, + val pointsOfInterest: List, + val skipIfPassivelyFocused: Boolean +) data class PrecaptureResult(val needsFlash: Boolean) @@ -33,15 +38,23 @@ suspend fun CameraCaptureSession.precapture( request.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO) var enableFlash = options.flash == Flash.ON + var afState = FocusState.Inactive + var aeState = ExposureState.Inactive + var awbState = WhiteBalanceState.Inactive + val precaptureModes = options.modes.toMutableList() // 1. Cancel any ongoing precapture sequences request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL) request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL) - if (options.flash == Flash.AUTO) { - // we want Auto-Flash, so check the current lighting conditions if we need it. + if (options.flash == Flash.AUTO || options.skipIfPassivelyFocused) { + // We want to read the current AE/AF/AWB values to determine if we need flash or can skip AF/AE/AWB precapture val result = this.capture(request.build(), false) - val aeState = result.get(CaptureResult.CONTROL_AE_STATE) - if (aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { + + afState = FocusState.fromAFState(result.get(CaptureResult.CONTROL_AF_STATE) ?: CaptureResult.CONTROL_AF_STATE_INACTIVE) + aeState = ExposureState.fromAEState(result.get(CaptureResult.CONTROL_AE_STATE) ?: CaptureResult.CONTROL_AE_STATE_INACTIVE) + awbState = WhiteBalanceState.fromAWBState(result.get(CaptureResult.CONTROL_AWB_STATE) ?: CaptureResult.CONTROL_AWB_STATE_INACTIVE) + + if (aeState == ExposureState.FlashRequired) { Log.i(TAG, "Auto-Flash: Flash is required for photo capture, enabling flash...") enableFlash = true } else { @@ -57,11 +70,27 @@ suspend fun CameraCaptureSession.precapture( MeteringRectangle(point, DEFAULT_METERING_SIZE, meteringWeight) }.toTypedArray() + if (options.skipIfPassivelyFocused) { + // If user allows us to skip precapture for values that are already focused, remove them from the precapture modes. + if (afState.isPassivelyFocused) { + Log.i(TAG, "AF is already focused, skipping...") + precaptureModes.remove(PrecaptureTrigger.AF) + } + if (aeState.isPassivelyFocused) { + Log.i(TAG, "AE is already focused, skipping...") + precaptureModes.remove(PrecaptureTrigger.AE) + } + if (awbState.isPassivelyFocused) { + Log.i(TAG, "AWB is already focused, skipping...") + precaptureModes.remove(PrecaptureTrigger.AWB) + } + } + // 2. Submit a precapture start sequence if (enableFlash && deviceDetails.hasFlash) { request.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH) } - if (options.modes.contains(PrecaptureTrigger.AF)) { + if (precaptureModes.contains(PrecaptureTrigger.AF)) { // AF Precapture if (deviceDetails.afModes.contains(CaptureRequest.CONTROL_AF_MODE_AUTO)) { request.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO) @@ -71,14 +100,14 @@ suspend fun CameraCaptureSession.precapture( } request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START) } - if (options.modes.contains(PrecaptureTrigger.AE) && deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.LIMITED)) { + if (precaptureModes.contains(PrecaptureTrigger.AE) && deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.LIMITED)) { // AE Precapture if (meteringRectangles.isNotEmpty() && deviceDetails.supportsExposureRegions) { request.set(CaptureRequest.CONTROL_AE_REGIONS, meteringRectangles) } request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START) } - if (options.modes.contains(PrecaptureTrigger.AWB)) { + if (precaptureModes.contains(PrecaptureTrigger.AWB)) { // AWB Precapture if (meteringRectangles.isNotEmpty() && deviceDetails.supportsWhiteBalanceRegions) { request.set(CaptureRequest.CONTROL_AWB_REGIONS, meteringRectangles) @@ -89,7 +118,7 @@ suspend fun CameraCaptureSession.precapture( // 3. Start a repeating request without the trigger and wait until AF/AE/AWB locks request.set(CaptureRequest.CONTROL_AF_TRIGGER, null) request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, null) - val result = this.setRepeatingRequestAndWaitForPrecapture(request.build(), *options.modes.toTypedArray()) + val result = this.setRepeatingRequestAndWaitForPrecapture(request.build(), *precaptureModes.toTypedArray()) Log.i(TAG, "AF/AE/AWB successfully locked!") // TODO: Set to idle again? diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForPrecapture.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForPrecapture.kt index a655657..70b3fb7 100644 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForPrecapture.kt +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForPrecapture.kt @@ -24,7 +24,12 @@ enum class PrecaptureTrigger { AWB } -enum class FocusState { +interface AutoState { + val isCompleted: Boolean + val isPassivelyFocused: Boolean +} + +enum class FocusState : AutoState { Inactive, Scanning, Focused, @@ -33,8 +38,10 @@ enum class FocusState { PassiveFocused, PassiveUnfocused; - val isCompleted: Boolean + override val isCompleted: Boolean get() = this == Focused || this == Unfocused + override val isPassivelyFocused: Boolean + get() = this == PassiveFocused companion object { fun fromAFState(afState: Int): FocusState = @@ -50,7 +57,7 @@ enum class FocusState { } } } -enum class ExposureState { +enum class ExposureState : AutoState { Locked, Inactive, Precapture, @@ -58,8 +65,10 @@ enum class ExposureState { Converged, FlashRequired; - val isCompleted: Boolean + override val isCompleted: Boolean get() = this == Converged || this == FlashRequired + override val isPassivelyFocused: Boolean + get() = this == Converged companion object { fun fromAEState(aeState: Int): ExposureState = @@ -75,13 +84,15 @@ enum class ExposureState { } } -enum class WhiteBalanceState { +enum class WhiteBalanceState : AutoState { Inactive, Locked, Searching, Converged; - val isCompleted: Boolean + override val isCompleted: Boolean + get() = this == Converged + override val isPassivelyFocused: Boolean get() = this == Converged companion object {