9

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?

Joe
  • 217
  • 5
  • 10

3 Answers3

7

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!

khose
  • 743
  • 1
  • 6
  • 19
  • Thanks so much! I had to go and learn about KVOs but that was probably time well spent. This seems to be working well for the most part, though I in some lighting situations I get quickly flashing back and forth between two ISOs. Have you worked out a set of values that operate smoothly for you? – Joe Apr 30 '15 at 15:23
  • Could you comment which sample project from Apple you used? – isaac May 05 '15 at 18:01
  • @isaac Sorry man, I should've included it at first. Thanks for pointing that out. Please, check this [link](https://developer.apple.com/library/ios/samplecode/AVCamManual/Introduction/Intro.html#//apple_ref/doc/uid/TP40014578-Intro-DontLinkElementID_2) – khose May 06 '15 at 12:31
  • 1
    Sure would be nice to get an example in swift. – Brandon A Mar 11 '17 at 01:46
  • 1
    Did you guys fix the quick flashing? – thelearner Mar 01 '18 at 20:15
  • example in Swift please – Volodymyr Kulyk Jul 17 '18 at 12:18
  • @dfi Partly. Used multiplication instead of addition. biasMultiplier = 1.0 / pow(2.0f, currentEV); CGFloat newISO = currentISO*biasMultiplier; – Battlechicken Oct 15 '18 at 15:19
1

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)

Volodymyr Kulyk
  • 6,455
  • 3
  • 36
  • 63
  • It works but the screen keeps flashing. How to fix it? – jdleung May 28 '21 at 07:42
  • @jdleung try to use natural light (window or outdoor). If exposure duration is low enough it could cause flashing with artificial light. – Volodymyr Kulyk May 28 '21 at 08:34
  • Yes, it gives better experience when at brighter scene. Changing `biasISO` to a smaller value can make the brightness transition goes more smoothly, but it also need more time to reach the accurate ISO. Is there any other better solution? Thanks. – jdleung May 28 '21 at 23:07
  • @jdleung better solution is to analyse brightness on each frame by yourself and set ISO considering it. We spent lots of hours to make such algorithm on C++. – Volodymyr Kulyk May 31 '21 at 10:31
  • Got it. There is no official API solution and it's a big job to do so by myself. I decide to let user set exposure to Auto or adjust duration and ISO separately. Thanks! – jdleung May 31 '21 at 23:27
1

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()
}
xaphod
  • 6,392
  • 2
  • 37
  • 45
  • I tried to implement like this: `NotificationCenter.default.addObserver(self, selector:#selector(exposureTargetOffsetChanged), name: Notification.Name(rawValue: "exposureTargetOffset"), object: nil)`, but `exposureTargetOffsetChanged` not get called. – jdleung May 28 '21 at 07:38