If you're new here, you may want to subscribe to my
RSS feed or follow me on
Twitter. Thanks for visiting!
跟着这个教程,你会经过实际动手的经验来学习Core Image技术,亲身体验如何应用一些不一样的滤镜来实时地产生各类神奇的效果。 Core Image是一个很强大的框架。它可让你简单地应用各类滤镜来处理图像,好比修改鲜艳程度, 色泽, 或者曝光。 它利用GPU(或者CPU,取决于客户)来很是快速、甚至实时地处理图像数据和视频的帧。多个Core Image滤镜能够叠加在一块儿,从而能够一次性地产生多重滤镜效果。这种多重滤镜的优势在于它能够生成一个改进的滤镜,从而一次性的处理图像达到目标效果,而不是对同一个图像顺序地屡次应用单个滤镜。每个滤镜都有属于它本身的参数。这些参数和滤镜信息,好比功能、输入参数等均可以经过程序来查询。用户也能够来查询系统从而获得当前可用的滤镜信息。到目前为止,Mac上只有一部分Core Image滤镜能够在iOS上使用。可是随着这些可以使用滤镜的数目愈来愈多,API能够用来发现新的滤镜属性。
Core Image 总览
开始以前,让咱们谈谈Core Image框架中最重要的几个类:
- CIContext. 全部图像处理都是在一个CIContext 中完成的,这很像是一个Core Image处理器或是OpenGL的上下文。
- CIImage. 这个类保存图像数据。它能够从UIImage、图像文件、或者是像素数据中构造出来。
- CIFilter. 滤镜类包含一个字典结构,对各类滤镜定义了属于他们各自的属性。滤镜有不少种,好比鲜艳程度滤镜,色彩反转滤镜,剪裁滤镜等等。
在新建一个项目过程当中,你会依次用到这些类。
让咱们开始吧
打开Xcode, 用iOSApplicationSingle View Application 模板建立一个项目。输入CoreImageFun做为产品的名字。选择iPhone做为设备类型,而且确保只勾选Use Storyboards和Use Automatic Reference Counting两个选项。 首先,让咱们导入Core Image框架。在Mac上,这个过程是QuartzCore框架的一部分;可是在iOS上,这个是单独的一个框架。在左侧文件导航栏中,进入项目文件夹。选择Build Phases标签页,扩展Link Binaries和Library group, 点击“+”来添加按钮;找到CoreImage框架而且双击完成添加。 第二步,下载
教程资源,把其中的image.png添加到项目中,咱们的建立设置就完成了。 以后,打开MainStoryboard.storyboard, 把图像视图拖拽到视图控制器中,并把它的模式设定为Aspect Fit使得它的位置和维度近似以下图所示:

同时,打开Assistant Editor,确保编辑器显示ViewController.h, 并从UIImageView拖拽到@interface如下。把Connection设置到指向Outlet, 并命名为imageView,以后点击Connect进行链接。 编译运行来确保到目前为止每一步都是正确的。若是一切正常,你将会看到一个空屏幕。这时,咱们的初始化设置就完成了,下面咱们就进入Core Image部分!
基本的图像滤镜
做为第一个尝试,咱们先简单的让图像经过一个CIFilter 以后显示在屏幕上。每一次当咱们想应用一个CIFilter的时候都要有如下四个步骤:
- 建立一个 CIImage 对象: CIImage 有以下的初始化方法: imageWithURL:, imageWithData:, imageWithCVPixelBuffer:, 和 imageWithBitmapData:bytesPerRow:size:format:colorSpace:。可是大多数时候你只会常常用到imageWithURL。
- 建立一个 CIContext: 一个 CIContext 能够是基于CPU或是GPU的。它能够被重用,因此你不用每次都建立一个。可是当输出CIImage对象的时候你至少必定会须要一个CIContext。
- 建立一个CIFilter: 当你建立滤镜的时候,你能够在上面配置必定数量的属性。具体的属性取决于你所要用的滤镜。
- 输出滤镜:这个滤镜会输出一个图像成为CIImage。 你能够用CIContext把它转化为一个UIImage ,具体过程以下。
让咱们看看这是如何实现的。把下面的代码加入到viewDidLoad中的ViewController.m里面。
// 1
NSString *filePath =
[[NSBundle mainBundle] pathForResource:@"image" ofType:@"png"];
NSURL *fileNameAndPath = [NSURL fileURLWithPath:filePath];
// 2
CIImage *beginImage =
[CIImage imageWithContentsOfURL:fileNameAndPath];
// 3
CIFilter *filter = [CIFilter filterWithName:@"CISepiaTone"
keysAndValues: kCIInputImageKey, beginImage,
@"inputIntensity", @0.8, nil];
CIImage *outputImage = [filter outputImage];
// 4
UIImage *newImage = [UIImage imageWithCIImage:outputImage];
self.imageView.image = newImage; |
让咱们依次看看这些代码都作了什么事情
- 前两行建立了一个NSURL 对象, 包含指向图形文件的路径。
- 下面,用imageWithContentsOfURL方法建立CIImage。
- 以后,建立CIFilter对象。一个 CIFilter 构造函数有两个输入,分别是滤镜的名字,还有规定了滤镜属性的键值和取值的字典。 每个滤镜会有它本身惟一的键值和一组有效的取值。CISepiaTone 滤镜只能选两个值: KCIInputImageKey (一个CIImage) 和 @”inputIntensity”。 后者是一个封装成NSNumber (用新的文字型语法)的浮点小数,取值在0和1 之间。大部分的滤镜有默认值,只有CIImage是个例外。你必须提供一个值给它,由于它没有默认值。从滤镜中导出CIImage很简单,只须要用outputImage方法。
- 一旦你有了导出的 CIImage,你就能够把它转化为一个 UIImage。 在新的iOS6中,UIImage 方法+ imageWithCIImage方法能够实现从CIImage 到UIImage 到转化。一旦转化完成,咱们就可让UIImage 显示在以前添加的图像视图里。
编辑运行项目,你将会看到你的图片以下图通常,已经被墨色调滤镜处理过。恭喜你,你已经成功掌握并运用了CIImage和CIFilters。
把它放在上下文中
在进行下一步以前,有一个优化的方法很实用。我前面提到过,你须要一个CIContext来进行CIFilter,可是在上面的例子中咱们没有提到这个对象。由于咱们调用的UIImage方法(imageWithCIImage)已经自动地为咱们完成了这个步骤。它生成了一个CIContext而且用它来处理图像的过滤。这使得调用Core Image的接口变得很简单。 可是,有一个主要的问题是,它的每次调用都会生成一个CIContext。CIContext原本是能够重用以便提升性能和效率的。好比下面咱们要谈到的例子,若是你想用滑动条来选择过滤参数取值,每次改变滤镜参数都会自动生成一个CIContext, 使得性能很是差。 让咱们想个好办法搞定这个问题。删除你以前添加到viewDidLoad里面的代码,用下面的代码取而代之:
CIImage *beginImage =
[CIImage imageWithContentsOfURL:fileNameAndPath];
// 1
CIContext *context = [CIContext contextWithOptions:nil];
CIFilter *filter = [CIFilter filterWithName:@"CISepiaTone"
keysAndValues: kCIInputImageKey, beginImage,
@"inputIntensity", @0.8, nil];
CIImage *outputImage = [filter outputImage];
// 2
CGImageRef cgimg =
[context createCGImage:outputImage fromRect:[outputImage extent]];
// 3
UIImage *newImage = [UIImage imageWithCGImage:cgimg];
self.imageView.image = newImage;
// 4
CGImageRelease(cgimg); |
再让我逐步解释一下这部分代码
- 在这部分代码中,你建立了CIContext对象。CIContext 构造函数的输入是一个NSDictionary。 它规定了各类选项,包括颜色格式以及内容是否应该运行在CPU或是GPU上。对于这个应用程序,默认值是能够用的。因此你只须要传入nil做为参数就行了。
- 在这里你用上下文对象里的一个方法来画一个CGImage。 调用上下文中的createCGImage:fromRect:和提供的CIImage能够生成一个CGImageRef。
- 下面,你用UIImage + imageWithCGImage,从CGImage中建立一个UIImage。
- 最后,开放 CGImageRef接口。 CGImage 是一个C接口,即便有ARC,也须要你本身来作内存管理。
编译运行,确保正常工做。 在这个例子中,添加CIContext的建立 和你本身来建立的区别不大。可是在下一部分中,你将会看到当你实现动态改变滤镜参数的时候的重大性能差异。
改变滤镜的取值
上面能够看到,Core Image滤镜很好用,可是这些只是很是初级的应用。让咱们添加一个滑动条使得咱们可以实时动态地调整图像设置。 打开MainStoryboard.storyboard, 拖拽一个滑动条到图像窗口的下部 (以下图)。

确保Assistant Editor 是可见的而且显示ViewController.h。 控制@interface下的滑动条。把Connection设置到Action,把名字设置成amountSliderValueChanged, 把Event设置成Value Changed,接下来让咱们把滑动条链接到输出。再一次控制@interface下的滑动条, 可是这一次把Connection设置到Outlet,把名字设置成amountSlider, 以后点击 Connect。 每一次滑动条改变位置,你须要从新用新的值进行图像过滤。可是你必定不想每次都重作整个过程,那将会很是的低效。你其实只须要在你的类中改变一小部分,从而使得你已经在viewDidLoad方法中建立的对象还能继续被使用。最重要的一步是在任何须要被用到的地方屡次重用CIContext。若是你每次都从新建立它,你的程序将会很是地慢。另外一步优化是你能够保存CIFilter和存有初始图像的CIImage。对每个输出你都须要生成一个新的CIFilter,可是每次初始用到的图像始终是同一个。 你须要添加一些实例变量来完成这个任务。 把下面的3个实例变量添加到ViewController.m里你本身的@implementation中。
@implementation ViewController {
CIContext *context;
CIFilter *filter;
CIImage *beginImage;
} |
而且, 改变viewDidLoad方法中的变量, 使得他们调用实例变量,而不是声明新的本地变量:
beginImage = [CIImage imageWithContentsOfURL:fileNameAndPath];
context = [CIContext contextWithOptions:nil];
filter = [CIFilter filterWithName:@"CISepiaTone"
keysAndValues:kCIInputImageKey, beginImage, @"inputIntensity",
@0.8, nil]; |
如今,你将实现changeValue方法来实现改变CIFilter 字典中@”inputIntensity”键值的功能。在咱们实现了这个改变以后,你还须要重复以下一些步骤:
- 从CIFilter 中获得CIImage
- 把CIImage转化成 CGImageRef.
- 把CGImageRef 转化成UIImage, 在图像视图中显示出来。
因此用以下的部分替换amountSliderValueChanged方法:
- (IBAction)amountSliderValueChanged:(UISlider *)slider {
float slideValue = slider.value;
[filter setValue:@(slideValue)
forKey:@"inputIntensity"];
CIImage *outputImage = [filter outputImage];
CGImageRef cgimg = [context createCGImage:outputImage
fromRect:[outputImage extent]];
UIImage *newImage = [UIImage imageWithCGImage:cgimg];
self.imageView.image = newImage;
CGImageRelease(cgimg);
} |
你将会注意到,在方法定义中,你已经把变量类型从(id)转化成了(UISlider *)。 你知道你只会用这个方法来从你的UISlider中得到数据,因此你能够作这个转变,不会影响其余的部分。若是咱们保持变量类型为(id)不变, 则必须把它转化为UISlider,不然下一行将会报错。确保头文件中的声明也作了相应修改。 你能够从滑动条中获取浮点数。滑动条有相应的默认设置 – 最小值0,最大值0,默认值0.5。这恰好是这个CIFilter的合理取值,简直太方便了! CIFilter 有相应的方法能够任由咱们在字典中设置不一样键值的取值。在这里你只须要把@”inputIntensity” 键设置成一个NSNumber 对象,它的取值是你从滑动条上获得的任意浮点数。 代码的其余部分应该看上去很像,由于都是遵循和viewDidLoad方法一样的逻辑。你将会反复重用这些代码。从如今开始,你将用changeSlider方法来为UIImageView提供CIFilter输出。 编译运行,你将会获得一个能够实时改变图片墨色调数值的滑动条!
从相册中读取照片
既然你如今能够改变滤镜的取值, 真正有趣的东西才刚刚开始。若是你不想要这幅花朵的图像怎么办呢?让咱们创建一个UIImagePickerController, 使得你能够任意从相册中选取图片读取到你的项目中来进行任意修改。你须要建立一个按钮来打开相册视图。因此打开ViewController.xib,把一个按钮拖拽到滑动条的右下方,且命名为“相册”(Photo Album)

确保Assistant Editor 是可见的而且显示ViewController.h。 控制@interface下的按钮。把Connection设置到Action,把名字设置成loadPhoto, 把Event设置成Touch Up Inside,以后点击Connect。 接下来切换到ViewController.m,实现loadPhoto方法以下:
- (IBAction)loadPhoto:(id)sender {
UIImagePickerController *pickerC =
[[UIImagePickerController alloc] init];
pickerC.delegate = self;
[self presentViewController:pickerC animated:YES completion:nil];
} |
第一行代码实例化一个新的UIImagePickerController。以后,你设置图像选取代理为ViewController。在这里,你将会看到一个警告消息。你须要把ViewController设置为UIImagePickerControllerDelegate和UINaviationControllerDelegate,而且在代理协议下实现全部的方法。 仍是在ViewController.m中,改变类型拓展以下:
@interface ViewController () <UIImagePickerControllerDelegate, UINavigationBarDelegate>
@end |
如今实现下面的两个方法:
- (void)imagePickerController:(UIImagePickerController *)picker
didFinishPickingMediaWithInfo:(NSDictionary *)info {
[self dismissViewControllerAnimated:YES completion:nil];
NSLog(@"%@", info);
}
- (void)imagePickerControllerDidCancel:
(UIImagePickerController *)picker {
[self dismissViewControllerAnimated:YES completion:nil];
} |
在两个方法里,你摒除了UIPickerController,而用新的代理来完成相应的功能。若是你在代理中没有相应的实现,那你就只有一直瞪着图像选择器发呆了。第一个方法是不完整的, 它只是一个标志位,用来注销所选图像的信息。imagePickerControllerDidCancel方法用来清除PickerController。 编译运行,按那个“相册”(Photo Album)按钮, 图像选择器就会跳出来,显示你相册里全部的照片。若是你在模拟器上运行, 可能就不会看到任何照片。在模拟器或者没有照相机的设备上,你能够用Safari浏览器保存照片到你的相册。打开Safari浏览器, 找到一个图片, 按住过一会,就会弹出一个对话框让你保存图片。下次你运行你的应用程序,就会看到这个图片了。 下面是在当你选定一个图片以后,控制台中应该显示的信息(会根据所选图片内容相应有所不一样):
2012-09-20 17:30:52.561 CoreImageFun[3766:c07] {
UIImagePickerControllerMediaType = "public.image";
UIImagePickerControllerOriginalImage = "";
UIImagePickerControllerReferenceURL = "assets-library://asset/asset.JPG?
id=253312C6-A454-45B4-A9DA-649126A76CA5&ext=JPG";
}
注意,在字典中有一个字段就是专门为被选择的原始图片而设置的。这个字段就正是你须要取出而且过滤的! 既然咱们已经知道怎么选取一个图片,那么咱们怎么设置CIImage beganImage来调用这个图片呢? 简单!只须要以下修改代理的方法:
- (void)imagePickerController:(UIImagePickerController *)picker
didFinishPickingMediaWithInfo:(NSDictionary *)info {
[self dismissViewControllerAnimated:YES completion:nil];
UIImage *gotImage =
[info objectForKey:UIImagePickerControllerOriginalImage];
beginImage = [CIImage imageWithCGImage:gotImage.CGImage];
[filter setValue:beginImage forKey:kCIInputImageKey];
[self amountSliderValueChanged:self.amountSlider];
} |
你须要从你选择的图片中建立一个新的CIImage。在UIImagePickerControllerOriginalImage键值是个常数的状况下,你能够经过寻找字典中的取值获得图片的 UIImage 代理。注意最好用一个常数,而不是一个硬编码的字符串,由于Apple能够在将来改变键的名字。从UIImagePickerController代理协议参考中你能够找到全部的常数键。 你须要转化这些成为一个 CIImage,可是并无一个方法能够把一个 UIImage转化成一个CIImage。然而你有[CIImage imageWithCGImage:] 方法。它能够经过调用UIImage.CGImage来从UIImage中获得CIImage。那么你彻底能够作同样的事情! 因而你设置滤镜字典中的相应键,使得导入的图片正是你刚刚常见的CIImage。 最后一行可能看起来会很奇怪。还记得我是怎么阐述changeView的代码是用最新的值来运行滤镜,而且根据运行结果更新图像视图的吗? 你须要再作一遍这个工做,因此你只须要调用一遍changeValue方法。即便滑动条的值没有改变,你仍然可使用哪一个方法的代码来完成这个工做。 你能够拆开那部分代码造成单独的方法。并且随着你作的事情愈来愈复杂,你也但愿用这种方式尽可能避免混淆。可是就当前这个问题而言,你的目的只是想用changeValue方法,因此你传入amountSlider,获得正确的值就行了。 编译运行,你如今就能够编辑更新你相册里的任意图片或照片了。

在把你的图片作了墨色调处理以后,怎么保持它呢。你能够截屏,可是你没那么土!让咱们学学如何保存处理后的图片到你的相册里。
保存到相册
为了保存到相册,你须要一个AssetsLibrary框架。进入到项目容器里,选择Build Phases标签页,扩展Link Binaries和Library group, 点击“+”来添加按钮。找到AssetsLibrary框架,选择进行添加。 以后把下面的#import 内容添加到ViewController.m的顶部。
#import <AssetsLibrary/AssetsLibrary.h> |
你须要明白一件事情,那就是当你保存一张照片到相册的时候,即便你退出了这个应用,这个过程仍然能够继续。 这点可能会致使一些问题,由于GPU在当你切换应用的时候会中止当前的工做。若是照片尚未保存完毕就退出了程序,那可能之后就找不到这个要保存的照片了。 对于这个问题的解决方法是利用CPU的CIRendering上下文。然而默认设备是GPU,并且GPU比CPU快不少。因此你其实能够建立第二个CIContext,只为了保存这个图片。 让咱们添加一个新按钮来实现对当前编辑照片的保存。打开MainStoryboard, 添加一个新按钮,标记为“保存”(Save to Album)。

以后把这个按钮链接到一个新的savePhoto方法, 就像你刚作完的过程同样。以后切换到ViewController.m 而且按照以下代码实现这个方法:
- (IBAction)savePhoto:(id)sender {
// 1
CIImage *saveToSave = [filter outputImage];
// 2
CIContext *softwareContext = [CIContext
contextWithOptions:@{kCIContextUseSoftwareRenderer : @(YES)} ];
// 3
CGImageRef cgImg = [softwareContext createCGImage:saveToSave
fromRect:[saveToSave extent]];
// 4
ALAssetsLibrary* library = [[ALAssetsLibrary alloc] init];
[library writeImageToSavedPhotosAlbum:cgImg
metadata:[saveToSave properties]
completionBlock:^(NSURL *assetURL, NSError *error) {
// 5
CGImageRelease(cgImg);
}];
} |
在这段代码中:
- 从滤镜中获得CIImage输出
- 建立一个新的、基于软件的CIContext
- 生成CGImageRef.
- 保存CGImageRef 到图片库
- 释放CGImage。最后一步在回调部分发生,使得只有在完成以后才会用到它。
编译而且在真正的设备上运行这个应用,你就能够永久保存你想要的图片到相册里。
图像元数据怎么处理呢?
让咱们简单谈谈图像的元数据。移动电话上拍摄的图像文件有一系列的数据相关联,好比GPS坐标,图像格式,图像朝向等等。具体来讲,图像的朝向是你须要保存的数据。加载原始图像到CIImage,转化为CGImage, 进而转化为UIImage的过程去除掉了原始图像的元数据。为了保存图像的朝向,你须要记录而且恢复这些相关图像信息到UIImage。你能够经过添加一个新的私有实例变量到ViewController.m当中来达到这个目的。
@implementation ViewController {
CIContext *context;
CIFilter *filter;
CIImage *beginImage;
UIImageOrientation orientation; // New!
} |
下一步,当从相册里加载原始图像的时候,能够经过imagePickerController: didFinishPickingMediaWithInfo方法设定相应的元数据值。把下面几行代码加入到 “beginImage = [CIImage imageWithCGImage:gotImage.CGImage]” 这一行代码的前面:
orientation = gotImage.imageOrientation; |
最终,改变amountSliderChanged中的代码,建立imageView对象中设定的UIImage:
UIImage *newImage = [UIImage imageWithCGImage:cgimg scale:1.0 orientation:orientation]; |
如今,若是你用非默认的朝向照一张照片, 这个朝向信息将会被保存下来。
还有其余什么滤镜能够用吗?
CIFilter 接口在Mac OS上有130个滤镜,外加能够定制滤镜的能力。 在iOS6中,有93个或更多;可是目前还不能实如今iOS平台上对滤镜的定制。但愿之后能够作到。 为了找到可用的滤镜信息,你能够利用 [CIFilter filterNamesInCategory:kCICategoryBuiltIn] 方法。 这个方法会返回一列可用滤镜的名字。并且,每个滤镜都有一个属性方法来返回一个包含滤镜信息的字典结构。这些信息包括滤镜的名字,滤镜的分类,滤镜的输入以及输入的默认值和可接受的值范围。 让咱们为你的类整理出一个方法。调用这个方法能够在日志文件中打印出全部可用滤镜信息。把下面这个方法加入到viewDidLoad的上面:
-(void)logAllFilters {
NSArray *properties = [CIFilter filterNamesInCategory:
kCICategoryBuiltIn];
NSLog(@"%@", properties);
for (NSString *filterName in properties) {
CIFilter *fltr = [CIFilter filterWithName:filterName];
NSLog(@"%@", [fltr attributes]);
}
} |
这个方法从filterNamesInCategory方法中获取可用滤镜的名字,先打印名字,以后对于在列表上的每个名字,建立一个相应的滤镜,而且记录该滤镜中的属性字典。以后在viewDidLoad的底部调用下面这个方法:
你将会在输出中看到下面的内容:

天啊,简直有太多的滤镜了!
更复杂的滤镜链
既然咱们已经学习了iOS6平台上全部可用的滤镜, 咱们能够进一步看看如何建立一个更复杂的滤镜链。为了达到这个目的,咱们须要建立一个专门的方法来处理CIImage。它将导入CIImage,过滤处理,以后返回一个CIImage。添加以下的方法:
-(CIImage *)oldPhoto:(CIImage *)img withAmount:(float)intensity { // 1 CIFilter *sepia = [CIFilter filterWithName:@"CISepiaTone"]; [sepia setValue:img forKey:kCIInputImageKey]; [sepia setValue:@(intensity) forKey:@"inputIntensity"]; // 2 CIFilter *random = [CIFilter filterWithName:@"CIRandomGenerator"]; // 3 CIFilter *lighten = [CIFilter filterWithName:@"CIColorControls"]; [lighten setValue:random.outputImage forKey:kCIInputImageKey]; [lighten setValue:@(1 - intensity) forKey:@"inputBrightness"]; [lighten setValue:@0.0 forKey:@"inputSaturation"]; // 4 CIImage *croppedImage = [lighten.outputImage imageByCroppingToRect:[beginImage extent] |