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:
@@ -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
|
||||
|
@@ -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")
|
||||
|
@@ -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,
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
|
@@ -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}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
||||
/**
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
*/
|
||||
|
@@ -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}
|
||||
|
Reference in New Issue
Block a user