iOS UIImageView scaling image down produces aliased image on iPad 2 - objective-c

I am using UIImageView to display thumbnails of images that can then be selected to be viewed at full size. The UIImageView has its content mode set to aspect fit.
The images are usually scaled down from around 500px x 500px to 100px x 100px. On the retina iPad they display really well while on the iPad2 they are badly aliased until the size gets closer to the native image size.
Examples:
Original Image
Retina iPad rendering at 100px x 100px
iPad 2 rendering at 100px x 100px
The difference between iPad 2 and new iPad might just be the screen resolution or could be that the GPU is better equipped to scale images. Either way, the iPad 2 rendering is very poor.
I have tried first reducing the image size by creating a new context, setting the interpolation quality to high and drawing the image into the context. In this case, the image looks fine on both iPads.
Before I continue down the image copy/resize avenue, I wanted to check there wasn't something simpler I was missing. I appreciate that UIImage isn't there to be scaled but I was under the impression UIImageView was there to handle scaling but at the moment it doesn't seem to be doing a good job scaling down. What (if anything) am I missing?
Update: Note: The drop shadow on the rendered / resized images is added in code. Disabling this made no difference to the quality of the scaling.

Another approach I've tried that does seem to be improving things is to set the minificationFilter:
[imageView.layer setMinificationFilter:kCAFilterTrilinear]
The quality is certainly improved and I haven't noticed a performance hit.

Applying a small minification filter bias can help out with this if you don't want to resample the image yourself:
imageView.layer.minificationFilter = kCAFilterTrilinear
imageView.layer.minificationFilterBias = 0.1
The left image has no filtering applied to it. The right image has a 0.1 filter bias.
Note that no explicit rasterization is required.
Playing around with very small values, you can usually come up with a value that smooths out the scaling artifacts just enough, and it's a lot easier than resizing the bitmap yourself. Certainly, you lose detail as the bias increases, so values even less than 0.1 are probably sufficient, though it all depends on the size the image view's frame that's displaying the image.
Just realize that trilinear filtering effectively enables mipmapping on the layer, which basically means it generates extra copies of the bitmap at progressively smaller scales. It's a very common technique used in rendering to increase render speed and also reduce scaling aliasing. The tradeoff is that it requires more memory, though the memory usage for successive downsampled bitmaps reduces exponentially.
Another potential advantage to this technique, though I have not tried it myself, is that you can animate minificationFilterBias. So if you're going to be scaling an image view down quite a lot as part of an animation, consider also animating the filter bias from 0.0 to whatever small value you've determined is appropriate for the scaled down size.
Finally, as others have noted, if your source image is very large, this technique isn't appropriate if overused, because Core Animation will always keep around the original bitmap. It's better to resize the image then discard the source image instead of using mipmapping in most cases, but for one-offs or cases where your image views are going to be deallocated quickly enough, this is fine.

if you just put the large image in a small imageview it will look real bad.
the solution is to properly resize the image... i'll add an example function that does the trick:
- (UIImage *)resizeImage:(UIImage*)image newSize:(CGSize)newSize {
CGRect newRect = CGRectIntegral(CGRectMake(0, 0, newSize.width, newSize.height));
CGImageRef imageRef = image.CGImage;
UIGraphicsBeginImageContextWithOptions(newSize, NO, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
CGAffineTransform flipVertical = CGAffineTransformMake(1, 0, 0, -1, 0, newSize.height);
CGContextConcatCTM(context, flipVertical);
CGContextDrawImage(context, newRect, imageRef);
CGImageRef newImageRef = CGBitmapContextCreateImage(context);
UIImage *newImage = [UIImage imageWithCGImage:newImageRef];
CGImageRelease(newImageRef);
UIGraphicsEndImageContext();
return newImage;
}
this function might take some time .. so you might want to save the result to a cache file.

If you're not afraid of wasting memory and know what you're doing for a particular case, this works beautifully.
myView.layer.shouldRasterize = YES;
myView.layer.rasterizationScale = 2;
The resulting quality is much better than setMinificationFilter.
I am using images that are 256x256 and scaling them to something like 48 px. Obviously a saner solution here would be to downscale the images to the exact destination size.

Next helped to me:
imageView.layer.minificationFilter = kCAFilterTrilinear
imageView.layer.shouldRasterize = true
imageView.layer.rasterizationScale = UIScreen.mainScreen().scale
Keep an eye on performance if used in scroll lists.

Related

Core Animation Bar

Currently I have an image... A battery Icon...
_MeterBar = [[UIImageView alloc] initWithFrame:CGRectMake(25, 40, 50, 40)];
_MeterBar.image = [UIImage imageNamed:#"battery-empty-icon.png"];
_MeterBar.backgroundColor = [UIColor clearColor];
I am attempting to add an animation that shows the battery loading to a specific point of the battery when a value is fetched from a web server.
Fetching the value is no problem but adding the animation is an issue. I basically copied and pasted some code a user answered before but I am stuck :/. When I add the animation, there is no image.....
CABasicAnimation *scaleToValue = [CABasicAnimation animationWithKeyPath:#"transform.scale.x"];
scaleToValue.toValue = [NSNumber numberWithFloat:100.0f];
scaleToValue.fromValue = [NSNumber numberWithFloat:0];
scaleToValue.duration = 1.0f;
scaleToValue.delegate = self;
_MeterBar.layer.anchorPoint = CGPointMake(0.5, 1);
[_MeterBar.layer addAnimation:scaleToValue forKey:#"scaleRight"];
CGAffineTransform scaleTo = CGAffineTransformMakeScale(1.0f, 50.0f);
_MeterBar.transform = scaleTo;
[_PowerNowView addSubview:_MeterBar];
The battery icon is more wide than tall so I'm animating from 0 -> a percentage on the x axis.
If anyone has any hints, tips, or sample examples that does this exact animation, I would greatly appreciate your help :)
Thank you.
P.S. Heres the URL of the battery Icon i found : Battery Icon
Sorry for not being clear but I would like to have a color layer seperate of the battery icon...
I would like to create a bar over the battery image and have the bar dynamically animate with respect to the size of the battery.... not actually resize the battery image itself....
So there would be two layers, one for the battery that is mainly static in size and the other layer being the colored BAR, maybe a green color to represent the amount of the BATTERY image that is filled.
A good reference of what I would like to do is what the MINT application does, whose purpose is to record your bank statements.
The final animated battery above is very nice but I would just like to show a bar within the battery and animate it with respect to the pulled data value.
A couple of things.
Your scale of 100 would make your layer 100 times larger than normal. If it's 300 pixels on a side, a scale of 100 would display it as 30,000 pixels on a side.
You want your to scale value to 1.0, not 100.
Also, I'm not sure if scaling the transform multiplies the previous scale by a growing factor. If it does, then a starting scale value of 0 would prevent it from ever growing, since anything times zero is still zero. Try a from value of .0001 instead of 0.0,
If you want your battery image to grow from left to right, wouldn't you want your anchor point to be (0,.5) rather than (.5, 1)?
And, when you change the anchor point, it not only changes the center for transforms, but it moves the position of the image. You will need to adjust the position to compensate. I always have to scratch my head and think about it really hard (and usually get it wrong anyway) but I think you would want to shift the X position of your view to the right by half of it's width.
Note that scaling your image will cause it to stretch oddly. It will start out short and fat and stretch out to normal size.
It might be better to attach a CAShapeLayer to the view as a mask layer, and animate the shape layer's path from an empty rectangle (Width = 0, height = full height of view) to a rectangle that is the full size of the view. That would cause the view to animate in a "wipe" animation where it is revealed from left to right without stretching.

How get a High Performance UIWebView -> UIImage on ipad retina?

Have page flip type application that needs to convert the contents of a fullscreen UIWebView to a UIImage very quickly (e.g. 200ms tops). Sacrificing quality for speed is OK. Having a real tough time getting anywhere near this on a iPad3 retina.
Do create a UIImage i am doing the common renderInContext method:
UIGraphicsBeginImageContextWithOptions(frame.size, YES, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
[view.layer renderInContext:context];
UIImage *renderedImage = UIGraphicsGetImageFromCurrentImageContext();
On a ipad3 retina display I typically see 400-500ms. Interestingly the second time the method is run it is much quicker (around 100ms), which doesn't help but suggests some sort of caching is happening.
I have tried the following things:
I have tried playing with the scale and opaque parameters UIGraphicsBeginImageContextWithOptions to ever possible combination. Setting the scale to say .5 or 1.0 actually makes it even slower.
Adding CGContextSetInterpolationQuality(context, kCGInterpolationNone). No change to performance.
webview.shouldRasterize=YES. No change to performance.
Shrinking the UIWebView in half and then scaling to fullscreen . This does help but is still around 300ms.
Any other ideas? Running this on a ipad1-2 is ok - but the ipad3 retina just kills the performance.

How do I override the Points to Pixels iOS specificity and have my image drawn at the right size?

I have a 64px by 64px redSquare.png file at a 326ppi resolution. I'm drawing it at the top left corner of my View Controller's window as follows:
myImage = [UIImage imageNamed:#"redSquare.png"];
myImageView = [[UIImageView alloc] initWithImage:myImage];
[self.view addSubview:myImageView];
Given that the iPhone 4S has a screen resolution of 960x640 (326ppi) there should be enough room for 9 more squares to fit next to the first one. However there's only room for 4 more. i.e. the square is drawn larger than what it should given my measurements.
// even tried resizing UIImageView in case it was
// resizing my image to a different size, by adding
// this next line, but no success there either :
myImageView.frame = CGRectMake(0, 0, 64, 64);
I believe it has to do with the way the device is "translating" my pixels. I read about the distinction between Points Versus Pixels in Apple's documentation but it doesn't mention how one can work around this problem. I know I'm measuring in pixels. Should I be measuring in points? And how could I do that? How exactly am I to resize my image so that it can hold 9 more same-sized squares next to it (i.e. on the same horizontal..) ?
Thank you
To display an image at full resolution on a Retina display, it needs to have #2x appended to the end of its name. In practice, this means you should save the image you're currently using as redSquare#2x.png and a version of that image in 32x32 pixels as redSquare.png.
Once you have done this, there is no need to change your code. The appropriate image will be displayed depending on the device's capabilities. This will allow your app to render correctly on both Retina and non-Retina devices.

drawInRect performance too slow

What I'm doing is merge 2 images into single image.
Here is the code.
UIGraphicsBeginImageContext(CGSizeMake(1024, 768));
[image1 drawInRect:CGRectMake(0, 0, 512, 768)];
[image2 drawInRect:CGRectMake(512, 0, 512, 768)];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
But drawInRect seems too slow.
Is there faster way to merge images?
Option 0 - Make sure you draw only when you need to draw, and only what you need to draw.
Option 1
If you know the destination size (perhaps self.frame.size?), you can create one image from the two source images (flatten) at the destination size and avoid interpolation. So that could:
Reduce the memory you need
Reduce the number of images you must draw
Avoid interpolation (High CPU if you want it to look good)
Look better - the composite can use High Quality interpolation.
Of course, this only makes sense when the composite varies at a frequency lower than it must be drawn.
Option 2
Even if you want two images and you know their sizes will not change, just resize them to the size they must be drawn at (well, monitor your memory usage if you are enlarging them).
Option 3
If that's not an option, you could alter the CGContext's state, and reduce interpolation quality. If you're used to similar CALayer transformations, you would probably be satisfied with low quality or no interpolation.
One thing you could do is not implement -drawRect: at all, but instead have two CALayers with your two images as their contents, then put one in front of the other. Not sure that would be faster, but I think it's likely (since CA could then handle the drawing asynchronously, on the GPU).

UIImage drawInRect: is very slow; is there a faster way?

This does exactly what it needs to, except that it takes about 400 milliseconds, which is 350 milliseconds too much:
- (void) updateCompositeImage { //blends together the background and the sprites
UIGraphicsBeginImageContext(CGSizeMake(480, 320));
[bgImageView.image drawInRect:CGRectMake(0, 0, 480, 320)];
for (int i=0;i<numSprites;i++) {
[spriteImage[spriteType[i]] drawInRect:spriteRect[i] blendMode:kCGBlendModeScreen alpha:spriteAlpha[i]];
}
compositeImageView.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
The images are fairly small, and there are only three of them (the for loop only iterates twice)
Is there any way of doing this faster? While still being able to use kCGBlendModeScreen and alpha?
you can:
get the UIImages' CGImages
then draw them to a CGBitmapContext
produce an image from that
using CoreGraphics in itself may be faster. the other bonus is that you can perform the rendering on a background thread. also consider how you can optimize that loop and profile using Instruments.
other considerations:
can you reduce the interpolation quality?
are the source images resized in any way (it can help if you resize them)
drawInRect is slow. Period. Even in small images it's grossly inefficient.
If you are doing a lot of repeat drawing, then have a look at CGLayer, which is designed to facilitate repeat-rendering of the same bits