Showing posts with label image unit. Show all posts
Showing posts with label image unit. Show all posts

Apr 10, 2007

Cocoa application with custom Core Image filter 5: calling the filter from a Cocoa app.

Last time in the village we packaged our convolution filter as an image unit. Since the filter was executable, we had to develop an Objective-C class that inherited from CIFilter and implemented a few methods. This time we will see how we can call our image unit from a Cocoa application. This technique will generalize to any other Image Unit in the system, built in or otherwise.

For the purposes of testing, I've created a simple Cocoa application called Convolver. As you can see, Convolver has one main window with two NSImageViews and a separate pallette with a 3x3 NSMatrix of coefficients. Dropping an image on the left view causes the right view to update with a processed version of the left image. Changing the coefficients immediately causes the filter to run and change the rightmost image.

The structure of this application is pretty simple. There is an application controller called ConvolverController and a very simple model (Convolver) which only takes an unprocessed NSImage with an array of coeficients and returns a processed one. Taking a look at the nib file there are the user interface objects, the controller and the model all instantiated in the nib.

The inspector for the ConvolverController object shows the outlets for the controller.

The sourceImage, convolutionMatrix and the File->Open menu all have their targets set to the convolve: method of the controller, so that opening an image or changing the coefficients of the matrix will cause the image to be processed again.

The Convolution controller class is very simple. Here's the interface for that class

/* ConvolverController */

#import <Cocoa/Cocoa.h>
#import “Convolver.h“

@interface ConvolverController : NSObject
{
IBOutlet NSMatrix *convolutionMatrix;
IBOutlet Convolver *convolver;
IBOutlet NSImageView *resultImage;
IBOutlet NSImageView *sourceImage;
IBOutlet NSWindow *window;
}
- (IBAction)convolve:(id)sender;
- (IBAction)openImage:(id)sender;
@end

and here's the implementation:

#import “ConvolverController.h“

@implementation ConvolverController

- (IBAction)convolve:(id)sender
{
NSImage *source = [sourceImage image];
NSImage *dest = [convolver processImage: source
withCoefficients: [convolutionMatrix cells]];
[resultImage setImage: dest];
}

- (IBAction)openImage:(id)sender
{
NSOpenPanel *panel = [NSOpenPanel openPanel];
[panel beginSheetForDirectory: nil
file:nil
types: [NSImage imageFileTypes]
modalForWindow: window
modalDelegate:self
didEndSelector:
@selector(openPanelDidEnd:returnCode:contextInfo:)

contextInfo:nil];
}

- (void)openPanelDidEnd:(NSOpenPanel *)panel
returnCode:(int)returnCode
contextInfo:(void *)contextInfo{

NSArray *files = [panel filenames];
NSString *filename = [files objectAtIndex:0];
NSImage *image =
[[[NSImage alloc]
initByReferencingFile:filename] autorelease];

[sourceImage setImage: image];
[self convolve: self];
}
@end
The convolve: method is called whenever the source image or coefficient matrix is changed. Notice that I pass the matrix cells unaltered to the model. At first I thought I would pull the information out of these cells and pass an array of NSNumber, but then I decided that I would just make sure that the model would take the floatValue: of whatever input it got to make sure I got the correct input type. Without strong typing, it seemed that I would have to do this in the model anyway, so I just do it there. The openImage: method opens a sheet to allow the user to select an image, and the openPanelDidEnd:returnCode:contextInfo: method sets the image file to the source image and calls convolve:

The implementation of the Convolver class and the processImage:withCoefficients: method are the most important part of this exercise. Here's the header:

/* Convolver */
#import <Cocoa/Cocoa.h>
#import <QuartzCore/QuartzCore.h>

@interface Convolver : NSObject
{
CIFilter *convolution;
NSDictionary *filterAttributes;
CIContext *context;
}
-(NSImage *)processImage:(NSImage *)image
withCoefficients:(NSArray *)coefficients;

@end

and here's the implementation:


#import “Convolver.h“

@implementation Convolver

-(id)init
{
if( self = [super init] ){
[CIPlugIn loadAllPlugIns];
convolution = [CIFilter filterWithName:
@“Convolution3by3“];

[convolution retain];
filterAttributes = [[convolution attributes]
retain];

}
return self;
}
The init method loads all the Core Image plugins in the system, and gets the Convolution3x3 filter from the system. We are not going to use the attributes but you can access them and use them if you wish by using the attributes message on the filter you load.

The only other method in this class is processImage:withCoefficients and I'll break it down for you a step at a time.

-(NSImage*)processImage:(NSImage*)image
withCoefficients:(NSArray*)coefficients

{
if( context == nil ){
context = [CIContext contextWithCGContext:
[[NSGraphicsContext currentContext]
graphicsPort]
options:nil];
[context retain];
}

The first thing we need to do is to get a Core Image graphics context which we get from our current graphics context.

Now, since we are using an NSImageView we need to convert the NSImage within into a CIImage. The way I am doing this is by using the initWithFocusedViewRect: method to get a bitmap representation of the NSImage and then using that bitmap to initialize a CIImage object with that bitmap. Personally I'm not sure why there are multiple image types in Cocoa (I'm sure someone at Apple would have the reason) but it's just something we have to deal with.

NSSize size = [image size];
[image lockFocus];

NSRect imageRect =
NSMakeRect(0, 0, size.width, size.height);


NSBitmapImageRep* rep = [[NSBitmapImageRep alloc]
initWithFocusedViewRect:imageRect];

[rep autorelease];
CIImage *bitmap = [[CIImage alloc]
initWithBitmapImageRep: rep];

[bitmap autorelease];
[image unlockFocus];

The next step is to set the parameters for the filter. We first call setDefaults to get the parameters in a known good state in case we don't want to set all of them. Core Image uses Key-Value coding to set all its parameters. Apple uses this technology so often, and it has turned out to be so useful for all kinds of applications that I don't know what we did before it. One minor annoyance is that we have to make NSNumber objects for each of the float parameters since Cocoa doesn't have any sort of automatic coercion like there is in Java 1.5.

[convolution setDefaults];
[convolution setValue:bitmap
forKey:@“inputImage“];


NSArray *keys = [NSArray arrayWithObjects:
@“r00“, @“r01“, @“r02“,
@“r10“, @“r11“, @“r12“,
@“r20“, @“r21“, @“r22“, nil];

NSEnumerator *en = [keys objectEnumerator];
int i = 0;
NSString *key;
while( key = [en nextObject] ){
NSNumber *param =
[NSNumber numberWithFloat:
[[coefficients objectAtIndex:i++] floatValue]];

NSLog(@“key %@ index %d value %@“, key, i-1, param);
[convolution setValue: param forKey: key];
}

Finally, we get the value for the “outputImage” key which calls the outputImage method in the filter class and actually produce the result.

CIImage *result =
[convolution valueForKey:@“outputImage“];


Now we have to convert back to a NSImage. Unfortunately from what I can tell, there's no way to just get a bitmap representation out of a CIImage object. If anyone knows of a better way to do this, please leave a comment! So, we draw the CIImage into our NSImage object and return it.

NSImage *outputImage =
[[[NSImage alloc] init] autorelease];

[outputImage setSize: size];

[outputImage lockFocus];
[result drawInRect: imageRect
fromRect: imageRect
operation: NSCompositeSourceOver
fraction:1.0];
[outputImage unlockFocus];

return outputImage;
}
@end

That's the end of this tale. There is another angle on this same problem, however. Instead of converting images and calling filters, we can embed the Quartz Composer composition we developed as a test directly into our application by using a QCView and controlling our composition using the QCPatchController. Next time we'll reimplement this app using those techniques.

Mar 29, 2007

Cocoa Application with custom Core Image filter 4: packaging the filter as an Image Unit

Greetings to all from my remote village. This month is turning out to be incredibly busy, so I'm glad to finally be posting this entry. Last time in the village, we introduced QuartzComposer to test the convolution filter. Now, it's time to package it into an Image Unit. In Core Image, an Image Unit is a way to provide an image processing function as a plug in so that any application in the system that uses Core Image can find and use its function.

Xcode provides an easy way to get started by providing a project template for an “Image Unit Plug in for Objective-C” under “StandardApplePlugins” in the XCode project window.

When this project is created, it already contains some stub Objective-C code, which we will never touch, An example kernel which will be deleted, and some .plist and .strings files for localization. There are two types of filters, executable and non-executable filters. Non-executable filters run entirely on the GPU and therefore there is no need for any objective-C code in the filter. A nonexecutable filter is determined if all “sample” calls in the filter are of the form color = sample(someSrc, samplerCoord(someSrc)); Since the sample instructions in our filter are of the form: sample(src, (samplerCoord(src) + loc)) which does not match the nonexecutable form, we have an executable filter.

For an executable filter, only the version number, filter class and filter name are read from the Description.plist file. The other parameters need to be defined in an Objective-C subclass of CIFilter. Here's the interface to that class:


#import <QuartzCore/QuartzCore.h>
#import <Cocoa/Cocoa.h>
@interface Convolution3by3: CIFilter
{
CIImage *inputImage;
NSNumber *r00;
NSNumber *r01;
NSNumber *r02;
NSNumber *r10;
NSNumber *r11;
NSNumber *r12;
NSNumber *r20;
NSNumber *r21;
NSNumber *r22;
}
- (NSArray *)inputKeys;
- (NSDictionary *)customAttributes;
- (CIImage *)outputImage;
@end


So, we declare our input parameters and override 4 methods: init, inputKeys, customAttributes, and outputImage. The names inputImage and outputImage are defined by convention and are used with key-value coding to call your filter. Also, filter processing is done in the outputImage method so that the filter is processed lazily - results are only generated as requested. Here's the implementation of init:


#import “Convolution3by3.h“
@implementation Convolution3by3

static CIKernel *convolutionKernel = nil;

- (id)init
{
if(convolutionKernel == nil){
NSBundle *bundle =
[NSBundle bundleForClass:[self class]];


NSString *code = [NSString stringWithContentsOfFile:
[bundle pathForResource:@“Convolution3by3“
ofType:@“cikernel“]];

NSArray *kernels = [CIKernel kernelsWithString:code];

convolutionKernel = [[kernels objectAtIndex:0] retain];
}
return [super init];
}


The init method loads the cikernel code out of the bundle and loads it into the static convolutionKernel variable. It is possible to define multiple kernels in a single cikernel file if you wanted to, but I would probably keep them separate in most cases.

The inputKeys method simply returns an array of parameters for the filter:


-(NSArray *)inputKeys
{
return [NSArray arrayWithObjects:@“inputImage“,
@“r00“, @“r01“, @“r02“,
@“r10“, @“r11“, @“r12“,
@“r20“, @“r21“, @“r22“, nil];
}


By far the longest method of the Image Unit is the customAttributes method where we return a NSDictionary containing a separate NSDictionary for each of the filter parameters. Here's what it looks like:


- (NSDictionary *)customAttributes
{
NSNumber *minValue = [NSNumber numberWithFloat:-10.0];
NSNumber *maxValue = [NSNumber numberWithFloat:10.0];
NSNumber *zero = [NSNumber numberWithFloat:0.0];
NSNumber *one = [NSNumber numberWithFloat:1.0];

return [NSDictionary dictionaryWithObjectsAndKeys:
[NSDictionary dictionaryWithObjectsAndKeys:
@“NSNumber“, kCIAttributeClass,
kCIAttributeTypeScalar, kCIAttributeType,
minValue, kCIAttributeSliderMin,
maxValue, kCIAttributeSliderMax,
zero, kCIAttributeDefault, nil],
@“r00“,
...


This repeats for all 9 parameters, the only one that is different is r11, which is the center parameter. The value of this parameter defaults to 1, so that by default the filter returns its input unaltered.


...
[NSDictionary dictionaryWithObjectsAndKeys:
@“NSNumber“, kCIAttributeClass,
kCIAttributeTypeScalar, kCIAttributeType,
minValue, kCIAttributeSliderMin,
maxValue, kCIAttributeSliderMax,
zero, kCIAttributeDefault, nil],
@“r22“, nil];
}


As you can see, the attributes of the filter include minimum and maximum slider values in case you wanted to build a dynamic user interface for the filter. These constants are all defined in CIFilter.h.

The final method is the outputImage method. It is very simple:


- (CIImage *)outputImage
{
CISampler *src=[CISampler samplerWithImage:inputImage];
return [self apply: convolutionKernel, src,
r00, r01, r02,
r10, r11, r12,
r20, r21, r22, nil];

}


All we do here is get a sampler for the input image and call apply: with the kernel and all the parameters. This call returns our resulting image.

Once you've compiled this filter you can make it available to the system by placing it in the /Library/Graphics/Image Units folder, or for just your user in ~/Library/Graphics/Image Units. Next time we will see how we call this image unit from a Cocoa program.