From ff9354e360e8f4b73fc53c1971e585c91972ef39 Mon Sep 17 00:00:00 2001 From: ujizin Date: Thu, 27 Apr 2023 11:21:23 -0300 Subject: [PATCH 1/2] refactor: adjust callback parameter for image analysis to be able for adding custom features as MlKit. --- .../ujizin/camposer/state/CameraAsState.kt | 4 ++-- .../ujizin/camposer/state/ImageAnalyzer.kt | 21 ++++--------------- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/camposer/src/main/java/com/ujizin/camposer/state/CameraAsState.kt b/camposer/src/main/java/com/ujizin/camposer/state/CameraAsState.kt index 8beb6b9..24d9857 100644 --- a/camposer/src/main/java/com/ujizin/camposer/state/CameraAsState.kt +++ b/camposer/src/main/java/com/ujizin/camposer/state/CameraAsState.kt @@ -1,6 +1,6 @@ package com.ujizin.camposer.state -import androidx.camera.core.ImageProxy +import androidx.camera.core.ImageAnalysis import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf @@ -66,7 +66,7 @@ public fun CameraState.rememberImageAnalyzer( imageAnalysisBackpressureStrategy: ImageAnalysisBackpressureStrategy = ImageAnalysisBackpressureStrategy.KeepOnlyLatest, imageAnalysisTargetSize: ImageTargetSize? = ImageTargetSize(this.imageAnalysisTargetSize), imageAnalysisImageQueueDepth: Int = this.imageAnalysisImageQueueDepth, - analyze: (ImageProxy) -> Unit, + analyze: ImageAnalysis.Analyzer, ): ImageAnalyzer = remember(this) { ImageAnalyzer( this, diff --git a/camposer/src/main/java/com/ujizin/camposer/state/ImageAnalyzer.kt b/camposer/src/main/java/com/ujizin/camposer/state/ImageAnalyzer.kt index 3c392f3..1a5da80 100644 --- a/camposer/src/main/java/com/ujizin/camposer/state/ImageAnalyzer.kt +++ b/camposer/src/main/java/com/ujizin/camposer/state/ImageAnalyzer.kt @@ -1,7 +1,6 @@ package com.ujizin.camposer.state import androidx.camera.core.ImageAnalysis -import androidx.camera.core.ImageProxy import androidx.compose.runtime.Immutable /** @@ -10,7 +9,7 @@ import androidx.compose.runtime.Immutable * @param imageAnalysisBackpressureStrategy the backpressure strategy applied to the image producer * @param imageAnalysisTargetSize the intended output size for ImageAnalysis * @param imageAnalysisImageQueueDepth the image queue depth of ImageAnalysis. - * @param analyzerCallback receive images and perform custom processing. + * @param analyzer receive images and perform custom processing. * * @see rememberImageAnalyzer * */ @@ -20,14 +19,9 @@ public class ImageAnalyzer( imageAnalysisBackpressureStrategy: ImageAnalysisBackpressureStrategy, imageAnalysisTargetSize: ImageTargetSize?, imageAnalysisImageQueueDepth: Int, - private var analyzerCallback: (ImageProxy) -> Unit, + internal var analyzer: ImageAnalysis.Analyzer, ) { - /** - * Hold Image analysis Analyzer to camera. - * */ - internal val analyzer: ImageAnalysis.Analyzer = Analyzer() - init { updateCameraState( imageAnalysisBackpressureStrategy, @@ -46,13 +40,6 @@ public class ImageAnalyzer( this.imageAnalysisImageQueueDepth = imageAnalysisImageQueueDepth } - @Immutable - private inner class Analyzer : ImageAnalysis.Analyzer { - override fun analyze(image: ImageProxy) { - this@ImageAnalyzer.analyzerCallback(image) - } - } - /** * Update actual image analysis instance. * */ @@ -62,13 +49,13 @@ public class ImageAnalyzer( ), imageAnalysisTargetSize: ImageTargetSize? = ImageTargetSize(cameraState.imageAnalysisTargetSize), imageAnalysisImageQueueDepth: Int = cameraState.imageAnalysisImageQueueDepth, - analyzerCallback: (ImageProxy) -> Unit = this.analyzerCallback, + analyzer: ImageAnalysis.Analyzer = this.analyzer, ) { updateCameraState( imageAnalysisBackpressureStrategy, imageAnalysisTargetSize, imageAnalysisImageQueueDepth ) - this.analyzerCallback = analyzerCallback + this.analyzer = analyzer } } From eafa66f53309d895bc5f91f3fed74c88706d017b Mon Sep 17 00:00:00 2001 From: ujizin Date: Tue, 15 Aug 2023 19:11:58 -0300 Subject: [PATCH 2/2] feat: add exposure compensation for camera preview --- .../camposer/ExposureCompensationTest.kt | 70 +++++++++++++++++++ .../java/com/ujizin/camposer/CameraPreview.kt | 7 +- .../com/ujizin/camposer/state/CameraState.kt | 48 ++++++++++++- 3 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 camposer/src/androidTest/java/com/ujizin/camposer/ExposureCompensationTest.kt diff --git a/camposer/src/androidTest/java/com/ujizin/camposer/ExposureCompensationTest.kt b/camposer/src/androidTest/java/com/ujizin/camposer/ExposureCompensationTest.kt new file mode 100644 index 0000000..1d185d6 --- /dev/null +++ b/camposer/src/androidTest/java/com/ujizin/camposer/ExposureCompensationTest.kt @@ -0,0 +1,70 @@ +package com.ujizin.camposer + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.test.junit4.ComposeContentTestRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import junit.framework.TestCase.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@LargeTest +internal class ExposureCompensationTest: CameraTest() { + + private lateinit var exposureCompensation: MutableState + + private val currentExposure: Int? + get() = cameraState.controller.cameraInfo?.exposureState?.exposureCompensationIndex + + @Test + fun test_minExposureCompensation() = with(composeTestRule) { + initCameraWithExposure(0) + + exposureCompensation.value = cameraState.minExposure + + runOnIdle { + assertEquals(cameraState.minExposure, currentExposure) + assertEquals(exposureCompensation.value, currentExposure) + } + } + + @Test + fun test_maxExposureCompensation() = with(composeTestRule) { + initCameraWithExposure(0) + + exposureCompensation.value = cameraState.maxExposure + + runOnIdle { + assertEquals(cameraState.maxExposure, currentExposure) + assertEquals(exposureCompensation.value, currentExposure) + } + } + + + @Test + fun test_invalidExposureCompensation() = with(composeTestRule) { + initCameraWithExposure(0) + + exposureCompensation.value = Int.MAX_VALUE + + runOnIdle { + assertNotEquals(cameraState.maxExposure, currentExposure) + assertNotEquals(exposureCompensation.value, currentExposure) + assertEquals(cameraState.initialExposure, currentExposure) + } + } + + private fun ComposeContentTestRule.initCameraWithExposure( + exposure: Int, + ) = initCameraState { state -> + exposureCompensation = remember { mutableStateOf(exposure) } + CameraPreview( + cameraState = state, + exposureCompensation = exposureCompensation.value + ) + } +} \ No newline at end of file diff --git a/camposer/src/main/java/com/ujizin/camposer/CameraPreview.kt b/camposer/src/main/java/com/ujizin/camposer/CameraPreview.kt index 81715eb..8681379 100644 --- a/camposer/src/main/java/com/ujizin/camposer/CameraPreview.kt +++ b/camposer/src/main/java/com/ujizin/camposer/CameraPreview.kt @@ -44,6 +44,7 @@ import androidx.camera.core.CameraSelector as CameraXSelector * @param flashMode flash mode to be added, default is off * @param scaleType scale type to be added, default is fill center * @param enableTorch enable torch from camera, default is false. + * @param exposureCompensation camera exposure compensation to be added * @param zoomRatio zoom ratio to be added, default is 1.0 * @param imageAnalyzer image analyzer from camera, see [ImageAnalyzer] * @param implementationMode implementation mode to be added, default is performance @@ -71,6 +72,7 @@ public fun CameraPreview( flashMode: FlashMode = cameraState.flashMode, scaleType: ScaleType = cameraState.scaleType, enableTorch: Boolean = cameraState.enableTorch, + exposureCompensation: Int = cameraState.initialExposure, zoomRatio: Float = 1F, imageAnalyzer: ImageAnalyzer? = null, implementationMode: ImplementationMode = cameraState.implementationMode, @@ -93,6 +95,7 @@ public fun CameraPreview( cameraState = cameraState, camSelector = camSelector, captureMode = captureMode, + exposureCompensation = exposureCompensation, imageCaptureMode = imageCaptureMode, imageCaptureTargetSize = imageCaptureTargetSize, flashMode = flashMode, @@ -129,6 +132,7 @@ internal fun CameraPreviewImpl( zoomRatio: Float, implementationMode: ImplementationMode, imageAnalyzer: ImageAnalyzer?, + exposureCompensation: Int, isImageAnalysisEnabled: Boolean, isFocusOnTapEnabled: Boolean, isPinchToZoomEnabled: Boolean, @@ -198,7 +202,8 @@ internal fun CameraPreviewImpl( enableTorch = enableTorch, zoomRatio = zoomRatio, imageCaptureMode = imageCaptureMode, - meteringPoint = meteringPointFactory.createPoint(x, y) + meteringPoint = meteringPointFactory.createPoint(x, y), + exposureCompensation = exposureCompensation, ) } diff --git a/camposer/src/main/java/com/ujizin/camposer/state/CameraState.kt b/camposer/src/main/java/com/ujizin/camposer/state/CameraState.kt index 2cc67c8..a105814 100644 --- a/camposer/src/main/java/com/ujizin/camposer/state/CameraState.kt +++ b/camposer/src/main/java/com/ujizin/camposer/state/CameraState.kt @@ -75,6 +75,37 @@ public class CameraState internal constructor(context: Context) { ) internal set + + /** + * Get range compensation range from camera. + * */ + private val exposureCompensationRange + get() = controller.cameraInfo?.exposureState?.exposureCompensationRange + + /** + * Get min exposure from camera. + * */ + public var minExposure: Int by mutableStateOf( + exposureCompensationRange?.lower ?: INITIAL_EXPOSURE_VALUE + ) + internal set + + /** + * Get max exposure from camera. + * */ + public var maxExposure: Int by mutableStateOf( + exposureCompensationRange?.upper ?: INITIAL_EXPOSURE_VALUE + ) + internal set + + public val initialExposure: Int = INITIAL_EXPOSURE_VALUE + get() = controller.cameraInfo?.exposureState?.exposureCompensationIndex ?: field + + /** + * Check if compensation exposure is supported. + * */ + public val isExposureSupported: Boolean by derivedStateOf { maxExposure != INITIAL_EXPOSURE_VALUE } + /** * Check if camera is streaming or not. * */ @@ -282,11 +313,16 @@ public class CameraState internal constructor(context: Context) { init { controller.initializationFuture.addListener({ - startZoom() + resetCamera() isInitialized = true }, mainExecutor) } + private fun startExposure() { + minExposure = exposureCompensationRange?.lower ?: INITIAL_EXPOSURE_VALUE + maxExposure = exposureCompensationRange?.upper ?: INITIAL_EXPOSURE_VALUE + } + /** * Take a picture with the camera. * @@ -352,6 +388,10 @@ public class CameraState internal constructor(context: Context) { controller.setZoomRatio(zoomRatio.coerceIn(minZoom, maxZoom)) } + private fun setExposureCompensation(exposureCompensation: Int) { + controller.cameraControl?.setExposureCompensationIndex(exposureCompensation) + } + /** * Start recording camera. * @@ -489,6 +529,7 @@ public class CameraState internal constructor(context: Context) { private fun resetCamera() { hasFlashUnit = controller.cameraInfo?.hasFlashUnit() ?: false startZoom() + startExposure() } private fun Set.sumOr(initial: Int = 0): Int = fold(initial) { acc, current -> @@ -511,7 +552,8 @@ public class CameraState internal constructor(context: Context) { zoomRatio: Float, imageCaptureMode: ImageCaptureMode, enableTorch: Boolean, - meteringPoint: MeteringPoint + meteringPoint: MeteringPoint, + exposureCompensation: Int ) { this.camSelector = camSelector this.captureMode = captureMode @@ -525,12 +567,14 @@ public class CameraState internal constructor(context: Context) { this.enableTorch = enableTorch this.isFocusOnTapSupported = meteringPoint.isFocusMeteringSupported this.imageCaptureMode = imageCaptureMode + setExposureCompensation(exposureCompensation) setZoomRatio(zoomRatio) } private companion object { private val TAG = this::class.java.name private const val INITIAL_ZOOM_VALUE = 1F + private const val INITIAL_EXPOSURE_VALUE = 0 } }