CIFilter 处理图片

来源:互联网 发布:阿里巴巴待遇知乎 编辑:程序博客网 时间:2024/04/30 12:00


 在这个教程中,你将学习最新的Core Image过滤技术,亲身体验如何应用一些不同的滤镜来实时地产生各种神奇的效果。

Core Image是一个很强大的框架。它可以让你简单地应用各种滤镜来处理图像,比如修改鲜艳程度, 色泽, 或者曝光。 它利用GPU(或者CPU,取决于客户)来非常快速、甚至实时地处理图像数据和视频的帧。每一个滤镜都有属于它自己的参数。这些参数和滤镜信息,比如功能、输入参数等都可以通过程序来查询。用户也可以来查询系统从而得到当前可用的滤镜信息。

Core Image 总览

开始之前,让我们谈谈Core Image框架中最重要的几个类:

  • CIContext. 所有图像处理都是在一个CIContext 中完成的,这很像是一个Core Image处理器或是OpenGL的上下文。
  • CIImage. 这个类保存图像数据。它可以从UIImage、图像文件、或者是像素数据中构造出来。
  • CIFilter. 滤镜类包含一个字典结构,对各种滤镜定义了属于他们各自的属性。滤镜有很多种,比如鲜艳程度滤镜,色彩反转滤镜,剪裁滤镜等等。

在新建一个项目过程中,你会依次用到这些类。

让我们开始吧

首先,让我们导入Core Image框架。在Mac上,这个过程是QuartzCore框架的一部分;但是在iOS上,这个是单独的一个框架。在左侧文件导航栏中,进入项目文件夹。选择Build Phases标签页,扩展Link Binaries和Library group, 点击“+”来添加按钮;找到CoreImage框架并且双击完成添加。

之后,打开MainStoryboard.storyboard, 把图像视图拖拽到视图控制器中,并把它的模式设定为Aspect Fit使得它的位置和维度近似如下图所示:

基本的图像滤镜

作为第一个尝试,我们先简单的让图像通过一个CIFilter 之后显示在屏幕上。每一次当我们想应用一个CIFilter的时候都要有以下四个步骤:

  1. 创建一个 CIImage 对象: CIImage 有如下的初始化方法: imageWithURL:, imageWithData:, imageWithCVPixelBuffer:, 和 imageWithBitmapData:bytesPerRow:size:format:colorSpace:。但是大多数时候你只会经常用到imageWithURL。
  2. 创建一个 CIContext: 一个 CIContext 可以是基于CPU或是GPU的。它可以被重用,所以你不用每次都创建一个。但是当输出CIImage对象的时候你至少一定会需要一个CIContext。
  3. 创建一个CIFilter: 当你创建滤镜的时候,你可以在上面配置一定数量的属性。具体的属性取决于你所要用的滤镜。
  4. 输出滤镜:这个滤镜会输出一个图像成为CIImage。 你可以用CIContext把它转化为一个UIImage ,具体过程如下。

让我们看看这是如何实现的。把下面的代码加入到viewDidLoad中的ViewController.m里面。

// 

     NSString * filePath = [[NSBundle mainBundle] pathForResource:@"tupian" ofType:@"jpg"];

    NSURL * urlPath = [NSURL fileURLWithPath:filePath ];

    CIImage * beginImage = [CIImage imageWithContentsOfURL:urlPath];

    CIFilter * filter = [CIFilter filterWithName:@"CISepiaTone" keysAndValues:kCIInputImageKey, beginImage,@"inputIntensity",@0.8, nil];

    CIImage * outputImage = [filter outputImage];

    _imageView.image = [UIImage imageWithCIImage:outputImage] ;

    

让我们依次看看这些代码都做了什么事情

  1. 前两行创建了一个NSURL 对象, 包含指向图形文件的路径。
  2. 下面,用imageWithContentsOfURL方法创建CIImage。
  3. 之后,创建CIFilter对象。一个 CIFilter 构造函数有两个输入,分别是滤镜的名字,还有规定了滤镜属性的键值和取值的字典。 每一个滤镜会有它自己唯一的键值和一组有效的取值。CISepiaTone 滤镜只能选两个值: KCIInputImageKey (一个CIImage) 和 @”inputIntensity”。 后者是一个封装成NSNumber (用新的文字型语法)的浮点小数,取值在0和1 之间。大部分的滤镜有默认值,只有CIImage是个例外。你必须提供一个值给它,因为它没有默认值。从滤镜中导出CIImage很简单,只需要用outputImage方法。
  4. 一旦你有了导出的 CIImage,你就可以把它转化为一个 UIImage。 在新的iOS6中,UIImage 方法+ imageWithCIImage方法可以实现从CIImage 到UIImage 到转化。一旦转化完成,我们就可以让UIImage 显示在之前添加的图像视图里。

编辑运行项目,你将会看到你的图片如下图一般,已经被墨色调滤镜处理过。恭喜你,你已经成功掌握并运用了CIImage和CIFilters。

把它放在上下文中

在进行下一步之前,有一个优化的方法很实用。我前面提到过,你需要一个CIContext来进行CIFilter,但是在上面的例子中我们没有提到这个对象。因为我们调用的UIImage方法(imageWithCIImage)已经自动地为我们完成了这个步骤。它生成了一个CIContext并且用它来处理图像的过滤。这使得调用Core Image的接口变得很简单。

但是,有一个主要的问题是,它的每次调用都会生成一个CIContext。CIContext本来是可以重用以便提高性能和效率的。比如下面我们要谈到的例子,如果你想用滑动条来选择过滤参数取值,每次改变滤镜参数都会自动生成一个CIContext, 使得性能非常差。

让我们想个好办法搞定这个问题。删除你之前添加到viewDidLoad里面的代码,用下面的代码取而代之:

    NSString * filePath = [[NSBundlemainBundle] pathForResource:@"tupian"ofType:@"jpg"];

    NSURL * urlPath = [NSURLfileURLWithPath:filePath ];

    CIImage * beginImage = [CIImageimageWithContentsOfURL:urlPath];

    CIContext * context = [CIContextcontextWithOptions:nil];

    CIFilter * filter = [CIFilterfilterWithName:@"CISepiaTone"keysAndValues:kCIInputImageKey, beginImage,@"inputIntensity",@0.8,nil];

    CIImage * outputImage = [filteroutputImage];

    CGImageRef cgimg = [contextcreateCGImage:outputImage fromRect:[outputImage extent]];

    _imageView.image = [UIImageimageWithCGImage:cgimg];

    CGImageRelease(cgimg);


再让我逐步解释一下这部分代码

  1. 在这部分代码中,你创建了CIContext对象。CIContext 构造函数的输入是一个NSDictionary。 它规定了各种选项,包括颜色格式以及内容是否应该运行在CPU或是GPU上。对于这个应用程序,默认值是可以用的。所以你只需要传入nil作为参数就好了。
  2. 在这里你用上下文对象里的一个方法来画一个CGImage。 调用上下文中的createCGImage:fromRect:和提供的CIImage可以生成一个CGImageRef。就是将 CIImage 转化成一个 CGImageRef .
  3. 下面,你用UIImage + imageWithCGImage,从CGImage中创建一个UIImage。
  4. 最后,开放 CGImageRef接口。 CGImage 是一个C接口,即使有ARC,也需要你自己来做内存管理。

编译运行,确保正常工作。

在这个例子中,添加CIContext的创建 和你自己来创建的区别不大。但是在下一部分中,你将会看到当你实现动态改变滤镜参数的时候的重大性能差别。

改变滤镜的取值

上面可以看到,Core Image滤镜很好用,但是这些只是非常初级的应用。让我们添加一个滑动条使得我们能够实时动态地调整图像设置。

打开MainStoryboard.storyboard, 拖拽一个滑动条到图像窗口的下部 (如下图)。


每一次滑动条改变位置,你需要重新用新的值进行图像过滤。但是你一定不想每次都重做整个过程,那将会非常的低效。你其实只需要在你的类中改变一小部分,从而使得你已经在viewDidLoad方法中创建的对象还能继续被使用。最重要的一步是在任何需要被用到的地方多次重用CIContext。如果你每次都重新创建它,你的程序将会非常地慢。另一步优化是你可以保存CIFilter和存有初始图像的CIImage。对每一个输出你都需要生成一个新的CIFilter,但是每次初始用到的图像始终是同一个。

你需要添加一些实例变量来完成这个任务。

把下面的3个实例变量添加到ViewController.m里你自己的@implementation中。

{

    CIContext *context;

    CIFilter *filter;

    CIImage *beginImage;

}

并且, 改变viewDidLoad方法中的变量, 使得他们调用实例变量,而不是声明新的本地变量:

现在,你将实现changeValue方法来实现改变CIFilter 字典中@”inputIntensity”键值的功能。在我们实现了这个改变之后,你还需要重复如下一些步骤:
  • 从CIFilter 中得到CIImage
  • 把CIImage转化成 CGImageRef.
  • 把CGImageRef 转化成UIImage, 在图像视图中显示出来。

所以用如下的部分实现方法:

- (IBAction)change:(UISlider *)slider {

    

    [filtersetValue:@(slider.value)forKey:@"inputIntensity"] ;

    CIImage * outputImage = [filteroutputImage];

    CGImageRef cgimg = [contextcreateCGImage:outputImage fromRect:[outputImage extent]];

    _imageView.image = [UIImageimageWithCGImage:cgimg];

    CGImageRelease(cgimg);

    

}

CIFilter 有相应的方法可以任由我们在字典中设置不同键值的取值。在这里你只需要把@”inputIntensity” 键设置成一个NSNumber 对象,它的取值是你从滑动条上得到的任意浮点数。

代码的其他部分应该看上去很像,因为都是遵循和viewDidLoad方法同样的逻辑。你将会反复重用这些代码。从现在开始,你将用changeSlider方法来为UIImageView提供CIFilter输出。

编译运行,你将会得到一个可以实时改变图片墨色调数值的滑动条!

从相册中读取照片

既然你现在可以改变滤镜的取值, 真正有趣的东西才刚刚开始。如果你不想要这幅花朵的图像怎么办呢?让我们建立一个UIImagePickerController, 使得你可以任意从相册中选取图片读取到你的项目中来进行任意修改。


接下来切换到ViewController.m,实现loadPhoto方法如下:

- (IBAction)selectPotos:(id)sender {

   UIImagePickerController * picker  = [[UIImagePickerControlleralloc] init];

    picker.delegate =self ;

    [selfpresentViewController:picker animated:YEScompletion:nil] ;

}


第一行代码实例化一个新的UIImagePickerController。之后,你设置图像选取代理为ViewController。在这里,你将会看到一个警告消息。你需要把ViewController设置为UIImagePickerControllerDelegateUINaviationControllerDelegate,并且在代理协议下实现所有的方法。
现在实现下面的两个方法:

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info {

    [selfdismissViewControllerAnimated:YEScompletion:nil];

    NSLog(@"%@",info) ;

}

- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {

    [selfdismissViewControllerAnimated:YEScompletion:nil];

}

第一个方法是不完整的, 它只是一个标志位,用来注销所选图像的信息。imagePickerControllerDidCancel方法用来清除PickerController

下面是在当你选定一个图片之后,控制台中应该显示的信息(会根据所选图片内容相应有所不同):

{

    UIImagePickerControllerMediaType = "public.image";

    UIImagePickerControllerOriginalImage = "<UIImage: 0x600000096080> size {1024, 726} orientation 0 scale 1.000000";

    UIImagePickerControllerReferenceURL = "assets-library://asset/asset.JPG?id=CF3EEB91-2623-4CFF-A107-AA302F09D852&ext=JPG";

}

注意,在字典中有一个字段就是专门为被选择的原始图片而设置的。这个字段就正是你需要取出并且过滤的!

既然我们已经知道怎么选取一个图片,那么我们怎么设置CIImage beganImage来调用这个图片呢?

简单!只需要如下修改代理的方法:

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info {

    

    [selfdismissViewControllerAnimated:YEScompletion:nil];

    NSLog(@"%@",info) ;

    UIImage * originalImage = info[UIImagePickerControllerOriginalImage];

    beginImage = [CIImageimageWithCGImage:originalImage.CGImage] ;

    [filtersetValue:beginImageforKey:kCIInputImageKey];

    [selfchange:_slider] ;

    _oldImage.image = originalImage ;

}

你需要从你选择的图片中创建一个新的CIImage。在UIImagePickerControllerOriginalImage键值是个常数的情况下,你可以通过寻找字典中的取值得到图片的 UIImage 代理。注意最好用一个常数,而不是一个硬编码的字符串,因为Apple可以在未来改变键的名字。从UIImagePickerController代理协议参考中你可以找到所有的常数键。

你需要转化这些成为一个 CIImage,但是并没有一个方法可以把一个 UIImage转化成一个CIImage。然而你有[CIImage imageWithCGImage:] 方法。它可以通过调用UIImage.CGImage来从UIImage中得到CIImage。那么你完全可以做一样的事情!

于是你设置滤镜字典中的相应键,使得导入的图片正是你刚刚常见的CIImage。

编译运行,你现在就可以编辑更新你相册里的任意图片或照片了。


保存到相册

你需要明白一件事情,那就是当你保存一张照片到相册的时候,即使你退出了这个应用,CPU创建的CIContext 依然会保存, GPU创建的会停止,

这点可能会导致一些问题,因为GPU在当你切换应用的时候会停止当前的工作。如果照片还没有保存完毕就退出了程序,那可能以后就找不到这个要保存的照片了。

对于这个问题的解决方法是利用CPU的CIRendering上下文。然而默认设备是GPU,而且GPU比CPU快很多。所以你其实可以创建第二个CIContext,只为了保存这个图片。

让我们添加一个新按钮来实现对当前编辑照片的保存。打开MainStoryboard, 添加一个新按钮,标记为“保存”(Save to Album)。

之后把这个按钮连接到一个新的savePhoto方法, 就像你刚做完的过程一样。之后切换到ViewController.m 并且按照如下代码实现这个方法:

- (IBAction)savePhoto:(id)sender {

    CIImage * saveImage = [filteroutputImage];

    CIContext * saveContext = [CIContextcontextWithOptions:@{kCIContextUseSoftwareRenderer :@YES}];

    CGImageRef cgimg = [saveContextcreateCGImage:saveImage fromRect:[saveImageextent]];

    _imageView.image = [UIImageimageWithCGImage:cgimg];


    UIImageWriteToSavedPhotosAlbum(self.imageView.image,self, @selector(imageSavedToPhotosAlbum:didFinishSavingWithError:contextInfo:),nil);

}

- (void)imageSavedToPhotosAlbum:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo {

    NSString *message =@"呵呵";

    if (!error) {

        message = @"成功保存到相册";

    }else

    {

        message = [error description];

    }

    NSLog(@"message is %@",message);

}

在这段代码中:
  1. 从滤镜中得到CIImage输出
  2. 创建一个新的、基于软件的CIContext
  3. 生成CGImageRef.
  4. 保存CGImageRef 到图片库
  5. 释放CGImage。最后一步在回调部分发生,使得只有在完成之后才会用到它。

编译并且在真正的设备上运行这个应用,你就可以永久保存你想要的图片到相册里。


图像元数据怎么处理呢?

让我们简单谈谈图像的元数据。移动电话上拍摄的图像文件有一系列的数据相关联,比如GPS坐标,图像格式,图像朝向等等。具体来说,图像的朝向是你需要保存的数据。加载原始图像到CIImage,转化为CGImage, 进而转化为UIImage的过程去除掉了原始图像的元数据。为了保存图像的朝向,你需要记录并且恢复这些相关图像信息到UIImage。你可以通过添加一个新的私有实例变量到ViewController.m当中来达到这个目的。

{

    CIContext *context;

    CIFilter *filter;

    CIImage *beginImage;

    

    UIImageOrientation orientation;

}


下一步,当从相册里加载原始图像的时候,可以通过imagePickerController: didFinishPickingMediaWithInfo方法设定相应的元数据值。把下面几行代码加入到 “beginImage = [CIImage imageWithCGImage:gotImage.CGImage]” 这一行代码的前面:

    orientation = originalImage.imageOrientation ;

最终,改变amountSliderChanged中的代码,创建imageView对象中设定的UIImage:

UIImage *newImage = [UIImage imageWithCGImage:cgimg scale:1.0 orientation:orientation];

现在,如果你用非默认的朝向照一张照片, 这个朝向信息将会被保存下来。


还有其他什么滤镜可以用吗?

为了找到可用的滤镜信息,你可以利用 [CIFilter filterNamesInCategory:kCICategoryBuiltIn] 方法。 这个方法会返回一列可用滤镜的名字。而且,每一个滤镜都有一个属性方法来返回一个包含滤镜信息的字典结构。这些信息包括滤镜的名字,滤镜的分类,滤镜的输入以及输入的默认值和可接受的值范围。

让我们为你的类整理出一个方法。调用这个方法可以在日志文件中打印出所有可用滤镜信息。把下面这个方法加入到viewDidLoad的上面:

- (void) logAllFilters {

    NSArray * properties = [CIFilterfilterNamesInCategory:kCICategoryBuiltIn];

    NSLog(@"%@",properties) ;

    

    for (NSString * filterNamein properties) {

        CIFilter * fltr = [CIFilterfilterWithName:filterName];

        NSLog(@"%@",[fltrattributes]) ;

    }

}

这个方法从filterNamesInCategory方法中获取可用滤镜的名字,先打印名字,之后对于在列表上的每一个名字,创建一个相应的滤镜,并且记录该滤镜中的属性字典。之后在viewDidLoad的底部调用下面这个方法:

[self logAllFilters];

你将会在输出中看到下面的内容:

天啊,简直有太多的滤镜了!


至此一个过滤周期就完成了,简单来说分以下几个步骤:

1 初始化CIContext,CIImage
2 初始化CIFilter并设置参数
3 得到输出的图片
4 将图片转化成能显示的UIImage类型
如果想一张图片有多种过滤效果就需要重复2,3两步,并且要将上一个过滤器输出的图片作为下一个过滤器的参数

简单吧!几行代码就可以得到丰富的效果哦.

最后全部代码 : 

#import "ViewController.h"

@interface ViewController ()<UIImagePickerControllerDelegate,UINavigationControllerDelegate>

{

    CIContext *context;

    CIFilter *filter;

    CIImage *beginImage;

    

    UIImageOrientation orientation;

}


@property (weak,nonatomic) IBOutletUISlider *slider;


@property (weak,nonatomic) IBOutletUIImageView *imageView;


@property (weak,nonatomic) IBOutletUIImageView *oldImage;


@end



/*

 CIContext. 所有图像处理都是在一个CIContext中完成的,这很像是一个Core Image处理器或是OpenGL的上下文。

 CIImage. 这个类保存图像数据。它可以从UIImage、图像文件、或者是像素数据中构造出来。

 CIFilter. 滤镜类包含一个字典结构,对各种滤镜定义了属于他们各自的属性。滤镜有很多种,比如鲜艳程度滤镜,色彩反转滤镜,剪裁滤镜等等

 */

@implementation ViewController


- (void)viewDidLoad {

    [superviewDidLoad];


    NSString * filePath = [[NSBundlemainBundle] pathForResource:@"tupian"ofType:@"jpg"];

    NSURL * urlPath = [NSURLfileURLWithPath:filePath ];

    beginImage = [CIImageimageWithContentsOfURL:urlPath];

    context = [CIContextcontextWithOptions:nil];// GPU 渲染,效率高,无法跨应用,home后停止执行,适合实时处理

    filter = [CIFilterfilterWithName:@"CISepiaTone"keysAndValues:kCIInputImageKey,beginImage,@"inputIntensity",@0.8,nil];

    CIImage * outputImage = [filteroutputImage];

    CGImageRef cgimg = [contextcreateCGImage:outputImage fromRect:[outputImage extent]];

    _imageView.image = [UIImageimageWithCGImage:cgimg];

    CGImageRelease(cgimg);

 

    [selflogAllFilters] ;

    

}



- (IBAction)change:(UISlider *)slider {

    

    [filtersetValue:@(slider.value)forKey:@"inputIntensity"] ;

    CIImage * outputImage = [filteroutputImage];

    CGImageRef cgimg = [contextcreateCGImage:outputImage fromRect:[outputImage extent]];

    _imageView.image = [UIImageimageWithCGImage:cgimg scale:1.0orientation:orientation];

    CGImageRelease(cgimg);

    

}


- (IBAction)selectPotos:(id)sender {

   UIImagePickerController * picker  = [[UIImagePickerControlleralloc] init];

    picker.delegate =self ;

    [selfpresentViewController:picker animated:YEScompletion:nil] ;

}


- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info {

    

    [selfdismissViewControllerAnimated:YEScompletion:nil];

    NSLog(@"%@",info) ;

    UIImage * originalImage = info[UIImagePickerControllerOriginalImage];

    orientation = originalImage.imageOrientation ;

    beginImage = [CIImageimageWithCGImage:originalImage.CGImage] ;

    [filtersetValue:beginImageforKey:kCIInputImageKey];

    [selfchange:_slider] ;

    

    _oldImage.image = originalImage ;

}

- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {

    [selfdismissViewControllerAnimated:YEScompletion:nil];

}


- (IBAction)savePhoto:(id)sender {

    CIImage * saveImage = [filteroutputImage];

    CIContext * saveContext = [CIContextcontextWithOptions:@{kCIContextUseSoftwareRenderer :@YES}]; // CPU渲染,效率低,可以跨应用,home后依然执行,适合保存图片

    CGImageRef cgimg = [saveContextcreateCGImage:saveImage fromRect:[saveImageextent]];

    _imageView.image = [UIImageimageWithCGImage:cgimg];


    UIImageWriteToSavedPhotosAlbum(self.imageView.image,self, @selector(imageSavedToPhotosAlbum:didFinishSavingWithError:contextInfo:),nil);


    CGImageRelease(cgimg);


    

}

- (void)imageSavedToPhotosAlbum:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo {

    NSString *message =@"";

    if (!error) {

        message = @"成功保存到相册";

    }else

    {

        message = [error description];

    }

    NSLog(@"message is %@",message);

}


- (void) logAllFilters {

    NSArray * properties = [CIFilterfilterNamesInCategory:kCICategoryBuiltIn];

    NSLog(@"%@",properties) ;

    

    for (NSString * filterNamein properties) {

        CIFilter * fltr = [CIFilterfilterWithName:filterName];

        NSLog(@"%@",[fltrattributes]) ;

    }

}




@end


0 0
原创粉丝点击