views:

796

answers:

4

resizing a camera UIImage returned by the UIImagePickerController takes a ridiculously long time if you do it the usual way as in this post.

[update: last call for creative ideas here! my next option is to go ask Apple, I guess.]

Yes, it's a lot of pixels, but the graphics hardware on the iPhone is perfectly capable of drawing lots of 1024x1024 textured quads onto the screen in 1/60th of a second, so there really should be a way of resizing a 2048x1536 image down to 640x480 in a lot less than 1.5 seconds.

So why is it so slow? Is the underlying image data the OS returns from the picker somehow not ready to be drawn, so that it has to be swizzled in some fashion that the GPU can't help with?

My best guess is that it needs to be converted from RGBA to ABGR or something like that; can anybody think of a way that it might be possible to convince the system to give me the data quickly, even if it's in the wrong format, and I'll deal with it myself later?

As far as I know, the iPhone doesn't have any dedicated "graphics" memory, so there shouldn't be a question of moving the image data from one place to another.

So, the question: is there some alternative drawing method besides just using CGBitmapContextCreate and CGContextDrawImage that takes more advantage of the GPU?

Something to investigate: if I start with a UIImage of the same size that's not from the image picker, is it just as slow? Apparently not...

Update: Matt Long found that it only takes 30ms to resize the image you get back from the picker in [info objectForKey:@"UIImagePickerControllerEditedImage"], if you've enabled cropping with the manual camera controls. That isn't helpful for the case I care about where I'm using takePicture to take pictures programmatically. I see that that the edited image is kCGImageAlphaPremultipliedFirst but the original image is kCGImageAlphaNoneSkipFirst.

Further update: Jason Crawford suggested CGContextSetInterpolationQuality(context, kCGInterpolationLow), which does in fact cut the time from about 1.5 sec to 1.3 sec, at a cost in image quality--but that's still far from the speed the GPU should be capable of!

Last update before the week runs out: user refulgentis did some profiling which seems to indicate that the 1.5 seconds is spent writing the captured camera image out to disk as a JPEG and then reading it back in. If true, very bizarre.

+2  A: 

Seems that you have made several assumptions here that may or may not be true. My experience is different than yours. This method seems to only take 20-30ms on my 3Gs when scaling a photo snapped from the camera to 0.31 of the original size with a call to:

CGImageRef scaled = CreateScaledCGImageFromCGImage([image CGImage], 0.31);

(I get 0.31 by taking the width scale, 640.0/2048.0, by the way)

I've checked to make sure the image is the same size you're working with. Here's my NSLog output:

2009-12-07 16:32:12.941 ImagePickerThing[8709:207] Info: {
    UIImagePickerControllerCropRect = NSRect: {{0, 0}, {2048, 1536}};
    UIImagePickerControllerEditedImage = <UIImage: 0x16c1e0>;
    UIImagePickerControllerMediaType = "public.image";
    UIImagePickerControllerOriginalImage = <UIImage: 0x184ca0>;
}

I'm not sure why the difference and I can't answer your question as it relates to the GPU, however I would consider 1.5 seconds and 30ms a very significant difference. Maybe compare the code in that blog post to what you are using?

Best Regards.

Matt Long
is your NSLog output from an image directly from the camera, or one loaded from the camera roll? my images don't have the EditedImage or CropRect key...
David Maymudes
Well, I modified my CGBitmapContextCreate code to match yours more, but I'm still seeing the CGContextDrawImage call take 1.5 seconds to complete. Perhaps the difference is a saved image vs. a camera image; any thoughts?
David Maymudes
I'm still getting between 20 and 30ms even when it's taking it from the camera directly. And yes, mine has the edited image and crop rect key in that mode as well. I'm not sure what could be causing the difference. Does your view controller call viewDidUnload or didReceiveMemoryWarning when you are using the camera?
Matt Long
See what kind of times you get with this project: http://www.cimgf.com/files/ImagePickerThing.zip
Matt Long
Thanks for posting the code; I see that you have [picker setAllowsImageEditing:YES] which probably explains the extra keys. I am taking pictures programmatically with takePicture so I don't know how those two things interact with each other, but I'll let you know.
David Maymudes
OK: your code is resizing the *cropped* version of the picture; if I change it to use [info objectForKey:@"UIImagePickerControllerOriginalImage"] instead, it takes 1.5 seconds. See update to question above.
David Maymudes
I second Matt. Resizing camera images doesn't take up a lot of time!
lostInTransit
lostInTransit--can you clarify? have you found a way to take live camera images and do something with them in <1 second?
David Maymudes
The reason this approach is 'faster' is that the OS is doing CGContextDrawImage itself between the camera taking the picture and the user being presented the picture for editing. It's the same speed, just different order of calls that makes the programmer's code faster because its getting the results of the CGContextDrawImage operation that was already performed.
refulgentis
+2  A: 

I have had the same problem and banged my head against it for a long time. As far as I can tell, the first time you access the UIImage returned by the image picker, it's just slow. As an experiment, try timing any two operations with the UIImage--e.g., your scale-down, and then UIImageJPEGRepresentation or something. Then switch the order. When I've done this in the past, the first operation gets a time penalty. My best hypothesis is that the memory is still on the CCD somehow, and transferring it into main memory to do anything with it is slow.

When you set allowsImageEditing=YES, the image you get back is resized and cropped down to about 320x320. That makes it faster, but it's probably not what you want.

The best speedup I've found is:

CGContextSetInterpolationQuality(context, kCGInterpolationLow)

on the context you get back from CGBitmapContextCreate, before you do CGContextDrawImage.

The problem is that your scaled-down images might not look as good. However, if you're scaling down by an integer factor--e.g., 1600x1200 to 800x600--then it looks OK.

jasoncrawford
Going to low quality seems to cut the time from about 1.5 seconds to 1.3 seconds, but again still very much less than it seems should be possible, so I'm still hoping for a better answer.
David Maymudes
Good luck and let me know if you find it. I've struggled with this a lot and I'm not sure there's a solution. Try posting a photo with Facebook, or even emailing a photo out of the native photo album. They all have a multi-second delay.
jasoncrawford
the one thing that gives me hope is that in those cases the photo app is dealing with a full-resolution image, so perhaps there's a faster method if you just want a lower-resolution version and don't have to swizzle so many pixels.
David Maymudes
+1  A: 

Use Shark, profile it, figure out what's taking so long.

I have to work a lot with MediaPlayer.framework and when you get properties for songs on the iPod, the first property request is insanely slow compared to subsequent requests, because in the first property request MobileMediaPlayer packages up a dictionary with all the properties and passes it to my app.

I'd be willing to bet that there is a similar situation occurring here.

EDIT: I was able to do a time profile in Shark of both Matt Long's UIImagePickerControllerEditedImage situation and the generic UIImagePickerControllerOriginalImage situation.

In both cases, a majority of the time is taken up by CGContextDrawImage. In Matt Long's case, the UIImagePickerController takes care of this in between the user capturing the image and the image entering 'edit' mode.

Scaling the percentage of time taken to CGContextDrawImage = 100%, CGContextDelegateDrawImage then takes 100%, then ripc_DrawImage (from libRIP.A.dylib) takes 100%, and then ripc_AcquireImage (which it looks like decompresses the JPEG, and takes up most of its time in _cg_jpeg_idct_islow, vec_ycc_bgrx_convert, decompress_onepass, sep_upsample) takes 93% of the time. Only 7% of the time is actually spent in ripc_RenderImage, which I assume is the actual drawing.

refulgentis
the slow part is a single call to CGContextDrawImage, so I don't think Shark can be very helpful here--will it break down what's happening within a single OS call? I'll give it a try anyway...
David Maymudes
yes it will – I'd try doing it myself but I can't figure out what code exactly is being used here :/
refulgentis
Huh, I used a modified version of Matt Long's sample code to replicate the issue, but every time I try to profile it with Shark, Shark crashes, very hard.Also, my theory that it is behaving similarly to MediaPlayer.framework is incorrect because you can do all sorts of things with the image at normal speed, except CGContextDrawImage();
refulgentis
I was able to do a time profile, basically its slow because of JPEG decompression. Read my edit to the above answer, if you'd like to see the time profiles I have them available.
refulgentis
still not sure what you're talking about here--are you saying that when a raw image comes from the camera, it's JPEG-compressed? That seems hard to believe... I see the bgrx_convert in your list, which makes it look like the OS is reordering pixel data, and again makes me wonder if there's some way to get the system to give me the un-reordered data slightly faster so I can work on it myself...
David Maymudes
As odd as it sounds, apparently yes. There's calls in the profile to [PLUICameraViewController cameraView:photoSaved:], then to [PLNotifyImagePickerOImageAvailability], and then to [ImagePickerThingViewController imagePickerController:didFinishPickingMediaWithInfo:], so it must save the image to disk in JPEG format and then tell the UIImagePickerController about it
refulgentis
that is utterly bizarre. obviously the goal is to find something I can do without having to scrounge around in the private frameworks...
David Maymudes
well, still not the answer I was hoping for, but perhaps the most new information of those who investigated.
David Maymudes
A: 

DO NOT USE CGBitmapImageContextCreate in this case! I spent almost a week in the same situation you are in. Performance will be absolutely terrible and it will eat up memory like crazy. Use UIGraphicsBeginImageContext instead:

// create a new CGImage of the desired size
UIGraphicsBeginImageContext(desiredImageSize);
CGContextRef c = UIGraphicsGetCurrentContext();

// clear the new image
CGContextClearRect(c, CGRectMake(0,0,desiredImageSize.width, desiredImageSize.height));

// draw in the image
CGContextDrawImage(c, rect, [image CGImage]);

// return the result to our parent controller
UIImage * result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

In the above example (from my own image resize code), "rect" is significantly smaller than the image. The code above runs very fast, and should do exactly what you need.

I'm not entirely sure why UIGraphicsBeginImageContext is so much faster, but I believe it has something to do with memory allocation. I've noticed that this approach requires significantly less memory, implying that the OS has already allocated space for an image context somewhere.

Ben Gotow
Just as a side note, after I discovered this I went through my code and started using UIGraphics... calls to create image contexts in lots of other places, too. You might be able to get some other speed boosts using this approach elsewhere!
Ben Gotow
I have a hard time believing this is right--I'm pretty sure UIGraphicsBeginImageContext is just a wrapper around CGBitmapImageContextCreate. What was the source of the image that you're using with the code above and seeing fast drawing?
David Maymudes
I tried using this, it doesn't lend any speed up. The slowness is from decoding the image from the camera, it has nothing to do with how the CGContext is created.
refulgentis
-1 for being reasonable sounding and well formatted but actually wrong.
David Maymudes
Also note that you can only create CGContexts like this on the main thread. If you want to compress the image in the background (e.g., to display an activity indicator in the meantime), you need to use CGBitmapContextCreate.
jasoncrawford