学习Swift中的CoreImage(图形核心编程)


原文地址:http://www.raywenderlich.com/76285/beginning-core-image-swift
泰然教程组:The Game。校对:glory。

Core Image是一个可以让你轻松使用图形过虑器的强力框架。在这里你几乎可以获得所有不同种类的效果,比如修改图像饱和度,色彩范围,亮度等。它甚至也可以利用CPU或者GPU来处理图像数据并且它的速度很快,快到可以对视频进行实时处理。

Core Image过滤器也可以把图像或者视频的多重效果同时串在一起。多重的过滤器会被合并为一个单独的过滤器来应用到图像中。相对于同时处理多个不同效果来说这样做更有效率。

在本教程中,你将会获得关于CoreImage的实践经验。你将会学会一些不同的效果,并且你将会明白将物效实时应用到图像是多么的容易。

提示:在撰写本教程的时候,据我们了解由于Xcdoe6和iOS8仍然还在beta测试阶段,所以关于它们的截图不能上传。因此,在我们确认可以上传前,是不会出现关于它们的截图的。

开始学习

在开始学习前,让我们先讨论下在CoreImage框架中比较重要的一些类:

·CIContext:所有的图像处理都在一个CIContext类中完成。它和CoreGraphics或者OpenGL环境比较相似。

·CIImage:这个类中保存着图像的数据。它可以使用UIImage对象,一个图像文件,或者像素数据来进行创建。

·CIFilter:该类中存在一个字典,存储定义了它所描述具体特效的各种属性。比如饱和度,颜色亮度,载切等等。

在本教程中以上的每个类你都将会使用的到。

创建项目CoreImageFun

打开Xcode使用iOS中Application下的Single View Application模板创建一个新的项目工程。使用CoreImageFun作为项目名称,选择Iphone作为默认设备,确认当前的使用语言选择的为Swift.

下载本教程的资源,把这里的image.png添加到你的工程中。

下一步,打开Main.storyboard,然后拖动并添加一个新的图像视图作为现有视图的子视图。在属性面板中,设置图像视图的显示模式为(Aspect Fit)恢复拉伸,这就就不会使图像失帧了。

下一步,确保视图大纲可见(在视图界面的左下方有一个小按钮),你可以在菜单Editor中的Show Document Outline进行设定。

鼠标右键点击图像视图并拖曳到父视图三次,并添加下面3条限定:

1:添加Top Space to Layout Guide限定,如果需要的话可以使用尺寸检查器来设定限定的大小为0。

2:添加Center Horizontally限定,也设定为0。

3:添Equal Width 限定。

最后,为了限定图像视图的高度,点击鼠标右键在图像视图中拖动,并添加Aspect Ratio限定,使用尺寸检查器将宽和高的比率为定为8:5,将值设置为0。最后,点击菜单Editor下的Resolve Auto Layout 下的All Views in View Controller 下的Update Frames,然后布局会按钮刚才的设定生效。

下一步,打开Assistant Editor,请确保当前视图显示的为ViewController.swift。鼠标右键点击视图拖动到刚才打开的ViewController类中。命名为imageView,点击connect按钮。

编译并运行项目,确定到目前为止一切都是正常的,运行起来后你应当看到一个没有任何内容的屏幕显示。初始化工作完成,现在开始CoreImage!

基础的图像过滤

你即将会把一张带有简单CIFilter效果的图像显示到屏幕上来了。每当你想在图像上使用CIFilter前都需要经过下面4步:

1:创建CIImage对象。CIIMage有好几种初始化方法,有:CIImage(contenttsOfURL),CIImage(data:),CIImage(CGImage:),CIIMage(bitmapData:bytesPerRow:size:format:colorSpace:)等等。大多数情况下,都会使用CIIMage(contentsOfURL:),大多数情况下。

2:创建CIContext.CIContext是使用CPU或者GPU的基础。CIContext创建起来相对比较低效,所以重复使用比一次次的创建更好。你经常会使用到它当输出CIImage对象的时候。

3:创建CIFilter.当你创建它的时候,你需要配置一个依赖于你所使用的CIFilter数字类型的属性。

4:获得filter的输出。filter会将输出对象转换为CIImage,你可以使用CIContext把它转换为UIImage,就像下面这样.

让我们来看一下它是如何工作的。把下面的代码添加到ViewController.swift的viewDidLoad()方法中:

// 1

let fileURL = NSBundle.mainBundle().URLForResource("image", withExtension: "png")



// 2

let beginImage = CIImage(contentsOfURL: fileURL)



// 3

let filter = CIFilter(name: "CISepiaTone")

filter.setValue(beginImage, forKey: kCIInputImageKey)

filter.setValue(0.5, forKey: kCIInputIntensityKey)



// 4

let newImage = UIImage(CIImage: filter.outputImage)

self.imageView.image = newImage

根据编号来逐条解释下:

1.这一行代码创建了一个NSURL对象,存储了你所使用的图像路径。

2.下一步,使用CIImage(contentsOfURL:)构造方法创建了一个CIImage对象。

3:下一步,创建CIFilter对象。CIFilter的构造方法中包括了这个过滤器的名字,还有一对用来描述过滤器的键值字典。每一个过滤器都有它自己的唯一键和值。CISepiaTone过滤器只有2个值,KCIInputImageKey和kCIInputIntensityKey,是0~1之前的浮点类型。这里设定的值为0.5。大多数过滤器如果没有设定都会使用默认的值。例外的是CIImage,必须要指定一个值。

4:使用outputImage属性会很容易的从过滤器中获得一个CIImage。一但你有了CIImage,你就需要将它转换为UIImage对象。UIImage(CIImage:)构造方法可以使用CIImage来创建一个UIImage。转换为UIImage对象后,你只需要尽早的将它显示到视图中。

编译并运行项目,你将会看到使用sepiaTone filter效果创建的图像。

恭喜,你现在已经学会使用CIImage和CIFilters了!

使用Context创建图像

在你继续之前,你应该了解下这样的一个优化建议。

前面我曾提到过,你需要使用一个CIContext对象来创建使用CIFilter,但到目前为止还未提及过它。这表明,UIImage(CIImage:)的构造方法已经把这些工作都完成了。它自动创建了一个CIContext对象并将它应用到filter.它使得使用Core Image API变得很容易。

但是它有一个不好的地方-每次在使用它的时候,它都会重新创建一个CIContext对象。CIContext实例意味着可重用性将大大性能。如果你相使用一个滑动条来更新filter的值,就像在本教程中之前的方法,每次更新都创建一个新的CIContext对象会使性能大大下降。

让我们适当的修改一下。删除之前在viewDidLoad()的第四步代码,使用如下代码进行替换:

//1

let context = CIcontext(options:nil)



//2

let cgimg = context.createCGImage(filter.outputImage,formRect:filter.outputimage.extend())



//3

let newImage = UIImage(CGImage:cgimg)

self.imageView.image = newImage

同样的,让我们一行一行的来看下。

1:第一行声明了一个CIContext对象,使用它来绘制一个CGImage对象。CIContext(options:)构造方法使用NSDictionary来指定例如颜色格式,异或者是context对象会运行在CPU或者GPU。该项目中,参数设置成默认值nil就可以了。

2:context对象调用createCGImage(outputImage:fromRect:)方法将会返回一个新的CGImage的实例。

3:下一步,使用UIImage(CGImage:)构造方法像之前使用CIImage一样,直接使用CGImage创建一个新的UIImage对象。

编译并运行,运行的结果应当和之前的效果一样。

在这个例子中,使用CIContext创建filter好像没有什么特别之处。但在下一节中,当你实现了它动态修改数值的能力,你就会明白对性能的重要性了。

修改Filter的值

很好,这只是core Image filters的开始阶段。让我们添加一个滑动条来支持实时改变filter的设置。

打开 Main.storyboard,拖动并添加一个silder,把它添加到imageView的下面,使用centered horizontally限定。选中当前视图,依次点击Editor \ Resolve Auto Layout Issues \ Selected Views \ Reset to Suggested Constraints,可以添加下width constraint。

打开Assistant Editor,显示当前ViewController.swift的视图,然后把视图中的滑动条拖动到代码中,命名为amountSlider,点击connect。

下面我们来实现滑动条动作的方法。右键点击视图中的silder选择Action并拖动到ViewControlll class结束符}的前面。命名为amountSliderValueChanged,确保Event被设置为Value Changed,点击connect.

每当滑动条中的数值发生变化后,你需要使用不同的值重新刷新显示图像。然后,你并不想重复这些相同的处理,这样做是很不明智也很耗时的。你需要修改类中的一些细节,使你的对象在viewDidLoad方法中可以复用。

你最想做的恐怕就是重用CIContext。如果你每次都重复创建它,程序会运行缓慢。另外的可以全局化的对象是CIFilter和CIImage。每次绘制图像都会生成一个新的CIImage,但是声明名全局变量,可以让它一起存在。

你需要在你的类中添加一些变量声明。在ViewController类中添加下面的代码:

var context: CIContext!

var filter: CIFilter!

var beginImage: CIImage!

这里需要注意一下声明这些变量的时候需要使用!符号将他们声明为隐式解析可选类型,因为在viewDidLoad方法前,你不会去初始化他们。或许你可以使用?来替换!,但是你应当清楚编译器可能会强制你将他们初始化为nil在使用他们前。隐匿解析可选类型使代码更有可读性,!无处不在。

修改viewDidLoad中的代码,将其中声明的变量修改为刚才的变量。

beginImage = CIImage(contentsOfURL: fileURL)



filter = CIFilter(name: "CISepiaTone")

filter.setValue(beginImage, forKey: kCIInputImageKey)

filter.setValue(0.5, forKey: kCIInputIntensityKey)



let outputImage = filter.outputImage



context = CIContext(options:nil)

let cgimg = context.createCGImage(outputImage, fromRect: outputImage.extent())

现在你需要实现changValue方法。在这个方法中你需要做的就是重置设置CIFilter 字典中inputIntensity的键值。

你需要通过以下几步来完成该操作。

·获得Filter输出的CIImage对象。

·将CIImage对象转换为CGImage对象。

·将CGImage对象转换为UIImage对象,并将它显示到图像视图中。

将下面的代码添加到amountSliderValueChanged(sender:):

@IBAction func amountSliderValueChanged(sender: UISlider) {



let sliderValue = sender.value



filter.setValue(sliderValue, forKey: kCIInputIntensityKey)

let outputImage = filter.outputImage



let cgimg = context.createCGImage(outputImage, fromRect: outputImage.extent())



let newImage = UIImage(CGImage: cgimg)

self.imageView.image = newImage

}

这里说明一下,你需要将方法的参数AndObject修改为UISlider。在本方法中你只接受UISlider的属性值检索,所以你可以返回进行一下修改。如果你不想修改AnyObject的话,你需要在下一行中使用之前将它进行强制转换,否则程序会报错。

silder对象会返回一个浮点类型的数值。设定silder的默认值为min 0, max 0, default 0.5。很方便,这些设定可以正确的应用到CIFilter!

CIFilter有现成的方法可以在它的存储结构中进行设定数值。在这里,你需要使用silder返回的数值来设定inputIntensity。Swift自动将CFloat类型转换为NSNumber类型来适用于setValue(value:forKey:)。

下面的代码应该很熟悉,和之前viewDidLoad方法里的实现一样。这些代码你会反复的使用。从现在开始,你会使用 amountSliderValueChanged(sender:) 来在UIImageView中输出显示CIFilter对象。

编译并运行,使用slider会实时改变图像的灰度值!

获取相册中的照片

现在你可以快速的修改Filter的数值了,事情变的越来越有趣!但是如果你根本就不想处理这张图片该咋办?下面将使用UIImagePickerController,它可以让你在相册中选择图像,并在程序中处理。

你需要创建一个按钮,来跳转到相册视图中,打开Main.storyboard,拖动进来一个按钮在视图的右下角,将按钮命名为"Photo Album".像之前一样,使用 Auto Layout to Reset to Suggested Constraints。按钮应当会在slider的下面。

打开Assistant Editor ,看到ViewControll.swift视图内容,然后鼠标右键拖动按钮到代码中结束符}的前面。连接类型为Action,将它命名为loadPhoto,确定Event为Touch Up Inside,然后点击Connect.

使用如下代码实现loadPhoto方法:

@IBAction func loadPhoto(sender : AnyObject) {

  let pickerC = UIImagePickerController()

  pickerC.delegate = self

  self.presentViewController(pickerC, animated: true, completion: nil)

}

代码的第一行实例化一个新的UIImagePickerController。然后设定图像句柄的delegate为self(就是ViewController).

这里会有一个警告。你需要把ViewController继续UIImagePickerControllerDelegate和UINavigationControllerDelegate。

还是在ViewController.swift中,使用如下代码进行修改替换:

func imagePickerController(picker: UIImagePickerController!, didFinishPickingMediaWithInfo info: NSDictionary!) {

  self.dismissViewControllerAnimated(true, completion: nil);

  println(info);

}

UIImagePickerControllerDelegate 还没有完全完成-他只是为选择的图像记录信息的占位符。不管你实现

UIImagePickerControllerDelegate 中的什么方法,你都需要在你的声明中替换UIImagePickerController。如果不这样,你可能永远只能盯着图像选择器了!

编译并运行,点击按钮。图像选择器会打开相册。

如果你是在模拟器中运行,你可能在相册中没有任何图片。在模拟器中,没有摄像头,你可以使用浏览器保存一个图像到你的相册中。打开模拟器中的浏览器,找到一张图片,保存图片。下一次你运行程序的时候,这张图片就会出现在你的相册中了。

下面这些内容是你在选择一张图片后在控制台看到的输出内容:

{

UIImagePickerControllerMediaType = "public.image";

UIImagePickerControllerOriginalImage = " size {1165, 770} orientation 0 scale 1.000000";

UIImagePickerControllerReferenceURL = "assets-library://asset/asset.PNG?id=DCFE1435-2C01-4820-9182-40A69B48EA67&ext=PNG";

}

用户选择一个图像后会有一个图像组织的数据结构实体。这就是你想得到的内容。

现在你有办法选择图像了,如果来使用这张图像呢?

简单,只需要使用下面的代码替换delegate方法:

func imagePickerController(picker: UIImagePickerController!, didFinishPickingMediaWithInfo info: NSDictionary!) {

  self.dismissViewControllerAnimated(true, completion: nil);



  let gotImage = info[UIImagePickerControllerOriginalImage] as UIImage



  beginImage = CIImage(image:gotImage)

  filter.setValue(beginImage, forKey: kCIInputImageKey)

  self.amountSliderValueChanged(amountSlider)

}

你需要为你选择的图像创建一个新的CIImage对象。你可以通过数据结构中的信息来获得这个UIImage对象 ,在UIImagePickerControllerOriginalImage 的后面。这里最好使用一个常量因为苹果系统在后面可能会修改这个键值。

你需要使用CIImage(image:)构造方法把它转换为CIImage对象。然后将数据结构的相应值进行设定,使用其为新创建的图像。

最后一行似乎是多余的。还记得之前我说过的在changeValue方法中的代码会实时使用最新的结果更新filter的内容吗?

你需要在做一次,但你只需要调用changeValue方法。即使slider的值没有变化,你仍然可以调用这个方法来完成处理流程。你可以将它转换为自己的方法,但现在的目标是通过调用amountSliderValueChanged方法来得到正确的结果。将amountSlider 传参得到正常的值。

编译并运行,现在你可以修改相册跌任何照片!

如果你创建出了完美的灰色图像,怎么保留下它呢?你可以截个图,但是更好的办法是把他重新保存到相册中。

将处理过的图像保存到相册

为了保存处理后的图像,你需要全用AssetsLibrary 框架。在ViewController.swift开始添加如下引用代码。

import AssetsLibrary

当你把一个图像存储到相册的时候,你需要知道一件事情,这个存储的过程可能需要一些时间,即使你把程序关闭它也可能还在进行。

这可能是一个问题,如果你从一个程序切换到另一个,GPU的会停止处理之前的任务。如果图像还没有被存储完成,那么它可能就没有被正确的保存,不能被查看。

解决的办法就是使用CPU基础下的CIContext 进行绘制。默认的行为是使用GPU进行处理,因为它更快,但是你不想因为添加保存功能而使程序的性能下降。因为,你需要创建第二个CIContext来存储图像。模拟器中不支持这种渲染模式。

在你的程序中添加另一个新的按钮,点击它,会将你对图像做的所有改动进行存储。打开Main.storyboard,添加一个新的按钮,命名为“Save to Album”。将按钮添加到slider的左边,添加 suggested constraints。

新的连接名称为savePhoto(sender:),如何连接前面已经讲过,添加如下代码:

@IBAction func savePhoto(sender: AnyObject) {

// 1

let imageToSave = filter.outputImage



// 2

let softwareContext = CIContext(options:[kCIContextUseSoftwareRenderer: true])



// 3

let cgimg = softwareContext.createCGImage(imageToSave, fromRect:imageToSave.extent())



// 4

let library = ALAssetsLibrary()

library.writeImageToSavedPhotosAlbum(cgimg,

  metadata:imageToSave.properties(),

  completionBlock:nil)

}

上面的代码:

1:获得filter的输出CIImage对象。

2:创建一个新的基于使用CPU的CIContext对象。

3:产生CGImage对象。

4:将图像保存到相册中。

编译并运行,现在你可以将自己处理的图像永久的进行保存了。

关于图像元数据

让我们来讨论一下图像元数据。图像文件在手机有他们各自相关的各种数据,如GPS坐标、图像格式和取向。

一些特殊的内容,你需要进行保护。处理加载一张UIImage为CIImage,绘制一个CGImage,转换回UIImage,拿掉图像的元数据。为了达到保护的目标,你需要记录它然后将它传回UIImage的构造方法。

在ViewController类中添加下面的定义。

var orientation: UIImageOrientation = .Up

下一步,添加下面的代码 到imagePickerController(picker:didFinishPickingMediaWithInfo:),在设定beginImage的前面:

orientation = gotImage.imageOrientation

这样存储的图像会被保护。

最后 ,在amountSliderValueChanged 中添加下面一行代码 ,创建并设定UIImage:

let newImage = UIImage(CGImage: cgimg, scale:1, orientation:orientation)

现在,如果你有一些照片为非默认定位,他将会被保存。

还有哪些其它的Filters?

在苹果系统的CIFilter API中有超过160种的filters,其中有126种在IOS8中可以使用。在IOS8的系统中,系统已经支持自定义图像了。

为了找出哪些filter在设备中被支持,你可以使用CIFilter的filterNamesInCategory(kCICategoryBuiltIn)方法。这个方法会返回一个可用名称的数组。

还有就是,每个filter方法都有一个叫做attributes()的方法,该方法会返回一个关于该filter的信息的数据结构。这些信息包括该filter的名称,类别,输入类型,默认的或者可接受的数值。

让我们一起在程序中添加一个方法来打出当前设备支持的filter信息。添加这个方法到ViewController方法中:

func logAllFilters() {

  let properties = CIFilter.filterNamesInCategory(kCICategoryBuiltIn)

  println(properties)



  for filterName: AnyObject in properties {

let fltr = CIFilter(name:filterName as String)

println(fltr.attributes())

  }

}

该方法使用filterNamesInCategory得到一个数组。它先打印出了filter的名称,然后将每个名称的filter进行实例化,并打印出他们各自的属性。

在viewDidLoad()方法中调用 如下方法:

self.logAllFilters()

你将会看到一些filters被你下面一样列出来:

[CIAttributeFilterDisplayName: Color Monochrome, inputColor: {

CIAttributeClass = CIColor;

CIAttributeDefault = "(0.6 0.45 0.3 1)";

CIAttributeType = CIAttributeTypeColor;

}, inputImage: {

CIAttributeClass = CIImage;

CIAttributeType = CIAttributeTypeImage;

}, CIAttributeFilterCategories: (

CICategoryColorEffect,

CICategoryVideo,

CICategoryInterlaced,

CICategoryNonSquarePixels,

CICategoryStillImage,

CICategoryBuiltIn

), inputIntensity: {

CIAttributeClass = NSNumber;

CIAttributeDefault = 1;

CIAttributeIdentity = 0;

CIAttributeSliderMax = 1;

CIAttributeSliderMin = 0;

CIAttributeType = CIAttributeTypeScalar;

}, CIAttributeFilterName: CIColorMonochrome]

WOW!,好多的filters!你可以在你的程序中对他们进行一些尝试。

更复杂一些的filter

现在我们已经看到所有IOS平台支持的滤镜,是时候创建一些复杂的滤镜了。为了做到这些,你需要创建一个专用的方法来处理CIImage。他会使用一个CIImage,将他修改为一个复古的图像,并返回一个修改后的CIImage。

添加如下的代码到ViewController:

func oldPhoto(img: CIImage, withAmount intensity: Float) -> CIImage {

  // 1

  let sepia = CIFilter(name:"CISepiaTone")

  sepia.setValue(img, forKey:kCIInputImageKey)

  sepia.setValue(intensity, forKey:"inputIntensity")



  // 2

  let random = CIFilter(name:"CIRandomGenerator")



  // 3

  let lighten = CIFilter(name:"CIColorControls")

  lighten.setValue(random.outputImage, forKey:kCIInputImageKey)

  lighten.setValue(1 - intensity, forKey:"inputBrightness")

  lighten.setValue(0, forKey:"inputSaturation")



  // 4

  let croppedImage = lighten.outputImage.imageByCroppingToRect(beginImage.extent())



  // 5

  let composite = CIFilter(name:"CIHardLightBlendMode")

  composite.setValue(sepia.outputImage, forKey:kCIInputImageKey)

  composite.setValue(croppedImage, forKey:kCIInputBackgroundImageKey)



  // 6

  let vignette = CIFilter(name:"CIVignette")

  vignette.setValue(composite.outputImage, forKey:kCIInputImageKey)

  vignette.setValue(intensity * 2, forKey:"inputIntensity")

  vignette.setValue(intensity * 30, forKey:"inputRadius")



  // 7

  return vignette.outputImage

}

下面一段一段的解释一下:

1:创建sepia滤镜像之前的方式一样。然后传递了一个浮点类型来设定sepia的强度效果。这个值在之后可以通过slider来进行修改。

2:为滤镜创建一个随机噪声模式如下图:

没有任何参数。之后你将使用这个随机噪声模式的纹理添加到你的复古图像中。

3:改变随机噪声模式生成。你想改变灰度,并减轻一点所以效果不那么显著。你会发现输入图像的键值会应用到输出图像的属性。使用一个输入滤镜作为输出滤镜的参数是很方便的。

4:imageByCroppingToRect() 得到一个输出的CIImage对象,将它植入矩形。在这步,你需要裁剪下CIRandomGenerator滤镜的输出因为它的范围过大。如果你没有裁剪到一些点,你会得到一个通知该滤镜没有限定的错误。CIImage并真正的包含图像数据,他们描述了一个创建他们的方法。在你调用CIContext方法前,他们是不会被真的处理的。

5:合并CIRandomGenerator滤镜的输出到speia滤镜。这个过滤器可以执行精确的操作像photoshop在图层设定高亮。ps中大多数滤镜都使用到了core image。

6:在混合输出带有黑色边界的图像上运行vignette滤镜。滑动条的设定比率会影响效果的强度。

7:最后 ,返回最后滤镜的输出。

这就是这个滤镜的所有操作。你现在应该对混合滤镜的形成有一个大概的了解了。不同的core imager滤镜合并,你可以做出各种各样的效果。

下一步就是实现amountSliderValueChanged()的方法。改变这2行代码:

filter.setValue(sliderValue, forKey: "inputIntensity")

let outputImage = filter.outputImage

let outputImage = self.oldPhoto(beginImage, withAmount: sliderValue)

这行代码会使用新的效果替换sepia的效果,更复杂的滤镜方法。使用滑动条的值来设定beginImage的强度,就是在viewDidLoad方法中使用的输入CIImage对象。编译并运行,你应当会看到一个更具文雅的复古物效,使用sepia来完成,一些噪声修饰和一些渐变。

这个噪声模式可以做的更精致,但我把它就留给你来做吧,亲爱的读者。现在你已经有足够的能力来使用core image处理图像了。疯狂吧,少年!

总结

这里是以上教程的所有代码。

这个教程包含了core image滤镜的基本使用。core image是一个比如便捷的技术,你应当有能力可以使用它来很快的处理一些整洁的滤镜来处理图像了。

记住,当这个程序启动的时候在控制台会打印出一所有支持的滤镜。为何不来尝试一下这些滤镜呢?你可以通过查看这里来了解所有的滤镜使用。

标签: Swift

?>