2012年1月14日 星期六

Filter4Cam 學習之 Paint with Incoming Video

since: 2012/01/11
update: 2012/01/14

reference: Conway's Game of Life Painted with Incoming Video (Core Image Tut) | Indie Ambitions

A. 結合 Core Image 與 Core Graphics
   1. 有一個優秀的 app 叫作 Composite, 它可以讓你將影像的輸入當成畫布來繪畫.
      使用 core image 的濾鏡也可以作到類似的功能.

   2. 在這篇文章將使用影像的輸入當成 cell images 來實作 Conway's Game Of Life
        (康威:生命遊戲).

   3. 並且, 更進一步地. 用來將影像的輸入當成畫布來繪製的處理, 使用到 Core Image
       和 Core Graphics. 我們將會使用 core graphics 來繪製 CGImage, 該 CGImage 能
       被轉換成 Core Image, 並且採用任意的 CIFilters 濾鏡效果來達成.

   4. 這篇文章將接續建置在之前的文章上.
        說明: 記得先將 MainStoryboard.storyboard 中的 buttonslider 設為 Hidden,
                  並移至銀幕的最下方.

---------------------------------------------------------------------------------------------

B. 繪畫功能
   1. 首先, 必需設定好繪畫(drawing)類別. 新增一個 CGContext 到 ViewController 類別,
       並新增一個方法用來設置. ViewController.h 檔案修改如下:
....
@interface ViewController : GLKViewController <AVCaptureVideoDataOutputSampleBufferDelegate>
{
    AVCaptureSession *session;
   
    CIContext *coreImageContext;
    EAGLContext *context;
    GLuint _renderBuffer;
    IBOutlet UISlider *sldr;
    CIImage *ciimg;
   
    //@add
    CGSize screenSize;
    CGContextRef cgcontext;
    CIImage *maskImage;
    float scl;
}

@property (strong, nonatomic) EAGLContext *context;

- (IBAction)snap:(id)sender;

//@add
-(void)setupCGContext;

@end

   2. 在實作檔: ViewController.m, 需要在 viewDidLoad 方法裡, 將螢幕大小存入變數中,
       並且呼叫 setupCGContext 方法.

- (void)viewDidLoad
{
    [super viewDidLoad];
       ....
    //@add
    CGSize scrn = [UIScreen mainScreen].bounds.size;

    //UIScreen mainScreen.bounds returns the device in portrait, we need to switch it to landscape
    screenSize = CGSizeMake(scrn.height, scrn.width);
   
    if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) {
        scl = [[UIScreen mainScreen] scale];
        screenSize = CGSizeMake(screenSize.width * scl, screenSize.height * scl);
    }
    [self setupCGContext];
}

    說明: 在這段程式碼區塊中, 檢查 devices 是否為 retina 的. 當使用 openGL 描繪到
               螢幕時是使用 pixels 的, 因此如果是在 retina 的 device 中, 就需要按比例提高
               繪製的圖形大小. 

   3. 接著, 在 ViewController.m 中, 實作 setupCGContext 方法:
//@add
-(void)setupCGContext {
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
   
    NSUInteger bytesPerPixel = 4;
    NSUInteger bytesPerRow = bytesPerPixel * screenSize.width;
    NSUInteger bitsPerComponent = 8;
   
    cgcontext = CGBitmapContextCreate(NULL, screenSize.width, screenSize.height, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast);
   
    CGColorSpaceRelease(colorSpace);
}

   4. 下一步, 需要新增一個方法用來畫出 CGImage, 然後轉成 CIImage.
      a. 在 ViewController.h, 新增方法宣告如下:
....
//@add
-(void)setupCGContext;
-(CIImage *)drawGameOfLife;

@end

      b. 在 ViewController.m, 實作方法如下:
....
//@add
-(CIImage *)drawGameOfLife {
    CGContextSetRGBFillColor(cgcontext, 1, 1, 1, 1);
    CGContextFillEllipseInRect(cgcontext, CGRectMake(0, 0, screenSize.width, screenSize.height));
   
    CGImageRef cgImg = CGBitmapContextCreateImage(cgcontext);
    CIImage *ci = [CIImage imageWithCGImage:cgImg];
    CGImageRelease(cgImg);
    return ci;
}
....

    說明: 之後會再修改此方法來畫出 Game Of Life 的狀況. 目前只畫出一個大橢圓.
               這方法的最後四行: 先從 context 建立一個 CGImage, 再從  CGImage 產生
               Core Image 的影像, 最後釋放掉 CGImage 並回傳 Core Image. 依此步驟,
               再套用濾鏡.

   5. 現在, 我們已經設置好建立 CIImage 的方法了, 必需去呼叫它並且使用 CIImage 的
       濾鏡功能. 也要去調整輸入影像的大小來符合所建立的 CIImage, 修改
       ViewController.m 裡的 captureOutput 方法如下:
//@update for new
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
   
    CVPixelBufferRef pixelBuffer = (CVPixelBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
   
    CIImage *image = [CIImage imageWithCVPixelBuffer:pixelBuffer];
   
    float heightSc = screenSize.height/(float)CVPixelBufferGetHeight(pixelBuffer);
    float widthSc = screenSize.width/(float)CVPixelBufferGetWidth(pixelBuffer);
   
    CGAffineTransform transform = CGAffineTransformMakeScale(widthSc, heightSc);
   
    image = [CIFilter filterWithName:@"CIAffineTransform" keysAndValues:kCIInputImageKey, image, @"inputTransform", [NSValue valueWithCGAffineTransform:transform],nil].outputImage;
   
    maskImage = [self drawGameOfLife];
   
    image = [CIFilter filterWithName:@"CIMinimumCompositing" keysAndValues:kCIInputImageKey, image, kCIInputBackgroundImageKey, maskImage, nil].outputImage;
   
    [coreImageContext drawImage:image atPoint:CGPointZero fromRect:[image extent] ];
   
    [self.context presentRenderbuffer:GL_RENDERBUFFER];
}

---------------------------------------------------------------------------------------------

C. 利用繪製的 CGImage 對輸入影像作濾鏡處理
   1. 第一個要套用到圖像(輸入的影像)的濾鏡是 CIAffineTransform(2D 仿射變換),
       它會放大或縮小影像來符合螢幕的尺寸.

   2. 第二個濾鏡為 CIMinimumCompositing(最低合成), 它使用我們建的圖像
        (黑底橢圓白)並且對照到其大小. 從 pixel 的角度看來, 這個濾鏡使用二個值之一
        的最小顏色值. 因為黑色是絕對的最小值, 而白色是絕對的最大值, 因此我們會得到
        有著黑色橢圓圍繞的外側以及從影格(video frame)傳來的顏色.

   3. 編譯並執行: (左右相反, 上下顛倒)

---------------------------------------------------------------------------------------------

D. 引進生命遊戲
   1. 開始來引進生命遊戲的模型. 在這邊不去涉及到實作, 基礎上採用 Alan Quatermain
      建立的版本, 並新增一些額外的屬性: 一個隨機的種子程序用來產生部分的 cell; 以及
      一個用來新增特定樣式(爬行動物)到 grid 的方法.

   2. 先下載此篇文章作者的專案原始碼 並解壓縮. 將 GOLModel.hGOLModel.m 加到
       專案裡.

   3. 在 ViewController.h 中引入 GOLModel.h 並建立一個 Game Of Life 的實體變數.
....
//@add
#import "GOLModel.h"

@interface ViewController : GLKViewController <AVCaptureVideoDataOutputSampleBufferDelegate>
{
.... 
    //@add
    GOLModel *GOL;
}

   4. 在 ViewController.mviewDidLoad 方法中作初始化.
- (void)viewDidLoad
{
    [super viewDidLoad];
    ....
    //@add
    GOL = [[GOLModel alloc] initGameWidth:30 andHeight:20];
    [GOL randomPopulate];
}

   5. 目前已經有了初始化的 GOL(Game Of Life) 物件, 需要改變 ViewController.m
       的 drawGameOfLife 方法來顯現出遊戲的狀態.

-(CIImage *)drawGameOfLife {
    //@add
    // 步驟 1: 經由將螢幕的大小除以 cells 的數目, 來取得每個 cell 的寬與高.
    int colwidth = screenSize.width / GOL.width;
    int rowheight = screenSize.height / GOL.height;

    //CGContextSetRGBFillColor(cgcontext, 1, 1, 1, 1);
    //@update
    // 步驟 2: 接著, 使用不透明度(opacity)為 0.4 來清除螢幕, 這會讓 cells 在完全消失後,
    //              還能產生幾個回合. 在視覺上較為有趣.
    CGContextSetRGBFillColor(cgcontext, 0, 0, 0, 0.4);
    CGContextFillEllipseInRect(cgcontext, CGRectMake(0, 0, screenSize.width, screenSize.height));

    //@add
    NSArray *ar = GOL.cells;
   
    // 步驟 3: 反覆地從 GOL 模型中, 計算每個 cell 的起始 x 與 y 的座標.
    for (int i = 0; i < [ar count]; i++) {
        BOOL CELLACTIVE = [[ar objectAtIndex:i] boolValue];
        int x = i % GOL.width;
        int y = (int)(i/GOL.width);

        // 步驟 4: 接著, 如果發現到一個活躍(active)的 cell, 就在適當的位置畫一個白圈. 
        //               +1 並使用寬與高; -2 來讓 cells 間留一些縫隙.
        if (CELLACTIVE) {
            CGContextSetRGBFillColor(cgcontext, 1, 1, 1, 1);
            CGContextFillEllipseInRect(cgcontext, CGRectMake(x * colwidth + 1, y * rowheight + 1, colwidth - 2, rowheight - 2));
        }
    }

        // 步驟 5: 如同之前的, 將 CGImage 轉成 CIImage.
        CGImageRef cgImg = CGBitmapContextCreateImage(cgcontext);
        CIImage *ci = [CIImage imageWithCGImage:cgImg];
        CGImageRelease(cgImg);
        return ci;
}

   6. 編譯並執行: (左右相反, 上下顛倒)
       產生隨機的一群小圓圈, 取代之前的一個大圓圈. 並在小圓圈中可以看到影像.

---------------------------------------------------------------------------------------------

E. 進行生命遊戲
   1. 現在開始讓遊戲進行. 新增一個 buttonMainStoryboard.storyboard 用來啟動
       與關閉遊戲. 給此 button 一個 IBOutlet: playGOLButton, 以及方法: playGOL.
       並做好相關的聯結. 我們也需要一個計時器: 新增一個實體變數:
       NSTimer *refreshTimer.

   2. 在 ViewController.h 中修改如下:
....
@interface ViewController : GLKViewController <AVCaptureVideoDataOutputSampleBufferDelegate>
{
....
    //@add
    GOLModel *GOL;
   
    //@add
    IBOutlet UIButton *playGOLButton;
    NSTimer *refreshTimer;
}

....
//@add
-(void)setupCGContext;
-(CIImage *)drawGameOfLife;

//@add
- (IBAction)playGOL:(id)sender;
-(void)updateGOL:(NSTimer *)timer;

@end

   3. 在 ViewController.m 中新增方法如下:

//@add
// 更改 buttons 的標籤, 接著新增計時器給 updateGOL 方法使用或關閉計時器.
- (IBAction)playGOL:(id)sender {
    if (refreshTimer) {
        [playGOLButton setTitle:@"Play" forState:UIControlStateNormal];
        [playGOLButton setTitle:@"Play" forState:UIControlStateHighlighted];
        [refreshTimer invalidate];
        refreshTimer = nil;
       
    } else {
        [playGOLButton setTitle:@"Stop" forState:UIControlStateNormal];
        [playGOLButton setTitle:@"Stop" forState:UIControlStateHighlighted];
       
        // 效能提升: 每 0.5 秒執行一次 drawGameOfLife, 取代每個 frame 執行.
        refreshTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(updateGOL:) userInfo:nil repeats:YES];
    }
}

//@add
// 對 GOL 物件呼叫 update 方法, 將會使遊戲進行一個步驟. 然後在更新的 GOL
// 模型上重新繪製 maskImage.
-(void)updateGOL:(NSTimer *)timer {
    [GOL update];
    maskImage = [self drawGameOfLife];
}

   4. 在  ViewController.m 裡的 captureOutput: 方法中, 修改如下: 
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
    ....
    //@update
    // 這邊呼叫了一次相同的 code, 可以將它移除(作註解)
    //maskImage = [self drawGameOfLife];
    ....
}

   5. 在 ViewController.m 裡的 viewDidLoad 方法最下方, 加入以下的 code, 不然會
       得到黑色的銀幕直到按下 Play button.
- (void)viewDidLoad
{
    [super viewDidLoad];
    ....

    //@add
    maskImage = [self drawGameOfLife];
}

   6. 編譯並執行:


---------------------------------------------------------------------------------------------

F. 更改生命遊戲裡 Cell 的值
   1. 下一件我們想作的事是: 在生命遊戲模型中增加多點觸控的互動機制.
       當我們觸控銀幕時, 對於每個觸控, 我們都要去計算 cell 在 GOL 中的位置
       並且開啓或關閉.

   2. 首先, 在 ViewController.m 裡的  viewDidLoad 方法中, 開啓 view 的多點觸控的功能.
- (void)viewDidLoad
{
    [super viewDidLoad];
    //@add
    [self.view setMultipleTouchEnabled:YES];
    ....
}

   3. 接著, 在 ViewController.m 中, 新增 touchesBegantouchesMoved 方法.
        (它們是非常相似的)
//@add
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    for (UITouch *t in touches) {
        CGPoint loc = [t locationInView:self.view];
        loc = CGPointMake(loc.x * scl, loc.y * scl);
        loc = CGPointMake(loc.x, screenSize.height - loc.y);
       
        int colWidth = (int)screenSize.width / GOL.width;
        int rowHeight = (int)screenSize.height / GOL.height;
       
        int x = floor(loc.x / (float)colWidth);
        int y = floor(loc.y / (float)rowHeight);
       
        if (refreshTimer) {
            [GOL spawnWalkerAtCellX:x andY:y];
        } else {
            [GOL toggleCellX:x andY:y];
        }
    }
    maskImage = [self drawGameOfLife];
}

//@add
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    for (UITouch *t in touches) {
       
        CGPoint loc = [t locationInView:self.view];
        loc = CGPointMake(loc.x * scl, loc.y * scl);
        loc = CGPointMake(loc.x, screenSize.height - loc.y);
       
        int colWidth = screenSize.width / GOL.width;
        int rowHeight = screenSize.height / GOL.height;
       
        int x = floor(loc.x / colWidth);
        int y = floor(loc.y / rowHeight);
       
        [GOL toggleCellX:x andY:y];
    }
    maskImage = [self drawGameOfLife];
}

    說明: 首先, 反覆地從 touches set 中, 取得每一個 touch.  對於每一個 touch, 我們先
               轉換觸控點的位置, 因為 view 使用一種座標系統, 而 Core Graphics 使用垂直
               轉換的系統.   

               一旦完成後, 使用銀幕的位置以及對於 cells 尺寸的瞭解來計算:
               在 GOL 模型中, 被觸控的 cell 之座標. 接著呼叫 GOL 的 -toggleCellX:andY:
               方法來開啓或關閉.

               最後, 當我們完成了對所有 touch 各自 cell 的轉換, 用 Core Graphics 來更新
               GOL 的繪製, 並回傳 CIImage 給連串的 CIFilter 使用.

               唯一的不同點是: 在 touchesBegan 方法中, 假如遊戲目前是啓動的, 採用呼叫
                spawnWalker 方法, 以取代切換個別的 cell 方式. 呼叫 spawnWalker 方法會
               在觸控點的位置建立特定的生命遊戲圖案. 這個持續性的圖案會移動越過銀幕
               直到碰見另一組啓用的 cell.

   4. 編譯並執行:
        你可以經由觸控來操縱生命遊戲, 不論遊戲是啓動的或停止的.

---------------------------------------------------------------------------------------------

G. 一些非必要的調整
   1. 為這個 app 加上一些非必要的視覺強化. 首先, 使用 perlin noise generator
       (由 Ken Perlin 發明的自然噪聲生成算法) 來決定 cell 的顏色.

   2. 先將原作者 專案原始碼 裡的 CZGPerlinGenerator.hCZGPerlinGenerator.m
       加到專案裡, 並調整 ViewController.h 如下:

//@add
#import "CZGPerlinGenerator.h"

@interface ViewController : GLKViewController <AVCaptureVideoDataOutputSampleBufferDelegate>
{
....
    //@add
    CZGPerlinGenerator *perlin;
}

.....

   3. 接著, 調整 ViewController.m 裡的 viewDidLoad 方法如下:

- (void)viewDidLoad
{
    [super viewDidLoad];
    ....
    [self setupCGContext];
   
    //@add
    perlin = [[CZGPerlinGenerator alloc] init];
    perlin.octaves = 1;
    perlin.zoom = 50;
    perlin.persistence = 0.5;  
   
    //@add
    GOL = [[GOLModel alloc] initGameWidth:30 andHeight:20];
    [GOL randomPopulate];
   
    //@add
    maskImage = [self drawGameOfLife];
}

   4. 最後, 調整 ViewController.m 裡的 drawGameOfLife 方法如下:

-(CIImage *)drawGameOfLife {
....
    for (int i = 0; i < [ar count]; i++) {
        BOOL CELLACTIVE = [[ar objectAtIndex:i] boolValue];
        int x = i % GOL.width;
        int y = (int)(i/GOL.width);
        // step 4: We next draw a white circle at the appropriate position if we’ve found an active cell. We add +1 and use a width and height – 2 to leave a littel padding between cells.
        if (CELLACTIVE) {
            //@add
            float r = [perlin perlinNoiseX:x * 5 y:y * 5 z:100 t:0] + .5;
            float g = [perlin perlinNoiseX:x * 5 y:y * 5 z:0 t:100] + .5;
            float b = [perlin perlinNoiseX:x * 5 y:y * 5 z:0 t:0] + .5;
           
            //CGContextSetRGBFillColor(cgcontext, 1, 1, 1, 1);
            CGContextSetRGBFillColor(cgcontext, r, g, b, 1);
            CGContextFillEllipseInRect(cgcontext, CGRectMake(x * colwidth + 1, y * rowheight + 1, colwidth - 2, rowheight - 2));
        }
    }
....
}

   5. Perlin noise 是一個隨機的數字產生器, 創造出逼真的外觀紋理.  如果你在 Photoshop
       中使用 Clouds 濾鏡, 會得到 Perlin noise 的二維紋理.

   6. 編譯並執行:
      你的 cells 會讓你覺得: 你是經由不同顏色的手機看出去的.

---------------------------------------------------------------------------------------------

H. 效能
   1. 最後, 討論一下效能的問題. 這個 app 在 iPhone 4S 跟 iPad 2 上執行得相當平順.
       如果是在 3GS 上執行的話, 應該會蠻不順的.

   2. 有一些調整可以來增進效能. 你可以將 CGContext 從 RGB 改成 greyscale(灰階)
       (不過, 就無法使用 perlin noise 的 code), 這有一點幫助. 你也可以降低 GOL 中
       cells 的數目.

   3. 降低影像的解析度也有幫助. 將輸入的影像從 720p 改成 640×480, 並且改變
        cgcontext 跟 core graphics 的 code 以使它畫出 640×480 的影像. 然後移動
        魚鱗狀的濾鏡來產生效果. 根據 WWDC 的 Core Image 影片, 效能會隨著影像
        解析度的比例而改變. 大約可降低 pixels 的數目 2.5 個係數.

2012年1月7日 星期六

Filter4Cam 學習之 Saving a Photo

since: 2012/01/07
update: 2012/01/07

reference: Saving a Photo With AVFoundation and Core Image | Indie Ambitions

A.調整 UI
   1. 接續之前的專案, 點選 MainStoryboard.storyboard 檔案, 加入一個 button,
       並對應到 -(void)snap:(id)sender 方法. 

   2. 接著, 新增一個 slider, 並對應到 sldr IBOutlet.
       這是可用來調整濾鏡的(在此尚未實作)

---------------------------------------------------------------------------------------------

B. 修改 ViewController.h
     新增一個 CIImage 實體變數.
@interface ViewController : GLKViewController <AVCaptureVideoDataOutputSampleBufferDelegate>
{
    //@add
    AVCaptureSession *session;
   
    CIContext *coreImageContext;
    EAGLContext *context;
    GLuint _renderBuffer;
    IBOutlet UISlider *sldr;
    //@add
    CIImage *ciimg;
}

//@add
@property (strong, nonatomic) EAGLContext *context;

- (IBAction)snap:(id)sender;

@end

---------------------------------------------------------------------------------------------

C. 將
照片儲存到相簿
    1. 將 AssetsLibrary framework 加入專案中.
    2. 修改 ViewController.h 檔案, 如下:
....
#import <ImageIO/ImageIO.h>
//@add
#import <AssetsLibrary/AssetsLibrary.h>
....

    3. 開啓 ViewController.m 檔案, 修改如下:
- (IBAction)snap:(id)sender {
    //@add
    CGImageRef cgimg = [coreImageContext createCGImage:ciimg fromRect:[ciimg extent]];

    ALAssetsLibrary *lib = [[ALAssetsLibrary alloc] init];

    [lib writeImageToSavedPhotosAlbum:cgimg metadata:nil completionBlock:^(NSURL *assetURL, NSError *error) {
        CGImageRelease(cgimg);
    }];
}

   // 將 CIImage *image 都改成 ciimg
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
   
    CVPixelBufferRef pixelBuffer = (CVPixelBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
   
    //CIImage *image = [CIImage imageWithCVPixelBuffer:pixelBuffer];
    //@update
    ciimg = [CIImage imageWithCVPixelBuffer:pixelBuffer];
   
    //@add filter
    /*
    image = [CIFilter filterWithName:@"CIFalseColor" keysAndValues:
             kCIInputImageKey, image,
             @"inputColor0", [CIColor colorWithRed:0.0 green:0.2 blue:0.0],
             @"inputColor1", [CIColor colorWithRed:0.0 green:0.0 blue:1.0],
             nil].outputImage;
    */
    //@update
    ciimg = [CIFilter filterWithName:@"CIFalseColor" keysAndValues:
             kCIInputImageKey, ciimg,
             @"inputColor0", [CIColor colorWithRed:0.0 green:0.2 blue:0.0],
             @"inputColor1", [CIColor colorWithRed:0.0 green:0.0 blue:1.0], nil].outputImage;
   
    //[coreImageContext drawImage:image atPoint:CGPointZero fromRect:[image extent] ];
    //@update
    [coreImageContext drawImage:ciimg atPoint:CGPointZero fromRect:[ciimg extent] ];
   
    [self.context presentRenderbuffer:GL_RENDERBUFFER];
}

    4. 編譯並執行

---------------------------------------------------------------------------------------------

D. 調整方向
   1. 修改 ViewController.m 檔案如下

-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
   
    CVPixelBufferRef pixelBuffer = (CVPixelBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
   
    ciimg = [CIImage imageWithCVPixelBuffer:pixelBuffer];
   
    //update: orientation
    UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];
    CGAffineTransform t;
    if (orientation == UIDeviceOrientationPortrait) {
        t = CGAffineTransformMakeRotation(-M_PI / 2);
    } else if (orientation == UIDeviceOrientationPortraitUpsideDown) {
        t = CGAffineTransformMakeRotation(M_PI / 2);
    } else if (orientation == UIDeviceOrientationLandscapeRight) {
        t = CGAffineTransformMakeRotation(M_PI);
    } else {
        t = CGAffineTransformMakeRotation(0);
    }
   
    ciimg = [ciimg imageByApplyingTransform:t];
   
    //@add filter
    ciimg = [CIFilter filterWithName:@"CIFalseColor" keysAndValues:
             kCIInputImageKey, ciimg,
             @"inputColor0", [CIColor colorWithRed:0.0 green:0.2 blue:0.0],
             @"inputColor1", [CIColor colorWithRed:0.0 green:0.0 blue:1.0], nil].outputImage;
   
    [coreImageContext drawImage:ciimg atPoint:CGPointZero fromRect:[ciimg extent] ];
   
    [self.context presentRenderbuffer:GL_RENDERBUFFER];
}

   2. 編譯並執行

---------------------------------------------------------------------------------------------

E. 滑動(Slider)
  1. 修改 ViewController.m 檔案如下:
//@add
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
   ....
   //@add filter
    /*
    ciimg = [CIFilter filterWithName:@"CIFalseColor" keysAndValues:
             kCIInputImageKey, ciimg,
             @"inputColor0", [CIColor colorWithRed:0.0 green:0.2 blue:0.0],
             @"inputColor1", [CIColor colorWithRed:0.0 green:0.0 blue:1.0], nil].outputImage;
     */

    //@add filter & slider
    ciimg = [CIFilter filterWithName:@"CIFalseColor" keysAndValues:
             kCIInputImageKey, ciimg,
             @"inputColor0", [CIColor colorWithRed:0.0 green:sldr.value blue:0.0],
             @"inputColor1", [CIColor colorWithRed:0.0 green:sldr.value blue:1.0], nil].outputImage;
   
    [coreImageContext drawImage:ciimg atPoint:CGPointZero fromRect:[ciimg extent] ];
   
    [self.context presentRenderbuffer:GL_RENDERBUFFER];
}

  2. 編譯並執行

Filter4Cam 學習之 Getting Raw Video Data

since: 2012/01/06
update: 2012/01/07

reference: Getting Raw Video Data into My App Quick and Dirty | Indie Ambitions

A. 建立專案
    1. Xcode > File > New > New Project... > iOS > Application > OpenGL Game > Next
        Product Name: RawVideoData
        Device Family: iPhone
        Use StroyBoard: checked
        Use Automatic Reference Counting: checked
        > Next

    2. 接下來將會使用到藉由 core image 直接畫到 render buffer 的功能.
        可以先將從樣板產生的 code 都移除(ViewController.m 中的), 我們不會使用到.
        預設產生的 frameworks 如下:

    3. 接著, 加入以下的 frameworks. 有些是 video 會用到的, 有則些是 Core Image
        會用到的.
        AVFoundation
        CoreVideo
        CoreMedia
        QuartzCore
        ImageIO
        CoreImage
   
---------------------------------------------------------------------------------------------

B. 引入 frameworks 並設定符合協定
   1. 開啓 ViewController.h 檔案, 修改如下:
#import <UIKit/UIKit.h>
#import <GLKit/GLKit.h>
//@add
#import <AVFoundation/AVFoundation.h>
#import <CoreMedia/CoreMedia.h>
#import <CoreVideo/CoreVideo.h>
#import <QuartzCore/QuartzCore.h>
#import <CoreImage/CoreImage.h>
#import <ImageIO/ImageIO.h>

//@interface ViewController : GLKViewController
@interface ViewController : GLKViewController <AVCaptureVideoDataOutputSampleBufferDelegate>
{
    //@add
    AVCaptureSession *session;
   
    CIContext *coreImageContext;
    EAGLContext *context;
    GLuint _renderBuffer;
}

//@add
@property (strong, nonatomic) EAGLContext *context;

@end

    說明: a. 這邊所使用的委派協定(delegate protocol), 表示這個類別將會接收到由
                   傳送者(delivers)所回傳(callback) 的原始像素資料(raw pixel data).
                   AVCaptureSession 將會來作此設定, 並且配置好控制此 session 的參數,
                   如: 解析度, 相機輸入 .... 等. 要畫出經由濾鏡處理過的 Core Image 結果,
                   需要使用到 Core Image context(CIContext).

              b. Core Image context 需要一個 render buffer 來寫入, 所以先在此宣告實體變數:
                  GLuint _renderBuffer , 之後將會拿來設定. 我們也需要一個 EAGLContext
                  參照, 來呈現 render buffer 的內容.

   2. 開啓 ViewController.m 檔案, 修改如下:
      (在 viewDidLoad 中, 實作設定相機與 context)

@implementation ViewController
//@add
@synthesize context = _context;

- (void)viewDidLoad
{
    [super viewDidLoad];
   
    //@add
    // section 1: 大致上與樣板預設的 code 相同, 主要是設置 EAGLContext
    self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
   
    if (!self.context) {
        NSLog(@"Failed to create ES context");
    }
   
    GLKView *view = (GLKView *)self.view;
    view.context = self.context;
    view.drawableDepthFormat = GLKViewDrawableDepthFormat24;
   
    // section 2: 設置 render buffer
    glGenRenderbuffers(1, &_renderBuffer);
    glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer);
   
    //section 3: 初始化 Core Image context.
    coreImageContext = [CIContext contextWithEAGLContext:self.context];
   
    // section 4: 設置相機輸入, 建立 session 並設定. 在這邊設定 session 為 640 pixels 寬,
    //                    480 pixels 高. 還有其他的選項, 包括 720
pixels 跟 1080 pixels.
    //                    Core Image 的解析度越高, 效能就越低. 一個單一簡單的濾鏡就能夠來
    //                    處理高解析度, 在此僅為測試的處理.

    NSError *error;

    session = [[AVCaptureSession alloc] init];
   
    [session beginConfiguration];
    [session setSessionPreset:AVCaptureSessionPreset640x480];
   
    // section 5: 設定輸入設備. 假如要指定前置或後置相機, 需要呼叫
    //                   devicesWithMediaType , 它會回傳一個設備的陣列. 若要取得前置相機,
    //                   可在  AVCaptureDevicePosition 屬性中, 重複地在陣列中尋找
    //                   AVCaptureDevicePositionFront. 在此為了簡單, 我們使用預設的設備.
    //
    AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

    AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];

    [session addInput:input];
   
    //
section 6: 設定輸出. 並忽略較慢的影格(frames). 如果需要記錄改變的地方, 可以
    //                    設定輸入資料的顏色格式.
    AVCaptureVideoDataOutput *dataOutput = [[AVCaptureVideoDataOutput alloc] init];

    [dataOutput setAlwaysDiscardsLateVideoFrames:YES];

    [dataOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA] forKey:(id)kCVPixelBufferPixelFormatTypeKey]];    
   
    //
section 7: 設定 delegate. 它將會收到回傳(callback)的每個 frame, 並且設為主要
    //                   的駐列(queue). 
    [dataOutput setSampleBufferDelegate:self queue:dispatch_get_main_queue()];
    [session addOutput:dataOutput];

    //
section 8: 完成設定並讓相機開始執行
    [session commitConfiguration];
    [session startRunning];
}

@end

---------------------------------------------------------------------------------------------

C. 回呼函式
   1. 開啓 ViewController.m 檔案, 新增 code 如下:
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
   
    // 將取得的 sampleBuffer 轉成 CVPixelBuffer
    CVPixelBufferRef pixelBuffer = (CVPixelBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
   
    // 接著, 將 CVPixelBuffer 利用 Core Image 的初始化方法再轉成 CIImage.
    CIImage *image = [CIImage imageWithCVPixelBuffer:pixelBuffer];
   
    // 然後使用 CIContext 物件將其內容畫到 render buffer 
    [coreImageContext drawImage:image atPoint:CGPointZero fromRect:[image extent] ];
   
    // 最後, 在 螢幕上呈現出來.
    [self.context presentRenderbuffer:GL_RENDERBUFFER];
}

    說明: 當要記錄 core image 濾鏡的輸出, 可以用 CIContext 的另一個方法,
               來將其寫入到 CVPixelBuffer.

   2. 編譯並執行: (上下顛倒, 左右相反)

---------------------------------------------------------------------------------------------

D. 相關處理
       開啓 ViewController.m 檔案, 修改如下:

- (void)viewDidUnload
{   
    [super viewDidUnload];
    //@add
    if ([EAGLContext currentContext] == self.context) {
        [EAGLContext setCurrentContext:nil];
    }
    self.context = nil;
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Release any cached data, images, etc. that aren't in use.
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    // Return YES for supported orientations
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
        return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
    } else {
        return YES;
    }
}

---------------------------------------------------------------------------------------------

E. 濾鏡
   1. 開啓 ViewController.m 檔案, 修改如下:

//@add
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
   
    CVPixelBufferRef pixelBuffer = (CVPixelBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
   
    CIImage *image = [CIImage imageWithCVPixelBuffer:pixelBuffer];
   
    //@add filter
    image = [CIFilter filterWithName:@"CIFalseColor" keysAndValues:
             kCIInputImageKey, image,
             @"inputColor0", [CIColor colorWithRed:0.0 green:0.2 blue:0.0],
             @"inputColor1", [CIColor colorWithRed:0.0 green:0.0 blue:1.0],
             nil].outputImage;
   
    [coreImageContext drawImage:image atPoint:CGPointZero fromRect:[image extent] ];
   
    [self.context presentRenderbuffer:GL_RENDERBUFFER];
}

    說明: 假色濾鏡(False Color filter), 將圖片的內容對應到二種顏色, 在此為
               暗綠與亮藍.

   2. 編譯並執行: (上下顛倒, 左右相反)

2012年1月6日 星期五

Filter4Cam 學習之 CALayers Tutorial

since: 2011/12/31
update: 2012/01/06

reference: Introduction to CALayers Tutorial | Ray Wenderlich

A. 什麼是 CALayers ?
   1. CALayers 只是一個用來在螢幕上描繪可視內容的矩形之類別, 沒錯這也是 UIViews
      做的事. 但是這只是一個手法: 每一個 UIView 所畫的內容, 都包含了一個 root layer!
      你可以從以下的 code 來存取這個 layer(預設已建好的):
      CALayer *myLayer = myView.layer;

   2. CALayer 類別的好處是: 它包含了一大堆可以設定的屬性讓你用來改變可見的外觀,
      例如:
      a. 圖層(layer) 的大小與位置.
      b. 圖層的背景顏色.
      c. 圖層的內容(圖像或用 Core Graphics 繪製的內容).
      d. 圖層的轉角是否使用圓形的.
      e. 為圖層設定陰影.
      f. 為圖層設定邊框.
      g. 其它等等.

   ---------------------------------------------------------------------------------------------

B. 開始新專案
   1. Xcode > File > New > New Project...
       iOS > Application > Single View Application > Next >
       Product Name: LayerFun
       Device Family: iPhone
       Used Storyboard: checked
       Used Automatic Reference Couting: checked
       > Next

   2. 首先, 將 CALayersCore Animation 會使用到的 framework: QuartzCore 加到專案
       裡. 接著修改 ViewController.m 檔案如下:
// Import QuartzCore.h at the top of the file
#import <QuartzCore/QuartzCore.h>

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //@add
    // set the layer's background color to orange
    self.view.layer.backgroundColor = [UIColor orangeColor].CGColor;
    // round the corners a bit by setting the corner radius
    self.view.layer.cornerRadius = 20.0;
    // shrink the frame a bit so it's easier to see (not work here)
    self.view.layer.frame = CGRectInset(self.view.layer.frame, 20, 20);
}

   3. 編譯並執行:

   ---------------------------------------------------------------------------------------------

C. CALayers 與 Sublayers

  1. 就像 UIViews 可以有 subviews, CALayers 也可以有 sublayers. 你可以建立一個新的
     CALayer 如下所示:

     CALayer *sublayer = [CALayer layer];

  2. 一旦有了 CALayer, 就可以設定任何你想要的屬性. 記住, 有一項屬性必須明確地設定:
     它的 frame (或 bounds/position). 接著, 便可以將新的 layer 當成其它 layer 的
      sublayer:
     [myLayer addSublayer:sublayer];

  3. 在 ViewController.m 檔案裡的 viewDidLoad 方法裡, 替目前 view 的 layer 加入一個
      sublayer :

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //@add
    self.view.layer.backgroundColor = [UIColor orangeColor].CGColor;
    self.view.layer.cornerRadius = 20.0;
    self.view.layer.frame = CGRectInset(self.view.layer.frame, 80, 80);
   
    //@add sublayer
    CALayer *sublayer = [CALayer layer];
    sublayer.backgroundColor = [UIColor blueColor].CGColor;
    // set shadows
    sublayer.shadowOffset = CGSizeMake(0, 3);
    sublayer.shadowRadius = 5.0;
    sublayer.shadowColor = [UIColor blackColor].CGColor;
    sublayer.shadowOpacity = 0.8;
    // sets the frame
    sublayer.frame = CGRectMake(30, 30, 128, 192);
    [self.view.layer addSublayer:sublayer];
}

   說明: 這些座標是相對於 parent layer's frame 的.

  4. 編譯並執行:

   ---------------------------------------------------------------------------------------------

D. 設定 CALayer 影像內容
   1. 在 ViewController.m 檔案裡的 viewDidLoad 方法裡, 在 addSublayer: 之前,
      加入以下的 code:

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    ....
   
    //@add
    sublayer.contents = (id) [UIImage imageNamed:@"BattleMapSplashScreen.jpg"].CGImage;

    sublayer.borderColor = [UIColor blackColor].CGColor;
    sublayer.borderWidth = 2.0;
   
    [self.view.layer addSublayer:sublayer];
}

   2. 編譯並執行:

   ---------------------------------------------------------------------------------------------

E. 圓角與影像內容
   1. 如果在有影像內容的 CALayer 設定 cornerRadius, 影像仍然會畫超出圓角的範圍.
       可以藉由設定 sublayer.masksToBounds = YES 來解決, 但是如此一來陰影就會被
       遮罩屏除, 而無法顯示.

   2. 解決辦法之一, 建立二個 layers. 外部的 layer 僅僅只是一個著色的 CALayer 並賦予
       外框與陰影. 內部的 layer 包含影像, 並且為圓角的遮罩. 如此一來, 外部的 layer 能夠
       畫出陰影, 並且內部的 layer 包含著影像.

   3. 在 ViewController.m 檔案裡的 viewDidLoad 方法裡, 修改以下的 code:
- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //@add
    self.view.layer.backgroundColor = [UIColor orangeColor].CGColor;
    self.view.layer.cornerRadius = 20.0;
    self.view.layer.frame = CGRectInset(self.view.layer.frame, 80, 80);

    CALayer *sublayer = [CALayer layer];
    sublayer.backgroundColor = [UIColor blueColor].CGColor;
    sublayer.shadowOffset = CGSizeMake(0, 3);
    sublayer.shadowRadius = 5.0;
    sublayer.shadowColor = [UIColor blackColor].CGColor;
    sublayer.shadowOpacity = 0.8;
    sublayer.frame = CGRectMake(30, 30, 128, 192);
    sublayer.borderColor = [UIColor blackColor].CGColor;
    sublayer.borderWidth = 2.0;
    sublayer.cornerRadius = 10.0;
    [self.view.layer addSublayer:sublayer];
   
    CALayer *imageLayer = [CALayer layer];
    imageLayer.frame = sublayer.bounds;
    imageLayer.cornerRadius = 10.0;
    imageLayer.contents = (id) [UIImage imageNamed:@"BattleMapSplashScreen.jpg"].CGImage;

    imageLayer.masksToBounds = YES;
    [sublayer addSublayer:imageLayer];
}

   4. 編譯並執行:

   ---------------------------------------------------------------------------------------------

F. CALayer 與客製化繪圖內容

   1. 如何利用 Core Graphics 來客製化繪製 layer 的內容:
      概念是: 將一個類別設為 layer 的 delegate, 並且該類別必需實作
      drawLayer:inContext 方法. 這樣便能夠在該方法內包含任何 Core Graphics 繪製
      的 code.  
    
   2. 現在試著做: 新增一個 layer 並且在其內繪製一個圖案. 你將會需要把 view controller
       設為 layer 的 delegate, 並且實作 drawLayer:inContext 方法來繪製圖案. 繪製圖案的
       code 將會使用到與 Core Graphics 101: Patterns 相同的 code.

   3. 在 ViewController.m 檔案裡的 viewDidLoad 方法裡, 新增以下的 code:
- (void)viewDidLoad
{
    [super viewDidLoad];
    ....
    //@add
    CALayer *customDrawn = [CALayer layer];
    customDrawn.delegate = self;
    customDrawn.backgroundColor = [UIColor greenColor].CGColor;
    customDrawn.frame = CGRectMake(30, 250, 128, 40);
    customDrawn.shadowOffset = CGSizeMake(0, 3);
    customDrawn.shadowRadius = 5.0;
    customDrawn.shadowColor = [UIColor blackColor].CGColor;
    customDrawn.shadowOpacity = 0.8;
    customDrawn.cornerRadius = 10.0;
    customDrawn.borderColor = [UIColor blackColor].CGColor;
    customDrawn.borderWidth = 2.0;
    customDrawn.masksToBounds = YES;
    [self.view.layer addSublayer:customDrawn];
    [customDrawn setNeedsDisplay];
}

    說明: a. customDrawn 這個 layer 將 delegate 設為 self (view controller). 意謂著,
                    self
(view controller) 必需去實作 drawLayer:inContext 這個方法來繪製
                    layer 的內容. 

              b. 新增 layer 之後, 必需藉由呼叫 setNeedsDisplay 方法來告訴 layer 要去
                   refresh 自己本身(並且呼叫 drawLayer:inContext) , 否則
                   drawLayer:inContext 將不會被呼叫到.

   4. 在 ViewController.h 檔案中, 新增以下的 code, 用來宣告繪製圖案的方法:
#import <UIKit/UIKit.h>

@interface ViewController : UIViewController
{
}

//@add
void MyDrawColoredPattern (void *info, CGContextRef context);

@end

   5. 在 ViewController.m 檔案中, 新增以下的 code, 用來定義繪製圖案方法的內容與
       實作 drawLayer:inContext 方法:

#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>

//@add: 如果 Xcode 出現: Implicit declaration of function 'radians' is invalid in C99
static inline double radians (double degrees) {return degrees * M_PI/180;}
....
void MyDrawColoredPattern (void *info, CGContextRef context) {
 
    CGColorRef dotColor = [UIColor colorWithHue:0 saturation:0 brightness:0.07 alpha:1.0].CGColor;
    CGColorRef shadowColor = [UIColor colorWithRed:1 green:1 blue:1 alpha:0.1].CGColor;
 
    CGContextSetFillColorWithColor(context, dotColor);
    CGContextSetShadowWithColor(context, CGSizeMake(0, 1), 1, shadowColor);
 
    CGContextAddArc(context, 3, 3, 4, 0, radians(360), 0);
    CGContextFillPath(context);
 
    CGContextAddArc(context, 16, 16, 4, 0, radians(360), 0);
    CGContextFillPath(context);
}
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)context {
 
    CGColorRef bgColor = [UIColor colorWithHue:0.6 saturation:1.0 brightness:1.0 alpha:1.0].CGColor;
    CGContextSetFillColorWithColor(context, bgColor);
    CGContextFillRect(context, layer.bounds);
 
    static const CGPatternCallbacks callbacks = { 0, &MyDrawColoredPattern, NULL };
 
    CGContextSaveGState(context);
    CGColorSpaceRef patternSpace = CGColorSpaceCreatePattern(NULL);
    CGContextSetFillColorSpace(context, patternSpace);
    CGColorSpaceRelease(patternSpace);
 
    CGPatternRef pattern = CGPatternCreate(NULL,
                                           layer.bounds,
                                           CGAffineTransformIdentity,
                                           24,
                                           24,
                                           kCGPatternTilingConstantSpacing,
                                           true,
                                           &callbacks);
    CGFloat alpha = 1.0;
    CGContextSetFillPattern(context, pattern, &alpha);
    CGPatternRelease(pattern);
    CGContextFillRect(context, layer.bounds);
    CGContextRestoreGState(context);
}

   6. 編譯並執行: