AV Foundation: AVCaptureVideoPreviewLayer and frame duration - ios

I am using the AV Foundation to process frames from the video camera (iPhone 4s, iOS 6.1.2). I am setting up AVCaptureSession, AVCaptureDeviceInput, AVCaptureVideoDataOutput per the AV Foundation programming guide. Everything works as expected and I am able to recieve frames in the captureOutput:didOutputSampleBuffer:fromConnection: delegate.
I also have a preview layer set like this:
AVCaptureVideoPreviewLayer *videoPreviewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:_captureSession];
[videoPreviewLayer setFrame:self.view.bounds];
videoPreviewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
[self.view.layer insertSublayer:videoPreviewLayer atIndex:0];
Thing is, I don't need 30 frames per second in my frame handling and I am not able to process them so fast anyway. So I am using this code to limit the frame duration:
// videoOutput is AVCaptureVideoDataOutput set earlier
AVCaptureConnection *conn = [videoOutput connectionWithMediaType:AVMediaTypeVideo];
[conn setVideoMinFrameDuration:CMTimeMake(1, 10)];
[conn setVideoMaxFrameDuration:CMTimeMake(1, 2)];
This works fine and limits the frames recieved by the captureOutput delegate.
However, this also limits the frames per second on the preview layer and preview video becomes very unresponsive.
I understand from the documentation that the frame duration is set independently on the connection and the preview layer has indeed a different AVCaptureConnection. Checking the mix/max frame durations on [videoPreviewLayer connection] shows that it's indeed set to the defaults (1/30 and 1/24) and different than the durations set on the connection of the AVCaptureVideoDataOutput.
So, is it possible to limit the frame duration only on the frame capturing output and still see a 1/24-1/30 frame duration on the preview video? How?
Thanks.

While you're correct that there are two AVCaptureConnections, that doesn't mean they can have independently set the minimum and maximum frame durations. This is because they are sharing the same physical hardware.
If connection #1 is activating the rolling shutter at a rate of (say) five frames/sec with a frame duration of 1/5 sec, there is no way that connection #2 can simultaneously activate the shutter 30 times/sec with a frame duration of 1/30 sec.
To get the effect you want would require two cameras!
The only way to get close to what you want is to follow an approach along the lines of that outlined by Kaelin Colclasure in the answer of 22 March.
You do have options of being a little more sophisticated within that approach, however. For example, you can use a counter to decide which frames to drop, rather than making the thread sleep. You can make that counter respond to the actual frame-rate that's coming through (which you can get from the metadata that comes in to the captureOutput:didOutputSampleBuffer:fromConnection: delegate along with the image data, or which you can calculate yourself by manually timing the frames). You can even do a very reasonable imitation of a longer exposure by compositing frames rather than dropping them—just as a number of "slow shutter" apps in the App Store do (leaving aside details—such as differing rolling shutter artefacts—there's not really that much difference between one frame scanned at 1/5 sec and five frames each scanned at 1/25 sec and then glued together).
Yes, it's a bit of work, but you are trying to make one video camera behave like two, in real time—and that's never going to be easy.

Think of it this way:
You ask the capture device to limit frame duration, so you get better exposure.
Fine.
You want to preview at higher frame rate.
If you were to preview at higher rate, then the capture device (the camera) would NOT have enough time to expose the frame so you get better exposure at the captured frames.
It is like asking to see different frames in preview than the ones captured.
I think that, if it was possible, it would also be a negative user experience.

I had the same issue for my Cocoa (Mac OS X) application. Here's how I solved it:
First, make sure to process the captured frames on a separate dispatch queue. Also make sure any frames you're not ready to process are discarded; this is the default, but I set the flag below anyway just to document that I'm depending on it.
videoQueue = dispatch_queue_create("com.ohmware.LabCam.videoQueue", DISPATCH_QUEUE_SERIAL);
videoOutput = [[AVCaptureVideoDataOutput alloc] init];
[videoOutput setAlwaysDiscardsLateVideoFrames:YES];
[videoOutput setSampleBufferDelegate:self
queue:videoQueue];
[session addOutput:videoOutput];
Then when processing the frames in the delegate, you can simply have the thread sleep for the desired time interval. Frames that the delegate is not awake to handle are quietly discarded. I implement the optional method for counting dropped frames below just as a sanity check; my application never logs dropping any frames using this technique.
- (void)captureOutput:(AVCaptureOutput *)captureOutput
didDropSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection;
{
OSAtomicAdd64(1, &videoSampleBufferDropCount);
}
- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection;
{
int64_t savedSampleBufferDropCount = videoSampleBufferDropCount;
if (savedSampleBufferDropCount && OSAtomicCompareAndSwap64(savedSampleBufferDropCount, 0, &videoSampleBufferDropCount)) {
NSLog(#"Dropped %lld video sample buffers!!!", savedSampleBufferDropCount);
}
// NSLog(#"%s", __func__);
#autoreleasepool {
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
CIImage * cameraImage = [CIImage imageWithCVImageBuffer:imageBuffer];
CIImage * faceImage = [self faceImage:cameraImage];
dispatch_sync(dispatch_get_main_queue(), ^ {
[_imageView setCIImage:faceImage];
});
}
[NSThread sleepForTimeInterval:0.5]; // Only want ~2 frames/sec.
}
Hope this helps.

Related

AVCaptureSessionPresetLow on iPhone 6

I'm a n00b to AVCaptureSession. I'm using OpenTok to implement video chat. I want to preserve bandwidth and the UI is designed so the video views are only 100 x 100 presently.
This is part of the code from an OpenTok example where it sets the preset:
- (void) setCaptureSessionPreset:(NSString*)preset {
AVCaptureSession *session = [self captureSession];
if ([session canSetSessionPreset:preset] &&
![preset isEqualToString:session.sessionPreset]) {
[_captureSession beginConfiguration];
_captureSession.sessionPreset = preset;
_capturePreset = preset;
[_videoOutput setVideoSettings:
[NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange],
kCVPixelBufferPixelFormatTypeKey,
nil]];
[_captureSession commitConfiguration];
}
}
When I pass in AVCaptureSessionPresetLow (on an iPhone 6) I get NO. Is there any way I can set AVCaptureSession so I can only capture video with a frame as close to 100 x 100 as possible?
Also, is this the correct strategy for trying to save bandwidth?
You cannot force the camera to a resolution it does not support.
A lower resolution frame size will lead to lower network traffic.
Lowering FPS is one other way.
A view size does not have to map to a resolution. You can always fit a frame in any size view.
Look at the Let-Build-OTPublisher app in OpenTok SDK and more specifically TBExampleVideoCapture.m file, on how resolution and FPS are handled.

How does one apply different frame rates to videoDataOutput and session's previewLayer in AVFoundation?

I am developing an augmented reality application with AVFoundation. Basically, I need to start up the camera, provide a instant preview, and get image samples every 1 second. Currently I am using AVCaptureVideoPreviewLayer for camera preview, and AVCaptureVideoDataOutput to get sample frames.
But the problem is, the reasonable frame rate for AVCaptureVideoPreviewLayer is way too high for AVCaptureVideoDataOutput. How can I apply different frame rates to them?
Thanks.
There is no answer yet, so I put my temporary solution here:
Firstly, add a property:
#property (assign, nonatomic) NSTimeInterval lastFrameTimestamp;
And the delegate method:
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
CMTime timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
NSTimeInterval currentFrameTimestamp = (double)timestamp.value / timestamp.timescale;
if (currentFrameTimestamp - self.lastFrameTimestamp > secondsBetweenSampling) {
self.lastFrameTimestamp = currentFrameTimestamp;
// deal with the sampleBuffer here
}
}
The idea is pretty simple. As we can see, I just keep the frame rate high so the preview layer is satisfied. But upon receiving an output, I check the timestamp every time to decide whether to deal with that frame or just ignore it.
Still looking for better ideas ;)

GPUImageMovieWriter - occasional black frames at either ends of the recorded video

I have recording app implementation where user can tap the "record" button to start/stop recording. I achieve this with a basic GPUImageVideoCamera with output set to a GPUImageView as well as a GPUImageMovieWriter.
50% of the time, the recorded clip ends up with a couple (or a single) black frame at either ends, sometimes both. The implementation is fairly straightforward, but here is it anyway.
gpuImageView = [[GPUImageView alloc] initWithFrame:cameraView.frame];
gpuImageView.fillMode = kGPUImageFillModePreserveAspectRatioAndFill;
[cameraView addSubview:gpuImageView];
videoCamera = [[GPUImageVideoCamera alloc] initWithSessionPreset:AVCaptureSessionPresetHigh cameraPosition:(usingBackCamera) ? AVCaptureDevicePositionBack : AVCaptureDevicePositionFront];
[videoCamera addTarget:gpuImageView];
[videoCamera addTarget:movieWriter];
videoCamera.audioEncodingTarget = movieWriter;
[videoCamera startCameraCapture];
double delayToStartRecording = 0.5;
dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, delayToStartRecording * NSEC_PER_SEC);
dispatch_after(startTime, dispatch_get_main_queue(), ^(void){
NSLog(#"Start recording");
[movieWriter startRecording];
});
And then stop the recording with the following (while the live camera continues to show up on GPUImageView.
[movieWriter finishRecording];
Has anyone else experienced this and/or found a solution to avoid black frames? I cannot pause/resume camera capture so to ensure seamless user experience.
Typically when this happens it's because the queue is writing audio to the file before it gets any video frames. Hard to tell from your implementation, but if you make sure the first thing you write to the file is a video frame and don't write any audio until after the first video frame is written, you won't have any black frames.

Luminosity from iOS camera

I'm trying to make an application and i have to calculate the brightness of the camera like this application : http://itunes.apple.com/us/app/megaman-luxmeter/id455660266?mt=8
I found this document : http://b2cloud.com.au/tutorial/obtaining-luminosity-from-an-ios-camera
But i don't know how to adapt it to the camera directly and not an image. Here is my code :
Image = [[UIImagePickerController alloc] init];
Image.delegate = self;
Image.sourceType = UIImagePickerControllerCameraCaptureModeVideo;
Image.showsCameraControls = NO;
[Image setWantsFullScreenLayout:YES];
Image.view.bounds = CGRectMake (0, 0, 320, 480);
[self.view addSubview:Image.view];
NSArray* dayArray = [NSArray arrayWithObjects:Image,nil];
for(NSString* day in dayArray)
{
for(int i=1;i<=2;i++)
{
UIImage* image = [UIImage imageNamed:[NSString stringWithFormat:#"%#%d.png",day,i]];
unsigned char* pixels = [image rgbaPixels];
double totalLuminance = 0.0;
for(int p=0;p<image.size.width*image.size.height*4;p+=4)
{
totalLuminance += pixels[p]*0.299 + pixels[p+1]*0.587 + pixels[p+2]*0.114;
}
totalLuminance /= (image.size.width*image.size.height);
totalLuminance /= 255.0;
NSLog(#"%# (%d) = %f",day,i,totalLuminance);
}
}
Here are the issues :
"Instance method '-rgbaPixels' not found (return type defaults to 'id')"
&
"Incompatible pointer types initializing 'unsigned char *' with an expression of type 'id'"
Thanks a lot ! =)
Rather than doing expensive CPU-bound processing of each pixel in an input video frame, let me suggest an alternative approach. My open source GPUImage framework has a luminosity extractor built into it, which uses GPU-based processing to give live luminosity readings from the video camera.
It's relatively easy to set this up. You simply need to allocate a GPUImageVideoCamera instance to represent the camera, allocate a GPUImageLuminosity filter, and add the latter as a target for the former. If you want to display the camera feed to the screen, create a GPUImageView instance and add that as another target for your GPUImageVideoCamera.
Your luminosity extractor will use a callback block to return luminosity values as they are calculated. This block is set up using code like the following:
[(GPUImageLuminosity *)filter setLuminosityProcessingFinishedBlock:^(CGFloat luminosity, CMTime frameTime) {
// Do something with the luminosity
}];
I describe the inner workings of this luminosity extraction in this answer, if you're curious. This extractor runs in ~6 ms for a 640x480 frame of video on an iPhone 4.
One thing you'll quickly find is that the average luminosity from the iPhone camera is almost always around 50% when automatic exposure is enabled. This means that you'll need to supplement your luminosity measurements with exposure values from the camera metadata to obtain any sort of meaningful brightness measurement.
Why do you place the camera image into an NSArray *dayArray? Five lines later you remove it from that array but treat the object as an NSString. An NSString does not have rgbaPixels. The example you copy-pasted has an array of filenames corresponding to pictures taken at different times of the day. It then opens those image files and performs the analysis of luminosity.
In your case, there is no file to read. Both outer for loops, i.e. on day and i will have to go away. You already got access to the Image provided through the UIImagePickerController. Right after adding the subview, you could in principle access pixels as in unsigned char *pixels = [Image rgbaPixels]; where Image is the image you got from UIImagePickerController.
However, this may not be what you want to do. I imagine that your goal is rather to show the UIImagePickerController in capture mode and then to measure luminosity continuously. To this end, you could turn Image into a member variable, and then access its pixels repeatedly from a timer callback.
You can import below class from GIT to resolve this issue.
https://github.com/maxmuermann/pxl
Add UIImage+Pixels.h & .m files into project. Now try to run.

AVCaptureSession "output sample buffer" reading pixel coordinate gives a wrong color

I use AVCaptureSession to initiate a video capture session and read pixel colors from video frames. The video setting is like this.
NSDictionary* videoSettings = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithUnsignedInt:kCVPixelFormatType_32BGRA],
kCVPixelBufferPixelFormatTypeKey,
nil];
with a delegate method below to get sample buffer, which I will later read pixel colors.
- (void)captureOutput:(AVCaptureOutput *)captureOutput // captureOutput is only the AVCaptureVideoDataOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection
{
NSAutoreleasePool * pool = [NSAutoreleasePool new]; // instrument tells it leaks
/******* START CALCULATION *******/
imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
CVPixelBufferLockBaseAddress(imageBuffer,0); // lock image buffer
buffer_baseAddress = (uint8_t *)CVPixelBufferGetBaseAddress(imageBuffer); // raw buffer data BGRA
...
The variable buffer_baseAddress is an array that stores pixel colors, in which under kCVPixelFormatType_32BGRA setting, the array will arrange like
[B][G][R][A][B][G][R][A][B]...[B][G][R][A]. So to get a color at pixel at some coordinate, I'll have to figure out 3 indices in the buffer. So at (x,y) = (10,0), BGR will be at index 40, 41, and 42.
Here is the problem. The first row (y == 0) of the sample buffer seems to give correct color at all times. But when I move to the second row onward (y > 0), I got wrong colors on some presets, or using front/back camera. It's like the buffer has some unknown, extra data appended at the end of each row, in a certain setting. Luckily from my experiments, I find that sample buffers got shifted by some amount in each row when I use AVCaptureSessionPresetHigh on back Camera, and AVCaptureSessionPresetMedium on both front/back cameras. I remembered setting some rowPadding = 0 in one of AVCaptureSession class doesn't help either. (I'm sorry I forgot what exact variable it was. It was several months back.)
What causes this problem? And what can I do to solve this?
Have a look at the section Converting a CMSampleBuffer to a UIImage in this page from the Apple Docs. I haven't tried it but it shows how to get the bytesPerRow - including padding - of the buffer.
It is normal for an image buffer to be padded to an optimal value, whether you are using CoreVideo, CoreImage, Quartz, Quicktime etc.. there will always be a way to find out what it is.
This is my current solution. Not the best one but it helps me get across it. I just test each of every AVCaptureSessionPreset and see which one got wrong pixels read. Then I try to guess the size of that extra padding and use it when I calculate the sample buffer index. This magic number is 8, took me days to find this out. So hopefully this will be helpful to someone, at least, as a workaround. :-)
// Look like this magic padding is affected by AVCaptureSessionPreset and front/back camera
if ([[self currentPresetSetting] isEqualToString:AVCaptureSessionPresetHigh] && ![self isUsingFrontFacingCamera]) {
buffer_rowPixelPadding = 8;
}
else if ([[self currentPresetSetting] isEqualToString:AVCaptureSessionPresetMedium]) {
buffer_rowPixelPadding = 8;
}
else {
buffer_rowPixelPadding = 0; // default
}

Resources