Often times a developer is faced with an all-too common problem: Too much information to display with not enough space to show it. For this, you turn to images. Pictures and graphics allow you to convey a variety of information far beyond simple text, combining emotion, information, and style. In iOS, you have multiple different methods with which to create, utilize, manipulate, and display images. New to iOS 5.0 even is the ability to apply filters to images, allowing for drastic alteration of display with very little actual code. By understanding these inherent functionalities and techniques in iOS, you are able to more easily implement stronger, more powerful, and more informative applications.
From the youngest age, every person is taught the most basic of images, dealing with shapes, colors, and pictures. In iOS too, you can start off with the basics of drawing simple shapes in a view. Many concepts dealt with in these first implementations will end up returning in more complex image-based recipes.
Start by creating a new project called “ShapesAndSizes”. Select the Single View Application template, as in Figure 15–1, in order to build the simplest, ready-to-run pre-configured application, and make sure the device family is set to “iPhone”. The box marked “Use Automatic Reference Counting” should also be checked. Figure 15–1.
Next, before building your user interface, you will start by implementing your custom drawing code in a subclass of UIView
.
Start by adding QuartzCore.framework
and CoreGraphics.framework
to your project by navigating to your project’s Target settings. Under the Build Phases tab, in the Link Binary With Libraries section, click the + button. Find the Quartz Core and Core Graphics frameworks in the window resembling Figure 15–2, and add them both separately.
Next, create new file called “MyView” using the “Objective-C class” template. When you enter the name, make sure the “Subclass of” field is set to “UIView”.
In the header file of this new class, add the required two import statements for your extra frameworks.
#import <QuartzCore/QuartzCore.h>
#import <CoreGraphics/CoreGraphics.h>
Now, to provide a simple view displaying a drawn rectangle and square, you will implement the -drawRect:
method as such:
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGRect drawingRect = CGRectMake(0.0, 20.0f, 100.0f, 180.0f);
const CGFloat *rectColorComponents = CGColorGetComponents([[UIColor greenColor] CGColor]);
CGContextSetFillColor(context, rectColorComponents);
CGContextFillRect(context, drawingRect);
CGRect ellipseRect = CGRectMake(140.0f, 200.0f, 75.0f, 50.0f);
const CGFloat *ellipseColorComponenets = CGColorGetComponents([[UIColor blueColor] CGColor]);
CGContextSetFillColor(context, ellipseColorComponenets);
CGContextFillEllipseInRect(context, ellipseRect);
}
This method makes use of the following steps to draw basic shapes:
CGContextRef
.CGRect
in which to draw.CGContextFillRect()
and CGContextFillEllipseInRect()
functions.In order to actually display this in your pre-configured view, you must add an instance of this class to your user interface. This can be done programmatically or through Interface Builder (the latter of which you will demonstrate).
In your view controller’s XIB file, drag a UIView
out from the utilities pane into your view, placing it with 20-point margins on each edge, as shown in Figure 15–3.
While your UIView
is selected, navigate to the third tab in the right-hand panel. Under the Custom Class section, make sure the Class field is changed from “UIView” to “MyView”, in order to specify the custom UIView
subclass to be used, resembling Figure 15–4.
Upon running this application, you will see the output of your drawing commands converted into a visual display, resulting in your simulated view in Figure 15–5.
Thankfully, you are not at all limited to drawing only rectangles and ellipses! You are able to use a few other functions to draw custom objects by creating “paths.” These paths consist of a movement from point to point, connected by lines, in order to draw a custom shape. Add the following code to your -drawRect:
method to draw a gray parallelogram.
CGContextBeginPatsh(context);
CGContextMoveToPoint(context, 0.0f, 0.0f);
CGContextAddLineToPoint(context, 100.0f, 0.0f);
CGContextAddLineToPoint(context, 140.0f, 100.0f);
CGContextAddLineToPoint(context, 40.0f, 100.0f);
CGContextClosePath(context);
CGContextSetGrayFillColor(context, 0.4f, 0.85f);
CGContextSetGrayStrokeColor(context, 0.0, 0.0);
CGContextFillPath(context);
It is important to note that when creating these paths, you do not have to add a final line back to your last point. By calling the CGContextClosePath()
function, your shape will automatically be closed between its ending point and starting point.
When you run your application now, you will see your view with a new parallelogram created from your path, as in Figure 15–6.
Just as you are able to put things into a CGContext
, you are also quite easily able to take them out. By making use of the UIGraphicsGetImageFromCurrentImageContext()
function, you can extract an image from whatever is currently drawn.
In the MyView
class, add a UIImage
property, making sure to synthesize it.
@property (nonatomic, strong) UIImage *image;
Now at the end of your -drawRect:
method, append the following code to draw the image if it is non-nil
.
if (self.image)
{
CGRect imageRect = CGRectMake(200.0f, 50.0f, 100.0f, 300.0f);
[image drawInRect:imageRect];
}
Add the same two import statements as earlier to your view controller, as well as a third for your MyView
class.
#import <QuartzCore/QuartzCore.h>
#import <CoreGraphics/CoreGraphics.h>
#import "MyView.h"
You will also need to reference your MyView
object, so connect the one you already added into the user interface to your view controller’s header file with the property customView
.
@property (strong, nonatomic) IBOutlet MyView *customView;
Add a UIButton
labeled “Snapshot” to the bottom of your user interface. Connect it to an IBAction
method called -snapShotPressed:
.
- (IBAction)snapshotPressed:(id)sender;
This method will then be implemented like so:
-(IBAction)snapshotPressed:(id)sender
{
//Acquire image of current layer
UIGraphicsBeginImageContext(self.view.bounds.size);
CGContextRef context = UIGraphicsGetCurrentContext();
[self.view.layer renderInContext:context];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
self.customView.image = image;
[self.customView setNeedsDisplay];
}
This method makes use of the -setNeedsDisplay
method in the UIView
class to instruct a UIView
to re-call its -drawRect
method in order to incorporate any recent changes.
Now, after testing the application again, upon pressing the Snapshot button, you should see a smaller screenshot of your own screen appear on the right side of the view, as in Figure 15–7.
While most of the functionalities you’ve built into this application have been pretty basic, you will see a variety of them come back in more complex forms in the later, more complex image recipes.
The absolutely simplest way of displaying an image in your application is by use of the UIImageView
class. You will start off by creating a simple application that can display an image chosen by the user, and then build on top of it to take full advantage of iOS’s image processing power.
In order to enhance the functionality of your application, you will specifically design it for the iPad, and then make use of the UISplitViewController
. Create a new project, and select the Master-Detail Application template. On the next screen, after entering the project name “ImageRecipes”, make sure the application’s device-family is set to “iPad”, and that the box marked “Use Automatic Reference Counting” is checked. No other boxes should be marked, so that your dialog resembles Figure 15–8.
Upon creating your application, you will be given a nicely configured project with a UISplitViewController
already set up with master and detail view controllers. If your simulator or device is in portrait mode, you will see only the view of the detail view controller, but if you rotate to landscape then you will get a nice mix of both views. You will not see both views when working in Interface Builder, but if you simulate the app, the generic view will resemble Figure 15–9.
Now, you will configure the detail view controller to include a bit more content. Add a UIImageView
, as well as two UIButton
s to your XIB’s interface so that your simulated application will look like Figure 15–10.
In order to make sure that both your buttons and image view will be correctly centered in your final display, first set them to the center of your XIB’s view as you normally would. Then, you will set the “autosizing” options for each element. Open up the utilities pane on the right side of your screen, and navigate to the fifth tab where you can set an element’s frame, shown in Figure 15–11.
Make sure that for each element, the Autosizing box, with its various red lines and arrows, is configured exactly as shown in Figure 15–11. This will specify that each element will, if the view changes, maintain its relative position to the top of the view and stretch horizontally to maintain its general position.
Connect each element to your header file using the property names selectImageButton
, clearImageButton
, and imageViewContent
. Each UIButton
will have its respective action, -selectImagePressed:
and -clearImagePressed:
.
You will be configuring your application to display a UIPopoverController
containing a UIImagePickerController
in order to allow users to select an image from their phone. To do this, you will need your detail view controller to conform to several extra protocols: UIImagePickerControllerDelegate
, UINavigationControllerDelegate
and UIPopoverControllerDelegate
.
@interface MainDetailViewController : UIViewController<UISplitViewControllerDelegate,
UIImagePickerControllerDelegate, UINavigationControllerDelegate,
UIPopoverControllerDelegate>
You will also create two extra properties in order to store your selected image, as well as a reference to your UIPopoverController
that you will use. Make sure both of these are properly synthesized, as well as properly nullified in the -viewDidUnload
method.
@property (strong, nonatomic) UIPopoverController *pop;
@property (strong, nonatomic) UIImage *selectedImage;
At this point, with your basic user interface configured, your overall detail view controller’s header file should resemble the following.
#import <UIKit/UIKit.h>
@interface MainDetailViewController : UIViewController<UISplitViewControllerDelegate,
UIImagePickerControllerDelegate, UIPopoverControllerDelegate,
UINavigationControllerDelegate>
@property (strong, nonatomic) id detailItem;
@property (strong, nonatomic) IBOutlet UILabel *detailDescriptionLabel;
@property (strong, nonatomic) IBOutlet UIButton *selectImageButton;
@property (strong, nonatomic) IBOutlet UIButton *clearImageButton;
@property (strong, nonatomic) IBOutlet UIImageView *imageViewContent;
-(IBAction)selectImagePressed:(id)sender;
-(IBAction)clearImagePressed:(id)sender;
@property (strong, nonatomic) UIPopoverController *pop;
@property (strong, nonatomic) UIImage *selectedImage;
@end
Now you can implement your -selectImagePressed:
method to present an interface to select a saved image to display.
-(void)selectImagePressed:(UIButton *)sender
{
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary])
{
picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
picker.delegate = self;
self.pop = [[UIPopoverController alloc] initWithContentViewController:picker];
pop.delegate = self;
[pop presentPopoverFromRect:sender.frame inView:self.view permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}
}
You can then implement your UIImagePickerControllerDelegate
protocol methods to properly handle the selection of an image or cancellation.
-(void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
{
[self.pop dismissPopoverAnimated:YES];
}
-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
UIImage *image = [info valueForKey:@"UIImagePickerControllerOriginalImage"];
self.selectedImage = image;
self.imageViewContent.image = image;
self.imageViewContent.contentMode = UIViewContentModeScaleAspectFill;
[self.pop dismissPopoverAnimated:YES];
}
As you can see, you configure your selected image to be displayed in your UIImageView
by using the image
property. You also set the contentMode
property to UIViewContentModeScaleAspectFill
, in order to ensure that the bounds of your UIImageView
are always filled by at least most of the image.
Finally, you can implement a simple method for -clearImagePressed:
to allow your view to be reset:
- (IBAction)clearImagePressed:(id)sender
{
self.selectedImage = nil;
self.imageViewContent.image = nil;
}
At this point, you can run your application, select an image, and display it in a UIImageView
, as in Figure 15–12!
If you are testing this application on the iOS simulator, you will need to actually have some images saved to display. The easiest way to save images to the simulator’s photo library is to use the Safari app on the simulator, navigate to your desired image, and then click and hold the mouse on the image. You will be given an option to save the image, and after this you can use it in your application.
Often the images that your applications have to deal with can come from a variety of sources, and usually will not fit your specific view’s display perfectly. To adjust for this, you can implement methods to scale and resize your images.
You will configure your application to overall have a single image selected in your first detail view controller. Upon selecting different rows in your master view controller’s table, you will change the display in your detail view controller to a variety of images. For now, you will configure your views to display your images as they are resized differently.
You will start off by creating a method to fully adjust the content of your detail view controller. Add the following handler to your detail view controller’s header file.
-(void)configureDetailsWithImage:(UIImage *)image label:(NSString *)label showsButtons:(BOOL)showButton;
You will implement this method like so:
-(void)configureDetailsWithImage:(UIImage *)image label:(NSString *)label showsButtons:(BOOL)showsButton
{
self.selectedImage = image;
self.imageViewContent.image = image;
self.detailDescriptionLabel.text = label;
if (showsButton == NO)
{
self.selectImageButton.enabled = NO;
self.selectImageButton.hidden = YES;
self.clearImageButton.enabled = NO;
self.clearImageButton.hidden = YES;
}
else if (showsButton == YES)
{
self.selectImageButton.enabled = YES;
self.selectImageButton.hidden = NO;
self.clearImageButton.enabled = YES;
self.clearImageButton.hidden = NO;
}
}
You will add a reference to the master view controller to your detail view controller to allow your chosen image to be passed back. Add the following import statement (or your own class name for the master view controller):
#import "MainMasterViewController.h"
Add the master view controller property, and make sure to synthesize it.
@property (strong, nonatomic) MainMasterViewController *masterViewController;
Add the following line to your -viewDidUnload
method.
[self setMasterViewController:nil];
Next, you will add a property to your master view controller class to store the chosen image. Make sure to properly synthesize and handle it as usual.
@property (strong, nonatomic) UIImage *mainImage;
Back in your detail view controller, you will update your -imagePickerController:didFinishPickingMediaWithInfo:
method to also send the chosen image back to the master view controller.
-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
UIImage *image = [info valueForKey:@"UIImagePickerControllerOriginalImage"];
self.selectedImage = image;
//New Line
self.masterViewController.mainImage = image;
self.imageViewContent.image = image;
self.imageViewContent.contentMode = UIViewContentModeScaleAspectFill;
[self.pop dismissPopoverAnimated:YES];
}
You will also adjust the implementation of your -clearImagePressed:
method accordingly.
- (IBAction)clearImagePressed:(id)sender
{
self.selectedImage = nil;
self.imageViewContent.image = nil;
self.masterViewController.mainImage = nil;
}
In your master view controller, you will later implement code to utilize your images in the actual table, so you will implement a custom setter method for the mainImage
property to reload the UITableView
’s data.
-(void)setMainImage:(UIImage *)image
{
mainImage = image;
NSIndexPath *currentIndexPath = self.tableView.indexPathForSelectedRow;
[self.tableView reloadData];
[self.tableView selectRowAtIndexPath:currentIndexPath animated:YES scrollPosition:UITableViewScrollPositionTop];
}
Next, you will create two different methods to resize an image. Add the following two handlers to your detail view controller’s header file.
+ (UIImage *)scaleImage:(UIImage *)image toSize:(CGSize)size;
+ (UIImage *)aspectScaleImage:(UIImage *)image toSize:(CGSize)size;
The first method will simply recreate the image within a specified size, completely ignoring the aspect ratio of the image.
+ (UIImage *)scaleImage:(UIImage *)image toSize:(CGSize)size
{
UIGraphicsBeginImageContext(size);
[image drawInRect:CGRectMake(0, 0, size.width, size.height)];
UIImage *scaledImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return scaledImage;
}
The second will, with a little calculation, determine the best way to resize the image in order to both preserve the aspect ratio and fit inside the given size.
+ (UIImage *)aspectScaleImage:(UIImage *)image toSize:(CGSize)size
{
UIGraphicsBeginImageContext(size);
if (image.size.height< image.size.width)
{
float ratio = size.height/image.size.height;
[image drawInRect:CGRectMake(0, 0, image.size.width*ratio, size.height)];
}
else
{
float ratio = size.width/image.size.width;
[image drawInRect:CGRectMake(0, 0, size.width, image.size.height*ratio)];
}
UIImage *aspectScaledImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return aspectScaledImage;
}
To make sure your view controllers are properly interacting, add the following two lines to your application delegate’s -application:didFinishLaunchingWithOptions:
after both view controllers have been created.
detailViewController.masterViewController = masterViewController;
masterViewController.detailViewController = detailViewController;
Now, to finish configuring the behavior of the master view controller, modify the following delegate methods:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if (self.mainImage == nil)
{
return 1;
}
else
{
return 3;
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
if (indexPath.row == 0)
cell.textLabel.text = NSLocalizedString(@"Selected Image", @"Detail");
else if (indexPath.row == 1)
cell.textLabel.text = NSLocalizedString(@"Resized Image", @"Detail");
else if (indexPath.row == 2)
cell.textLabel.text = NSLocalizedString(@"Scaled Image", @"Detail");
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath
*)indexPath
{
if (self.mainImage != nil)
{
UIImage *image;
NSString *label;
BOOL showsButtons;
if (indexPath.row == 0)
{
image = self.mainImage;
label = @"Select an Image to Display";
showsButtons = YES;
}
else if (indexPath.row == 1)
{
image = [MainDetailViewController scaleImage:self.mainImage toSize:self.detailViewController.imageViewContent.frame.size];
label = @"Chosen Image Resized";
showsButtons = NO;
}
else if (indexPath.row == 2)
{
image = [MainDetailViewController aspectScaleImage:self.mainImage toSize:self.detailViewController.imageViewContent.frame.size];
label = @"Chosen Image Scaled";
showsButtons = NO;
}
[self.detailViewController configureDetailsWithImage:image label:label showsButtons:showsButtons];
}
}
Your original image, as shown previously, ended up expanding beyond the frame of the UIImageView
in order to maintain its aspect ratio. Since this can cause some issues in blocking other elements, you would most likely want to use one of your resized images. Upon running this application, you can see the quite vast differences in your options in presenting differently sized images.
Figure 15–13 displays an example of the same image used previously, but resized simply to fit within your image view’s frame.
As you can see, you have managed to fit the entire image into a smaller space, ensuring that no other view elements are obstructed by your image. The issue with this option, however, is that your image’s dimensions have been changed, resulting in a slightly deformed picture. This may not be quite obvious with this particular image, but when dealing with images of people, the distortion of physical features will become quite obvious and unsightly. To solve this, you make use of the “aspect-scaled” image, as displayed in Figure 15–14.
Compared to the previous and original images, you can see that this image is clearly of higher quality, as it lacks any distortion of size. Unfortunately, since you have chosen to have your image “fill” the UIImageView
, you end up with an image cropped from your original. This kind of resizing works incredibly well if you need to create small thumbnails from larger images, as the issue of cropping the image becomes fairly negligible with more miniscule sizes.
Obviously, if you are not dealing with thumbnails, but instead with presenting large pictures, then the above cropping is significantly less than ideal. You can hone your image scaling method to allow your image to “fit” the image view, and simply give the rest of the view a black background. With this change, your previous “thumbnail creation method” will no longer be functional, so be sure to save a new copy of your project if you want to keep a copy of the previous setup.
First, adjust your configuration method to the following code in order to change the contentMode
property of your UIImageView
.
-(void)configureDetailsWithImage:(UIImage *)image label:(NSString *)label
showsButtons:(BOOL)showsButton
{
self.selectedImage = image;
self.imageViewContent.image = image;
self.detailDescriptionLabel.text = label;
/////BEGIN NEW CODE
if ([label isEqualToString:@"Chosen Image Scaled"])
{
self.imageViewContent.contentMode = UIViewContentModeScaleAspectFit;
self.imageViewContent.backgroundColor = [UIColor blackColor];
}
else
{
self.imageViewContent.contentMode = UIViewContentModeScaleAspectFill;
}
/////END NEW CODE
if (showsButton == NO)
{
self.selectImageButton.enabled = NO;
self.selectImageButton.hidden = YES;
self.clearImageButton.enabled = NO;
self.clearImageButton.hidden = YES;
}
else if (showsButton == YES)
{
self.selectImageButton.enabled = YES;
self.selectImageButton.hidden = NO;
self.clearImageButton.enabled = YES;
self.clearImageButton.hidden = NO;
}
}
Your image scaling method will require a slightly different method of calculation to correctly scale your images this way. The following code shows the new implementation. Pay careful attention to the fact that you have changed the CGSize
with which you are creating your image context.
+ (UIImage *)aspectScaleImage:(UIImage *)image toSize:(CGSize)size
{
if (image.size.height< image.size.width)
{
float ratio = size.height/image.size.height;
CGSize newSize = CGSizeMake(image.size.width*ratio, size.height);
UIGraphicsBeginImageContext(newSize);
[image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
}
else
{
float ratio = size.width/image.size.width;
CGSize newSize = CGSizeMake(size.width, image.size.height*ratio);
UIGraphicsBeginImageContext(newSize);
[image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
}
UIImage *aspectScaledImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return aspectScaledImage;
}
With the newest changes, your images can now appear scaled down in size without being clipped! By adding the black background to your UIImageView
, you provide a simple backdrop for your images to go on, allowing a very general, all-encompassing functionality for displaying resized images, as in Figure 15–15.
You have covered three simple yet different methods for resizing a UIImage
, each with their own advantages and issues.
UIImageView
. Since this would, of course, leave blank space around the image, you applied a black background. This is an especially useful technique to use when displaying large images in an application that has no control over the original image size. It allows for any image to be comfortably fit in a given space, yet maintains a visually appealing black background no matter the case.The Core Image framework, a group of classes entirely new in iOS 5.0, allows you to creatively apply a great variety of different types of “filters” to images.
Start by importing the CoreImage.framework
library into your project. Navigate to your application’s Build Phases tab, and then click the + button under the Link Binary With Libraries area. In the dialog resembling Figure 15–16, find the Core Image framework, and add it.
Add an import statement for this framework to your main view controller’s header file.
#import <CoreImage/CoreImage.h>
Next add an NSMutableArray
property to the same controller in order to store the filtered images you will display. Make sure to properly synthesize and handle it as usual.
@property (strong, nonatomic) NSMutableArray *images;
This property will also need a custom getter to ensure it is properly initialized.
-(NSMutableArray *)images
{
if (!images)
{
images = [[NSMutableArray alloc] initWithCapacity:3];
}
return images;
}
Now, you will modify your -setMainImage:
method again to include proper handling of this array.
-(void)setMainImage:(UIImage *)image
{
[self.images removeAllObjects];
if (image != nil)
{
[self.images addObject:image];
[self populateImagesWithImage:image];
}
mainImage = image;
NSIndexPath *currentIndexPath = self.tableView.indexPathForSelectedRow;
[self.tableView reloadData];
[self.tableView selectRowAtIndexPath:currentIndexPath animated:YES scrollPosition:UITableViewScrollPositionTop];
}
This method, -populateImagesWithImage:
, which will contain most of your Core Image code, will be implemented as follows. Remember to place the method declaration in your header file as well.
-(void)populateImagesWithImage:(UIImage *)image
{
CIImage *main = [[CIImage alloc] initWithImage:image];
CIFilter *hueAdjust = [CIFilter filterWithName:@"CIHueAdjust"];
[hueAdjust setDefaults];
[hueAdjust setValue:main forKey:@"inputImage"];
[hueAdjust setValue:[NSNumber numberWithFloat: 3.14/2.0f]
forKey:@"inputAngle"];
CIImage *outputHueAdjust = [hueAdjust valueForKey:@"outputImage"];
CIContext *context = [CIContext contextWithOptions:nil];
UIImage *outputImage1 = [UIImage imageWithCGImage:[context createCGImage:outputHueAdjust
fromRect:outputHueAdjust.extent]];
[self.images addObject:outputImage1];
CIFilter *strFilter = [CIFilter filterWithName:@"CIStraightenFilter"];
[strFilter setDefaults];
[strFilter setValue:main forKey:@"inputImage"];
[strFilter setValue:[NSNumber numberWithFloat:3.14f] forKey:@"inputAngle"];
CIImage *outputStr = [strFilter valueForKey:@"outputImage"];
UIImage *outputImage2 = [UIImage imageWithCGImage:[context createCGImage:outputStr fromRect:outputStr.extent]];
[self.images addObject:outputImage2];
}
As you can see from this method, creating a CIImage
requires the following steps:
CIImage
of the intended input image.CIImage
using the “outputImage” key.UIImage
from the CIImage
by use of a CIContext
.Here, you have chosen to apply two different filters: a “Hue Adjustment”, and a “Straighten Filter”. The former will change the hue of your image, while the latter is used to rotate an image to straighten it out.
NOTE:There are an incredibly large number of filters that can be applied to images, all with their own specific parameters and keys. In order to find details for a specific filter, use the Apple documentation at http://developer.apple.com/library/ios/#DOCUMENTATION/GraphicsImaging/Reference/CoreImageFilterReference/Reference/reference.html
.
Now, you can specify these newly created filtered images to your view controller by modifying your -tableView:didSelectRowAtIndexPath:
method again.
(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath
*)indexPath
{
if (self.mainImage != nil)
{
UIImage *image;
NSString *label;
BOOL showsButtons;
if (indexPath.row == 0)
{
image = self.mainImage;
CGSize contentSize = self.detailViewController.imageViewContent.frame.size;
image = [MainDetailViewController aspectScaleImage:image toSize:contentSize];
label = @"Select an Image to Display";
showsButtons = YES;
}
else
{
image = [self.images objectAtIndex:indexPath.row];
CGSize contentSize = self.detailViewController.imageViewContent.frame.size;
image = [MainDetailViewController aspectScaleImage:image toSize:contentSize];
showsButtons = NO;
if (indexPath.row == 1)
{
label = @"Hue Adjustment";
}
else if (indexPath.row == 2)
{
label = @"Straightening Filter";
}
}
[self.detailViewController configureDetailsWithImage:image label:label showsButtons:showsButtons];
}
}
As you can see, you have updated all of your displays to, instead of showing differently resized examples of the same image, show the original image with its different filters. You have adopted your third method of resizing images for all these to be displayed.
You will also update your -tableView:cellForRowAtIndexPath:
to include your newest implementation.
-(UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
if (indexPath.row == 0)
cell.textLabel.text = NSLocalizedString(@"Selected Image", @"Detail");
else if (indexPath.row == 1)
cell.textLabel.text = NSLocalizedString(@"Hue Adjust", @"Detail");
else if (indexPath.row == 2)
cell.textLabel.text = NSLocalizedString(@"Straighten Filter", @"Detail");
return cell;
}
Back in your detail view controller, you must make a few extra changes to fully configure your new functionalities.
You will adjust your configuration method to a more general case, now that you are formatting all of your display images similarly.
-(void)configureDetailsWithImage:(UIImage *)image label:(NSString *)label showsButtons:(BOOL)showsButton
{
self.selectedImage = image;
self.imageViewContent.image = image;
self.detailDescriptionLabel.text = label;
self.imageViewContent.contentMode = UIViewContentModeScaleAspectFit;
self.imageViewContent.backgroundColor = [UIColor blackColor];
if (showsButton == NO)
{
self.selectImageButton.enabled = NO;
self.selectImageButton.hidden = YES;
self.clearImageButton.enabled = NO;
self.clearImageButton.hidden = YES;
}
else if (showsButton == YES)
{
self.selectImageButton.enabled = YES;
self.selectImageButton.hidden = NO;
self.clearImageButton.enabled = YES;
self.clearImageButton.hidden = NO;
}
}
Finally, your UIImagePickerControllerDelegate
protocol method will also require some adjustment. The new code will resemble the following:
-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
UIImage *image = [info valueForKey:@"UIImagePickerControllerOriginalImage"];
self.selectedImage = image;
self.masterViewController.mainImage = image;
CGSize contentSize = self.imageViewContent.frame.size;
self.imageViewContent.image = [MainDetailViewController aspectScaleImage:image toSize:contentSize];
self.imageViewContent.contentMode = UIViewContentModeScaleAspectFit;
self.imageViewContent.backgroundColor = [UIColor blackColor];
[self.pop dismissPopoverAnimated:YES];
}
Upon running your application now, you will be able to see the outputs of your two types of filters used.
Shown in Figure 15–17 is the example of your hue adjustment. Your chosen input angle of 3.14/2.0 will drastically change the hues of your image.
In the same application, your second filter, with its specified input angle of 3.14, will rotate your given image by 180 degrees, as is done in Figure 15–18.
You are also quite easily able to combine multiple filters in series by simply specifying the output image of one filter as the input image of another. Add the following code to the -populateImagesWithImage:
method to create a combination filter.
CIFilter *seriesFilter = [CIFilter filterWithName:@"CIStraightenFilter"];
[seriesFilter setDefaults];
[seriesFilter setValue:outputHueAdjust forKey:@"inputImage"];
[seriesFilter setValue:[NSNumber numberWithFloat:3.14/2.0f] forKey:@"inputAngle"];
CIImage *outputSeries = [seriesFilter valueForKey:@"outputImage"];
UIImage *outputImage3 = [UIImage imageWithCGImage:[context createCGImage:outputSeries
fromRect:outputSeries.extent]];
[self.images addObject:outputImage3];
Update your -tableView:numberOfRowsInSection:
method to show a fourth cell.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if (self.mainImage == nil)
{
return 1;
}
else
{
return 4;
}
}
Add a fourth case to your -tableView:cellForRowAtIndexPath:
method to display the name of this fourth cell.
else if (indexPath.row == 3)
cell.textLabel.text = NSLocalizedString(@"Series Filter", @"Detail");
Finally, add another case, directly after the others used to set label
for rows 1 and 2, to your -tableView:didSelectRowAtIndexPath:
to correctly set the UILabel
.
else if (indexPath.row == 3)
{
label = @"Series Filter";
}
Now, upon testing the application, your new double-filter will combine the effects of your previous two, resulting in a hue-adjusted and rotated image, as in Figure 15–19.
NOTE: The majority of the processing work, when dealing with the Core Image framework, comes from when the UIImage
is created from the CIImage
using the CIContext
. The creation of a CIImage
itself is a very fast operation. In your application, you have chosen to create all of your filtered images at once in order to allow for quick navigation between each display. This is why, upon selecting an image, your simulator may take a couple seconds to actually display your images and refresh. If you were building this application for release, you would want to convey in some way to the user that work is being done through a UIActivityIndicatorView
or UIProgressView
.
Now, you will make use of your earlier resizing functionality to create a thumbnail for each filtered image, so that it can be displayed in your master’s UITableView
.
Since you ended up taking your thumbnail-resizing method out of your previous recipe, here is the method again that you will use.
+ (UIImage *)scaleImageThumbnail:(UIImage *)image toSize:(CGSize)size
{
UIGraphicsBeginImageContext(size);
if (image.size.height< image.size.width)
{
float ratio = size.height/image.size.height;
[image drawInRect:CGRectMake(0, 0, image.size.width*ratio, size.height)];
}
else
{
float ratio = size.width/image.size.width;
[image drawInRect:CGRectMake(0, 0, size.width, image.size.height*ratio)];
}
UIImage *aspectScaledImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return aspectScaledImage;
}
Make sure also to place this method’s handler in the detail view controller’s header file, so that your master view controller is able to call it.
+ (UIImage *)scaleImageThumbnail:(UIImage *)image toSize:(CGSize)size;
Now, you just need to modify your -tableView:cellForRowAtIndexPath:
again to include the selection of an image for the cell’s imageView
. In entirety, the method should resemble the following code.
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
/////NEW THUMBNAIL CODE
if ([self.images count] >0)
{
CGSize thumbnailSize = CGSizeMake(120, 75);
UIImage *displayImage = [self.images objectAtIndex:indexPath.row];
if (displayImage)
{
UIImage *thumbnailImage = [MainDetailViewController scaleImageThumbnail:displayImage toSize:thumbnailSize];
cell.imageView.image = thumbnailImage;
}
}
/////END OF THUMBNAIL CODE
if (indexPath.row == 0)
cell.textLabel.text = NSLocalizedString(@"Selected Image", @"Detail");
else if (indexPath.row == 1)
cell.textLabel.text = NSLocalizedString(@"Hue Adjust", @"Detail");
else if (indexPath.row == 2)
cell.textLabel.text = NSLocalizedString(@"Straighten Filter", @"Detail");
else if (indexPath.row == 3)
cell.textLabel.text = NSLocalizedString(@"Series Filter", @"Detail");
return cell;
}
When you test your application now, your master view controller’s cells will each have a scaled thumbnail version of the larger image they refer to, as in Figure 15–20.
Along with the incredibly flexible use of filters, the Core Image framework has also brought to iOS 5.0 the possibility of feature detection, allowing you to effectively “search” images for key components, such as faces.
We will develop a new, smaller project to implement your new facial detection application, rather than continuing on with your previous recipe. Create a new project for the iPhone device family, making use of the Single View Application template, shown in Figure 15–21.
Once your project is created, add the Core Image framework to your project, just as in the previous recipe.
Add two instances of UIImageView
and a UIButton
to your view, so as to resemble Figure 15–22.
Connect each of the elements to your view controller. Your UIButton
should have the property name findFaceButton
, and perform the action -findFacePressed:
. Make your upper UIImageView
imageViewMain
, and your lower one imageViewAlt
.
#import <UIKit/UIKit.h>
@interface MainViewController : UIViewController
@property (strong, nonatomic) IBOutlet UIImageView *imageViewMain;
@property (strong, nonatomic) IBOutlet UIImageView *imageViewAlt;
@property (strong, nonatomic) IBOutlet UIButton *findFaceButton;
- (IBAction)findFacePressed:(id)sender;
@end
Next, find an image to be displayed in your application, and add it to your project. You can do this by dragging the file from the Finder into your project’s navigation pane. When the dialog for adding files appears, make sure the box marked “Copy items into destination group’s folder (if needed)” is checked, as it is in Figure 15–23. In order to properly test this application, try to find an image with an easily visible face.
Now, you can build your -viewDidLoad
method to configure your UIImageView
elements, as well as set the initial image to your main image view. Make sure to change the name of the image (“testImage.JPG” in the following code) to your own file name.
- (void)viewDidLoad
{
[super viewDidLoad];
self.imageViewMain.backgroundColor = [UIColor blackColor];
self.imageViewMain.contentMode = UIViewContentModeScaleAspectFit;
self.imageViewAlt.backgroundColor = [UIColor blackColor];
self.imageViewAlt.contentMode = UIViewContentModeScaleAspectFit;
UIImage *image = [UIImage imageNamed:@"testImage.JPG"];
if (image != nil)
{
self.imageViewMain.image = image;
}
else
{
[self.findFaceButton setTitle:@"No Image" forState:UIControlStateNormal];
self.findFaceButton.enabled = NO;
self.findFaceButton.alpha = 0.6;
}
}
Finally you can implement your -findFacePressed:
method to do your feature detection. You will have this method determine the location of any faces in your given image, create a UIImage
from the last face found, and then display it in your alternate image view.
-(IBAction)findFacePressed:(id)sender
{
UIImage *image = self.imageViewMain.image;
CIImage *coreImage = [[CIImage alloc] initWithImage:image];
CIContext *context = [CIContext contextWithOptions:nil];
CIDetector *detector = [CIDetector detectorOfType:@"CIDetectorTypeFace"context:context
options:[NSDictionary dictionaryWithObjectsAndKeys:@"CIDetectorAccuracyHigh",
@"CIDetectorAccuracy", nil]];
NSArray *features = [detector featuresInImage:coreImage];
if ([features count] >0)
{
CIImage *faceImage = [coreImage imageByCroppingToRect:[[features lastObject] bounds]];
UIImage *face = [UIImage imageWithCGImage:[context createCGImage:faceImage fromRect:faceImage.extent]];
self.imageViewAlt.image = face;
[self.findFaceButton setTitle:[NSString stringWithFormat:@"%i Face(s) Found", [features count]] forState:UIControlStateNormal];
self.findFaceButton.enabled = NO;
self.findFaceButton.alpha = 0.6;
}
else
{
[self.findFaceButton setTitle:@"No Faces Found"forState:UIControlStateNormal];
self.findFaceButton.enabled = NO;
self.findFaceButton.alpha = 0.6;
}
}
This method contains the following steps:
CIImage
object from your initial UIImage
.CIContext
with which to analyze images.CIDetector
with type and options parameters.
type
parameter specifies the specific feature to identify. Currently, the only possible value for this is CIDetectorTypeFace
, which allows you to specifically look for faces.options
parameter allows you to specify the accuracy with which you want to look for features. Low accuracy will be faster, but high accuracy will be more precise.CIDetectorTypeFace
type, these objects will all be instances of the CIFaceFeature
class.CIImage
using the -imageByCroppingToRect:
method with the original image, as well as the bounds specified by the last CIFaceFeature
found in the image. These bounds specify the CGRect
in which the face exists.UIImage
out of your CIImage
(done exactly as in the previous recipe), and then display it in your UIImageView
.Upon running your application, you will be able to detect any faces inside your images, the latter of which will be displayed in your lower UIImageView
, as in Figure 15–24.
Images create our world. From the simplest of picture books that children love to read to the massive amounts of visual data transmitted around the Internet through Twitter, e-mail, Facebook, Tumblr, and every other web service, pictures and images have certainly become one of the key foundations of modern culture. As such, by learning to create, handle, manipulate, and display images in your applications, you are able to acquire a significantly greater connection with your users, imparting a more powerful emotional response and interacting with more flexibility and control than ever. From building colored shapes to displaying photographs, to even the newest iOS 5.0 additions of manipulating images, you are able to create more interesting and useful applications. The Core Image framework even furthers this ability with the addition of image filters, to create wildly different images, and facial detection software, to provide more in-depth information from your application. At this point of technological development, it seems quite fair to say that a picture being worth a thousand words is a vast understatement.