[SwiftUI 100 天] Instafilter 用 Core Image 实现基础的图片滤镜
2020/5/27 23:25:52
本文主要是介绍[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
设置墨色滤镜的强度,然后从滤镜中读取出输出的图像,用 CIContext
to 进行渲染,然后放回 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及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~
这篇关于[SwiftUI 100 天] Instafilter 用 Core Image 实现基础的图片滤镜的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2022-10-05Swift语法学习--基于协议进行网络请求
- 2022-08-17Apple开发_Swift语言地标注释
- 2022-07-24Swift 初见
- 2022-05-22SwiftUI App 支持多语种 All In One
- 2022-05-10SwiftUI 组件参数简写 All In One
- 2022-04-14SwiftUI 学习笔记
- 2022-02-23Swift 文件夹和文件操作
- 2022-02-17Swift中使用KVO
- 2022-02-08Swift 汇编 String array
- 2022-01-30SwiftUI3.0页面反向传值