I'm using the camera video feed for some image processing and would like to optimise for fastest shutter speed. I know you can manually set exposure duration and ISO using
setExposureModeCustomWithDuration:ISO:completionHandler:
but this requires one to set both the values by hand. Is there a method or clever trick to allow you to set the exposure duraction manually but have the ISO handle itself to try to correctly expose the image?
I'm not sure if this solution is the best one, since I was struggling with this as you were. What I've done is to listen to changes in the exposure offset and, from them, adjust the ISO until you reach an acceptable exposure level. Most of this code has been taken from the Apple sample code
So, First of all, you listen for changes on the ExposureTargetOffset. Add in your class declaration:
static void *ExposureTargetOffsetContext = &ExposureTargetOffsetContext;
Then, once you have done your device setup properly:
[self addObserver:self forKeyPath:@"captureDevice.exposureTargetOffset" options:NSKeyValueObservingOptionNew context:ExposureTargetOffsetContext];
(Instead of captureDevice, use your property for the device)
Then implement in your class the callback for KVO:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
if (context == ExposureTargetOffsetContext){
float newExposureTargetOffset = [change[NSKeyValueChangeNewKey] floatValue];
NSLog(@"Offset is : %f",newExposureTargetOffset);
if(!self.device) return;
CGFloat currentISO = self.device.ISO;
CGFloat biasISO = 0;
//Assume 0,3 as our limit to correct the ISO
if(newExposureTargetOffset > 0.3f) //decrease ISO
biasISO = -50;
else if(newExposureTargetOffset < -0.3f) //increase ISO
biasISO = 50;
if(biasISO){
//Normalize ISO level for the current device
CGFloat newISO = currentISO+biasISO;
newISO = newISO > self.device.activeFormat.maxISO? self.device.activeFormat.maxISO : newISO;
newISO = newISO < self.device.activeFormat.minISO? self.device.activeFormat.minISO : newISO;
NSError *error = nil;
if ([self.device lockForConfiguration:&error]) {
[self.device setExposureModeCustomWithDuration:AVCaptureExposureDurationCurrent ISO:newISO completionHandler:^(CMTime syncTime) {}];
[self.device unlockForConfiguration];
}
}
}
}
With this code, the Shutter speed will remain constant and the ISO will be adjusted to leave the image not too under or overexposed.
Don't forget to remove the observer whenever is needed. Hope this suits you.
Cheers!
The accepted answer will take a long time to ramp up/down ISO if there's a large change in the lighting condition. Here's an example (Swift 4) that changes the value of the ISO in proportion to the amount of exposure offset.
fileprivate var settingISO = false
@objc func exposureTargetOffsetChanged(notification: Notification) {
guard !settingISO, self.device.exposureMode == .custom, let exposureTargetOffset = notification.userInfo?["newValue"] as? Float else {
return
}
var isoChange = Float(0.0)
let limit = Float(0.05)
let isoChangeStep: Float
if abs(exposureTargetOffset) > 1 {
isoChangeStep = 500
} else if abs(exposureTargetOffset) > 0.5 {
isoChangeStep = 200
} else if abs(exposureTargetOffset) > 0.2 {
isoChangeStep = 50
} else if abs(exposureTargetOffset) > 0.1 {
isoChangeStep = 20
} else {
isoChangeStep = 5
}
if exposureTargetOffset > limit {
isoChange -= isoChangeStep
} else if exposureTargetOffset < -limit {
isoChange += isoChangeStep
} else {
return
}
var newiso = self.device.iso + isoChange
newiso = max(self.device.activeFormat.minISO, newiso)
newiso = min(self.device.activeFormat.maxISO, newiso)
guard newiso != self.device.iso, (try? self.device.lockForConfiguration()) != nil else { return }
self.settingISO = true
Camera.log("exposureTargetOffset=\(exposureTargetOffset), isoChange=\(isoChange), newiso=\(newiso)")
self.device.setExposureModeCustom(duration: self.customDuration ?? AVCaptureDevice.currentExposureDuration, iso: newiso) { (_) in
self.settingISO = false
}
self.device.unlockForConfiguration()
}
Code sample provided by @khose on swift:
private var device: AVCaptureDevice?
private var exposureTargetOffsetContext = 0
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if device == nil {
return
}
if keyPath == "exposureTargetOffset" {
let newExposureTargetOffset = change?[NSKeyValueChangeKey.newKey] as! Float
print("Offset is : \(newExposureTargetOffset)")
let currentISO = device?.iso
var biasISO = 0
//Assume 0,01 as our limit to correct the ISO
if newExposureTargetOffset > 0.01 { //decrease ISO
biasISO = -50
} else if newExposureTargetOffset < -0.01 { //increase ISO
biasISO = 50
}
if biasISO != Int(0) {
//Normalize ISO level for the current device
var newISO = currentISO! + Float(biasISO)
newISO = newISO > (device?.activeFormat.maxISO)! ? (device?.activeFormat.maxISO)! : newISO
newISO = newISO < (device?.activeFormat.minISO)! ? (device?.activeFormat.minISO)! : newISO
try? device?.lockForConfiguration()
device?.setExposureModeCustom(duration: AVCaptureDevice.currentExposureDuration, iso: newISO, completionHandler: nil)
device?.unlockForConfiguration()
}
}
}
Usage:
device?.addObserver(self, forKeyPath: "exposureTargetOffset", options: NSKeyValueObservingOptions.new, context: &exposureTargetOffsetContext)