Core Image是一个强大而高效的图像处理框架。您可以使用框架提供的内置过滤器创建漂亮的效果,也可以创建自定义过滤器和图像处理器。您可以调整颜色、几何形状并执行复杂的卷积。
制作漂亮的滤镜是一门艺术,最伟大的艺术家之一是列奥纳多·达·芬奇 (Leonardo da Vinci)。在本教程中,您将为达芬奇的名画添加一些有趣的元素。
在此过程中,您将:
- 了解 Core Image 的类和内置过滤器。
- 使用内置过滤器创建过滤器。
- 使用自定义颜色内核转换图像的颜色。
- 使用自定义扭曲内核转换图像的几何形状。
- 学习调试 Core Image 问题。
注意:由于 Apple 错误,本教程不适用于 Xcode 13 和 iOS 15。您现在必须使用 Xcode 12。
准备好你的画笔,哎呀,我的意思是你的 Xcode 准备好了。是时候潜入 Core Image 的奇妙世界了!
入门
您将看到达芬奇最著名的四部作品。点击一幅画会打开一张纸,但图像的输出是空的。
在本教程中,您将为这些图像创建过滤器,然后查看在输出中应用过滤器的结果。
向下滑动以关闭工作表。接下来,点击右上角的过滤器列表。
该按钮应显示可用内置过滤器的列表。但是等等,它目前是空的。你接下来会解决这个问题。:]
介绍核心图像类
在填充过滤器列表之前,您需要了解 Core Image 框架的基本类。
-
CIImage:表示准备好处理或由 Core Image 过滤器生成的图像。一个
CIImage
对象具有内所有图像的数据,但并不是真正的图像。这就像一个食谱,它包含了做一道菜的所有成分,但不是这道菜本身。您将在本教程后面看到如何渲染要显示的图像。
-
CIFilter:获取一张或多张图像,通过应用变换处理每张图像并产生 a
CIImage
作为其输出。您可以链接多个过滤器并创建有趣的效果。的对象CIFilters
是可变的并且不是线程安全的。 -
CIContext:呈现过滤器的处理结果。例如,
CIContext
帮助从CIImage
对象创建 Quartz 2D 图像。
要了解有关这些类的更多信息,请参阅Core Image 教程:入门。
现在您已经熟悉了 Core Image 类,是时候填充过滤器列表了。
获取内置过滤器列表
打开RayVinci并选择FilterListView.swift。替换filterList
在FilterListView
用:
让filterList = CIFilter .filterNames(inCategory: nil )
复制代码
在这里,您通过使用filterNames(inCategory:)
和nil
作为类别传递来获取 Core Image 提供的所有可用内置过滤器的列表。您可以查看可用的类别列表中CIFilter
的开发者文档。
打开FilterDetailView.swift。替换Text("Filter Details")
在body
用:
// 1
if let ciFilter = CIFilter (name: filter) {
// 2
ScrollView { Text (ciFilter.attributes.description) } } else {
// 3
文本(“未知过滤器!”) }
复制代码
在这里,你:
ciFilter
使用过滤器名称初始化过滤器 。由于名称是一个字符串并且可能拼写错误,因此初始化程序返回一个可选的。因此,您需要检查过滤器是否存在。- 您可以使用 来检查过滤器的各种属性
attributes
。如果过滤器存在,您将在此处创建ScrollView
并填充Text
视图中的属性描述。 - 如果过滤器不存在或未知,您将显示一个
Text
解释该情况的视图。
构建并运行。点按过滤器列表。哇,过滤器太多了!
点按任何过滤器以查看其属性。
很了不起,不是吗?你才刚刚开始!在下一部分中,您将使用其中一个内置滤镜让阳光照在“蒙娜丽莎”上。:]
使用内置过滤器
现在您已经看到了可用过滤器的列表,您将使用其中一个来创建一个有趣的效果。
打开ImageProcessor.swift。在顶部,在类声明之前,添加:
enum ProcessEffect { case builtIn case colorKernel case warpKernel case blendKernel }
复制代码
在这里,您声明ProcessEffect
为enum
. 它包含您将在本教程中使用的所有过滤器案例。
将以下内容添加到ImageProcessor
:
// 1
private func applyBuiltInEffect ( input : CIImage ) {
// 2
let noir = CIFilter ( 名称: "CIPhotoEffectNoir" , 参数:[ “输入图像”:输入] ) ? .outputImage
// 3
let sunGenerate = CIFilter ( name: "CISunbeamsGenerator" , 参数: [
"inputStriationStrength" : 1 , "inputSunRadius" : 300 , "inputCenter" : CIVector ( x: input.extent.width - input.extent.width / 5 , y: input.extent.height - input.extent.height / 10 ) ]) ? .outputImage
// 4
let CompositeImage = input.applyingFilter( "CIBlendWithMask" , 参数: [ kCIInputBackgroundImageKey: noir as Any , kCIInputMaskImageKey: sunGenerate as Any ]) }
复制代码
在这里,你:
- 声明一个将 a
CIImage
作为输入并应用内置过滤器的私有方法。 - 您可以通过创建一个黑暗的,穆迪开始黑色的使用效果
CIPhotoEffectNoir
。CIFilter
以字典的形式将字符串作为名称和参数。您从 中获取生成的过滤图像outputImage
。 - 接下来,您使用
CISunbeamsGenerator
. 这将创建一个阳光遮罩。在参数中,您设置:- inputStriationStrength:表示阳光的强度。
- inputSunRadius:表示太阳的半径。
- inputCenter:阳光中心的 x 和 y 位置。在这种情况下,您将位置设置在图像的右上角。
- 在这里,您可以使用
CIBlendWithMask
. 您可以input
通过将结果设置CIPhotoEffectNoir
为背景图像和sunGenerate
蒙版图像来应用过滤器。此组合的结果是CIImage
.
ImageProcessor
具有output
,一个已发布的属性,它是UIImage
. 您需要将合成结果转换为 aUIImage
以显示它。
在 中ImageProcessor
,添加以下内容@Published var output = UIImage()
:
让上下文= CIContext ()
复制代码
在这里,您创建一个CIContext
所有过滤器都将使用的实例。
将以下内容添加到ImageProcessor
:
ivate func renderAsUIImage ( _ image : CIImage ) -> UIImage ? { if let cgImage = context.createCGImage(image, from: image.extent) { return UIImage (cgImage: cgImage) }
返回 零 }
复制代码
在这里,您用于context
创建CGImage
from的实例CIImage
。
使用cgImage
,然后创建一个UIImage
. 用户将看到此图像。
显示内置过滤器的输出
将以下内容添加到末尾applyBuiltInEffect(input:)
:
outputImage = renderAsUIImage(compositeImage) { 输出=输出图像 }
复制代码
这会将compositeImage
a转换CIImage
为UIImage
using renderAsUIImage(_:)
。然后将结果保存到output
.
将以下新方法添加到ImageProcessor
:
// 1
func process ( Painting : Painting , effect : ProcessEffect ) {
// 2
guard
let paintImage = UIImage (named: Painting.image), let input = CIImage (image:paintImage) else { print ( "Invalid input image" ) 返回 }
switch effect {
// 3
case .builtIn: applyBuiltInEffect(输入:输入)
默认值: 打印(“不支持的效果”) } }
复制代码
在这里,你:
- 创建一个方法作为 的入口点
ImageProcessor
。它需要一个实例Painting
和一个effect
来应用。 - 检查有效图像。
- 如果效果类型为
.builtIn
,则调用applyBuiltInEffect(input:)
以应用过滤器。
打开PaintWall.swift。下面selectedPainting = paintings[index]
在 的action
闭包中Button
添加:
var effect = ProcessEffect .builtIn
if let Painting = selectedPainting {
switch index {
case 0 :
效果= .builtIn
默认:
效果= .builtIn
}
ImageProcessor .shared.process(绘画:绘画,效果:效果)
}
复制代码
在这里,您将effect
要.builtIn
为第一张画。您还将其设置为默认效果。然后,通过调用应用过滤器process(painting:, effect:)
上ImageProcessor
。
构建并运行。点击*“蒙娜丽莎”*。您将看到在输出中应用了一个内置过滤器!
让蒙娜丽莎阳光普照的伟大工作。难怪她在笑!现在是使用CIKernel创建过滤器的时候了。
认识 CIKernel
使用CIKernel,您可以放置称为kernel 的自定义代码,以逐个像素地操作图像。GPU 处理这些像素。您使用Metal Shading Language编写内核,与较旧的Core Image Kernel Language相比,它具有以下优势,自 iOS 12 起已弃用:
- 支持 Core Image 内核的所有强大功能,如连接和平铺。
- 在构建时预编译,带有错误诊断。这样,您无需等待运行时出现错误。
- 提供语法高亮和语法检查。
有不同类型的内核:
- CIColorKernel:改变像素的颜色但不知道像素的位置。
- CIWarpKernel:改变像素的位置但不知道像素的颜色。
- CIBlendKernel:以优化的方式混合两个图像。
要创建和应用内核,您需要:
- 首先,向项目添加自定义构建规则。
- 然后,添加 Metal 源文件。
- 加载内核。
- 最后,初始化并应用内核。
接下来,您将实施这些步骤中的每一个。准备好享受有趣的旅程吧!
创建构建规则
您需要编译 Core Image Metal 代码并将其与特殊标志链接。
在项目导航器中选择RayVinci目标。然后,选择构建规则选项卡。单击*+*添加新的构建规则。
然后,设置第一个新的构建规则:
- 将进程设置为名称匹配的源文件: . 然后将**.ci.metal*设置为值。
- 取消选中Run once per architecture。
- 添加以下脚本:
xcrun metal -c -I $MTL_HEADER_SEARCH_PATHS -fcikernel "${INPUT_FILE_PATH}" \ -o "${SCRIPT_OUTPUT_FILE_0}"
复制代码
这会使用所需的*-fcikernel*标志调用 Metal 编译器。
复制代码
- 在输出文件中添加以下内容:
$(DERIVED_FILE_DIR)/${INPUT_FILE_BASE}.air
复制代码
这会产生一个以*.ci.air*结尾的输出二进制文件。
复制代码
接下来,再次单击*+*添加另一个新的构建规则。
对于第二个新构建规则,请按照以下步骤操作:
- 将进程设置为名称匹配的源文件: . 然后将**.ci.air*设置为值。
- 取消选中Run once per architecture。
- 添加以下脚本:
xcrun metallib -cikernel "${INPUT_FILE_PATH}" -o "${SCRIPT_OUTPUT_FILE_0}"
复制代码
这将使用所需的*-cikernel*标志调用 Metal 链接器。
复制代码
- 在输出文件中添加以下内容:
$(METAL_LIBRARY_OUTPUT_DIR)/$(INPUT_FILE_BASE).metallib
复制代码
这会在应用程序包中生成一个以*.ci.metallib*结尾的文件。
复制代码
接下来,是时候添加 Metal 源了。
添加金属源
首先,您将为颜色内核创建一个源文件。在Project Navigator中,突出RayVinci权下RayVinci项目。
右键单击并选择新建组。将此新组命名为Filters。然后,突出显示该组并添加一个名为ColorFilterKernel.ci.metal的新 Metal 文件。
打开文件并添加:
// 1
#包含 <CoreImage/CoreImage.h>
// 2
extern "C" { 命名空间核心图像{
// 3 float4 colorFilterKernel(sample_t s){
// 4 float4 交换颜色; 交换颜色.r = sg; 交换颜色.g = sb; 交换颜色.b = sr; swappedColor.a = sa;
返回交换颜色; } } }
复制代码
这是代码分解:
- 包含 Core Image 标头可让您访问框架提供的类。这会自动包含 Core Image Metal 内核库CIKernelMetalLib.h。
- 内核需要位于
extern "C"
外壳内,以便在运行时可以通过名称访问它。接下来,您指定 的命名空间coreimage
。您在coreimage
命名空间中声明所有扩展以避免与 Metal 发生冲突。 - 在这里,您声明
colorFilterKernel
,它接受类型为的输入sample_t
。sample_t
表示来自输入图像的单个颜色样本。colorFilterKernel
返回float4
表示像素的 RGBA 值的 。 - 然后,您声明一个新的
float4
、swappedColor
并交换输入样本中的 RGBA 值。然后返回具有交换值的样本。
接下来,您将编写代码来加载和应用内核。
加载内核代码
要加载和应用内核,首先要创建CIFilter
.
在过滤器组中创建一个新的 Swift 文件。将其命名为ColorFilter.swift并添加:
// 1
导入CoreImage
class ColorFilter : CIFilter { // 2
var inputImage: CIImage ?
// 3
static var kernel: CIKernel = { () -> CIColorKernel in
guard let url = Bundle .main.url( forResource: "ColorFilterKernel.ci" , withExtension: "metallib" ), let data = try? 数据(contentsOf:url)else { fatalError(“无法加载metallib”) }
守卫 让内核= 尝试? CIColorKernel ( 函数名称: "colorFilterKernel" , fromMetalLibraryData: data) else { fatalError ( "无法创建颜色内核" ) }
返回内核 }()
// 4
覆盖 var outputImage: CIImage ? { guard let inputImage = inputImage else { return nil } return ColorFilter .kernel.apply( 范围:inputImage.extent, roiCallback:{ _,RECT的
回报矩形 }, 参数:[输入图像]) } }
复制代码
在这里,你:
-
首先导入 Core Image 框架。
-
子类化
CIFilter
包括两个主要步骤:- 指定输入参数。在这里,您使用
inputImage
. - 覆盖
outputImage
.
- 指定输入参数。在这里,您使用
-
然后,您声明一个静态属性
kernel
,用于加载ColorFilterKernel.ci.metallib的内容。这样,库只加载一次。然后CIColorKernel
使用ColorFilterKernel.ci.metallib的内容创建一个实例。 -
接下来,你
override outputImage
。在这里,您使用apply(extent:roiCallback:arguments:)
. 该extent
决定多少输入图像的被传递给内核。您传递了整个图像,因此过滤器将应用于整个图像。
roiCallback
确定rect
渲染rect
in所需的输入图像的outputImage
。在这里,rect
ofinputImage
和outputImage
不会改变,因此您返回相同的值并将inputImage
参数数组中的 传递给内核。
现在您已经创建了颜色内核过滤器,您将把它应用到图像上。
应用颜色内核过滤器
打开ImageProcessor.swift。将以下方法添加到ImageProcessor
:
private func applyColorKernel ( input : CIImage ) {
let filter = ColorFilter ()
filter.inputImage = input
if let outputImage = filter.outputImage,
let renderImage = renderAsUIImage(outputImage) {
输出=渲染图像
}
}
复制代码
在这里,您声明applyColorKernel(input:)
. 这将 aCIImage
作为输入。您可以通过创建 的实例来创建自定义过滤器ColorFilter
。
过滤器outputImage
应用了颜色内核。然后创建一个UIImage
using实例renderAsUIImage(_:)
并将其设置为输出。
接着,手柄.colorKernel
在process(painting:effect:)
如下所示。在上面添加这个新案例default
:
案例.colorKernel:
applyColorKernel(输入:输入)
复制代码
在这里,您调用applyColorKernel(input:)
以应用自定义颜色内核过滤器。
最后,打开PaintingWall.swift。在's 闭包的switch
正下方的语句中添加以下内容:case 0``Button``action
案例 1:
效果= .colorKernel
复制代码
这将.colorKernel
第二幅画的效果设置为 。
构建并运行。现在点击第二幅画*“最后的晚餐”*。您将看到应用的颜色内核过滤器和图像中交换的 RGBA 值。
很好!接下来,您将对达芬奇的神秘“救世主”创建酷炫的扭曲效果。
创建扭曲内核
与颜色内核类似,您将从添加 Metal 源文件开始。在过滤器组中创建一个名为WarpFilterKernel.ci.metal的新 Metal 文件。打开文件并添加:
# include <CoreImage/CoreImage.h>
//1
extern "C" {
命名空间核心图像{
//2
float2 warpFilter(destination dest) {
浮动y = dest.coord().y + tan(dest.coord().y / 10 ) * 20 ;
浮动x = dest.coord().x + tan(dest.coord().x/ 10 ) * 20 ;
返回float2(x,y);
}
}
}
复制代码
这是您添加的内容:
-
就像在颜色内核 Metal 源中一样,您包含 Core Image 标头并将该方法包含在一个
extern "C"
外壳中。然后指定coreimage
命名空间。 -
接下来,您
warpFilter(_:)
使用 type 的输入参数进行声明destination
,允许访问您当前正在计算的像素的位置。它返回输入图像坐标中的位置,然后您可以将其用作源。您可以使用 访问目标像素的 x 和 y 坐标
coord()
。然后,您应用简单的数学运算来转换坐标并将它们作为源像素坐标返回,以创建有趣的平铺效果。注意:尝试用in替换
tan
,你会得到一个有趣的失真效果!:]sin``warpFilter(_:)
加载扭曲内核
与您为颜色内核创建的过滤器类似,您将创建一个自定义过滤器来加载和初始化扭曲内核。
在过滤器组中创建一个新的 Swift 文件。将其命名为WarpFilter.swift并添加:
导入CoreImage
// 1
类 WarpFilter : CIFilter {
var inputImage: CIImage ?
// 2
static var kernel: CIWarpKernel = { () -> CIWarpKernel in
guard let url = Bundle .main.url(
forResource: "WarpFilterKernel.ci" ,
withExtension: "metallib" ),
let data = try? 数据(contentsOf:url)else {
fatalError(“无法加载metallib”)
}
守卫 让内核= 尝试? CIWarp内核(
函数名称: "warpFilter" ,
fromMetalLibraryData: data) else {
fatalError ( “无法创建扭曲内核” )
}
返回内核
}()
// 3
覆盖 var outputImage: CIImage ? {
守卫 让inputImage = inputImage else { return .none }
返回 WarpFilter .kernel.apply(
范围:inputImage.extent,
roiCallback:{ _,RECT的
回报矩形
},
图像:输入图像,
参数:[])
}
}
复制代码
在这里,你:
- 创建
WarpFilter
为CIFilter
withinputImage
作为输入参数的子类。 - 接下来,您声明静态属性
kernel
以加载WarpFilterKernel.ci.metallib的内容。然后创建一个CIWarpKernel
使用 的内容的实例.metallib
。 - 最后,您通过覆盖
outputImage
. 在 中override
,您将内核应用于inputImage
usingapply(extent:roiCallback:arguments:)
并返回结果。
应用扭曲内核过滤器
打开ImageProcessor.swift。将以下内容添加到ImageProcessor
:
private func applyWarpKernel ( input : CIImage ) {
let filter = WarpFilter ()
filter.inputImage = input
if let outputImage = filter.outputImage,
let renderImage = renderAsUIImage(outputImage) {
输出=渲染图像
}
}
复制代码
在这里,您声明applyColorKernel(input:)
,它将CIImage
作为输入。然后创建一个WarpFilter
和 set的实例inputImage
。
过滤器outputImage
应用了扭曲内核。然后创建一个UIImage
using实例renderAsUIImage(_:)
并将其保存到输出。
接下来,将以下案例添加到process(painting:effect:)
,下面case .colorKernel
:
案例.warpKernel:
applyWarpKernel(输入:输入)
复制代码
在这里,您处理.warpKernel
并调用applyWarpKernel(input:)
应用扭曲内核过滤器的情况。
最后,打开PaintingWall.swift。添加下面的情况下,在switch
右下面的语句case 1
中action
:
案例 2:
效果= .warpKernel
复制代码
这.warpKernel
为第三幅画设置了效果。
构建并运行。点击 Salvator Mundi 的画作。你会看到一个有趣的基于扭曲的瓷砖效果应用。
恭喜!您将自己的风格应用于杰作!;]
挑战:实现混合内核
的CIBlendKernel
是用于混合两个图像进行了优化。作为一个有趣的挑战,为CIBlendKernel
. 一些提示:
- 创建一个
CIFilter
接受两个图像的子类:输入图像和背景图像。 - 使用内置的可用
CIBlendKernel
内核。对于此挑战,请使用内置的乘法混合内核。 - 创建一个方法
ImageProcessor
,将混合内核过滤器应用于图像并将结果设置为输出。您可以使用项目资产中提供的multi_color图片作为滤镜的背景图片。此外,处理.blendKernel
. - 将此过滤器应用于PaintingWall.swift 中的第四张图片。
您将在下载的材料中找到在最终项目中实施的解决方案。祝你好运!
调试核心图像问题
了解 Core Image 如何渲染图像可以帮助您在图像未按预期显示时进行调试。最简单的方法是在调试时使用Core Image Quick Look。
使用核心图像快速查看
打开ImageProcessor.swift。将断点你在哪里设置线条output
在applyColorKernel(input:)
。构建并运行。点击*“最后的晚餐”*。
当您遇到断点时,将鼠标悬停在 上outputImage
。您会看到一个显示地址的小弹出框。
单击眼睛符号。将出现一个窗口,显示制作图像的图形。很酷吧?
使用 CI_PRINT_TREE
CI_PRINT_TREE是基于与 Core Image Quick Look 相同的基础架构的调试功能。它有多种模式和操作。
选择和编辑的RayVinci方案。选择Run选项卡并添加CI_PRINT_TREE作为值为7 pdf的新环境变量。
CI_PRINT_TREE的值采用以下形式graph_type output_type options
。
graph_type
表示核心图像渲染的阶段。以下是您可以指定的值:
- 1:显示颜色空间的初始图形。
- 2 : 一个优化的图表,显示了 Core Image 是如何优化的。
- 4:显示您需要多少内存的连接图。
- 7:详细日志记录。这将打印所有上述图表。
对于output_type
,您可以指定PDF或PNG。它将文档保存到一个临时目录。
构建并运行。在模拟器中选择*“最后的晚餐”。现在,通过使用终端导航到/tmp*来打开 Mac 上的临时目录。
您将看到所有图形为 PDF 文件。打开以*_initial_graph.pdf*作为后缀的文件之一。
输入在底部,输出在顶部。红色节点代表彩色内核,而绿色节点代表扭曲内核。您还将看到每个步骤的投资回报率和范围。