SwiftUI:集成 Core Image
就像Core Data是Apple处理数据的内置框架一样,Core Image 是Apple处理图像的框架。这不是绘画,或者至少在大多数情况下不是绘画,而是关于更改现有图像:应用锐化,模糊,渐晕,像素化等等。如果您曾经使用过Apple的 Photo Booth 应用程序中可用的所有各种照片效果,那么应该可以清楚地了解 Core Image 的优点!
但是,Core Image 不能很好地集成到 SwiftUI 中。实际上,我甚至不会说它很好地集成到了UIKit中——苹果做了一些工作来提供帮助,但是仍然需要很多思考。不过,请相信我:了解了所有工作原理后,结果将非常出色,并且您会发现它为将来的应用程序打开了全部功能。
首先,我们将添加一些代码以提供基本图像。我将以一种稍微有些奇怪的方式来构造它,但是一旦我们在Core Image中混合它就会有意义:我们将创建Image
视图作为可选的@State
属性,并强制其宽度与屏幕上,然后添加onAppear()
修饰符以实际加载图像。
将示例图像添加到资产目录,然后将ContentView
结构体修改为如下:
struct ContentView: View {
@State private var image: Image?
var body: some View {
VStack {
image?
.resizable()
.scaledToFit()
}
.onAppear(perform: loadImage)
}
func loadImage() {
image = Image("Example")
}
}
首先,请注意 SwiftUI 处理可选视图的过程非常顺利——可以正常工作!但是,请注意我是如何将onAppear()
修饰符附加到图像周围的VStack
的,因为如果可选图像为nil,则它将不会触发onAppear()
函数。
无论如何,当该代码运行时,它应显示您添加的示例图像,并按比例缩放以适合屏幕。
现在,对于复杂的部分:Image
实际上是什么?如您所知,这是一个视图,这意味着我们可以在SwiftUI视图层次结构中对其进行定位和调整大小。它还可以处理从我们的资产目录和SF Symbols中加载图像,并且还可以从其他许多源中加载图像。但是,最终它会被显示出来——我们无法将其内容写入磁盘,也无法通过应用一些简单的SwiftUI过滤器对其进行转换。
如果我们要使用Core Image,SwiftUI的Image
视图是一个很好的终点,但是在其他地方使用则没有用。也就是说,如果我们要动态创建图像,应用Core Image滤镜,将其保存到用户的照片库中,依此类推,那么SwiftUI的图像就无法胜任。
Apple提供了其他三种图像类型供您使用,如果要使用Core Image,我们需要同时使用这三种图像。它们听起来可能很相似,但是它们之间有一些细微的区别,因此,如果要从Core Image中获取有意义的信息,请务必正确使用它们。
除了SwiftUI的Image
视图外,其他三种图片类型是:
-
UIImage
,它来自UIKit。这是一种非常强大的图像类型,能够处理各种图像类型,包括位图(如PNG),矢量(如SVG),甚至是形成动画的序列。UIImage
是UIKit的标准图像类型,在三种类型中,它最接近SwiftUI的图像类型。 -
CGImage
,来自Core Graphics。这是一种更简单的图像类型,实际上只是一个二维像素阵列。 -
CIImage
,来自Core Image。它将存储生成图像所需的所有信息,但实际上不会将其转换为像素(除非要求)。苹果将CIImage
称为“图像配方”,而不是实际图像。
各种图像类型之间存在一些互操作性:
- 我们可以从
CGImage
创建UIImage
,从UIImage
创建CGImage
。 - 我们可以从
UIImage
和CGImage
创建CIImage
,也可以从CIImage
创建CGImage
。 - 我们可以从
UIImage
和CGImage
创建一个SwiftUI的Image
。
我知道,我知道:这很令人困惑,但是希望一旦您看到代码,就会感觉更好。重要的是这些图像类型是纯数据——我们无法将它们放入SwiftUI视图层次结构中,但是我们可以自由地对其进行操作然后将结果呈现在SwiftUI图像中。
我们将更改loadImage()
,以便它从示例图像中创建一个UIImage
,然后使用 Core Image 对其进行操作。更具体地说,我们将从两项任务开始:
- 我们需要将示例图像加载到
UIImage
中,该图像具有一个名为UIImage(name:)
的初始化程序,以从资产目录中加载图像。它返回一个可选的UIImage
,因为我们可能指定了不存在的图像。 - 我们将其转换为
CIImage
,这是Core Image想要使用的。
因此,首先用以下代码替换当前的loadImage()
实现:
func loadImage() {
guard let inputImage = UIImage(named: "Example") else { return }
let beginImage = CIImage(image: inputImage)
// more code to come
}
下一步将创建一个 Core Image 上下文和一个 Core Image 过滤器。过滤器是完成以某种方式转换图像数据的实际工作的东西,例如模糊,锐化,调整颜色等,上下文则将处理后的数据转换为我们可以使用的CGImage
。
这两种数据类型均来自 Core Image,因此您需要分别导入他们才能将其提供给我们。因此,请先在 ContentView.swift 顶部附近添加以下内容:
import CoreImage
import CoreImage.CIFilterBuiltins
接下来,我们将创建上下文并进行过滤。在此示例中,我们将使用棕褐色调滤镜,该滤镜会应用棕色调,使照片看起来像是很久以前拍摄的。
因此,替换 // more code to come
:
let context = CIContext()
let currentFilter = CIFilter.sepiaTone()
现在,我们可以自定义过滤器以更改其工作方式。棕褐色是一个简单的滤镜,因此它只有两个有趣的属性:inputImage
是我们要更改的图像,而intensity
是应该应用棕褐色效果的强度,指定范围为0(原始图像)和1(完整棕褐色)。
因此,在前两行下面添加以下两行代码:
currentFilter.inputImage = beginImage
currentFilter.intensity = 1
所有这些都不是很难的,但是这就是改变的地方:我们需要将过滤器的输出转换为可以在视图中显示的SwiftUI Image
。这是我们需要使用所有四种图像类型的地方,因为最简单的操作是:
- 从我们的过滤器读取输出图像,它将是
CIImage
。这可能会失败,因此它返回一个可选值。 - 询问我们的上下文,从该输出图像创建
CGImage
。这也可能会失败,因此再次返回一个可选值。 - 将该
CGImage
转换为UIImage
。 - 将该
UIImage
转换为SwiftUIImage
。
您可以直接从CGImage
转到SwiftUI Image
,但是它需要额外的参数,并且只会增加更多的复杂性!
这是loadImage()
的最终代码:
// get a CIImage from our filter or exit if that fails
guard let outputImage = currentFilter.outputImage else { return }
// attempt to get a CGImage from our CIImage
if let cgimg = context.createCGImage(outputImage, from: outputImage.extent) {
// convert that to a UIImage
let uiImage = UIImage(cgImage: cgimg)
// and convert that to a SwiftUI image
image = Image(uiImage: uiImage)
}
如果再次运行该应用程序,您应该会看到示例图像现在已应用了棕褐色效果,这全归功于Core Image。
现在,您可能会认为,要获得一个相当简单的结果,这是一件繁重的工作,但是现在您已经掌握了Core Image的所有基础知识,因此切换到不同的过滤器相对容易。
话虽这么说,Core Image 有点……好吧……我们说“创意”。它最早是在iOS 5.0中引入的,到那时,Swift已经在Apple内部开发,但您真的不知道——在很长一段时间内,它的API都是您所能想到的最少Swifty的东西,尽管Apple慢慢地破破烂烂,您仍然会发现一些奇怪的现象。
为了证明这一点,我们可以用如下的像素化滤镜替换棕褐色调:
let currentFilter = CIFilter.pixellate()
currentFilter.inputImage = beginImage
currentFilter.scale = 100
提示:模拟器如果没有效果的话,请使用真机测试。
运行该程序后,您会看到我们的图像看起来像是像素化的。比例尺为100应该表示像素跨100个点,但是由于我的图像太大,因此像素相对较小。
现在让我们尝试这样的水晶效果:
let currentFilter = CIFilter.crystallize()
currentFilter.inputImage = beginImage
currentFilter.radius = 200
当运行时,我们应该看到整洁的水晶效果,但是实际上发生的是我们的代码崩溃了。我们的代码是有效的Swift和有效的Core Image代码,但仍然没有正常工作。
您在这里看到的是一个错误,也许在您观看此视频时它已经修复。这是由于Apple在修补Core Image怪异性方面没有做得特别出色,如果我们改用较旧的API,它的效果很好:
let currentFilter = CIFilter.crystallize()
currentFilter.setValue(beginImage, forKey: kCIInputImageKey)
currentFilter.radius = 200
kCIInputImageKey
是一个特殊的常量,用于指定过滤器的输入图像,如果您对其进行深入研究,您会发现它实际上是一个字符串——Core Image曾经而且仍然在幕后,是一个完全字符串输入的API。
当您意识到只有部分Apple的Core Image过滤器使用了新的Swifty API时,这一点变得更加明显。例如,如果要应用旋转扭曲,则需要使用旧的API,这很痛苦:
- 我们使用过滤器的确切名称创建一个
CIFilter
实例。 - 我们需要通过每次使用不同的键重复调用
setValue()
来设置其值。 - 由于
CIFilter
不是特定的过滤器,因此Swift将允许我们发送该过滤器不支持的值。
例如,这是我们如何使用旋转扭曲的方法:
guard let currentFilter = CIFilter(name: "CITwirlDistortion") else { return }
currentFilter.setValue(beginImage, forKey: kCIInputImageKey)
currentFilter.setValue(2000, forKey: kCIInputRadiusKey)
currentFilter.setValue(CIVector(x: inputImage.size.width / 2, y: inputImage.size.height / 2), forKey: kCIInputCenterKey)
提示:CIVector
是Core Image的存储点和方向的方法。
如果您运行该代码,您会看到最终结果看起来仍然不错,并且希望Apple在未来的几个月和几年中会继续清理此API。
尽管较新的API更好用,但我们在该项目中大多会使用较旧的API,因为它可以让我们使用任何类型的过滤器。
文中使用的四种滤镜效果图如下:
文中四种效果图