Использование CoreImage в Mac OS X

Операционная система Mac OS X начиная с версии 10.4 (Tiger) содержит в себе мощную библиотеку обработки изображений, называемую Core Image.

Основными особенностями данной библиотеки является то, что она ориентирована на работу с GPU (однако при необходимости может переключаться на работу на CPU), все вычисления производятся с использованием 32-битовых float чисел и сами значения компонент цвета также хранятся как 32-битовые float-значения.

В состав Core Image входит более 150 уже готовых различных фильтров и эффектов и в нее можно легко добавлять свои фильтры.

На следующем рисунке приведена архитектура графической системы Mac OS X и место Core Image в ней.

Рис 1. Архитектура графической системы в Mac OS X 10.4.

Быстрота и эффективность Core Image позволяют легко использовать эту библиотеку не только для обработки статических изображений, но и для обработки видео в реальном времени.

Ключевым понятием в Core Image является фильтр (эффект). Каждый фильтр имеет свое уникальное имя (например, CIHueAdjust) и набор входных и выходных параметров (атрибутов).

Кроме этого каждый фильтр также принадлежит к одной или нескольким категориям. Категории позволяют легко группировать фильтры по типу осуществляемого преобразования (эффекта).

В следующей таблице приводится список стандартных категорий.

Таблица 1. Стандартные категории фильтров.

kCICategoryDistortionEffectИскажение изображения
kCICategoryGeometryAdjustment
kCICategoryCompositeOperationНаложение одного изображения на другое
kCICategoryHalftoneEffect
kCICategoryColorAdjustmentНастройка цвета
kCICategoryTransitionЭффекты перехода между двумя изображениями
kCICategoryTileEffect
kCICategoryGeneratorЭффекты, порождающие изображения
kCICategoryGradientЭффекты, связанные с градиентом
kCICategoryStylize
kCICategorySharpen
kCICategoryBlurРазмытия

Взаимодействие программы с фильтрами Core Image осуществляется через набор именованных атрибутов (фактически используя для этого протокол NSKeyValueCoding).

Послав фильтру сообщение attributes можно получить полный список всех его атрибутов (как входных, так и выходных) в виде объекта класса NSDictionary, сопоставляющему имени каждого атрибута словарь (объект класса NSDictionary), описывающий его свойства, такие как тип, значение по умолчанию, минимальное и максимальное значения и т.п.

При этом для передачи фильтру строковых значений используются объекты классы NSString, для передачи чисел - объект класса NSNumber. Для передачи 2-3-4-мерных векторов используются объекты класса CIVector, для передачи цветов - объекты класса CIColor и для передачи изображений - объекты класса CIImage. Для установки значения всех параметров по умолчания фильтру посылается сообщение setDefaults.

[filter setDefaults];
[filter setValue: src forKey: @"inputImage"];
[filter setValue: [NSNumber numberWithFloat: angle] forKey: @"inputAngle"];

Передача изображений в Core Image, равно как и получение уже готовых изображений из Core Image, осуществляется при помощи объектов класса CIImage. Важной особенностью объектов данного класса (точнее всей библиотеки Core Image) является так называемое lazy evaluation.

Это означает, что никаких операций с изображениями не будет производится до тех пор, пока не потребуются результаты этих операций. При этом операции могут применяться не ко всему изображению, а лишь к той его части, которая действительно нужна. Поэтому объекты класса CIImage не являются готовыми изображениями, а лишь способом ("рецептом") его получения.

Помимо этого Core Image умеет объединять несколько фильтров в один render-pass. За счет всего этого (а также за счет проведения вычислений на GPU) удается получить очень высокую скорость обработки изображений.

При этом можно построить сложную систему фильтров, когда выходы одних фильтров являются входами других фильтров, но ровно до того момента когда результирующее изображение действительно понадобится, никаких вычислений производится не будет. Запрос на использование части результирующего изображения приведет в выполнению (возможно объединенных) только тех фильтров, которые действительно нужны для запрашиваемой части выходного изображения.

Можно получить список имен всех фильтров для заданной категории при помощи сообщения filterNamesInCategory:, посылаемого объекту класса CIFilter.

NSArray * names = [CIFilter filterNamesInCategory: KCICategoryDistortionEffect];

Также очень легко получить готовый фильтр по его имени при помощи сообщения filterWithName:. Обратите внимание, что полученный Вами при этом объект уже autorelease'd, это значит, что если вы хотите его дальше использовать то ему необходимо послать сообщение retain.

CIFilter * filter = [CIFilter filterWithName: @"CIHueAdjust"];

[filter retain];
[filter setDefaults];

Рассмотрим пример использования фильтра CIHueAdjust для обработки изображения.

Создадим новый проект CI-Example типа Cocoa Application и добавим в него QuartzCore.framework ( именно он содержит Core Image).

Рис 2. Окно проекта CI-Example.

После этого сразу же переходим к редактированию главного nib-файла приложения (для этого двойным щелчком мыши по файлу MainMenu.nib (English) в списке файлов проекта вызовите Interface Builder).

Открыв закладку Classes в главном окне выделим класс NSView и нажмем Enter. В результате этого мы создадим новый класс, унаследовав его от класса NSView.

Переименуем его в DemoView, откроем (при помощи Shift-Command-I) панель инспектора и добавим нашему классу для метода (action'а) - open: и setAmount: (см. рисунок).

Рис 3. Методы класса DemoView.

При помощи команды меню Classes/Create Files For DemoView создадим файлы DemoView.h и DemoView.m и добавим их в наш проект.

После этого изменим заголовок окна на "Hue Adjust Demo" и уберем у него признак Zoom (and resize). После этого перетащим на наше окно компоненту CustomView с панели компонент (см. рисунок). В качестве класса этой компоненты выберем наш класс DemoView.

Перетащим а палитры компонент слайдер и расположим его под DemoView, перед слайдером поставим метку "Amount:"

Рис 4. Вид главного окна нашего приложения.

Удерживая нажатой клавишу Ctrl, выделим мышью слайдер и протянем от него (не отпуская кнопку мыши) связь к DemoView и подключим к методу setAmount:. При этом любое изменение значения слайдера будет приводить к посылке сообщения setAmount: нашему объекту DemoView.

Точно также протянем связь от пункта меню File|Open к методу open: объекта DemoView.

Сохраним nib-файл и вернемся обратно в XCode и приступим к редактированию файлов DemoView.h и DemoView.m.

Ниже приводится каким должен стать файл DemoView.h.

#import <Cocoa/Cocoa.h>
#import	<QuartzCore/CIImage.h>
#import	<QuartzCore/CIContext.h>
#import	<QuartzCore/CIFilter.h>

@interface DemoView : NSView
{
    CIImage   * src;
    CIImage   * res;
    CIFilter  * filter;
    CIContext * context;
}
- (IBAction)open:(id)sender;
- (IBAction)setAmount:(id)sender;
- (void) openPanelDidEnd: (NSOpenPanel *) panel returnCode: (int) returnCode contextInfo: (void  *) contextInfo;

Как видно из листинга мы добавили ряд instance-переменных и один метод.

В качестве instance-переменных выступают контекст Core Image (класса CIContext), используемый фильтр (объект класса CIFilter) и два изображения (класса CIImage) - входное (src) и выходное (res).

Перейдем теперь непосредственно к реализации методов данного класса. Проще всего дело обстоит с его конструктором (метод initWithFrame:) - автоматический сгенерированный шаблон нас полностью устраивает, т.к. при выделении памяти под объект, она сразу же зануляется, т.е. все наши instance-переменные автоматически примут значение nil.

Задачей деструктора (метода dealloc) является освобождение всех созданных объектов, соответствующий код приводится ниже.

- (void) dealloc
{
    [filter release];
    [context release];
    [src release];

    [super dealloc];
}

Также крайне прост метод setAmount: - его задача заключается в получении нового значения из слайдера и передача его нашему фильтру. После его самому себе посылается сообщение setNeedsDisplay:, чтобы вызвать перерисовку компоненты.

- (IBAction)setAmount:(id)sender
{
    float angle = [sender floatValue];

    [filter setValue: [NSNumber numberWithFloat: angle] forKey: @"inputAngle"];
    [self setNeedsDisplay: YES];
}

Ключевым методом является drawRect:, отвечающий за вывод преобразованного изображения. Еще одной важной задачей этого метода является создания контекста Core Image и фильтра. Эти действия производятся именно в этом методе (а не в awakeFromNib) поскольку для создания контекста Core Image нам нужен настроенный графический контекст, а он будет гарантированно готов только к вызову drawRect:.

Итак данный метод создает контекст Core Image и фильтр (если они уже не созданы) и задает для фильтра значения атрибутов (если они еще не были установлены).

Далее задачей метода является получение обработанного изображения и вывод его при помощи метода контекста drawImage:atPoint:fromRect:. Обратите внимание, что Core Image использует структуры CGRect, CGPoint вместо NSRect и NSPoint.

- (void)drawRect:(NSRect)rect
{
    [super drawRect: rect];

    if ( context == nil ) 
    { 
        context = [CIContext contextWithCGContext: [[NSGraphicsContext currentContext] graphicsPort] options: nil]; 
	
        [context retain]; 
    } 
	
    if ( src != nil )
    {
        if ( filter == nil )
        {
            filter = [CIFilter filterWithName: @"CIHueAdjust"];

            if ( filter == nil )
                NSRunAlertPanel ( @"Error", @"Cannot create CIFilter", @"OK", nil, nil );

            [filter retain];
            [filter setDefaults];
            [filter setValue: src forKey: @"inputImage"];
            [filter setValue: [NSNumber numberWithFloat: -0.5] forKey: @"inputAngle"];		
        }

        CGRect extent = [src extent];

        res = [filter valueForKey: @"outputImage"];

        if ( res == nil )
            NSRunAlertPanel ( @"Error", @"res is nil", @"OK", nil, nil );

        [context drawImage: res atPoint: extent.origin fromRect: extent ];
    }
}

Рассмотрим теперь реализацию метода open: задачей которого является создание диалога для выбора файла с исходным изображением и загрузка его.

С этой целью мы создаем соответствующее диалоговое окно (панель) при помощи сообщения openPanel объекту класса NSOpenPanel и задаем для него список допустимых расширений файлов.

Существуют два способа вызова диалога - как модального диалогово окна, блокирующего все приложение до пор пока окно диалога не будет закрыто, или же как диалога, прикрепленного к текущему окну и блокирующего только его.

Хотя для нашего приложения оба этих подхода одинаково приемлемы, мы далее рассмотрим только второй из них. При его использовании окно диалога "выходит" из заголовка нашего окна и "приклеивается" к нему. После закрытия окна диалога оно "убирается" в заголовок нашего окна с изображением.

Подобная функциональность достигается при помощи метода beginSheetForDirectory:file:types:modalForWindow:modalDelegate:didEndSelector:contextInfo: класса NSOpenPanel.

Важными параметрами при вызове этого метода являются делегат и селектор сообщения, посылаемого делегату при закрытии диалога. Это очень распространенный прием в Cocoa - задание объекта (делегата) и селектора сообщения, которого надо посылать делегату. За счет этого существенно сокращается количество создаваемых пользователем классов и уменьшается необходимость в наследовании - во многих случаях всю специфику можно вынести в один объект (обычно контроллер), который и является делегатом для большого количества стандартных объектов и осуществляет "настройку" их поведения.

- (IBAction) open: (id)sender
{
    NSOpenPanel * panel = [NSOpenPanel openPanel];
    NSArray     * types = [NSArray arrayWithObjects: @"tiff", @"png", @"tga", @"gif", @"jpg", @"bmp", nil];

    [panel beginSheetForDirectory: @"" file: nil types: types modalForWindow: [self window] modalDelegate: self 
           didEndSelector: @selector (openPanelDidEnd:returnCode:contextInfo:) contextInfo: nil];
}

В качестве делегата нам проще всего использовать именно объект DemoView (чтобы не плодить новые сущности), поэтому в его описание мы и добавляем метод openPanelDidEnd:returnCode:contextInfo:, который будет вызываться при закрытии диалога.

Важными параметрами этого метода являются передаваемый делегат, указатель на диалоговое окно и код, несущий в себе информацию о том как именно был закрыт диалог. Нормальному завершению диалога (т.е. с выбором файла в нашем случае) соответствует константа NSOKButton.

В случае, если код завершения диалога равен этой константе, то нам необходимо получить путь к выбранному файлу (при помощи сообщения filename) и построить по нему объект класса CIImage.

Самым простым способом получения объекта CIImage по заданному пути является создание по заданному пути объекта NSURL и использование метода imageWithContentsOfURL: класса CIImage.

После создания изображения осуществляется настройка фильтра и размеров окна и запрос на полную перерисовку себя.

- (void) openPanelDidEnd: (NSOpenPanel *) panel returnCode: (int) returnCode contextInfo: (void  *) contextInfo
{
    if ( returnCode !=  NSOKButton )
        return;

    NSString * path  = [panel filename];
    NSURL    * url   = [NSURL fileURLWithPath: path];
    CIImage  * img   = [CIImage imageWithContentsOfURL: url];

    if ( img == nil )
    {
        NSRunAlertPanel ( @"Error", @"Cannot create CIImage from file", @"OK", nil, nil );

        return;
    }

    [src autorelease];
	
    src = [img retain];

    CGRect content =  [img extent];
    NSSize imgSize = NSMakeSize ( CGRectGetWidth ( content ), CGRectGetHeight ( content ) );

    if ( imgSize.width < 250 )
        imgSize.width = 250;

    [[self window] setContentSize: NSMakeSize ( imgSize.width + 40, imgSize.height + 358 - 293 )];
    [[self window] setTitleWithRepresentedFilename: path];

    [filter setValue: src forKey: @"inputImage"];
    [self setNeedsDisplay: YES];
}

На следующих рисунках приводятся скриншоты работающей программы.

Рис 5. Выбор изображения при помощи "приклеенного" окна диалога.

Рис 6. Работа приложения.

Ниже приводится полный текст файла DemoView.m

#import "DemoView.h"

@implementation DemoView

- (id)initWithFrame:(NSRect)frameRect
{
    if ((self = [super initWithFrame:frameRect]) != nil) 
    {
                      // Add initialization code here
    }

    return self;
}

- (void) dealloc
{
    [filter release];
    [context release];
    [src release];

    [super dealloc];
}

- (void)drawRect:(NSRect)rect
{
    [super drawRect: rect];

    if ( context == nil ) 
    { 
        context = [CIContext contextWithCGContext: [[NSGraphicsContext currentContext] graphicsPort] options: nil]; 
	
        [context retain]; 
    } 
	
    if ( src != nil )
    {
        if ( filter == nil )
        {
            filter = [CIFilter filterWithName: @"CIHueAdjust"];

            if ( filter == nil )
                NSRunAlertPanel ( @"Error", @"Cannot create CIFilter", @"OK", nil, nil );

            [filter retain];
            [filter setDefaults];
            [filter setValue: src forKey: @"inputImage"];
            [filter setValue: [NSNumber numberWithFloat: -0.5] forKey: @"inputAngle"];		
        }

        CGRect extent = [src extent];

        res = [filter valueForKey: @"outputImage"];

        if ( res == nil )
            NSRunAlertPanel ( @"Error", @"res is nil", @"OK", nil, nil );

        [context drawImage: res atPoint: extent.origin fromRect: extent ];
    }
}

- (IBAction) open: (id)sender
{
    NSOpenPanel * panel = [NSOpenPanel openPanel];
    NSArray     * types = [NSArray arrayWithObjects: @"tiff", @"png", @"tga", @"gif", @"jpg", @"bmp", nil];

    [panel beginSheetForDirectory: @"" file: nil types: types modalForWindow: [self window] modalDelegate: self 
           didEndSelector: @selector (openPanelDidEnd:returnCode:contextInfo:) contextInfo: nil];
}

- (void) openPanelDidEnd: (NSOpenPanel *) panel returnCode: (int) returnCode contextInfo: (void  *) contextInfo
{
    if ( returnCode !=  NSOKButton )
        return;

    NSString * path  = [panel filename];
    NSURL    * url   = [NSURL fileURLWithPath: path];
    CIImage  * img   = [CIImage imageWithContentsOfURL: url];

    if ( img == nil )
    {
        NSRunAlertPanel ( @"Error", @"Cannot create CIImage from file", @"OK", nil, nil );

        return;
    }

    [src autorelease];
	
    src = [img retain];

    CGRect content =  [img extent];
    NSSize imgSize = NSMakeSize ( CGRectGetWidth ( content ), CGRectGetHeight ( content ) );

    if ( imgSize.width < 250 )
        imgSize.width = 250;

    [[self window] setContentSize: NSMakeSize ( imgSize.width + 40, imgSize.height + 358 - 293 )];
    [[self window] setTitleWithRepresentedFilename: path];

    [filter setValue: src forKey: @"inputImage"];
    [self setNeedsDisplay: YES];
}

- (IBAction)setAmount:(id)sender
{
    float angle = [sender floatValue];

    [filter setValue: [NSNumber numberWithFloat: angle] forKey: @"inputAngle"];
    [self setNeedsDisplay: YES];
}

@end

Transitions

В качестве следующего примера мы рассмотрим использование transition-эффектов для перехода от одного изображения к другому.

При этом мы заранее ограничим список используемых эффектов лишь теми, которые не требуют дополнительных параметров, кроме двух изображений и времени (задающего стадию перехода).

Создадим новый проект CI-Transition типа Cocoa Application и добавим в него CoreGraphics.framework.

Рис 7. Окно проекта CI-Transition.

После этого, как и ранее перейдем к редактированию ресурсов.

Сразу же создадим новый класс TransitionView унаследовав его от NSView и добавим ему один outlet - popup (класса NSPopUpButton) и один метод - setTransition: после чего создадим соответствующие файлы и добавим их в проект CI-Transition.

Для работы нам понадобятся два изображения одинакого размера, между которыми и будет собственно происходит переход. Откроем закладку Images в окне проекта и перетащим туда файлы wall.tiff и RedBricks.tiff.

Разместим в главном окне один объект CustomView (задав в качестве класса TransitionView), метку с текстом "Transition:" и компоненту NSPopUpButton для выбора типа эффекта. Заполним меню этой компоненты списком фильтров, которые мы собираемся использовать.

Рис 8.

После этого сохраняем ресурс и возвращаемся обратно в XCode. Откроем файлы TransitionView.h и TransitionView.m.

Ниже приводится окончательный вид файла TransitionView.h.

#import <Cocoa/Cocoa.h>
#import <QuartzCore/CIImage.h>
#import <QuartzCore/CIContext.h>
#import <QuartzCore/CIFilter.h>
#import <QuartzCore/CIVector.h>

@interface TransitionView : NSView
{
    CIImage   * img1;
    CIImage   * img2;
    CIImage   * res;
    CIFilter  * filter;
    CIContext * context;
    NSTimeInterval start;
    
    IBOutlet NSPopUpButton * popup;
}
- (IBAction)setTransition:(id)sender;
- (void) nextFrame: (id) sender;

Как видно, основным отличием от ранее рассмотренного файла DemoView.h является наличие двух исходных изображений, ссылки на кнопку, выбирающую фильтр и переменной, хранящей время начала анимации (start).

Как и ранее нас полностью устраивает сгенерированный метод initWithFrame:, а в метод dealloc надо лишь добавить уничтожение второго исходного изображения.

- (void) dealloc
{
    [filter release];
    [context release];
    [img1 release];
    [img2 release];
    
    [super dealloc];
}

Однако нам необходимо произвести инициализацию приложения - загрузить изображения и создать и запустить таймер, отвечающий за анимацию. Проще всего это сделать в методе awakeFromNib - к этому моменту все объекты из nib-файла будут уже созданы и готовы к работе.

- (void) awakeFromNib
{
                            // load images
    NSURL    * url1 = [NSURL fileURLWithPath: [[NSBundle mainBundle] pathForResource: @"RedBricks" ofType: @"tiff"]];
    NSURL    * url2 = [NSURL fileURLWithPath: [[NSBundle mainBundle] pathForResource: @"wall" ofType: @"tiff"]];
    
    img1 = [CIImage imageWithContentsOfURL: url1];
    img2 = [CIImage imageWithContentsOfURL: url2];

                            // setup animation
    [self setTransition: popup];
    
    NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval: 1.0 / 20.0 target: self selector: @selector (nextFrame:) userInfo: nil repeats: YES];
    
    [[NSRunLoop currentRunLoop] addTimer: timer forMode: NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] addTimer: timer forMode: NSEventTrackingRunLoopMode];
    
    start = [NSDate timeIntervalSinceReferenceDate];
}

Метод nextFrame: будет вызываться таймером с заданной нами частотой (20 раз в секунду), единственной его задачей является вызвать перерисовку данной компоненты.

- (void) nextFrame: (id) sender
{
    [self setNeedsDisplay: YES];
}

Также довольно прост метод setTransition:, вызываемый при выборе нового эффекта с popup-кнопке. Основное, что он должен сделать - это создать новый фильтр, заменить текущий фильтр на новый, установить настройки по умолчанию для фильтра и вызвать перерисовку данной компоненты.

- (IBAction) setTransition: (id) sender
{
    NSString * filterName = [sender titleOfSelectedItem];
    
    if ( filterName == nil )
        filterName = @"CISwipeTransition";
        
    [filter autorelease];
    
    NSLog ( @"setting filter %@", filterName );
    
    filter = [CIFilter filterWithName: filterName];
    
    [filter setDefaults];
    [filter retain];
    
    [self setNeedsDisplay: YES];
}

Задачей метода drawRect: как и ранее будет создание контекста Core Image, получение текущего времени, а также создание правильного изображения, соответствующего переходу от одного исходного изображения к другому.

В этом примере мы будем менять местами два исходных изображения когда переход из одного в другое будет завершен. Ниже приводится исходный код для метода drawRect:.

- (void) drawRect: (NSRect) rect
{
    if ( context == nil )
    {
                            // get context
        context = [CIContext contextWithCGContext: [[NSGraphicsContext currentContext] graphicsPort] options: nil]; 
            
        [context retain];
    }
    
    float      t = fmod ( 0.3*([NSDate timeIntervalSinceReferenceDate] - start), 2 );
    
    [super drawRect: rect];
    
    if ( t > 1 )
    {
        [filter setValue: img2   forKey: @"inputImage" ];
        [filter setValue: img1   forKey: @"inputTargetImage"];
        [filter setValue: [NSNumber numberWithFloat: (t-1)] forKey: @"inputTime"];
    }
    else
    {
        [filter setValue: img1   forKey: @"inputImage" ];
        [filter setValue: img2   forKey: @"inputTargetImage"];
        [filter setValue: [NSNumber numberWithFloat: t] forKey: @"inputTime"];
    }
    
    CGRect  extent = [img1 extent];
        
    res = [filter valueForKey: @"outputImage"];
        
    if ( res == nil )
        NSRunAlertPanel ( @"Error", @"res is nil", @"OK", nil, nil );

    [context drawImage: res atPoint: extent.origin fromRect: extent ];
}

Ниже приводятся некоторые изображения, соответствующие различным эффектам перехода.

Ниже приводится полный текст файла TransitionView.m.

#import "TransitionView.h"

@implementation TransitionView

- (id)initWithFrame:(NSRect)frameRect
{
    if ((self = [super initWithFrame:frameRect]) != nil) 
    {
        // Add initialization code here
    }
    return self;
}

- (void) dealloc
{
    [filter release];
    [context release];
    [img1 release];
    [img2 release];
    
    [super dealloc];
}

- (void) awakeFromNib
{
                            // load images
    NSURL    * url1 = [NSURL fileURLWithPath: [[NSBundle mainBundle] pathForResource: @"RedBricks" ofType: @"tiff"]];
    NSURL    * url2 = [NSURL fileURLWithPath: [[NSBundle mainBundle] pathForResource: @"wall" ofType: @"tiff"]];
    
    img1 = [CIImage imageWithContentsOfURL: url1];
    img2 = [CIImage imageWithContentsOfURL: url2];

                            // setup animation
    [self setTransition: popup];
    
    NSLog ( @"transition popup - %@", popup );
	
    NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval: 1.0 / 20.0 target: self selector: @selector (nextFrame:) userInfo: nil repeats: YES];
    
    [[NSRunLoop currentRunLoop] addTimer: timer forMode: NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] addTimer: timer forMode: NSEventTrackingRunLoopMode];
    
    start = [NSDate timeIntervalSinceReferenceDate];
}

- (void) drawRect: (NSRect) rect
{
    if ( context == nil )
    {
                            // get context
        context = [CIContext contextWithCGContext: [[NSGraphicsContext currentContext] graphicsPort] options: nil]; 
            
        [context retain];
    }
    
    float      t = fmod ( 0.3*([NSDate timeIntervalSinceReferenceDate] - start), 2 );
    
    [super drawRect: rect];
    
    if ( t > 1 )
    {
        [filter setValue: img2   forKey: @"inputImage" ];
        [filter setValue: img1   forKey: @"inputTargetImage"];
        [filter setValue: [NSNumber numberWithFloat: (t-1)] forKey: @"inputTime"];
    }
    else
    {
        [filter setValue: img1   forKey: @"inputImage" ];
        [filter setValue: img2   forKey: @"inputTargetImage"];
        [filter setValue: [NSNumber numberWithFloat: t] forKey: @"inputTime"];
    }
    
    CGRect  extent = [img1 extent];
        
    res = [filter valueForKey: @"outputImage"];
        
    if ( res == nil )
        NSRunAlertPanel ( @"Error", @"res is nil", @"OK", nil, nil );

    [context drawImage: res atPoint: extent.origin fromRect: extent ];
}

- (IBAction) setTransition: (id) sender
{
    NSString * filterName = [sender titleOfSelectedItem];
    
    if ( filterName == nil )
        filterName = @"CISwipeTransition";
        
    [filter autorelease];
    
    NSLog ( @"setting filter %@", filterName );
    
    filter = [CIFilter filterWithName: filterName];
    
    [filter setDefaults];
    [filter retain];
    
    [self setNeedsDisplay: YES];
}

- (void) nextFrame: (id) sender
{
    [self setNeedsDisplay: YES];
}
@end

По этой ссылке можно скачать весь проект CI-Example. Проект CI-Transition доступен для скачивания по этой ссылке.

Используются технологии uCoz