1. 程式人生 > >iOS CoreImage專題(二) —— 進階

iOS CoreImage專題(二) —— 進階

前言

上一章節我們簡單的介紹了CoreImage以及其濾鏡的簡單使用,包括對輸出影象的渲染。這一部分我們將使用更高階一些的技巧:濾鏡鏈、影象轉場、人臉檢測、自動影象增強等。

管理執行緒安全

在我們開始之前,先看一個比較重要的東西,那就是執行緒安全。

CIContext和CIImage物件都是不可變的,也就是說他們可以安全地共享給多個執行緒。多執行緒可以使用相同的GPU或者CPU下的CIContext物件來渲染CIImage物件。然而,這種情況並不能用於CIFilter物件,因為它是可變的。一個CIFilter物件不能安全地共享給多個執行緒。如果你的app是多執行緒的,每個執行緒必須建立它自己的CIFilter物件。否則你的app可能會產生一些意料之外的效果。

濾鏡鏈

你可以建立一個濾鏡鏈來獲得一種amazing的效果。建立濾鏡鏈就是把一個濾鏡的的輸出影象作為下一個濾鏡的輸入影象。我們來看看如何在一個影象上應用多個濾鏡 – 變暗(CIGloom)以及凹凸變形(CIBumpDistortion)。
變暗的濾鏡通過削弱一個影象的強光部分來讓其變暗。下面的程式碼建立了一個濾鏡然後把剛才調整色調的輸出影象作為變暗濾鏡的輸入影象。這就是把濾鏡連成一串,簡單吧。

    CIFilter *gloom = [CIFilter filterWithName:@"CIGloom"];
    [gloom setDefaults];    // 1
[gloom setValue: result forKey: kCIInputImageKey]; [gloom setValue: @25.0f forKey: kCIInputRadiusKey]; // 2 [gloom setValue: @0.75f forKey: kCIInputIntensityKey]; // 3 result = [gloom valueForKey: kCIOutputImageKey]; // 4

1. 設定預設引數。在OS X中必須要手動呼叫才能設定預設引數,iOS則不需要,因為在iOS中在呼叫filterWithName的時候已經自動設定好了。
2. 設定輸入半徑為25。輸入半徑指定了效果的範圍,可以從0變化到100,其預設值為10。之前提到了你能在程式碼中通過一個濾鏡的屬性字典來找到最大值、最小值和預設值。
3. 設定輸入強度為0.75。輸入強度是一個標量值,它指定了原圖和輸出圖的線性混合。最小值為0,最大值為1,預設值為1。
4. 獲取輸出影象,而此時並有繪製圖像。

程式碼到這裡只是請求了一個輸出影象而並沒有繪製圖像,下圖展示瞭如果你在這時就繪製圖像(在處理了色調和變暗後)的話影象將會顯示成什麼樣子。

凹凸變形濾鏡(CIBumpDistortion)在原圖中一個指定的點建立一個凸起效果。下面的程式碼將展示如何建立、設定以及將其應用在之前的濾鏡(變暗濾鏡)的輸出影象上。凹凸變形需要三個引數:指定效果的位置,效果的半徑以及輸入比例。

    CIFilter *bumpDistortion = [CIFilter filterWithName:@"CIBumpDistortion"]; //1
    [bumpDistortion setDefaults]; //2
    [bumpDistortion setValue: result forKey: kCIInputImageKey];
    [bumpDistortion setValue: [CIVector vectorWithX:200 Y:150]
                      forKey: kCIInputCenterKey]; //3
    [bumpDistortion setValue: @100.0f forKey: kCIInputRadiusKey]; //4
    [bumpDistortion setValue: @3.0f forKey: kCIInputScaleKey]; //5
    result = [bumpDistortion valueForKey: kCIOutputImageKey];
  1. 通過濾鏡名稱建立濾鏡
  2. 同樣的需要在OS X中設定為預設值(iOS中則不需要)
  3. 設定效果的中心點在影象中的位置
  4. 設定凹凸半徑為100畫素
  5. 設定輸入比例為3。輸入比例指定了效果的方向和數量。預設值為-0.5。取值範圍是-10.0到10.0。設定為0則表示沒有效果。負值建立一個向外凸起的效果,正值則建立向內凸起的效果。

最終的渲染影象:

使用轉場效果

轉場效果通常被用於幻燈片的切換或者在視訊中從一個場景切換到另一個場景。這種效果是隨著時間的推移而渲染的,所以你要設定一個計時器。接下來要講的是如何設定計時器,你將會學會如何設定拷貝機(copy machine)轉場濾鏡(CICopyMachine)並將其應用於兩張圖片。拷貝機轉場將建立一根光條,就像你在拷貝機或者影象掃描器上看到的那樣。這根光條在源影象上從左掃到右,掃過的地方將變成目標影象。下圖展示了從一個滑雪靴到滑雪者的轉場中,這個濾鏡在作用前、作用時和作用後看起來是怎樣的。

你需要通過以下步驟來實現一個轉場濾鏡的效果:
1. 建立一個CIImage物件用來轉場。
2. 設定並排程一個計時器。
3. 建立一個CIContext物件。
4. 建立一個CIFilter物件,將用來應用於影象。
5. 在OS X中,需要設定濾鏡的預設值。
6. 設定濾鏡引數。
7. 設定源影象和目標影象。
8. 計算時間。
9. 應用濾鏡。
10. 繪製結果。
11. 重複8-10步直到轉場完成。

你會發現這些步驟中的好幾步都跟用一個普通濾鏡處理影象一樣。不同之處在於使用了計時器來在整個轉場的過程中重複地繪製效果。
在下面的程式碼中,我們將封裝一個UIView,在他的initWithFrame:方法裡面獲取兩張圖片(boots.png和skier.png)然後把它們設定為源影象和目標影象。使用了一個計時器,它每1/30秒重複一次。thumbnailWidth和thumbnailHeight兩個變數用來限制被渲染的影象是如何顯示到檢視上的。
這個例子在官方例子的基礎上作了修改,因為官方的例子是基於OS X的CoreImage方法來呈現的,一些方法在iOS中並不存在。

@interface CICopyMachineView ()
{
    CGFloat thumbnailWidth;
    CGFloat thumbnailHeight;
    NSTimeInterval base;
    CIFilter * transition;
    CIContext * context;
}

@property (nonatomic, strong) CIImage * sourceImage;
@property (nonatomic, strong) CIImage * targetImage;

@end

@implementation CICopyMachineView


- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        NSTimer    *timer;
        NSURL      *url;
        thumbnailWidth  = CGRectGetWidth(frame);
        thumbnailHeight = CGRectGetHeight(frame);
        url   = [NSURL fileURLWithPath: [[NSBundle mainBundle]
                                         pathForResource: @"boots" ofType: @"png"]];
        [self setSourceImage: [CIImage imageWithContentsOfURL: url]];
        url   = [NSURL fileURLWithPath: [[NSBundle mainBundle]
                                         pathForResource: @"skier" ofType: @"png"]];
        [self setTargetImage: [CIImage imageWithContentsOfURL: url]];
        timer = [NSTimer scheduledTimerWithTimeInterval: 1.0/30.0
                                                 target: self
                                               selector: @selector(timerFired:)
                                               userInfo: nil
                                                repeats: YES];
        base = [NSDate timeIntervalSinceReferenceDate];
        [[NSRunLoop currentRunLoop] addTimer: timer
                                     forMode: NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] addTimer: timer
                                     forMode: UITrackingRunLoopMode];
    }
    return self;
}

建立一個轉場濾鏡就和建立普通濾鏡一樣,使用filterWithName:方法來建立濾鏡。然後它會自動呼叫setDefaults來對所有輸入引數進行初始化。按例使用thumbnail變數來指定效果的中心。在這個例子中效果的中心被設定到了影象的中心上,而效果的中心被設定為影象的中心並不是必須的。

- (void)setupTransition
{
    CGFloat w = thumbnailWidth;
    CGFloat h = thumbnailHeight;
    CIVector *extent = [CIVector vectorWithX: 0  Y: 0  Z: w  W: h];
    transition  = [CIFilter filterWithName: @"CICopyMachineTransition"];
    // Set defaults on OS X; not necessary on iOS.
    [transition setDefaults];
    [transition setValue: extent forKey: kCIInputExtentKey];
}

timerFired:方法將在每次計時器觸發時刻到來時被回撥。在這個方法中我們將獲取一個矩形,它的寬高和檢視寬高相等。接下來我們要設定一個渲染時刻。如果我們的CIContext物件還沒有被建立,我們就建立一個(懶載入)。同樣的,如果我們的轉場濾鏡還沒有被建立,我們就建立一個。最後我們獲取渲染後的輸出影象,它是一個CGImageRef(指向CGImage結構體的指標),我們可以用它來渲染檢視圖層的內容,這樣就能加以顯示了。imageForTransition:方法每當渲染時刻到來時會被呼叫,它將濾鏡應用於影象,並返回作用後的影象。

- (void)timerFired:(NSTimer *)timer
{
    CGRect  cg = self.bounds;
    CGFloat t = 0.4 * ([NSDate timeIntervalSinceReferenceDate] - base);
    if (context == nil) {
        context = [CIContext contextWithOptions:nil];
    }

    if (transition == nil) {
        [self setupTransition];
    }

    CIImage * image = [self imageForTransition: t + 0.1];
    CGImageRef cgImage = [context createCGImage:image fromRect:cg];
    self.layer.contents = (__bridge id)cgImage;
    CGImageRelease(cgImage);
}

imageForTransition:方法將基於當前渲染時刻指出誰才是真正的源影象誰才是真正的目標影象,這樣我們就能讓拷貝機的掃描效果迴圈地往返出現。如果你不想要迴圈效果,那麼就刪掉if-else程式碼塊。

- (CIImage *)imageForTransition: (float)t
{
    // Remove the if-else construct if you don't want the transition to loop
    if (fmodf(t, 2.0) < 1.0f) {
        [transition setValue: _sourceImage  forKey: kCIInputImageKey];
        [transition setValue: _targetImage  forKey: kCIInputTargetImageKey];
    } else {
        [transition setValue: _targetImage  forKey: kCIInputImageKey];
        [transition setValue: _sourceImage  forKey: kCIInputTargetImageKey];
    }
    [transition setValue: @( 0.5 * (1 - cos(fmodf(t, 1.0f) * M_PI)) )
                  forKey: kCIInputTimeKey];
    CIFilter  *crop = [CIFilter filterWithName: @"CICrop"
                                 keysAndValues:
                       kCIInputImageKey, [transition valueForKey: kCIOutputImageKey],
                       @"inputRectangle", [CIVector vectorWithX: 0  Y: 0
                                                              Z: thumbnailWidth  W: thumbnailHeight],
                       nil];
    return [crop valueForKey: kCIOutputImageKey];
}

最後我們在一個controller中呼叫這個view,就能看到炫酷的效果了。如果使用模擬器執行出現了一些卡幀,不用驚慌,那是因為我們的模擬器並沒有被配置GPU,它只能使用CPU進行渲染,這會導致CPU佔用率頗高,而我們的真機擁有GPU,可以相當高效地進行渲染。

#import "CICopyMachineViewController.h"
#import "CICopyMachineView.h"

@implementation CICopyMachineViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    CICopyMachineView * view = [[CICopyMachineView alloc] initWithFrame:CGRectMake(100, 100, 276, 208)];
    [self.view addSubview:view];

    self.view.backgroundColor = [UIColor whiteColor];
}

@end

影象中的人臉檢測

CoreImage能夠分析並發現影象中的人臉。它只進行人臉檢測,而不是人臉識別。人臉檢測是識別含有人臉特徵的矩形,而人臉識別則是特定人臉的識別(這張臉屬於謝耳朵,萊昂納德等)。在CoreImage檢測到人臉後,它可以提供臉部特徵的資訊,比如眼睛和嘴巴的位置。它還可以在視訊中追蹤一張指定的臉的位置。

知道一張影象裡面的臉在哪些位置能讓你進行一些其他的操作,如剪裁或調整人臉影象的影象品質(色調平衡,紅眼校正等)。你也可以進行其他有趣的操作,比如:
· 匿名人臉濾鏡配方,它展現瞭如何將一個畫素化濾鏡僅作用於影象中的人臉。
· 白暈人臉配方,它展現瞭如何在人臉周圍加點光暈。

人臉檢測

在下面的程式碼中,我們將使用CIDetector類來找到影象中的人臉。

CIContext *context = [CIContext contextWithOptions:nil]; //1
    NSDictionary *opts = @{ CIDetectorAccuracy : CIDetectorAccuracyHigh }; //2
    CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeFace
                                              context:context
                                              options:opts]; //3
    opts = @{ CIDetectorImageOrientation :
                  [[myImage properties] valueForKey:CIDetectorImageOrientation] }; // 4
NSArray *features = [detector featuresInImage:myImage options:opts]; //5
  1. 建立一個上下文。當你建立一個檢測器的時候可以傳一個nil的上下文。
  2. 建立一個選項字典,它用來指定檢測器的精度。你可以指定低精度或者高精度。低精度(CIDetectorAccuracyLow)執行得更快,而這個例子中的高精度將會比較慢。
  3. 建立一個人臉檢測器。你唯一能建立的識別器型別就是人臉檢測。
  4. 找到人臉,我們需要設定一個選項字典。讓CoreImage知道影象的轉向是相當重要的,如此一來檢測器就知道它能在哪找到擺正的臉。大多數情況你都需要從影象中獲取影象的轉向,然後將它作為選項字典的值。
  5. 使用檢測器來發現影象中的特徵,該影象必須是一個CIImage物件。CoreImage將會返回一個CIFeature物件的陣列,每個元素都代表了影象中的一張臉。

在你獲得人臉的陣列後,你就可以從中找到這些臉的特徵,比如它們的眼睛和嘴巴在哪裡。接下來我們就來看看如何讀取人臉的各種特徵。

獲取人臉和人臉的特徵所在的矩形界限

人臉特徵包括:
· 左眼和右眼的位置。
· 嘴巴的位置
· 追蹤ID和追蹤邊框數,CoreImage使用它們來在視訊段落中追蹤一張人臉。

當你通過CIDetector物件獲取一個人臉特徵的陣列後,你就可以遍歷出臉及臉部特徵所在的邊框。

for (CIFaceFeature *f in features)
    {
        NSLog(@"%@",NSStringFromCGRect(f.bounds));
        if (f.hasLeftEyePosition)
            NSLog(@"Left eye %g %g", f.leftEyePosition.x, f.leftEyePosition.y);
        if (f.hasRightEyePosition)
            NSLog(@"Right eye %g %g", f.rightEyePosition.x, f.rightEyePosition.y);
        if (f.hasMouthPosition)
            NSLog(@"Mouth %g %g", f.mouthPosition.x, f.mouthPosition.y);
}

自動影象增強

CoreImage的自動增強特徵分析了影象的直方圖,人臉區域內容和元資料屬性。接下來它將返回一個CIFilter物件的陣列,每個CIFilter的輸入引數已經被設定好了,這些設定能夠自動去改善被分析的影象。

自動影象增強濾鏡

下表列出了CoreImage用作自動影象增強的濾鏡。這些濾鏡將會解決在照片中被發現的那些常見問題。

濾鏡 目的
CIRedEyeCorrection 修復由相機閃光造成的紅眼,琥珀眼和白眼
CIFaceBalance 調整臉的顏色使其膚色看起來比較舒服
CIVibrance 增加影象飽和度,不改變膚色
CIToneCurve 調整影象對比度
CIHighlightShadowAdjust 調整陰影細節

使用自動增強濾鏡

自動增強API只有兩個方法:autoAdjustmentFilters以及autoAdjustmentFiltersWithOptions:。在大多數情況下,你會使用那個提供了選項字典的方法。

你可以設定這些選項:
· 影象轉向,這對於CIRedEyeCorrection和CIFaceBalance濾鏡來說相當關鍵,設定了影象轉向就能讓CoreImage正確的找到人臉。
· 是否只應用紅眼校正。(設定kCIImageAutoAdjustEnhance為false)
· 是否應用除了紅眼校正以外的所有濾鏡。(設定kCIImageAutoAdjustRedEye為false)。

autoAdjustmentFiltersWithOptions:方法返回選項濾鏡的陣列,你將會把這些濾鏡做成一個濾鏡鏈,然後應用於被分析的影象。下面的程式碼首先建立了一個選項字典,然後獲取影象的轉向並將它設定為CIDetectorImageOrientation鍵的值。

CIImage * myImage = [CIImage imageWithContentsOfURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"tbbt" ofType:@"png"]]];

    id orientationProperty = [[myImage properties] valueForKey:(__bridge id)kCGImagePropertyOrientation];
    NSDictionary *options = nil;
    if (orientationProperty) {
        options = @{CIDetectorImageOrientation : orientationProperty};
    }

    NSArray *adjustments = [myImage autoAdjustmentFiltersWithOptions:options];
    for (CIFilter *filter in adjustments) {
        [filter setValue:myImage forKey:kCIInputImageKey];
        myImage = filter.outputImage;
}

記住,輸入引數的值已經被CoreImage設定好了,這樣就能產生最佳結果。
你不必馬上就去應用這些濾鏡進行渲染,你可以把濾鏡名稱和引數值先存起來待會用。把這些引數儲存起來稍後執行將會消除再次分析影象的成本。

總結

在這一章節中我們介紹了幾種CoreImage的進階技巧,我們整個專題的demo全部放到了這裡:
github link for demo

在下一章節中,我們將基於上面的一些技巧來看看如何通過子類化一個CIFilter物件來封裝我們的自定義濾鏡。