阅读 334

[SwiftUI 100 天] Instafilter 用 Core Image 实现基础的图片滤镜

译自 www.hackingwithswift.com/books/ios-s…
更多内容,欢迎关注公众号 「Swift花园」
喜欢文章?不如来个 🔺💛➕三连?关注专栏,关注我 🚀🚀🚀

用 Core Image 实现基础的图片滤镜

我们的项目现在有了一张用户选择的图片,下一步是让用户把各种 Core Image 滤镜应用到这种图片。 我们先从一个简单的滤镜开始,然后扩展到 action sheet 的用法。

要在应用中使用 Core Image,首先需要添加下面两条 import 到 ContentView.swift:

import CoreImage
import CoreImage.CIFilterBuiltins复制代码

接下来我们需要用到上下文和滤镜。一个 Core Image 上下文是一个负责把 CIImage 渲染成 CGImage 的对象,或者用更实际的术语来说的话,它是一个负责把图像转换成像素的对象。上下文的开销很昂贵,所以我们最好是只创建一次上下文,然后保持这个上下文。至于滤镜,我们用使用 CISepiaTone 作为默认滤镜,不过因为我们待会要灵活地切换滤镜,所以我们可以用 @State 来表示。

把下面两个属性添加到 ContentView

@State private var currentFilter = CIFilter.sepiaTone()
let context = CIContext()复制代码

有了上下文和滤镜后,我们现在需要写一个方法处理导入的图片 —— 也就是说,基于 filterIntensity 设置墨色滤镜的强度,然后从滤镜中读取出输出的图像,用 CIContextto 进行渲染,然后放回 image 属性,以便显示在屏幕上。

func applyProcessing() {
    currentFilter.intensity = Float(filterIntensity)

    guard let outputImage = currentFilter.outputImage else { return }

    if let cgimg = context.createCGImage(outputImage, from: outputImage.extent) {
        let uiImage = UIImage(cgImage: cgimg)
        image = Image(uiImage: uiImage)
    }
}复制代码

下一步是修改 loadImage() 的工作方式。目前它的功能是给 image 属性赋值,但我们现在不需要这个逻辑了。它要做的事把选择的图片发给滤镜,然后调用 applyProcessing() 执行滤镜处理。

Core Image 滤镜有一个专门的 inputImage 属性,可以让我们发送一个滤镜需要的 CIImage,但通常这个过程极度不稳定,会导致应用崩溃 —— 更稳健的做法是用滤镜的 setValue() 方法,通 kCIInputImageKey 键来设置。

把当前的 loadImage() 方法替换成下面这样:

func loadImage() {
    guard let inputImage = inputImage else { return }

    let beginImage = CIImage(image: inputImage)
    currentFilter.setValue(beginImage, forKey: kCIInputImageKey)
    applyProcessing()
}复制代码

运行代码,你会看到应用的基本流程已经能很好地工作了。我们可以选择图片,然后看着它被应用上墨色滤镜。但我们添加的强度滑块却不起作用,尽管它已经被绑定到滤镜从中读取数值的 filterIntensity

原因应该不难想到:尽管滑块能改变 filterIntensity 的值,但改变这个属性本身并不会自动触发 applyProcessing() 方法被再次调用。因此,我们需要手动执行这个动作,但这个要求不像给 filterIntensity 添加一个@State 属性观察者那么简单。

相反,我们需要用到一个自定义绑定,它不仅能读取 filterIntensity,还能在更新 filterIntensity 同时调用 applyProcessing()

依赖这个属性的自定义绑定要在视图的 body 属性内创建,因为 Swift 不允许一个属性引用另外一个属性。 把下面这段代码添加到 body 属性的最前面:

let intensity = Binding<Double>(
    get: {
        self.filterIntensity
    },
    set: {
        self.filterIntensity = $0
        self.applyProcessing()
    }
)复制代码

重要: 由于现在 body 属性里有了一些逻辑,你必须在 NavigationView 之前写 return 关键字,像这样: return NavigationView {.

有了自定义绑定,我们需要把它附着到滑竿。修改滑块的代码:

Slider(value: intensity)复制代码

记住,因为 intensity 已经是一个绑定了,所以我们不需要再在前面写一个 $ 符号,我们得写成 intensity 而不是 $intensity

再次运行应用,需要提醒的是:尽管 Core Image 在所有的 iPhone 设备上都非常快,它在模拟器上是很慢的。也就是说,你也可以验证功能的正确性,但是别指望代码在模拟器上跑得飞快。

译自 www.hackingwithswift.com/books/ios-s…

用 ActionSheet 自定义滤镜

目前为止我们涉及了 SwiftUI,`UIImagePickerController 和 Core Image,但应用还不堪用 —— 毕竟,只有一个墨色滤镜,不算有趣。

为了让应用整体可用,我们要让用户可以选择他们想用的滤镜。我们将通过一个 action sheet 来实现这个功能。在 iPhone 上,action sheet 是一个从屏幕底部滑出来的按钮列表,你可以添加任意多的按钮,需要的时候甚至可以滚动。

首先还是添加一个控制 action sheet 是否显示的状态属性:

@State private var showingFilterSheet = false复制代码

然后用 actionSheet() modifier 添加 action sheet,它的工作方式同 sheet()alert 一模一样:我们提供一个要监控的条件,当条件满足时,action sheet 就显示。

sheet() 下面添加这个 modifier:

.actionSheet(isPresented: $showingFilterSheet) {
    // 这里放置 action sheet
}复制代码

然后把 // 切换滤镜 的按钮动作替换成下面这行代码:

self.showingFilterSheet = true复制代码

关于 action sheet 要展示的内容,我们可以提供一个标题,一条消息和一组按钮。这些按钮的工作方式和 Alert 一样:我们提供按钮的文本标签和按钮被选择时要执行的动作。

对于这个应用里的 action sheet,我们打算让用户从不同的 Core Image 滤镜中做出选择。每个滤镜被选中时都会被立刻应用。为了实现这个功能,我们需要写一个方法,在每个新滤镜被选择时修改 currentFilter,然后调用 loadImage()

计划里有一个障碍要扫清,它是由 Apple 封装 Core Image APIs 的方式导致的,我们要让这些 API 变得更易用。你看,底层的 Core Image API 完全是基于字符串的。

当我们把 CIFilter.sepiaTone() 赋给一个属性时,我们得到的是一个遵循 CISepiaTone 协议的 CIFilter 对象。这个协议暴露了 intensity 参数给我们使用,但其内部实际上是调用 setValue(_:forKey:)

潜在的这种灵活性可以被我们利用来编写适用所有滤镜的代码,只要我们小心处理,不传入非法的参数值。

currentFilter 属性修改成这样:

@State private var currentFilter: CIFilter = CIFilter.sepiaTone()复制代码

CIFilter.sepiaTone() 仍然返回的是一个遵循 CISepiaTone 协议的 CIFilter 对象,但显式的类型注解表明我们放弃了一部分关于类型的数据:我们只强调滤镜必须是 CIFilter 而不再要求其必须遵循 CISepiaTone 了。

这样做的结果是我们失去了访问 intensity 属性的能力,也就是说下面这行代码将不再能工作了:

currentFilter.intensity = Float(filterIntensity)复制代码

相应地,我们需要用 setValue(:_forKey:) 调用来替代。协议本来做的事情其实就是这个调用,只不过多了一层类型安全的价值。

把编译不过的代码替换成下面这行:

currentFilter.setValue(filterIntensity, forKey: kCIInputIntensityKey)复制代码

kCIInputIntensityKey 是另一个 Core Image 常量,用这个键跟直接设置墨色滤镜的 intensity 参数的效果是一样的。

做出这个改变后,我们回到 action sheet:我们想要的是能够修改滤镜,然后调用 loadImage()。因此,可以添加下面这个方法:

func setFilter(_ filter: CIFilter) {
    currentFilter = filter
    loadImage()
}复制代码

然后把 // 这里放置 action sheet 注释替换成一系列可供选择的 Core Image 滤镜。

代码如下:

ActionSheet(title: Text("Select a filter"), buttons: [
    .default(Text("Crystallize")) { self.setFilter(CIFilter.crystallize()) },
    .default(Text("Edges")) { self.setFilter(CIFilter.edges()) },
    .default(Text("Gaussian Blur")) { self.setFilter(CIFilter.gaussianBlur()) },
    .default(Text("Pixellate")) { self.setFilter(CIFilter.pixellate()) },
    .default(Text("Sepia Tone")) { self.setFilter(CIFilter.sepiaTone()) },
    .default(Text("Unsharp Mask")) { self.setFilter(CIFilter.unsharpMask()) },
    .default(Text("Vignette")) { self.setFilter(CIFilter.vignette()) },
    .cancel()
])复制代码

我从大量的 Core Image 滤镜中挑选出上面那些,你可以用代码补全尝试其他的滤镜,然后看看效果。

运行应用,选择图片,然后把墨色滤镜换成暗角 —— 这是一种在照片边缘应用变黑效果的滤镜(如果运行在模拟器上,需要耐心等待一会)。

然后再尝试高斯模糊,它本该会虚化图片,但实际却导致应用崩溃了。当我们移除 CISepiaTone 这样具体的滤镜类型约束后,我们是通过 setValue(_:forKey:) 来发送参数值的,这样做不能确保安全。因为高斯模糊并不支持强度参数,所以应用崩溃了。

为了修正这个问题,同时让滑块承载更多的功能 —— 我们要添加一些代码,读取 setValue(_:forKey:) 可以支持的键,只有在当前滤镜支持的情况下才设置强度。采用这种策略,我们实际上可以查询尽可能多的键,并且对所有支持的参数进行设置。比如,墨色滤镜可以设置强度,但高斯模糊则可以设置半径(模糊的粒度),等等。

这种条件化的方式可以应用于所选的所有滤镜,唯一需要小心处理的地方是确保你在合理的范围内控制 filterIntensity —— 举个例子,1 像素的模糊基本上看不见,所以为了让效果更明显,我会给它乘上 200 倍。

把这行代码:

currentFilter.setValue(filterIntensity, forKey: kCIInputIntensityKey)复制代码

替换成:

let inputKeys = currentFilter.inputKeys
if inputKeys.contains(kCIInputIntensityKey) { 		  
    currentFilter.setValue(filterIntensity, forKey: kCIInputIntensityKey) 
}
if inputKeys.contains(kCIInputRadiusKey) { 
    currentFilter.setValue(filterIntensity * 200, forKey: kCIInputRadiusKey) 
}
if inputKeys.contains(kCIInputScaleKey) { 
    currentFilter.setValue(filterIntensity * 10, forKey: kCIInputScaleKey) 
}复制代码

这样修改之后我们就可以安全地运行应用了。尝试各种不同的滤镜,看看你会发现什么!


我的公众号 这里有Swift及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~




关注下面的标签,发现更多相似文章
评论