SwiftUI:集成 Core Image

SwiftUI:集成 Core Image_第1张图片
Instafilter - 原图

就像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
  • 我们可以从UIImageCGImage创建CIImage,也可以从CIImage创建CGImage
  • 我们可以从UIImageCGImage创建一个SwiftUI的Image

我知道,我知道:这很令人困惑,但是希望一旦您看到代码,就会感觉更好。重要的是这些图像类型是纯数据——我们无法将它们放入SwiftUI视图层次结构中,但是我们可以自由地对其进行操作然后将结果呈现在SwiftUI图像中。

我们将更改loadImage(),以便它从示例图像中创建一个UIImage,然后使用 Core Image 对其进行操作。更具体地说,我们将从两项任务开始:

  1. 我们需要将示例图像加载到UIImage中,该图像具有一个名为UIImage(name:)的初始化程序,以从资产目录中加载图像。它返回一个可选的UIImage,因为我们可能指定了不存在的图像。
  2. 我们将其转换为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转换为SwiftUI Image

您可以直接从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,这很痛苦:

  1. 我们使用过滤器的确切名称创建一个CIFilter实例。
  2. 我们需要通过每次使用不同的键重复调用setValue()来设置其值。
  3. 由于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,因为它可以让我们使用任何类型的过滤器。

文中使用的四种滤镜效果图如下:

文中四种效果图

译自 Integrating Core Image with SwiftUI

你可能感兴趣的:(SwiftUI:集成 Core Image)