iOS camera: manual exposure duration but auto ISO?
Asked Answered
K

3

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?

Kyanize answered 23/4, 2015 at 9:39 Comment(0)
E
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!

Emanate answered 28/4, 2015 at 21:28 Comment(7)
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?Kyanize
Could you comment which sample project from Apple you used?Parapsychology
@Parapsychology Sorry man, I should've included it at first. Thanks for pointing that out. Please, check this linkEmanate
Sure would be nice to get an example in swift.Wyn
Did you guys fix the quick flashing?Acerate
example in Swift pleaseSoudan
@dfi Partly. Used multiplication instead of addition. biasMultiplier = 1.0 / pow(2.0f, currentEV); CGFloat newISO = currentISO*biasMultiplier;Giusto
S
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)

Soudan answered 20/7, 2018 at 18:6 Comment(5)
It works but the screen keeps flashing. How to fix it?Lancashire
@Lancashire try to use natural light (window or outdoor). If exposure duration is low enough it could cause flashing with artificial light.Soudan
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.Lancashire
@Lancashire 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++.Soudan
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!Lancashire
C
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()
}
Coquetry answered 26/11, 2018 at 21:42 Comment(1)
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.Lancashire

© 2022 - 2024 — McMap. All rights reserved.