Skip to content

Commit 4a544b6

Browse files
committed
fix(core): when settings zoom ratio, crop sensor region must be full sensor array
See #97
1 parent 0d62e55 commit 4a544b6

File tree

2 files changed

+145
-86
lines changed

2 files changed

+145
-86
lines changed

core/src/main/java/io/github/thibaultbee/streampack/utils/CameraSettings.kt

Lines changed: 131 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import android.hardware.camera2.params.MeteringRectangle
2626
import android.os.Build
2727
import android.util.Range
2828
import android.util.Rational
29+
import androidx.annotation.RequiresApi
2930
import io.github.thibaultbee.streampack.internal.sources.camera.CameraController
3031
import io.github.thibaultbee.streampack.internal.utils.*
3132
import io.github.thibaultbee.streampack.internal.utils.extensions.clamp
@@ -68,7 +69,7 @@ class CameraSettings(context: Context, cameraController: CameraController) {
6869
/**
6970
* Current camera zoom API.
7071
*/
71-
val zoom = Zoom(context, cameraController)
72+
val zoom = Zoom.build(context, cameraController)
7273

7374
/**
7475
* Current focus API.
@@ -339,104 +340,151 @@ class Exposure(private val context: Context, private val cameraController: Camer
339340
}
340341
}
341342

343+
sealed class Zoom(
344+
protected val context: Context,
345+
protected val cameraController: CameraController
346+
) {
347+
abstract val availableRatioRange: Range<Float>
348+
internal abstract val cropSensorRegion: Rect
342349

343-
class Zoom(private val context: Context, private val cameraController: CameraController) {
344-
// Keep the zoomRation for Android version < R
345-
private var persistentZoomRatio = 1f
350+
abstract var zoomRatio: Float
346351

347352
/**
348-
* Get current camera zoom ratio range.
349-
*
350-
* @return zoom ratio range.
353+
* Sets the zoom on pinch scale gesture.
351354
*
352-
* @see [zoomRatio]
355+
* @param scale the scale factor
353356
*/
354-
val availableRatioRange: Range<Float>
355-
get() = cameraController.cameraId?.let { context.getZoomRatioRange(it) }
356-
?: DEFAULT_ZOOM_RATIO_RANGE
357+
fun onPinch(scale: Float) {
358+
val scaledRatio: Float = zoomRatio * speedUpZoomByX(scale, 2)
359+
// Clamp the ratio with the zoom range.
360+
zoomRatio = scaledRatio.clamp(availableRatioRange.lower, availableRatioRange.upper)
361+
}
357362

358-
/**
359-
* Set or get the current zoom ratio.
360-
*
361-
* @see [availableRatioRange]
362-
*/
363-
var zoomRatio: Float
364-
/**
365-
* Get the zoom ratio.
366-
*
367-
* @return the current zoom ratio
368-
*/
369-
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
370-
cameraController.getSetting(CaptureRequest.CONTROL_ZOOM_RATIO) ?: DEFAULT_ZOOM_RATIO
363+
private fun speedUpZoomByX(scaleFactor: Float, ratio: Int): Float {
364+
return if (scaleFactor > 1f) {
365+
1.0f + (scaleFactor - 1.0f) * ratio
371366
} else {
372-
synchronized(this) {
373-
persistentZoomRatio
374-
}
367+
1.0f - (1.0f - scaleFactor) * ratio
375368
}
376-
/**
377-
* Set the zoom ratio.
378-
*
379-
* @param value zoom ratio
380-
*/
381-
set(value) {
382-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
383-
cameraController.setRepeatingSetting(
384-
CaptureRequest.CONTROL_ZOOM_RATIO,
385-
value.clamp(availableRatioRange)
369+
}
370+
371+
class CropScalerRegionZoom(context: Context, cameraController: CameraController) :
372+
Zoom(context, cameraController) {
373+
// Keep the zoomRatio
374+
private var persistentZoomRatio = 1f
375+
private var currentCropRect: Rect? = null
376+
377+
override val availableRatioRange: Range<Float>
378+
get() = cameraController.cameraId?.let {
379+
Range(
380+
DEFAULT_ZOOM_RATIO,
381+
context.getScalerMaxZoom(it)
386382
)
387-
} else {
383+
}
384+
?: DEFAULT_ZOOM_RATIO_RANGE
385+
386+
override var zoomRatio: Float
387+
get() = synchronized(this) {
388+
persistentZoomRatio
389+
}
390+
set(value) {
388391
synchronized(this) {
389392
val clampedValue = value.clamp(availableRatioRange)
390393
cameraController.cameraId?.let { cameraId ->
394+
currentCropRect = getCropRegion(
395+
context.getCameraCharacteristics(cameraId),
396+
clampedValue
397+
)
391398
cameraController.setRepeatingSetting(
392399
CaptureRequest.SCALER_CROP_REGION,
393-
getCropRegion(
394-
context.getCameraCharacteristics(cameraId),
395-
clampedValue
396-
)
400+
currentCropRect
397401
)
398402
}
399403
persistentZoomRatio = clampedValue
400404
}
401405
}
402-
}
403406

404-
/**
405-
* Sets the zoom on pinch scale gesture.
406-
*
407-
* @param scale the scale factor
408-
*/
409-
fun onPinch(scale: Float) {
410-
val scaledRatio: Float = zoomRatio * speedUpZoomByX(scale, 2)
411-
// Clamp the ratio with the zoom range.
412-
zoomRatio = scaledRatio.clamp(availableRatioRange.lower, availableRatioRange.upper)
413-
}
407+
override val cropSensorRegion: Rect
408+
get() {
409+
synchronized(this) {
410+
return if (currentCropRect != null) {
411+
currentCropRect!!
412+
} else {
413+
val cameraId = cameraController.cameraId
414+
?: throw IllegalStateException("Camera ID is null")
415+
return context.getCameraCharacteristics(cameraId)
416+
.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!!
417+
}
418+
}
419+
}
414420

415-
private fun speedUpZoomByX(scaleFactor: Float, ratio: Int): Float {
416-
return if (scaleFactor > 1f) {
417-
1.0f + (scaleFactor - 1.0f) * ratio
418-
} else {
419-
1.0f - (1.0f - scaleFactor) * ratio
421+
companion object {
422+
/**
423+
* Calculates sensor crop region for a zoom ratio (zoom >= 1.0).
424+
*
425+
* @return the crop region.
426+
*/
427+
private fun getCropRegion(sensorRect: Rect, zoomRatio: Float): Rect {
428+
val xCenter: Int = sensorRect.width() / 2
429+
val yCenter: Int = sensorRect.height() / 2
430+
val xDelta = (0.5f * sensorRect.width() / zoomRatio).toInt()
431+
val yDelta = (0.5f * sensorRect.height() / zoomRatio).toInt()
432+
return Rect(xCenter - xDelta, yCenter - yDelta, xCenter + xDelta, yCenter + yDelta)
433+
}
434+
435+
/**
436+
* Calculates sensor crop region for a zoom ratio (zoom >= 1.0).
437+
*
438+
* @return the crop region.
439+
*/
440+
private fun getCropRegion(
441+
characteristics: CameraCharacteristics,
442+
zoomRatio: Float
443+
): Rect {
444+
val sensorRect =
445+
characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)
446+
?: throw IllegalStateException("Sensor rect is null")
447+
return getCropRegion(sensorRect, zoomRatio)
448+
}
420449
}
421450
}
422451

452+
@RequiresApi(Build.VERSION_CODES.R)
453+
class RZoom(context: Context, cameraController: CameraController) :
454+
Zoom(context, cameraController) {
455+
override val availableRatioRange: Range<Float>
456+
get() = cameraController.cameraId?.let { context.getZoomRatioRange(it) }
457+
?: DEFAULT_ZOOM_RATIO_RANGE
458+
459+
override var zoomRatio: Float
460+
get() = cameraController.getSetting(CaptureRequest.CONTROL_ZOOM_RATIO)
461+
?: DEFAULT_ZOOM_RATIO
462+
set(value) {
463+
cameraController.setRepeatingSetting(
464+
CaptureRequest.CONTROL_ZOOM_RATIO,
465+
value.clamp(availableRatioRange)
466+
)
467+
}
468+
469+
override val cropSensorRegion: Rect
470+
get() {
471+
val cameraId = cameraController.cameraId
472+
?: throw IllegalStateException("Camera ID is null")
473+
return context.getCameraCharacteristics(cameraId)
474+
.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!!
475+
}
476+
}
477+
423478
companion object {
424-
val DEFAULT_ZOOM_RATIO = 1f
479+
const val DEFAULT_ZOOM_RATIO = 1f
425480
val DEFAULT_ZOOM_RATIO_RANGE = Range(DEFAULT_ZOOM_RATIO, DEFAULT_ZOOM_RATIO)
426481

427-
/**
428-
* Calculates sensor crop region for a zoom ratio (zoom >= 1.0).
429-
*
430-
* @return the crop region.
431-
*/
432-
internal fun getCropRegion(characteristics: CameraCharacteristics, zoomRatio: Float): Rect {
433-
val sensorRect =
434-
characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!!
435-
val xCenter: Int = sensorRect.width() / 2
436-
val yCenter: Int = sensorRect.height() / 2
437-
val xDelta = (0.5f * sensorRect.width() / zoomRatio).toInt()
438-
val yDelta = (0.5f * sensorRect.height() / zoomRatio).toInt()
439-
return Rect(xCenter - xDelta, yCenter - yDelta, xCenter + xDelta, yCenter + yDelta)
482+
fun build(context: Context, cameraController: CameraController): Zoom {
483+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
484+
RZoom(context, cameraController)
485+
} else {
486+
CropScalerRegionZoom(context, cameraController)
487+
}
440488
}
441489
}
442490
}
@@ -534,10 +582,10 @@ class Focus(private val context: Context, private val cameraController: CameraCo
534582
}
535583

536584
companion object {
537-
val DEFAULT_LENS_DISTANCE = 0f
585+
const val DEFAULT_LENS_DISTANCE = 0f
538586
val DEFAULT_LENS_DISTANCE_RANGE = Range(DEFAULT_LENS_DISTANCE, DEFAULT_LENS_DISTANCE)
539587

540-
val DEFAULT_MAX_NUM_OF_METERING_REGION = 0
588+
const val DEFAULT_MAX_NUM_OF_METERING_REGION = 0
541589
}
542590
}
543591

@@ -699,7 +747,7 @@ class FocusMetering(
699747
return
700748
}
701749

702-
val cameraId = cameraController.cameraId ?: throw IllegalStateException("Camera ID is null")
750+
val cropRegion = zoom.cropSensorRegion
703751

704752
disableAutoCancel()
705753

@@ -712,9 +760,6 @@ class FocusMetering(
712760
return
713761
}
714762

715-
val cropRegion =
716-
Zoom.getCropRegion(context.getCameraCharacteristics(cameraId), zoom.zoomRatio)
717-
718763
val afRectangles =
719764
getMeteringRectangles(
720765
afPoints,
@@ -981,22 +1026,28 @@ class FocusMetering(
9811026
adjustedPoint: PointF,
9821027
cropRegion: Rect
9831028
): MeteringRectangle {
1029+
Logger.e(">>TEST", "cropRegion: $cropRegion")
1030+
Logger.e(">>TEST", "adjustedPoint: $adjustedPoint")
1031+
Logger.e(">>TEST", "size: $size")
9841032
val centerX = (cropRegion.left + adjustedPoint.x * cropRegion.width()).toInt()
9851033
val centerY = (cropRegion.top + adjustedPoint.y * cropRegion.height()).toInt()
9861034
val width = (size * cropRegion.width())
9871035
val height = (size * cropRegion.height())
9881036

1037+
Logger.e(">>TEST", "centerX: $centerX - centerY: $centerY")
1038+
Logger.e(">>TEST", "width: $width - height: $height")
1039+
9891040
val focusRect = Rect(
9901041
(centerX - width / 2).toInt(), (centerY - height / 2).toInt(),
9911042
(centerX + width / 2).toInt(),
9921043
(centerY + height / 2).toInt()
9931044
)
994-
1045+
Logger.e(">>TEST", "focusRect1: $focusRect")
9951046
focusRect.left = focusRect.left.clamp(cropRegion.right, cropRegion.left)
9961047
focusRect.right = focusRect.right.clamp(cropRegion.right, cropRegion.left)
9971048
focusRect.top = focusRect.top.clamp(cropRegion.bottom, cropRegion.top)
9981049
focusRect.bottom = focusRect.bottom.clamp(cropRegion.bottom, cropRegion.top)
999-
1050+
Logger.e(">>TEST", "focusRect2: $focusRect")
10001051
return MeteringRectangle(focusRect, DEFAULT_METERING_WEIGHT_MAX)
10011052
}
10021053
}

core/src/main/java/io/github/thibaultbee/streampack/utils/ContextExtensionsForCamera.kt

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import android.hardware.camera2.params.DynamicRangeProfiles.STANDARD
2525
import android.os.Build
2626
import android.util.Range
2727
import android.util.Size
28+
import androidx.annotation.RequiresApi
2829

2930
/**
3031
* Get camera characteristics.
@@ -246,13 +247,20 @@ fun Context.getExposureMaxMeteringRegionsSupported(cameraId: String) =
246247
* @param cameraId camera id
247248
* @return zoom ratio range.
248249
*/
250+
@RequiresApi(Build.VERSION_CODES.R)
249251
fun Context.getZoomRatioRange(cameraId: String): Range<Float>? {
250-
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
251-
getCameraCharacteristics(cameraId).get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE)
252-
} else {
253-
getCameraCharacteristics(cameraId).get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)
254-
?.let { maxZoom -> Range(1f, maxZoom) }
255-
}
252+
return getCameraCharacteristics(cameraId).get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE)
253+
}
254+
255+
/**
256+
* Gets max scaler zoom.
257+
*
258+
* @param cameraId camera id
259+
* @return max scaler zoom.
260+
*/
261+
fun Context.getScalerMaxZoom(cameraId: String): Float {
262+
return getCameraCharacteristics(cameraId).get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)
263+
?: 1.0f
256264
}
257265

258266
/**

0 commit comments

Comments
 (0)