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:
parent
a7e706150e
commit
ef58d13b87
71
docs/docs/guides/EXPOSURE.mdx
Normal file
71
docs/docs/guides/EXPOSURE.mdx
Normal file
@ -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
|
||||
<Camera {...props} exposure={-1} />
|
||||
```
|
||||
|
||||
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 (
|
||||
<ReanimatedCamera
|
||||
{...props}
|
||||
animatedProps={animatedProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
#### 🚀 Next section: [HDR](hdr)
|
@ -22,4 +22,4 @@ Focussing adjusts auto-focus (AF) and auto-exposure (AE).
|
||||
|
||||
<br />
|
||||
|
||||
#### 🚀 Next section: [HDR](hdr)
|
||||
#### 🚀 Next section: [Exposure](exposure)
|
||||
|
@ -31,6 +31,7 @@ module.exports = {
|
||||
},
|
||||
'guides/zooming',
|
||||
'guides/focusing',
|
||||
'guides/exposure',
|
||||
'guides/hdr',
|
||||
'guides/stabilization',
|
||||
'guides/performance',
|
||||
|
BIN
docs/static/img/exposure.jpg
vendored
Normal file
BIN
docs/static/img/exposure.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 123 KiB |
@ -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}
|
||||
|
Loading…
Reference in New Issue
Block a user