diff --git a/docs/docs/guides/EXPOSURE.mdx b/docs/docs/guides/EXPOSURE.mdx new file mode 100644 index 0000000..206d0c3 --- /dev/null +++ b/docs/docs/guides/EXPOSURE.mdx @@ -0,0 +1,71 @@ +--- +id: exposure +title: Exposure +sidebar_label: Exposure +--- + +### Adjusting exposure + +To adjust the exposure of the Camera, you can use the Camera's [`exposure`](/docs/api/interfaces/CameraProps#exposure) property: + +```tsx + +``` + +Values for the `exposure` prop range from [`format.minExposure`](/docs/api/interfaces/CameraDeviceFormat#maxExposure) to [`format.maxExposure`](/docs/api/interfaces/CameraDeviceFormat#minExposure), inclusively. By default (`undefined`), it is set to neutral auto exposure. + +Instead of manually adjusting ISO and Exposure-Duration, this acts as an "exposure compensation bias", meaning the Camera will still continuously automatically adjust exposure as it goes, but premultiplies the given exposure value to it's ISO and Exposure Duration settings. + +### Examples + +![Exposure Example (-2, 0, 2)](/img/exposure.jpg) + +### Animating + +Just like [`zoom`](zooming), this property can be animated using Reanimated. + +1. Add the `exposure` prop to the whitelisted animateable properties: + + ```tsx + import Reanimated, { addWhitelistedNativeProps } from "react-native-reanimated" + + const ReanimatedCamera = Reanimated.createAnimatedComponent(Camera) + addWhitelistedNativeProps({ + exposure: true, + }) + ``` + +2. Implement your animation, for example with an exposure slider: + + ```jsx + function App() { + // 1. create shared value for exposure slider (from -1..0..1) + const exposureSlider = useSharedValue(0) + + // 2. map slider to [minExposure, 0, maxExposure] + const exposureValue = useDerivedValue(() => { + if (format == null) return 0 + + return interpolate(exposureSlider.value, + [-1, 0, 1], + [format.minExposure, 0, format.maxExposure]) + }, [exposureSlider, format]) + + // 3. pass it as an animated prop + const animatedProps = useAnimatedProps(() => ({ + exposure: exposureValue.value + }), [exposureValue]) + + // 4. render Camera + return ( + + ) + } + ``` + +
+ +#### 🚀 Next section: [HDR](hdr) diff --git a/docs/docs/guides/FOCUSING.mdx b/docs/docs/guides/FOCUSING.mdx index e949c69..408cd98 100644 --- a/docs/docs/guides/FOCUSING.mdx +++ b/docs/docs/guides/FOCUSING.mdx @@ -22,4 +22,4 @@ Focussing adjusts auto-focus (AF) and auto-exposure (AE).
-#### 🚀 Next section: [HDR](hdr) +#### 🚀 Next section: [Exposure](exposure) diff --git a/docs/sidebars.js b/docs/sidebars.js index 9f05eb7..beb8618 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -31,6 +31,7 @@ module.exports = { }, 'guides/zooming', 'guides/focusing', + 'guides/exposure', 'guides/hdr', 'guides/stabilization', 'guides/performance', diff --git a/docs/static/img/exposure.jpg b/docs/static/img/exposure.jpg new file mode 100644 index 0000000..6500cee Binary files /dev/null and b/docs/static/img/exposure.jpg differ diff --git a/package/android/src/main/java/com/mrousavy/camera/CameraView.kt b/package/android/src/main/java/com/mrousavy/camera/CameraView.kt index 780664a..123a8c6 100644 --- a/package/android/src/main/java/com/mrousavy/camera/CameraView.kt +++ b/package/android/src/main/java/com/mrousavy/camera/CameraView.kt @@ -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 diff --git a/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt b/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt index 4bbc35a..222f464 100644 --- a/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt +++ b/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt @@ -140,8 +140,12 @@ class CameraViewManager : ViewGroupManager() { @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") diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt index e2adbf9..a0c2769 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt @@ -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, diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt index 1798128..89a8692 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt @@ -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) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt index f432b52..1e0c8b2 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt @@ -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) diff --git a/package/android/src/main/java/com/mrousavy/camera/types/CameraDeviceFormat.kt b/package/android/src/main/java/com/mrousavy/camera/types/CameraDeviceFormat.kt index ade2acd..163262a 100644 --- a/package/android/src/main/java/com/mrousavy/camera/types/CameraDeviceFormat.kt +++ b/package/android/src/main/java/com/mrousavy/camera/types/CameraDeviceFormat.kt @@ -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, @@ -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, diff --git a/package/example/ios/Podfile.lock b/package/example/ios/Podfile.lock index 8c15864..c960617 100644 --- a/package/example/ios/Podfile.lock +++ b/package/example/ios/Podfile.lock @@ -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 diff --git a/package/example/src/CameraPage.tsx b/package/example/src/CameraPage.tsx index 4760327..b976586 100644 --- a/package/example/src/CameraPage.tsx +++ b/package/example/src/CameraPage.tsx @@ -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} diff --git a/package/ios/CameraView.swift b/package/ios/CameraView.swift index c23a5d3..8198d2c 100644 --- a/package/ios/CameraView.swift +++ b/package/ios/CameraView.swift @@ -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 } diff --git a/package/ios/CameraViewManager.m b/package/ios/CameraViewManager.m index 3931b55..4fc8983 100644 --- a/package/ios/CameraViewManager.m +++ b/package/ios/CameraViewManager.m @@ -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); diff --git a/package/ios/Core/CameraConfiguration.swift b/package/ios/Core/CameraConfiguration.swift index 4895aed..8a97d7f 100644 --- a/package/ios/Core/CameraConfiguration.swift +++ b/package/ios/Core/CameraConfiguration.swift @@ -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 diff --git a/package/ios/Core/CameraSession+Configuration.swift b/package/ios/Core/CameraSession+Configuration.swift index 7e60742..c278db8 100644 --- a/package/ios/Core/CameraSession+Configuration.swift +++ b/package/ios/Core/CameraSession+Configuration.swift @@ -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 /** diff --git a/package/ios/Core/CameraSession.swift b/package/ios/Core/CameraSession.swift index a4426a5..ac32609 100644 --- a/package/ios/Core/CameraSession.swift +++ b/package/ios/Core/CameraSession.swift @@ -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) diff --git a/package/src/CameraDevice.ts b/package/src/CameraDevice.ts index d5a4888..9501a9a 100644 --- a/package/src/CameraDevice.ts +++ b/package/src/CameraDevice.ts @@ -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 */ diff --git a/package/src/CameraProps.ts b/package/src/CameraProps.ts index 85bf8a0..42b008d 100644 --- a/package/src/CameraProps.ts +++ b/package/src/CameraProps.ts @@ -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}