本文授权转载,作者:吴白
Core Image初见
Core Image是一个OS X和iOS的图像处理框架,Core Image很强大,不仅可以做图片处理,还可以做人脸识别等多种工作。它有两个基本概念:滤镜和滤镜图表。一个滤镜是一个对象,有很多输入和输出,并执行一些变换。例如,模糊滤镜可能需要输入图像和一个模糊半径来产生适当的模糊后的输出图像。一个滤镜图表是一个链接在一起的滤镜网络,使得一个滤镜的输出可以是另一个滤镜的输入。以这种方式,可以实现精心制作的效果。
Core Image有一个插件架构,它允许用户编写自定义的滤镜并与系统提供的滤镜集成来扩展其功能。提供了强大高效的图像处理功能,用来对基于像素的图像进行操作与分析。Core Image 最大化利用其所运行之上的硬件。每个滤镜实际上的实现,即内核,是由一个GLSL(即OpenGL的着色语言)的子集来书写的。当多个滤镜连接成一个滤镜图表,Core Image便把内核串在一起来构建一个可在GPU上运行的高效程序。只要有可能,Core Image都会把工作延迟。通常情况下,直到滤镜图表的最后一个滤镜的输出被请求之前都不会发生分配或处理。为了完成工作,Core Image需要一个称为上下文(context)的对象。这个上下文是框架真正工作的地方,它需要分配必要的内存,并编译和运行滤镜内核来执行图像处理。建立一个上下文是非常昂贵的,所以你会经常想创建一个反复使用的上下文。
Core Image框架中的对象
CIImage是CoreImage框架中最基本代表图像的对象,它不仅包含元图像数据,还包含作用在原图像上的滤镜链。在CIImage被CIContext渲染出来之前,它是依赖于滤镜链的,滤镜是不会更改CIImage中的图像数据。关于创建CIImage,CIImage是不能直接有UIImage转化而来的,有以下几种创建CIImage的类方法:
1.CIImage *image=[CIImage imageWithContentsOfURL:myURL]; 2.CIImage *image=[CIImage imageWithData:myData]; 3.CIImage *image=[CIImage imageWithCGImage:myCgimage]; 4.CIImage *image=[CIImage imageWithCVPixelBuffer:CVBuffer];
更多可以参考开发者文档 CIImage 。
CIFilter用来表示CoreImage提供的各种滤镜。滤镜使用键-值来设置输入值,一旦这些值设置好,CIFilter就可以用来生成新的CIImage输出图像了。这里的输出的图像不会进行实际的图像渲染,他只包含一个对输入图像的引用以及需要应用与数据上的滤镜链。iOS永远在最佳的时间选择渲染图像。
CIFilter提供了一个简单的方法查询可用的滤镜种类:
[CIFilter filterNamesInCategory:kCICategoryBuiltIn];//搜索属于kCICategoryBuiltIn(A filter provided by Core Image)类别的所有滤镜名字,返回一个数组 [CIFilter filterNamesInCategories];//搜索所有可用的滤镜名称 [CIFilter attributes]//会返回filter详细信息
下面是我程序返回的一个叫做CISepiaTone滤镜返回的详细信息:
举例分析如下:
CIAttributeFilterCategories = (//滤镜所示种类,通常一个滤镜可以属于几种 CICategoryColorEffect, //总类,这只是根据滤镜效果,作用来分类的 CICategoryVideo, //可以用种类名来搜索Fileter; CICategoryInterlaced, CICategoryNonSquarePixels, CICategoryStillImage, CICategoryBuiltIn ); CIAttributeFilterDisplayName = "Sepia Tone"; CIAttributeFilterName = CISepiaTone; //滤镜的名称,通过该名称来 inputImage = { //滤镜使用需要输入的参数,该 CIAttributeClass = CIImage; //参数类型为CIImage。 CIAttributeType = CIAttributeTypeImage; }; inputIntensity = { //输入强度,参数的名称 CIAttributeClass = NSNumber; //类型 CIAttributeDefault = 1; //默认值 CIAttributeIdentity = 0; CIAttributeMax = 1; //最大值 CIAttributeMin = 0; //最小值 CIAttributeSliderMax = 1; CIAttributeSliderMin = 0; CIAttributeType = CIAttributeTypeScalar; }; }
程序中使用CISepiaTone的代码为:
CIFilter *filter = [CIFilter filterWithName:@"CISepiaTone"]; [filter setValue:inputImage forKey:@"inputImage"]; [filter setValue:[NSNumber numberWithFloat:0.8] forKey:@"inputIntensity"];
第一行:指定使用哪一个过滤器,通过[CIFilter filterNamesInCategory: kCICategoryBuiltIn]能得到所有过滤器的列表
第二行:指定需要处理的图片
第三行:指定过滤参数,每个过滤器的参数都不一样,可以在官方文档里搜索“Core Image Filter Reference”查看
CIContext用来渲染CIImage,将作用在CIImage上的滤镜链应用到原始的图片数据中。CIContext可以是基于CPU的,也可以是基于GPU的,这两种渲染的区别是:使用CPU渲染的iOS会采用GCD来对图像进行渲染,这保证了CPU渲染在大部分情况下更可靠,比CPU渲染更容易使用,可以在后台实现渲染过程;而GPU渲染方式使用OpenGL ES2.0来渲染图像,这种方式CPU完全没有负担,应用程序的运行循环不会受到图像渲染的影响,而且渲染比CPU渲染更快但是GPU渲染无法在后台运行。
对于复杂的图像滤镜使用GPU更好,但是如果在处理视频并保存文件,或保存照片到照片库中时为避免程序退出对图片保存造成影响,这时应该使用CPU进行渲染。默认情况是用CPU渲染的。
//CPU渲染 CIContext *context = [CIContext contextWithOptions:[NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:kCIContextUseSoftwareRenderer]]; // 创建基于GPU的CIContext对象 CIContext * context = [CIContext contextWithOptions: nil]; // 基于GPU EAGLContext *glContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; CIContext *context = [CIContext contextWithEAGLContext:glContext];
GPU受限于硬件纹理尺寸,如果你的程序在后台继续处理和保存图片的话,需要使用CPU,因为当app切换到后台状态时GPU处理会被打断。基于GPU的CIContext对象无法跨应用访问。比如你打开UIImagePickerController要选张照片进行美化,如果你直接在UIImagePickerControllerDelegate的委托方法里调用CIContext对象进行处理,那么系统会自动将其降为基于CPU的,速度会变慢,所以正确的方法应该是在委托方法里先把照片保存下来,回到主类里再来处理。
1.在imageView中使用:
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"image" ofType:@"png"]; NSURL *fileNameAndPath = [NSURL fileURLWithPath:filePath]; CIImage *beginImage = [CIImage imageWithContentsOfURL:fileNameAndPath]; CIFilter *filter = [CIFilter filterWithName:@"CISepiaTone" keysAndValues: kCIInputImageKey, beginImage, @"inputIntensity", @0.8, nil]; CIImage *outputImage = [filter outputImage]; UIImage *newImage = [UIImage imageWithCIImage:outputImage]; self.imageView.image = newImage;
结果
2.将图片保存到photoLibrary
需要引入#import
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"image" ofType:@"png"]; NSURL *fileNameAndPath = [NSURL fileURLWithPath:filePath]; CIImage *beginImage = [CIImage imageWithContentsOfURL:fileNameAndPath]; CIFilter *filter = [CIFilter filterWithName:@"CISepiaTone" keysAndValues: kCIInputImageKey, beginImage, @"inputIntensity", @0.8, nil]; CIImage *saveToSave = [filter outputImage]; CIContext *softwareContext = [CIContext contextWithOptions:@{kCIContextUseSoftwareRenderer : @(YES)} ]; CGImageRef cgImg = [softwareContext createCGImage:saveToSave fromRect:[saveToSave extent]]; ALAssetsLibrary* library = [[ALAssetsLibrary alloc] init]; [library writeImageToSavedPhotosAlbum:cgImg metadata:[saveToSave properties] completionBlock:^(NSURL *assetURL, NSError *error) { CGImageRelease(cgImg); }];
CIDetector用来分析CIImage,得到CIFeature。每个CIDetector都要用一个探测器来初始化,这个类型高数探测器要在图像中寻找什么特征。当一个CIDetector分析一张图片时,返回一个探测到的CIFeature的数组,如果CIDetector 被初始化为寻找面孔,那么返回的数组会被填上CIFaceFeature对象,每个CIFaceFeature都包含一个面部的CGrect引用(按照图像的坐标系),以及检测到的面孔的左眼,右眼,嘴部位置的CGPoint;
CIContext *context = [CIContext contextWithOptions:nil]; UIImage *imageInput = [_inputImageView image]; CIImage *image = [CIImage imageWithCGImage:imageInput.CGImage]; //设置识别参数 NSDictionary *param = [NSDictionary dictionaryWithObject:CIDetectorAccuracyHigh forKey:CIDetectorAccuracy]; //声明一个CIDetector,并设定识别类型 CIDetector* faceDetector = [CIDetector detectorOfType:CIDetectorTypeFace context:context options:param]; //取得识别结果 NSArray *detectResult = [faceDetector featuresInImage:image]; NSMutableArray *arrM = [NSMutableArray arrayWithCapacity: detectResult.count]; UIView *resultView = [[UIView alloc] initWithFrame:_inputImageView.frame]; [self.view addSubview:resultView]; for(CIFaceFeature* faceFeature in detectResult) { //脸部 UIView* faceView = [[UIView alloc] initWithFrame:faceFeature.bounds]; faceView.layer.borderWidth = 1; faceView.layer.borderColor = [UIColor orangeColor].CGColor; [resultView addSubview:faceView]; //左眼 if (faceFeature.hasLeftEyePosition) { [arrM addObject:[NSValue valueWithCGPoint:faceFeature.hasLeftEyePosition]]; } //右眼 if (faceFeature.hasRightEyePosition) { } //嘴巴 if (faceFeature.hasMouthPosition) { } } [resultView setTransform:CGAffineTransformMakeScale(1, -1)];
CoreImage关注点
CoreImage在iOS上有很高的效率,但是滤镜和渲染操作也会对主线程造成影响。应该将CoreImage滤镜渲染操作放在后台线程执行,当这些操作结束后再返回主线程进行界面的更新。
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^(void){ //CGImageRef cgImage = [self autoAdjustImage]; NSArray *filters; // Create Core Image CGImageRef cgImg = self.imageView.image.CGImage; CIImage *coreImage = [CIImage imageWithCGImage:cgImg]; // Iterate through all of our filters and apply // them to the CIImage for(CIFilter *filter in filters){ [filter setValue:coreImage forKey:kCIInputImageKey]; coreImage = filter.outputImage; } // Create a new CGImageRef by rendering through CIContext // This won't slow down main thread since we're in a background // dispatch queue CGImageRef newImg = [self.imageContext createCGImage:coreImage fromRect:[coreImage extent]]; dispatch_async(dispatch_get_main_queue(), ^(void){ // Update our image view on the main thread // You can also perform any other UI updates needed // here such as hidding activity spinners self.imageView.image = [UIImage imageWithCGImage:newImg]; [self.adjustSpinner stopAnimating]; [sender setEnabled:YES]; }); });
上面这段代码,就是为了防止阻塞主线程,用GCD异步执行滤镜与渲染操作,在获取渲染后的照片以后,返回主线程进行界面的更新。
其次,不要重复应用滤镜,即使是同一个滤镜也不要应用两次,因为滤镜后输出照片包含滤镜链,在进行照片渲染是会将滤镜链效果叠加到原始数据上,这时会造成问题。比如,有一个CIImage,上面配置了强度为0.5的棕色滤镜,现在通过滑块将强度改为0.6,这个滤镜应该用在新的CIImage上,如果不是新的CIImage上,那么原来的CIImage中将包含强度为0.5和0.6的棕色滤镜,而我们只想0.6的棕色滤镜,这样就造成错误,这一点在编写程序的时候一定要切忌。
app中应用的滤镜太多,改变速率太快,如果是根据滑块来产生事件的话,一定要注意在使用滑条值前要首先判断更改的滤镜当前是否正在起作用,如果该滤镜正在生成新的渲染图片,则应该这次滑块的更新。这一点是很重要的,弄的不好常常导致程序崩溃,出现内存泄露问题。
在使用CoreImage时,一定要记住CIImage对象在开始时不会操作图像数据,直到使用CIContext渲染图片时才会这么做。最好在后台执行图像处理的操作,然后在主线程中修改界面。
1:创建一个新的CIImage;
2:创建一个行的CIFIlter,并通过键-值设置各种输入值,这些值有些是有默认值的,有些没有默认值,需要编程者的设置;
3:从CIFilter中生成输出图像,如果存在滤镜链则将输出图像作为输入参数传入到下一个滤镜,跳回步骤2继续进行,如果到达滤镜末,则调用CIContext渲染CIImage对象。这个context可以是基于CPU或GPU的,基于CPU的产出CGImageRef对象,基于GPU的调用OpenGL ES在屏幕上画出结果,默认是基于CPU的。
请记住:代码量是提高开发水平的唯一途径,知识点是为了服务开发而来的,千万不要本末倒置。
如果您想深入学习,可以参考下面的参考文档。
参考文档: Core Image Filter Reference