图像处理意味着应用滤镜 - 图像滤镜是一个软件,它逐个像素地检查输入图像,并在算法上应用一些效果以创建输出图像。在Core Image中,图像处理依赖于CIFilter和CIImage类,它们描述了滤镜及其输入和输出。要应用滤镜并显示或导出结果,您可以使用Core Image和其他系统框架之间的集成,或者使用CIContext类创建自己的渲染工作流。本章介绍了使用这些类来应用滤镜和呈现结果的关键概念。
在应用中,有许多方法可以使用Core Image进行图像处理。清单1-1显示了一个基本示例,并提供了本章中进一步说明的地方。
清单1-1 将滤镜应用于图像的基础
import CoreImage
let context = CIContext() // 1
let filter = CIFilter(name: "CISepiaTone")! // 2
filter.setValue(0.8, forKey: kCIInputIntensityKey)
let image = CIImage(contentsOfURL: myURL) // 3
filter.setValue(image, forKey: kCIInputImageKey)
let result = filter.outputImage! // 4
let cgImage = context.createCGImage(result, from: result.extent) // 5
这段代码的作用:
Core Image滤镜处理并生成Core Image图像。一个CIImage实例是表示图像的一个不可变的对象。这些对象不直接表示图像位图数据 - 相反,CIImage对象是用于产生图像的“配置”。一个配置可能要求从文件加载图像; 另一个可能代表滤镜或滤镜的输出。仅当您请求渲染图像以供显示或输出时,Core Image才会执行这些配置。
要应滤镜,请创建一个或多个CIImage表示要由滤镜处理的图像对象,并将它们分配给滤镜的输入参数(例如kCIInputImageKey)。您几乎可以从任何图像数据源创建Core Image图像对象,包括:
有关创建CIImage对象的完整方法列表,请参阅CIImage类参考。
因为CIImage对象描述了如何生成图像(而不是包含图像数据),所以它也可以表示滤镜的输出。当您访问CIFilter对象的outputImage属性时,Core Image仅识别并存储执行滤镜所需的步骤。仅当您请求渲染图像以供显示或输出时,才会执行这些步骤。您可以使用其中一个CIContextrender或多个draw方法显式请求渲染(请参阅使用Core Image Context构建您自己的工作流),或者通过使用的众多系统框架之一与Core Image一起工作显示图像来隐式请求渲染(请参阅其他框架集成)。
延迟处理直到渲染时使Core Image快速高效。在渲染时,Core Image可以查看是否需要将多个滤镜应用于图像。如果是这样,它会自动连接多个“配置”并组织它们以消除冗余操作,这样每个像素只处理一次而不是多次。
CIFilter该类的实例是表示图像处理效果的可变对象以及控制该效果行为的任何参数。要使用滤镜,可以创建一个CIFilter对象,设置其输入参数,然后访问其输出图像(请参阅下面的图像是滤镜的输入和输出)。filterWithName:使用系统已知的滤镜名称调用初始化程序以实例化滤镜对象(请参阅查询系统滤镜或Core Image滤镜参考)。
大多数滤镜都有一个或多个输入参数,可让您控制处理的完成方式。每个输入参数都有一个属性类,用于指定其数据类型,例如NSNumber。输入参数可以选择具有其他属性,例如其默认值,允许的最小值和最大值,参数的显示名称以及CIFilter类参考中描述的其他属性。例如,CIColorMonochrome滤镜有三个输入参数 - 要处理的图像,单色和颜色强度。
滤镜参数定义为键值对; 要使用参数,通常使用valueForKey:和setValue:forKey:键或基于键值编码构建的其他功能(例如Core Animation)。键是标识属性的常量,值是与键关联的设置。Core Image属性值通常使用属性值数据类型中列出的数据类型之一。
表1-1 属性值数据类型
数据类型 | 对象 | 描述 |
---|---|---|
字符串 | NSString | 文本,通常用于显示给用户 |
浮点值 | NSNumber | 标量值,例如强度级别或半径 |
矢量 | CIVector | 一组浮点值,可指定位置,大小,矩形或未标记的颜色分量值 |
颜色 | CIColor | 一组颜色组件值,标记有指定如何解释它们的颜色空间 |
图片 | CIImage | 一个图像; 请参阅图像是滤镜的输入和输出 |
变换 | NSData, NSAffineTransform | 要应用于图像的坐标转换 |
重要说明: CIFilter对象是可变的,因此您无法在不同的线程之间安全地共享它们。每个线程都必须创建自己的CIFilter对象。但是,滤镜的输入和输出CIImage对象是不可变的,因此可以安全地在线程之间传递。
每个Core Image滤镜都会生成一个输出CIImage对象,因此您可以将此对象用作另一个滤镜的输入。例如,图1-1中所示的滤镜序列将颜色效果应用于图像,然后添加发光效果,最后从结果中裁剪出一个部分。
Core Image优化了此类滤镜链的应用,以快速有效地呈现结果。CIImage链中的每个对象都不是完全渲染的图像,而只是用于渲染的“配置”。Core Image不需要单独执行每个滤镜,浪费时间和内存渲染中间像素缓冲区,这些缓冲区永远不会被看到。相反,Core Image将滤镜组合到一个操作中,甚至可以在以不同顺序应用滤镜时重新组织滤镜,从而更有效地生成相同的结果。图1-2显示了图1-1中示例滤镜链的更准确的再现。
请注意,在图1-2中,裁剪操作已从最后移动到第一个。该滤镜导致原始图像的大部分区域被裁剪出最终输出。因此,无需对这些像素应用颜色和锐化滤镜。通过首先执行裁剪,Core Image确保昂贵的图像处理操作仅适用于最终输出中可见的像素。
清单1-2显示了如何设置如上所示的滤镜链。
清单1-2 创建滤镜链
func applyFilterChain(to image: CIImage) -> CIImage {
// CIPhotoEffectInstant建滤镜仅采用输入图像
let colorFilter = CIFilter(name: "CIPhotoEffectProcess", withInputParameters:
[kCIInputImageKey: image])!
//将颜色滤镜的结果传递给Bloom滤镜并为发光效果设置其参数。
let bloomImage = colorFilter.outputImage!.applyingFilter("CIBloom",
withInputParameters: [
kCIInputRadiusKey: 10.0,
kCIInputIntensityKey: 1.0
])
// imageByCroppingToRect是一种方便的方法创建CICrop滤镜并访问其outputImage。
let cropRect = CGRect(x: 350, y: 350, width: 150, height: 150)
let croppedImage = bloomImage.cropping(to: cropRect)
return croppedImage
}
清单1-2还显示了一些用于配置滤镜和访问其结果的便捷方法。总之,您可以使用以下任何方法单独或作为滤镜链的一部分应用滤镜:
大多数内置Core Image滤镜都在主输入图像上运行(可能还有影响处理的附加输入图像)并创建单个输出图像。但是,您可以使用其他几种类型来创建有趣的效果,或者与其他滤镜结合使用以生成更复杂的工作流程。
一个合成(或混合)的滤镜根据预先设定的公式组合两个图像。例如:
有关合成滤镜的完整列表,请查询CICategoryCompositeOperation类别。
注意: 您可以通过对每个图像应用几何调整来排列输入图像,然后再对其进 请参阅CICategoryGeometryAdjustment滤镜类别或imageByApplyingTransform:方法。
一个滤镜生成器不采取任何输入图像。相反,这些滤镜使用其他输入参数从头开始创建新图像。这些生成器产生的输出图像可以单独使用,也可以同其他滤镜组合在滤镜链中以产生更有趣的图像。内置Core Image滤镜中的一些示例包括:
要查找生成器滤镜,请查询CICategoryGenerator和CICategoryGradient类别。
一个缩减滤镜操作在输入图像上,并不是在传统意义上创建输出图像,而是输出描述有关输入图像的信息。例如:
所有Core Image滤镜都必须生成一个CIImage对象作为输出,因此缩减滤镜产生的信息仍然是图像。但是,您通常不显示这些图像 - 而是从单像素或单行图像中读取颜色值,或将其用于其他滤镜的输入。
有关缩减滤镜的完整列表,请查询CICategoryReduction类别。
一个过渡滤镜采用了两个输入的图像和变量,输出图像为他们之间的一个特定的变化点,该变量是时间,所以可以使用一个过渡滤镜来创建一个的动画以一个图像开始,以另一个图像结束,并且从一个到另一个使用有趣的视觉效果执行。Core Image提供了几个内置的过渡滤镜,包括:
Core Image可与iOS,macOS和tvOS中的其他几种技术进行互操作。由于这种紧密集成,您可以使用Core Image轻松地在应用程序的用户界面中为游戏,视频或图像添加视觉效果,而无需构建复杂的渲染代码。以下部分介绍了在应用程序中使用Core Image的几种常用方法,以及为每种方法提供的便利系统框架。
UIKit和AppKit提供了简单的方法来向静态图像添加Core Image处理,无论这些图像是出现在应用程序的UI中还是其工作流程的一部分。例如:
注意: 请勿使用Core Image创建作为用户界面设计一部分的模糊效果(如半透明侧边栏,工具栏以及macOS,iOS和tvOS系统界面的背景中所见)。相反,请参阅NSVisualEffectView(macOS)或UIVisualEffectView(iOS / tvOS)类,它们自动匹配系统外观并提供高效的实时渲染。
在iOS和tvOS中,您可以在使用UIImage对象的任何位置应用Core Image滤镜。清单1-3显示了使用带有图像视图的滤镜的简单方法。
清单1-3 将滤镜应用于图像视图(iOS / tvOS)
class ViewController: UIViewController {
let filter = CIFilter(name: "CISepiaTone",
withInputParameters: [kCIInputIntensityKey: 0.5])!
@IBOutlet var imageView: UIImageView!
func displayFilteredImage(image: UIImage) {
// 使用输入图像创建一个Core Image对象
let inputImage = CIImage(image: image)!
// 设置为滤镜的输入图像参数
filter.setValue(inputImage, forKey: kCIInputImageKey)
// 用UIImage表示并显示滤镜的输出
imageView.image = UIImage(CIImage: filter.outputImage!)
}
}
在macOS中,使用该initWithBitmapImageRep:方法从位图创建CIImage图像对象,可在任何支持NSImage对象的地方使用NSCIImageRep类创建图像。
AVFoundation框架提供了许多用于处理视频和音频内容的高级实用程序。其中包括AVVideoComposition类,您可以使用它将视频和音频轨道组合或编辑到单个演示文稿中。(有关合成的一般信息,请参阅“ AVFoundation编程指南中的编辑”。)您可以使用AVVideoComposition对象在播放或导出期间将Core Image滤镜应用于视频的每个帧,如清单1-4所示。
清单1-4 将滤镜应用于视频合成
let filter = CIFilter(name: "CIGaussianBlur")!
let composition = AVVideoComposition(asset: asset, applyingCIFiltersWithHandler: { request in
// 避免边缘模糊
let source = request.sourceImage.clampingToExtent()
filter.setValue(source, forKey: kCIInputImageKey)
// 根据视频的时间改变滤镜参数
let seconds = CMTimeGetSeconds(request.compositionTime)
filter.setValue(seconds * 10.0, forKey: kCIInputRadiusKey)
// 裁剪为原始图像大小
let output = filter.outputImage!.cropping(to: request.sourceImage.extent)
// 提供滤镜输出
request.finish(with: output, context: nil)
})
使用videoCompositionWithAsset:applyingCIFiltersWithHandler:初始化程序创建合成时,您将提供一个处理程序,负责将滤镜应用于每个视频帧。AVFoundation在播放或导出期间自动调用您的处理程序。在处理程序中,首先使用提供的AVAsynchronousCIImageFilteringRequest对象来检索要使用滤镜的视频帧(以及补充信息,如帧时间),然后提供滤镜后的图像以供合成使用。
要使用创建的视频合成进行播放,请使用与合成源相同的资源创建AVPlayerItem对象,然后将合成分配给播放器项的videoComposition属性。要将合成导出到新的影片文件,请从同一源资源创建AVAssetExportSession对象,然后将合成分配给导出会话的videoComposition属性。
提示: 清单1-4还显示了另一种有用的Core Image技术。默认情况下,模糊滤镜还会通过模糊图像像素以及(在滤镜的图像处理空间中)环绕图像的透明像素来柔化图像的边缘。在某些情况下,例如过滤视频时,这种效果可能是不合需要的。
要避免此效果,请使用imageByClampingToExtent方法(或CIAffineClamp滤镜)在模糊之前在所有方向上无限延伸图像的边缘像素。Clamping会创建无限大小的图像,因此您还应该在模糊后裁剪图像。
SpriteKit是一种用于构建2D游戏和其他类型应用程序的技术,这些应用程序具有高度动态的动画内容; SceneKit用于处理3D资源,渲染和动画3D场景以及构建3D游戏。(有关每种技术的更多信息,请参阅SpriteKit编程指南和SceneKit框架参考。)两种框架都提供高性能的实时渲染,可以通过简单的方法将Core Image处理添加到场景的全部或部分。
在SpriteKit中,您可以使用SKEffectNode类添加Core Image滤镜。要查看正在使用的此类的示例,请使用Game模板(对于iOS或tvOS)创建一个新的Xcode项目,选择SpriteKit作为游戏技术,并修改法GameScene类中的touchesBegan:withEvent:方法以使用清单1-5中的代码。(对于macOS Game模板,您可以对该mouseDown:方法进行类似的修改。)
清单1-5 在SpriteKit中应用滤镜
override func touchesBegan(touches: Set, withEvent event: UIEvent?) {
for touch in touches {
let sprite = SKSpriteNode(imageNamed:"Spaceship")
sprite.setScale(0.5)
sprite.position = touch.location(in: self)
sprite.run(.repeatForever(.rotate(byAngle: 1, duration:1)))
let effect = SKEffectNode()
effect.addChild(sprite)
effect.shouldEnableEffects = true
effect.filter = CIFilter(name: "CIPixellate",
withInputParameters: [kCIInputScaleKey: 20.0])
self.addChild(effect)
}
}
请注意,SKScene类本身是SKEffectNode子类,因此您还可以将Core Image滤镜应用于整个SpriteKit场景。
在SceneKit中,SCNNode类的filters属性可以将Core Image滤镜应用于3D场景的任何元素。要查看此属性,请使用Game模板(对于iOS,tvOS或macOS)创建一个新的Xcode项目,选择SceneKit作为游戏技术,并修改GameViewController类中的viewDidLoad方法以使用清单1-6中的代码。
清单1-6 在SceneKit中应用滤镜
// 在模块中找到这一行
let ship = rootNode.childNode(withName: "ship", recursively: true)!
// 添加如下代码
let pixellate = CIFilter(name: "CIPixellate",
withInputParameters: [kCIInputScaleKey: 20.0])!
ship.filters = [ pixellate ]
您还可以在SceneKit节点上设置滤镜参数的动画 - 有关详细信息,请参阅该filters属性的参考文档。
在SpriteKit和SceneKit中,您可以使用过渡来更改视图的场景,增加视觉效果。(请参阅SpriteKit的presentScene:transition: 方法和SceneKit的presentScene:withTransition:incomingPointOfView:completionHandler:方法。)使用SKTransition类及其transitionWithCIFilter:duration:初始化程序从任何Core Image过渡滤镜创建过渡动画。
在macOS中,您可以使用filters属性将滤镜应用于任何CALayer视图的内容,并添加随时间变化的滤镜参数的动画。见滤镜加到OS X浏览视觉效果和先进的动画技巧的核心动画编程指南。
使用上一节中列出的技术应用Core Image滤镜时,这些框架会自动管理Core Image用于处理图像和呈现结果以供显示的基础资源。这种方法既可以最大限度地提高这些工作流程的性能,又可以更轻松地进行设置。但是,在某些情况下,使用CIContext该类自己管理这些资源更为谨慎。通过直接管理Core Image上下文,您可以精确控制应用程序的性能特征,或者将Core Image与较低级别的呈现技术集成。
Core Image Context表示执行滤镜和生成图像所需的CPU或GPU计算技术,资源和设置。有几种上下文可供选择,因此您应该选择最适合您应用程序工作流程的选项以及您可能正在使用的其他技术。以下部分讨论了一些常见的情况; 有关完整的选项集,请参阅CIContext类参考。
重要: Core Image Context是管理大量资源和状态的重量级对象。反复创建和销毁上下文会产生很大的性能成本,因此如果您计划执行多个图像处理操作,请尽早创建上下文并将其存储以供将来重用。
如果您对应用程序与其他图形技术的互操作性没有限制,那么创建Core Image上下文很简单:只需使用基本init或initWithOptions:初始化程序。执行此操作时,Core Image会自动在内部管理资源,根据当前设备和您指定的任何选项选择适当或最佳的CPU或GPU渲染技术。此方法非常适合于渲染已处理图像以输出到文件(例如,使用writeJPEGRepresentationOfImage:toURL:colorSpace:options:error:方法)的任务。
注意: 没有显式指定的渲染目标的上下文不能使用该drawImage:inRect:fromRect:方法,因为该方法的行为会根据使用的渲染目标而更改。而是使用CIContext以名称开头render或create的方法来指定显式目标。
如果您想要实时渲染Core Image结果,即在滤镜参数中设置动画效果,生成动画过渡效果,或者处理已经每秒渲染多次的视频或其他可视内容,请小心使用此方法。
Metal框架提供对GPU的低开销访问,实现图形渲染和并行计算工作流的高性能。此类工作流程是图像处理不可或缺的一部分,因此Core Image尽可能在Metal上构建。如果您正在构建使用Metal渲染图形的应用程序,或者如果您想利用Metal来获取动画滤镜输出或过滤动画输入(例如实时视频)的实时性能,请使用Metal设备创建Core Image上下文。
清单1-7和清单1-8显示了使用MetalKit视图(MTKView)渲染Core Image输出的示例。(重要步骤在每个清单中编号,然后进行描述。)
清单1-7 使用Core Image渲染设置Metal视图
class ViewController: UIViewController, MTKViewDelegate { // 1
// Metal资源
var device: MTLDevice!
var commandQueue: MTLCommandQueue!
var sourceTexture: MTLTexture! // 2
// Core Image资源
var context: CIContext!
let filter = CIFilter(name: "CIGaussianBlur")!
let colorSpace = CGColorSpaceCreateDeviceRGB()
override func viewDidLoad() {
super.viewDidLoad()
device = MTLCreateSystemDefaultDevice() // 3
commandQueue = device.newCommandQueue()
let view = self.view as! MTKView // 4
view.delegate = self
view.device = device
view.framebufferOnly = false
context = CIContext(mtlDevice: device) // 5
// 其他设置
}
}
要点: 将Metal视图,图层或纹理用作Core Image渲染目标时,始终将framebufferOnly属性设置为NO。
5. 创建与视图使用相同的Metal设备的Core Image上下文。通过共享Metal资源,Core Image可以处理纹理内容并渲染到视图,而无需将图像数据复制到单独的CPU或GPU内存缓冲区的性能成本。CIContext对象的创建成本很高,因此您只需执行一次,并在每次处理图像时重复使用它。
drawInMTKView:每次视图需要显示时,MetalKit都会调用该方法。(默认情况下,MetalKit可以每秒调用此方法多达60次。有关详细信息,请参阅视图的preferredFramesPerSecond属性。)清单1-8显示了从Core Image上下文渲染的方法的基本实现。
清单1-8 在Metal视图中使用Core Image滤镜绘图
public func draw(in view: MTKView) {
if let currentDrawable = view.currentDrawable { // 1
let commandBuffer = commandQueue.commandBuffer()
let inputImage = CIImage(mtlTexture: sourceTexture)! // 2
filter.setValue(inputImage, forKey: kCIInputImageKey)
filter.setValue(20.0, forKey: kCIInputRadiusKey)
context.render(filter.outputImage!, // 3
to: currentDrawable.texture,
commandBuffer: commandBuffer,
bounds: inputImage.extent,
colorSpace: colorSpace)
commandBuffer.present(currentDrawable) // 4
commandBuffer.commit()
}
}
此示例仅显示使用Metal渲染Core Image所需的最少代码。在实际应用程序中,您可能会在Core Image管理的渲染之前或之后执行其他渲染过程,或者将Core Image输出渲染到辅助纹理中,并在另一个渲染过程中使用该纹理。有关使用Metal绘图的更多信息,请参阅“Metal编程指南”。
Core Image还可以使用OpenGL(macOS)或OpenGL ES(iOS和tvOS)进行基于GPU的高性能渲染。如果您需要支持Metal不可用的旧硬件,或者您希望将Core Image集成到现有的OpenGL或OpenGL ES工作流程中,请使用此选项。
在任一情况下,使用imageWithTexture:size:flipped:colorSpace:初始化程序从OpenGL或OpenGL ES纹理创建CIImage对象。使用GPU内存中已有的图像数据可以通过删除冗余复制操作来提高性能。
要在OpenGL或OpenGL ES中渲染Core Image输出,请使GL上下文变为当前并设置目标帧缓冲,然后调用drawImage:inRect:fromRect:方法。
如果您的应用程序不需要实时性能并使用CoreGraphics绘制视图内容(例如,在drawRect:UIKit或AppKit视图的方法中),请使用contextWithCGContext:options:初始化程序创建直接与Core Graphics上下文一起使用的Core Image上下文’已经用于其他绘图。(在macOS中,请改用CIContext当前NSGraphicsContext对象的属性。)有关CoreGraphics上下文的信息,请参阅“ Quartz 2D编程指南”。