CoreImage淺談與使用

本文主要介紹一下CoreImage的圖像處理框架的應用以及我在使用過程中的坑點。本文提供一個簡易的Demo,對于CoreImage不了解的,可以借助demo快速上手。CoreImage是前一段時間了解的,現在記錄一下,供大家參考。

概覽

本文主要從一下幾個方面來介紹:
1.CoreImage概念
2.內建濾鏡的使用
3.CPU/GPU的不同選擇方案
4.人臉檢測
5.自動增強濾鏡
6.自定義濾鏡
7.注意點

一、CoreImage的概念

Core Image一個圖像處理和分析技術,同時也提供了對視頻圖像實時處理的技術。iOS5中新加入的一個框架,里面提供了強大高效的圖像處理功能,用來對基于像素的圖像進行操作與分析。還提供了很多強大的濾鏡,可以實現你想要的效果,它的處理數據基于CoreGraphics,CoreVideo,和Image I/O框架,既可以使用GPU也可以使用CPU的渲染路徑。
CoreImage封裝了底層圖形處理的實現細節,你不必關心OpenGL和OpenGL ES是如何利用GPU的,也不必知曉GCD是如何利用多核進行處理的。

image.png

其內置了很多強大的濾鏡(Filter) (目前數量超過了190種), 這些Filter 提供了各種各樣的效果, 并且還可以通過 濾鏡鏈 將各種效果的 Filter疊加 起來形成強大的自定義效果。
一個 濾鏡 是一個對象,有很多輸入和輸出,并執行一些變換。
一個 濾鏡鏈 是一個鏈接在一起的濾鏡網絡,使得一個濾鏡的輸出可以是另一個濾鏡的輸入。
image.png

iOS8 之后更是支持自定義 CIFilter,可以定制滿足業務需求的復雜效果。

官方解釋:
image.png

翻譯:底層細節都幫你做好了,放心調用API就行了 。

二、內建濾鏡的使用

首先介紹一下CoreImage中三個最重要的對象:

  • CIImage
    保存圖像數據的類,是一個不可變對象,它表示一個圖像數據。CIImage對象,你可以通過UIImage,圖像文件或者像素數據來創建,也可以從一個CIFilter對象的輸出來獲取。
    下面有一段官方解釋:

    image.png

  • CIFilter
    表示應用的濾鏡,這是框架對圖片屬性進行細節處理的類。它對所有的像素進行操作,用一些鍵-值設置來決定具體操作的程度。

    image.png

    每個源圖像的像素由CISampler對象提取(簡單地取樣器sampler)。
    顧名思義,采樣器sampler檢索圖像的樣本,并將其提供給內核。過濾器創建者為每個源圖像提供一個采樣器。過濾器客戶端不需要知道有關采樣器的任何信息。
    過濾器創建者在內核中定義每塊像素圖像處理計算,Core Image確定是否使用GPU或CPU執行計算。 Core Image根據設備功能使用Metal,OpenGL或OpenGL ES實現圖像的處理。

  • CIContex
    表示上下文,也是實現對圖像處理的具體對象。可以基于CPU或者GPU ,用于繪制渲染,可以從其中取得圖片的信息。
    代碼展示:

- (UIImage *)addEffect:(NSString *)filtername fromImage:(UIImage *)image{
    ///note 1
//        CIImage * image1 = [image CIImage];
//        NSLog(@"%@",image1);
    //因為: UIImage 對象可能不是基于 CIImage 創建的(由 imageWithCIImage: 生成的),這樣就無法獲取到 CIImage 對象
    //解決方法一:
//    NSString * path = [[NSBundle mainBundle] pathForResource:@"tu.jpg" ofType:nil];
//    UIImage * tempImage = [UIImage imageWithCIImage:[CIImage imageWithContentsOfURL:[NSURL fileURLWithPath:path]]];
//    CIImage * tempCIimg = [tempImage CIImage];
//    NSLog(@"%@",tempCIimg);
    
    //解決方法2
    CIImage * ciimage = [[CIImage alloc] initWithImage:image];
    CIFilter * filter = [CIFilter filterWithName:filtername];
    [filter setValue:ciimage forKey:kCIInputImageKey];
    // 已有的值不改變, 其他的設為默認值
    [filter setDefaults];
    //渲染并輸出CIImage
    CIImage * outimage = [filter outputImage];
    
    //UIImage * newImage = [UIImage imageWithCIImage:outimage]; //每次創建都會開辟新的CIContext上下文,耗費空間

    // 獲取繪制上下文
    CIContext * context = [CIContext contextWithOptions:nil];//(GPU上創建)
    //self.context; //
    //創建CGImage
    CGImageRef cgimage = [context createCGImage:outimage fromRect:[outimage extent]];
    UIImage * newImage = [UIImage imageWithCGImage:cgimage];
    CGImageRelease(cgimage);
    return newImage;
}

上方有一個注意點:UIImage 對象可能不是基于 CIImage 創建的(由 imageWithCIImage: 生成的),這樣就無法獲取到 CIImage 對象。正確寫法如代碼中的解決方法1 和 解決方法2.
該方法是傳入兩個參數(一張圖片、濾鏡的名稱),具體濾鏡有哪些可以看官方文檔,也可以自己通過下面方法輸出:

// 打印濾鏡名稱
// `kCICategoryBuiltIn`內置; `kCICategoryColorEffect`色彩
- (void)showFilters {
    NSArray *filterNames = [CIFilter filterNamesInCategory:kCICategoryColorEffect];
    for (NSString *filterName in filterNames) {
        NSLog(@"%@", filterName);
        // CIFilter *filter = [CIFilter filterWithName:filterName];
        // NSDictionary *attributes = filter.attributes;
        // NSLog(@"%@", attributes); // 查看屬性
    }
}

例如一個CIMotionBlur濾鏡可以做如下處理:
image.png

所以一個濾鏡的基本使用可以分為四步:

  • Create a CIImage :
  • Create a CIContext
  • Create a CIFilter
  • Get the filter output
    創建過濾器時,您可以在其上配置許多依賴于您正在使用的過濾器的屬性。過濾器為您提供輸出圖像作為CIImage ,您可以使用CIContext將其轉換為UIImage。

三、CPU/GPU的不同選擇

CIContext上下文是繪制操作發生的地方,它決定了CoreImage是使用GPU還是CPU來渲染。

image.png
image.png

上圖分別給出了CPU和GPU的創建方式,其中contextWithOptions創建GPU方式的上下文沒有實時性,雖然渲染是在GPU上執行,但是其輸出的image是不能顯示的,只有當其被復制回CPU存儲器上時,才會被轉成一個可被顯示的image類型,比如UIImage。該方式處理流程如下圖:
image.png

對照上圖,當使用 Core Image 在 GPU 上渲染圖片的時候,先是把圖像傳遞到 GPU 上,然后執行濾鏡相關操作。但是當需要生成 CGImage 對象的時候,圖像又被復制回 CPU 上。最后要在視圖上顯示的時候,又返回 GPU 進行渲染。這樣在 GPU 和 CPU 之前來回切換,會造成很嚴重的性能損耗。
如果需要很高的實時性,則需要基于EAGLContext創建上下文,該種方式也是GPU上處理的,代碼如下:
image.png
處理流程如下圖:
image.png
這種方式創建的上下文是利用實時渲染的特效,而不是每次操作都產生一個 UIImage,然后再設置到視圖上。
核心實現代碼:

 //獲取openGLES渲染環境
        EAGLContext * context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
        //初始化GLKview 并制定openGLES渲染環境 + 綁定
        _showView = [[GLKView alloc] initWithFrame:frame context:context];
        //A default implementation for views that draw their content using OpenGL ES.
        /*
         Binds the context and drawable. This needs to be called when the currently bound framebuffer
         has been changed during the draw method.
         */
        [_showView bindDrawable];
        
        //添加進圖層
        [self addSubview:_showView];
        
        //創建上下文CIContext - GPU方式 :但是必須在主線程
        _context = [CIContext contextWithEAGLContext:context options:@{kCIContextWorkingColorSpace:[NSNull null]}];

然后再使用context進行繪制:

[_context drawImage:ciimage inRect:CGRectMake(0, 0, viewSize.width*scale, viewSize.height*scale) fromRect:[ciimage extent]];

具體詳細實現過程可以看:demo中的GLESvcGLESView實現。
兩種方式的對比
GPU方式:處理速度更快,因為利用了 GPU 硬件的并行優勢。可以使用 OpenGLES 或者 Metal 來渲染圖像,這種方式CPU完全沒有負擔,但是 GPU 受限于硬件紋理尺寸,當 App 切換到后臺狀態時 GPU 處理會被打斷。
CPU方式:會采用GCD對圖像進行渲染處理,這保證了CPU方式比較可靠,并且更容易使用,可以在后臺實現渲染過程。

四、人臉檢測

CIDetecror是Core Image框架中提供的一個識別類,包括對人臉、形狀、條碼、文本的識別。
人臉識別功能不單單可以對人臉進行獲取,還可以獲取眼睛和嘴等面部特征信息。但是CIDetector不包括面紋編碼提取, 只能判斷是不是人臉,而不能判斷這張人臉是誰的。
實現代碼:

-(void)detector:(CIImage *)image{
    //CIContext * context = [CIContext contextWithOptions:nil];
    NSDictionary * param = [NSDictionary dictionaryWithObject:CIDetectorAccuracyLow forKey:CIDetectorAccuracy];
    CIDetector * faceDetector = [CIDetector detectorOfType:CIDetectorTypeFace context:_context options:param];
    NSArray * detectResult = [faceDetector featuresInImage:image];
    printf("count: %lu \n",(unsigned long)detectResult.count);
    if (detectResult.count == 0) {
        self.resultView.hidden = YES;
        return;
    }
    self.resultView.hidden = NO;
    for (CIFaceFeature * feature in detectResult) {
        [UIView animateWithDuration:5/60.0f animations:^{
            self.face.frame = CGRectMake(feature.bounds.origin.x/scaleValue, feature.bounds.origin.y/scaleValue, feature.bounds.size.width/scaleValue, feature.bounds.size.height/scaleValue);
            if (feature.hasLeftEyePosition) {
                self.leftEye.center = CGPointMake(feature.leftEyePosition.x/scaleValue, feature.leftEyePosition.y/scaleValue);
            }
            if (feature.hasRightEyePosition) {
                self.rightEye.center = CGPointMake(feature.rightEyePosition.x/scaleValue, feature.rightEyePosition.y/scaleValue);
            }
            if (feature.hasMouthPosition) {
                self.mouth.center = CGPointMake(feature.mouthPosition.x/scaleValue, feature.mouthPosition.y/scaleValue);
            }
            ///note: UI坐標系 和 CoreImage坐標系不一樣:左下角為原點
        }];
        //_resultView.transform = CGAffineTransformMakeScale(1, -1);
    }
}

此處有一個注意點,UI坐標系 和 CoreImage坐標系不一樣,CoreImage坐標系左下角為原點,UI坐標系左上角為圓點。

五、自動增強濾鏡

CoreImage的自動增強特征分析了圖像的直方圖,人臉區域內容和元數據屬性。接下來它將返回一個CIFilter對象的數組,每個CIFilter的輸入參數已經被設置好了,這些設置能夠自動去改善被分析的圖像。這種也是常見的自動美顏方式。下表列出了CoreImage用作自動圖像增強的濾鏡。這些濾鏡將會解決在照片中被發現的那些常見問題。
image.png

實現代碼:

///自動圖像增強
-(UIImage *)autoAdjust:(CIImage *)image{
    id orientationProperty = [[image properties] valueForKey:(__bridge id)kCGImagePropertyOrientation];
    NSDictionary *options = nil;
    if (orientationProperty) {
        options = @{CIDetectorImageOrientation : orientationProperty};
        //用于設置識別方向,值是一個從1 ~ 8的整型的NSNumber。如果值存在,檢測將會基于這個方向進行,但返回的特征仍然是基于這些圖像的。
    }
    NSArray *adjustments = [image autoAdjustmentFiltersWithOptions:options];
    for (CIFilter *filter in adjustments) {
        [filter setValue:image forKey:kCIInputImageKey];
        image = filter.outputImage;
    }
    CIContext * context = [CIContext contextWithOptions:nil];//(GPU上創建) //self.context;
    //創建CGImage
    CGImageRef cgimage = [context createCGImage:image fromRect:[image extent]];
    UIImage * newImage = [UIImage imageWithCGImage:cgimage];
    return newImage;
}

六、自定義濾鏡

什么時候需要自定義濾鏡?
1 對于一種表達效果,我們使用了多種濾鏡,并且后續還會繼續使用這種效果。
2 對于一些高級的、apple并沒有提供的一些效果,需要對算法進行封裝的。
封裝方法:

1可以基于已存在的濾鏡來子類化一個CIFilter,還可以描述具有多個濾鏡鏈的配方
2用屬性來聲明濾鏡的輸入參數,屬性名必須以input為前綴,例如:inputImage
3可用重寫setDefaults方法來設置默認參數。 在iOS中,CIFilter被創建后會自動調用該方法
4需要重寫outputImage方法
例如我這邊有一個對視頻每一幀添加水印的方法:

-(void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection{
    CVPixelBufferRef pixelBuffer = (CVPixelBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
    CIImage *ciimage = [CIImage imageWithCVPixelBuffer:pixelBuffer];
    if (_witchBtn.isOn) {
        //[self.filter setValue:ciimage forKey:kCIInputImageKey];
        //ciimage = [_filter outputImage];
//        CLColorInvertFilter * customeFilter = [[CLColorInvertFilter alloc] init];
//        customeFilter.inputImage = ciimage;
//        ciimage = [customeFilter outputImage];
        //自定義添加水印
        _customerFilter = [[HZChromaKeyFilter alloc] initWithInputImage:[UIImage imageNamed:@"tu.jpg"] backgroundImage:ciimage];
        ciimage = _customerFilter.outputImage;
    }
    [self.gpuView drawCIImage:ciimage];
    
}

而具體的HZChromaKeyFilter類的實現:

@interface HZChromaKeyFilter : CIFilter

-(instancetype)initWithInputImage:(UIImage *)image
                  backgroundImage:(CIImage *)bgImage;

@property (nonatomic,readwrite,strong) UIImage *inputFilterImage;
@property (nonatomic,readwrite,strong) CIImage *backgroundImage;
@end
@implementation HZChromaKeyFilter
-(instancetype)initWithInputImage:(UIImage *)image backgroundImage:(CIImage *)bgImage{
    self=[super init];
    
    if (!self) {
        return nil;
    }
    

    self.inputFilterImage=image;
    self.backgroundImage=bgImage;
    
    return self;
    
}
static int angle = 0;

-(CIImage *)outputImage{
    
    CIImage *myImage = [[CIImage alloc] initWithImage:self.inputFilterImage];
    //位移
    CIImage * tempImage = myImage;//[scaleFilter outputImage];
    CGSize extsz1 = self.backgroundImage.extent.size;
    CGSize extsz2 = tempImage.extent.size;
    CGAffineTransform transform = CGAffineTransformMakeTranslation(extsz1.width-extsz2.width -100, extsz2.height+100);
    transform = CGAffineTransformRotate(transform, M_PI*2*(angle/360.0));
    angle ++;
    if (angle == 360) {
        angle = 0;
    }
    CIFilter * transformFilter = [CIFilter filterWithName:@"CIAffineTransform"];
    [transformFilter setValue:tempImage forKey:@"inputImage"];
    [transformFilter setValue:[NSValue valueWithCGAffineTransform:transform] forKey:@"inputTransform"];
    
    CIImage *backgroundCIImage = self.backgroundImage; //[[CIImage alloc] initWithImage:self.backgroundImage];
    CIImage *resulImage = [[CIFilter filterWithName:@"CISourceOverCompositing"  keysAndValues:kCIInputImageKey,transformFilter.outputImage,
                            kCIInputBackgroundImageKey,backgroundCIImage,nil]
                           valueForKey:kCIOutputImageKey];

    return resulImage;
}

具體可以參考demo中的HZChromaKeyFilter.h實現。

七、注意點

  • 不要每次渲染都去創建一個CIConcext,上下文中保存了大量的狀態信息,重用會更加高效
  • 當使用GPU的上下文時,應當避免使用CoreAnimation。如果希望同時使用它們,則應該使用CPU上下文。 涉及GPU的處理應該放到主線程來完成。
  • 避免CPU和GPU之間進行沒必要的紋理切換
  • 保證圖像不要超過CPU和GPU的限制。

參考文獻:

1 官方教材

2 iOS原生系統架構

3 CoreImage基礎

4 CoreImage進階系列

5 圖形圖像處理:位圖圖像原圖修改

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容