From 4a544b603ff62828dae0dc76d6f29b1d80e74b9c Mon Sep 17 00:00:00 2001 From: ThibaultBee <37510686+ThibaultBee@users.noreply.github.com> Date: Tue, 6 Feb 2024 18:32:34 +0100 Subject: [PATCH] fix(core): when settings zoom ratio, crop sensor region must be full sensor array See https://github.com/ThibaultBee/StreamPack/issues/97 --- .../streampack/utils/CameraSettings.kt | 211 +++++++++++------- .../utils/ContextExtensionsForCamera.kt | 20 +- 2 files changed, 145 insertions(+), 86 deletions(-) diff --git a/core/src/main/java/io/github/thibaultbee/streampack/utils/CameraSettings.kt b/core/src/main/java/io/github/thibaultbee/streampack/utils/CameraSettings.kt index 8e3e6c946..7d8218844 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/utils/CameraSettings.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/utils/CameraSettings.kt @@ -26,6 +26,7 @@ import android.hardware.camera2.params.MeteringRectangle import android.os.Build import android.util.Range import android.util.Rational +import androidx.annotation.RequiresApi import io.github.thibaultbee.streampack.internal.sources.camera.CameraController import io.github.thibaultbee.streampack.internal.utils.* import io.github.thibaultbee.streampack.internal.utils.extensions.clamp @@ -68,7 +69,7 @@ class CameraSettings(context: Context, cameraController: CameraController) { /** * Current camera zoom API. */ - val zoom = Zoom(context, cameraController) + val zoom = Zoom.build(context, cameraController) /** * Current focus API. @@ -339,104 +340,151 @@ class Exposure(private val context: Context, private val cameraController: Camer } } +sealed class Zoom( + protected val context: Context, + protected val cameraController: CameraController +) { + abstract val availableRatioRange: Range + internal abstract val cropSensorRegion: Rect -class Zoom(private val context: Context, private val cameraController: CameraController) { - // Keep the zoomRation for Android version < R - private var persistentZoomRatio = 1f + abstract var zoomRatio: Float /** - * Get current camera zoom ratio range. - * - * @return zoom ratio range. + * Sets the zoom on pinch scale gesture. * - * @see [zoomRatio] + * @param scale the scale factor */ - val availableRatioRange: Range - get() = cameraController.cameraId?.let { context.getZoomRatioRange(it) } - ?: DEFAULT_ZOOM_RATIO_RANGE + fun onPinch(scale: Float) { + val scaledRatio: Float = zoomRatio * speedUpZoomByX(scale, 2) + // Clamp the ratio with the zoom range. + zoomRatio = scaledRatio.clamp(availableRatioRange.lower, availableRatioRange.upper) + } - /** - * Set or get the current zoom ratio. - * - * @see [availableRatioRange] - */ - var zoomRatio: Float - /** - * Get the zoom ratio. - * - * @return the current zoom ratio - */ - get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - cameraController.getSetting(CaptureRequest.CONTROL_ZOOM_RATIO) ?: DEFAULT_ZOOM_RATIO + private fun speedUpZoomByX(scaleFactor: Float, ratio: Int): Float { + return if (scaleFactor > 1f) { + 1.0f + (scaleFactor - 1.0f) * ratio } else { - synchronized(this) { - persistentZoomRatio - } + 1.0f - (1.0f - scaleFactor) * ratio } - /** - * Set the zoom ratio. - * - * @param value zoom ratio - */ - set(value) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - cameraController.setRepeatingSetting( - CaptureRequest.CONTROL_ZOOM_RATIO, - value.clamp(availableRatioRange) + } + + class CropScalerRegionZoom(context: Context, cameraController: CameraController) : + Zoom(context, cameraController) { + // Keep the zoomRatio + private var persistentZoomRatio = 1f + private var currentCropRect: Rect? = null + + override val availableRatioRange: Range + get() = cameraController.cameraId?.let { + Range( + DEFAULT_ZOOM_RATIO, + context.getScalerMaxZoom(it) ) - } else { + } + ?: DEFAULT_ZOOM_RATIO_RANGE + + override var zoomRatio: Float + get() = synchronized(this) { + persistentZoomRatio + } + set(value) { synchronized(this) { val clampedValue = value.clamp(availableRatioRange) cameraController.cameraId?.let { cameraId -> + currentCropRect = getCropRegion( + context.getCameraCharacteristics(cameraId), + clampedValue + ) cameraController.setRepeatingSetting( CaptureRequest.SCALER_CROP_REGION, - getCropRegion( - context.getCameraCharacteristics(cameraId), - clampedValue - ) + currentCropRect ) } persistentZoomRatio = clampedValue } } - } - /** - * Sets the zoom on pinch scale gesture. - * - * @param scale the scale factor - */ - fun onPinch(scale: Float) { - val scaledRatio: Float = zoomRatio * speedUpZoomByX(scale, 2) - // Clamp the ratio with the zoom range. - zoomRatio = scaledRatio.clamp(availableRatioRange.lower, availableRatioRange.upper) - } + override val cropSensorRegion: Rect + get() { + synchronized(this) { + return if (currentCropRect != null) { + currentCropRect!! + } else { + val cameraId = cameraController.cameraId + ?: throw IllegalStateException("Camera ID is null") + return context.getCameraCharacteristics(cameraId) + .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!! + } + } + } - private fun speedUpZoomByX(scaleFactor: Float, ratio: Int): Float { - return if (scaleFactor > 1f) { - 1.0f + (scaleFactor - 1.0f) * ratio - } else { - 1.0f - (1.0f - scaleFactor) * ratio + companion object { + /** + * Calculates sensor crop region for a zoom ratio (zoom >= 1.0). + * + * @return the crop region. + */ + private fun getCropRegion(sensorRect: Rect, zoomRatio: Float): Rect { + val xCenter: Int = sensorRect.width() / 2 + val yCenter: Int = sensorRect.height() / 2 + val xDelta = (0.5f * sensorRect.width() / zoomRatio).toInt() + val yDelta = (0.5f * sensorRect.height() / zoomRatio).toInt() + return Rect(xCenter - xDelta, yCenter - yDelta, xCenter + xDelta, yCenter + yDelta) + } + + /** + * Calculates sensor crop region for a zoom ratio (zoom >= 1.0). + * + * @return the crop region. + */ + private fun getCropRegion( + characteristics: CameraCharacteristics, + zoomRatio: Float + ): Rect { + val sensorRect = + characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE) + ?: throw IllegalStateException("Sensor rect is null") + return getCropRegion(sensorRect, zoomRatio) + } } } + @RequiresApi(Build.VERSION_CODES.R) + class RZoom(context: Context, cameraController: CameraController) : + Zoom(context, cameraController) { + override val availableRatioRange: Range + get() = cameraController.cameraId?.let { context.getZoomRatioRange(it) } + ?: DEFAULT_ZOOM_RATIO_RANGE + + override var zoomRatio: Float + get() = cameraController.getSetting(CaptureRequest.CONTROL_ZOOM_RATIO) + ?: DEFAULT_ZOOM_RATIO + set(value) { + cameraController.setRepeatingSetting( + CaptureRequest.CONTROL_ZOOM_RATIO, + value.clamp(availableRatioRange) + ) + } + + override val cropSensorRegion: Rect + get() { + val cameraId = cameraController.cameraId + ?: throw IllegalStateException("Camera ID is null") + return context.getCameraCharacteristics(cameraId) + .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!! + } + } + companion object { - val DEFAULT_ZOOM_RATIO = 1f + const val DEFAULT_ZOOM_RATIO = 1f val DEFAULT_ZOOM_RATIO_RANGE = Range(DEFAULT_ZOOM_RATIO, DEFAULT_ZOOM_RATIO) - /** - * Calculates sensor crop region for a zoom ratio (zoom >= 1.0). - * - * @return the crop region. - */ - internal fun getCropRegion(characteristics: CameraCharacteristics, zoomRatio: Float): Rect { - val sensorRect = - characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!! - val xCenter: Int = sensorRect.width() / 2 - val yCenter: Int = sensorRect.height() / 2 - val xDelta = (0.5f * sensorRect.width() / zoomRatio).toInt() - val yDelta = (0.5f * sensorRect.height() / zoomRatio).toInt() - return Rect(xCenter - xDelta, yCenter - yDelta, xCenter + xDelta, yCenter + yDelta) + fun build(context: Context, cameraController: CameraController): Zoom { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + RZoom(context, cameraController) + } else { + CropScalerRegionZoom(context, cameraController) + } } } } @@ -534,10 +582,10 @@ class Focus(private val context: Context, private val cameraController: CameraCo } companion object { - val DEFAULT_LENS_DISTANCE = 0f + const val DEFAULT_LENS_DISTANCE = 0f val DEFAULT_LENS_DISTANCE_RANGE = Range(DEFAULT_LENS_DISTANCE, DEFAULT_LENS_DISTANCE) - val DEFAULT_MAX_NUM_OF_METERING_REGION = 0 + const val DEFAULT_MAX_NUM_OF_METERING_REGION = 0 } } @@ -699,7 +747,7 @@ class FocusMetering( return } - val cameraId = cameraController.cameraId ?: throw IllegalStateException("Camera ID is null") + val cropRegion = zoom.cropSensorRegion disableAutoCancel() @@ -712,9 +760,6 @@ class FocusMetering( return } - val cropRegion = - Zoom.getCropRegion(context.getCameraCharacteristics(cameraId), zoom.zoomRatio) - val afRectangles = getMeteringRectangles( afPoints, @@ -981,22 +1026,28 @@ class FocusMetering( adjustedPoint: PointF, cropRegion: Rect ): MeteringRectangle { + Logger.e(">>TEST", "cropRegion: $cropRegion") + Logger.e(">>TEST", "adjustedPoint: $adjustedPoint") + Logger.e(">>TEST", "size: $size") val centerX = (cropRegion.left + adjustedPoint.x * cropRegion.width()).toInt() val centerY = (cropRegion.top + adjustedPoint.y * cropRegion.height()).toInt() val width = (size * cropRegion.width()) val height = (size * cropRegion.height()) + Logger.e(">>TEST", "centerX: $centerX - centerY: $centerY") + Logger.e(">>TEST", "width: $width - height: $height") + val focusRect = Rect( (centerX - width / 2).toInt(), (centerY - height / 2).toInt(), (centerX + width / 2).toInt(), (centerY + height / 2).toInt() ) - + Logger.e(">>TEST", "focusRect1: $focusRect") focusRect.left = focusRect.left.clamp(cropRegion.right, cropRegion.left) focusRect.right = focusRect.right.clamp(cropRegion.right, cropRegion.left) focusRect.top = focusRect.top.clamp(cropRegion.bottom, cropRegion.top) focusRect.bottom = focusRect.bottom.clamp(cropRegion.bottom, cropRegion.top) - + Logger.e(">>TEST", "focusRect2: $focusRect") return MeteringRectangle(focusRect, DEFAULT_METERING_WEIGHT_MAX) } } diff --git a/core/src/main/java/io/github/thibaultbee/streampack/utils/ContextExtensionsForCamera.kt b/core/src/main/java/io/github/thibaultbee/streampack/utils/ContextExtensionsForCamera.kt index c0eebf389..7f0c6649e 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/utils/ContextExtensionsForCamera.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/utils/ContextExtensionsForCamera.kt @@ -25,6 +25,7 @@ import android.hardware.camera2.params.DynamicRangeProfiles.STANDARD import android.os.Build import android.util.Range import android.util.Size +import androidx.annotation.RequiresApi /** * Get camera characteristics. @@ -246,13 +247,20 @@ fun Context.getExposureMaxMeteringRegionsSupported(cameraId: String) = * @param cameraId camera id * @return zoom ratio range. */ +@RequiresApi(Build.VERSION_CODES.R) fun Context.getZoomRatioRange(cameraId: String): Range? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - getCameraCharacteristics(cameraId).get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE) - } else { - getCameraCharacteristics(cameraId).get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM) - ?.let { maxZoom -> Range(1f, maxZoom) } - } + return getCameraCharacteristics(cameraId).get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE) +} + +/** + * Gets max scaler zoom. + * + * @param cameraId camera id + * @return max scaler zoom. + */ +fun Context.getScalerMaxZoom(cameraId: String): Float { + return getCameraCharacteristics(cameraId).get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM) + ?: 1.0f } /**