Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to control exposure compensation on CameraPreview. #30

Merged
merged 2 commits into from
Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<Int>

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
)
}
}
7 changes: 6 additions & 1 deletion camposer/src/main/java/com/ujizin/camposer/CameraPreview.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -93,6 +95,7 @@ public fun CameraPreview(
cameraState = cameraState,
camSelector = camSelector,
captureMode = captureMode,
exposureCompensation = exposureCompensation,
imageCaptureMode = imageCaptureMode,
imageCaptureTargetSize = imageCaptureTargetSize,
flashMode = flashMode,
Expand Down Expand Up @@ -129,6 +132,7 @@ internal fun CameraPreviewImpl(
zoomRatio: Float,
implementationMode: ImplementationMode,
imageAnalyzer: ImageAnalyzer?,
exposureCompensation: Int,
isImageAnalysisEnabled: Boolean,
isFocusOnTapEnabled: Boolean,
isPinchToZoomEnabled: Boolean,
Expand Down Expand Up @@ -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,
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
Expand Down
48 changes: 46 additions & 2 deletions camposer/src/main/java/com/ujizin/camposer/state/CameraState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* */
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -489,6 +529,7 @@ public class CameraState internal constructor(context: Context) {
private fun resetCamera() {
hasFlashUnit = controller.cameraInfo?.hasFlashUnit() ?: false
startZoom()
startExposure()
}

private fun Set<Int>.sumOr(initial: Int = 0): Int = fold(initial) { acc, current ->
Expand All @@ -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
Expand All @@ -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
}
}

Expand Down
21 changes: 4 additions & 17 deletions camposer/src/main/java/com/ujizin/camposer/state/ImageAnalyzer.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.ujizin.camposer.state

import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.compose.runtime.Immutable

/**
Expand All @@ -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
* */
Expand All @@ -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,
Expand All @@ -46,13 +40,6 @@ public class ImageAnalyzer(
this.imageAnalysisImageQueueDepth = imageAnalysisImageQueueDepth
}

@Immutable
private inner class Analyzer : ImageAnalysis.Analyzer {
override fun analyze(image: ImageProxy) {
[email protected](image)
}
}

/**
* Update actual image analysis instance.
* */
Expand All @@ -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
}
}