feat: Implement exposure (#2173)

* feat: Implement `exposure` (iOS)

* Update Podfile.lock

* Format

* Expose exposure in format

* Set exposure in Camera2

* fix: Fix exposure calculation

* Add exposure docs
This commit is contained in:
Marc Rousavy
2023-11-19 15:26:43 +01:00
committed by GitHub
parent a7e706150e
commit ef58d13b87
19 changed files with 156 additions and 8 deletions

View File

@@ -78,6 +78,7 @@ class CameraView(context: Context) :
var isActive = false
var torch: Torch = Torch.OFF
var zoom: Float = 1f // in "factor"
var exposure: Float = 1f // TODO: Implement exposure bias
var orientation: Orientation = Orientation.PORTRAIT
var enableZoomGesture: Boolean = false
set(value) {
@@ -198,6 +199,7 @@ class CameraView(context: Context) :
config.fps = fps
config.enableLowLightBoost = lowLightBoost ?: false
config.torch = torch
config.exposure = exposure
// Zoom
config.zoom = zoom

View File

@@ -140,8 +140,12 @@ class CameraViewManager : ViewGroupManager<CameraView>() {
@ReactProp(name = "zoom")
fun setZoom(view: CameraView, zoom: Double) {
val zoomFloat = zoom.toFloat()
view.zoom = zoomFloat
view.zoom = zoom.toFloat()
}
@ReactProp(name = "exposure")
fun setExposure(view: CameraView, exposure: Double) {
view.exposure = exposure.toFloat()
}
@ReactProp(name = "orientation")

View File

@@ -33,6 +33,7 @@ data class CameraConfiguration(
var enableLowLightBoost: Boolean = false,
var torch: Torch = Torch.OFF,
var videoStabilizationMode: VideoStabilizationMode = VideoStabilizationMode.OFF,
var exposure: Float? = null,
// Zoom
var zoom: Float = 1f,
@@ -94,7 +95,8 @@ data class CameraConfiguration(
val sidePropsChanged = outputsChanged || // depend on outputs
left?.torch != right.torch || left.enableLowLightBoost != right.enableLowLightBoost || left.fps != right.fps ||
left.zoom != right.zoom || left.videoStabilizationMode != right.videoStabilizationMode || left.isActive != right.isActive
left.zoom != right.zoom || left.videoStabilizationMode != right.videoStabilizationMode || left.isActive != right.isActive ||
left.exposure != right.exposure
return Difference(
deviceChanged,

View File

@@ -6,6 +6,7 @@ import android.hardware.camera2.CameraManager
import android.hardware.camera2.CameraMetadata
import android.os.Build
import android.util.Range
import android.util.Rational
import android.util.Size
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReadableArray
@@ -68,6 +69,8 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, private val
private val cameraConfig = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
private val isoRange = characteristics.get(CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE) ?: Range(0, 0)
private val exposureRange = characteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE) ?: Range(0, 0)
private val exposureStep = characteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP) ?: Rational(1, 1)
private val digitalStabilizationModes =
characteristics.get(CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES) ?: IntArray(0)
private val opticalStabilizationModes =
@@ -174,6 +177,9 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, private val
map.putInt("videoWidth", videoSize.width)
map.putInt("minISO", isoRange.lower)
map.putInt("maxISO", isoRange.upper)
// TODO: Implement minExposureBias
map.putDouble("minExposure", exposureRange.lower.toDouble() / exposureStep.toDouble())
map.putDouble("maxExposure", exposureRange.upper.toDouble() / exposureStep.toDouble())
map.putInt("minFps", fpsRange.lower)
map.putInt("maxFps", fpsRange.upper)
map.putDouble("maxZoom", maxZoom)

View File

@@ -434,6 +434,12 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
captureRequest.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_NIGHT)
}
// Set Exposure Bias
val exposure = config.exposure?.toInt()
if (exposure != null) {
captureRequest.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposure)
}
// Set Zoom
// TODO: Check if that zoom value is even supported
captureRequest.setZoom(config.zoom, cameraCharacteristics)

View File

@@ -13,6 +13,8 @@ data class CameraDeviceFormat(
val maxFps: Double,
val minISO: Double,
val maxISO: Double,
val minExposure: Double,
val maxExposure: Double,
val fieldOfView: Double,
val maxZoom: Double,
val videoStabilizationModes: List<VideoStabilizationMode>,
@@ -46,6 +48,8 @@ data class CameraDeviceFormat(
value.getDouble("maxFps"),
value.getDouble("minISO"),
value.getDouble("maxISO"),
value.getDouble("minExposure"),
value.getDouble("maxExposure"),
value.getDouble("fieldOfView"),
value.getDouble("maxZoom"),
videoStabilizationModes,

View File

@@ -507,7 +507,7 @@ PODS:
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.10)
- SocketRocket (0.6.1)
- VisionCamera (3.6.4):
- VisionCamera (3.6.6):
- React
- React-callinvoker
- React-Core
@@ -747,7 +747,7 @@ SPEC CHECKSUMS:
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
VisionCamera: db57ca079c4f933f1ddd07f2f8fb2d171a826b85
VisionCamera: 47d2342b724c78fb9ff3e2607c21ceda4ba21e75
Yoga: 8796b55dba14d7004f980b54bcc9833ee45b28ce
PODFILE CHECKSUM: 27f53791141a3303d814e09b55770336416ff4eb

View File

@@ -189,6 +189,7 @@ export function CameraPage({ navigation }: Props): React.ReactElement {
onError={onError}
enableZoomGesture={false}
animatedProps={cameraAnimatedProps}
exposure={0}
enableFpsGraph={true}
orientation="portrait"
photo={true}

View File

@@ -46,6 +46,7 @@ public final class CameraView: UIView, CameraSessionDelegate {
@objc var isActive = false
@objc var torch = "off"
@objc var zoom: NSNumber = 1.0 // in "factor"
@objc var exposure: NSNumber = 1.0
@objc var enableFpsGraph = false
@objc var videoStabilizationMode: NSString?
@objc var resizeMode: NSString = "cover" {
@@ -218,6 +219,9 @@ public final class CameraView: UIView, CameraSessionDelegate {
// Zoom
config.zoom = zoom.doubleValue
// Exposure
config.exposure = exposure.floatValue
// isActive
config.isActive = isActive
}

View File

@@ -44,6 +44,7 @@ RCT_EXPORT_VIEW_PROPERTY(pixelFormat, NSString);
// other props
RCT_EXPORT_VIEW_PROPERTY(torch, NSString);
RCT_EXPORT_VIEW_PROPERTY(zoom, NSNumber);
RCT_EXPORT_VIEW_PROPERTY(exposure, NSNumber);
RCT_EXPORT_VIEW_PROPERTY(enableZoomGesture, BOOL);
RCT_EXPORT_VIEW_PROPERTY(enableFpsGraph, BOOL);
RCT_EXPORT_VIEW_PROPERTY(orientation, NSString);

View File

@@ -39,6 +39,9 @@ class CameraConfiguration {
// Zoom
var zoom: CGFloat?
// Exposure
var exposure: Float?
// isActive (Start/Stop)
var isActive = false
@@ -59,6 +62,7 @@ class CameraConfiguration {
enableLowLightBoost = other.enableLowLightBoost
torch = other.torch
zoom = other.zoom
exposure = other.exposure
isActive = other.isActive
audio = other.audio
} else {
@@ -77,6 +81,7 @@ class CameraConfiguration {
let sidePropsChanged: Bool
let torchChanged: Bool
let zoomChanged: Bool
let exposureChanged: Bool
let audioSessionChanged: Bool
@@ -113,6 +118,8 @@ class CameraConfiguration {
torchChanged = left?.isActive != right.isActive || left?.torch != right.torch
// zoom (depends on format)
zoomChanged = formatChanged || left?.zoom != right.zoom
// exposure (depends on format)
exposureChanged = formatChanged || left?.exposure != right.exposure
// audio session
audioSessionChanged = left?.audio != right.audio

View File

@@ -285,6 +285,20 @@ extension CameraSession {
device.videoZoomFactor = clamped
}
// pragma MARK: Exposure
/**
Configures exposure (`exposure`) as a bias that adjusts exposureTime and ISO.
*/
func configureExposure(configuration: CameraConfiguration, device: AVCaptureDevice) {
guard let exposure = configuration.exposure else {
return
}
let clamped = max(min(exposure, device.maxExposureTargetBias), device.minExposureTargetBias)
device.setExposureTargetBias(clamped)
}
// pragma MARK: Audio
/**

View File

@@ -154,13 +154,17 @@ class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, AVC
if difference.zoomChanged {
self.configureZoom(configuration: config, device: device)
}
// 8. Configure exposure bias
if difference.exposureChanged {
self.configureExposure(configuration: config, device: device)
}
}
}
// 8. Start or stop the session if needed
// 9. Start or stop the session if needed
self.checkIsActive(configuration: config)
// 9. Enable or disable the Torch if needed (requires session to be running)
// 10. Enable or disable the Torch if needed (requires session to be running)
if difference.torchChanged {
try self.withDeviceLock { device in
try self.configureTorch(configuration: config, device: device)

View File

@@ -80,6 +80,14 @@ export interface CameraDeviceFormat {
* Minimum supported ISO value
*/
minISO: number
/**
* The minimum Exposure-Bias value this format supports. When setting the `exposure` to this value, the image is almost completely dark (under-exposed).
*/
minExposure: number
/**
* The maximum Exposure-Bias value this format supports. When setting the `exposure` to this value, the image is almost completely bright (over-exposed).
*/
maxExposure: number
/**
* The video field of view in degrees
*/

View File

@@ -110,6 +110,19 @@ export interface CameraProps extends ViewProps {
enableZoomGesture?: boolean
//#endregion
//#region Camera Controls
/**
* Specifies the Exposure bias of the current camera. A lower value means darker images, a higher value means brighter images.
*
* The Camera will still continue to auto-adjust exposure and focus, but will premultiply the exposure setting with the provided value here.
*
* This values ranges from {@linkcode CameraDeviceFormat.minExposure format.minExposure} to {@linkcode CameraDeviceFormat.maxExposure format.maxExposure}.
*
* The value between min- and max supported exposure is considered the default, neutral value.
*/
exposure?: number
//#endregion
//#region Format/Preset selection
/**
* Selects a given format. By default, the best matching format is chosen. See {@linkcode CameraDeviceFormat}