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 图形图像处理:位图图像原图修改

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容