Swift函数式编程二(封装Core Image)
代码地址
前言
Core Image是一个强大的图像处理框架,但是API略显笨拙。它的API是弱类型的,通过键值编码(KVC)来配置图像滤镜(Filter)的,在使用参数的类型或名字时,都使用字符串来进行表示,这十分容易出错,极有可能导致运行时错误。因此打算利用类型来规避这些问题,最终实现一组类型安全且高度模块化的API。
滤镜类型
CIFilter是Core Image的核心类之一,用于创建图片滤镜。几乎总是通过kCIInputImageKey键提供输入图像来实例化CIFilter,再通过kCIOutputImageKey键取回处理后的图像。取回的结果可以作为下一个滤镜的输入值。尝试分装应用这些键值对的细节,从而提供一个强类型的API。于是将Filter定义为一个函数,该函数接受一个图像作为参数并返回一个新图像。将在这个类型的基础上进行后续的构建:
/// 图片滤镜
typealias Filter = (CIImage) -> CIImage
构建滤镜
已经定义了Filter类型,于是就可以定义函数来构建特定的滤镜了。这些函数接受特定滤镜所需的参数后,构造并返回一个Filter类型的值。
1、高斯模糊
高斯模糊很简单只需要半径这一个参数。这个函数返回一个新函数,新函数接受CIImage类型的参数并返回一个新的CIImage对象。返回值符合之前定义的Filter类型(CIImage -> CIImage)。
/// 高斯模糊滤镜
///
/// - Parameter radius: 半径
/// - Returns: 高斯模糊滤镜
func gaussianBlur(radius: Double) -> Filter {
return { image in
let parameters: [String: Any] = [kCIInputRadiusKey: radius, kCIInputImageKey: image]
guard let filter = CIFilter(name: "CIGaussianBlur", parameters: parameters) else { fatalError() }
guard let outputImage = filter.outputImage else { fatalError() }
return outputImage
}
}
2、颜色叠加
创建一个可以在图像上覆盖纯色叠层的滤镜,CoreImage不包括这样的滤镜,但是可以用已经存在的滤镜来组成。
将使用两个基础滤镜:颜色生成(CIConstantColorGenerator)与覆盖合成(CISourceOverCompositing)。
下面这段代码与高斯模糊相似,有一个区别就是颜色生成不检测输入图像。因此不需要给函数中的图像参数命名,用_来强调参数被忽略。
/// 颜色生成滤镜
///
/// - Parameter color: 颜色
/// - Returns: 颜色生成滤镜
func colorGenerator(color: CIColor) -> Filter {
return { _ in
let parameters: [String: Any] = [kCIInputColorKey: color]
guard let filter = CIFilter(name: "CIConstantColorGenerator", parameters: parameters) else { fatalError() }
guard let outputImage = filter.outputImage else { fatalError() }
return outputImage
}
}
下面定义合成滤镜,这里将图像裁剪为输入图像一致的尺寸,这并不是必须的只是为了展示效果。
/// 合成滤镜
///
/// - Parameter overLay: 前景层
/// - Returns: 合成滤镜
func compositeSourceOver(overLay: CIImage) -> Filter {
return { image in
let parameters: [String: Any] = [kCIInputBackgroundImageKey: image, kCIInputImageKey: overLay]
guard let filter = CIFilter(name: "CISourceOverCompositing", parameters: parameters) else { fatalError() }
guard let outputImage = filter.outputImage else { fatalError() }
return outputImage.cropped(to: image.extent)
}
}
最后创建颜色叠层滤镜,首先调用colorGenerator函数返回一个滤镜Filter,再执行滤镜得到一个CIIMage新叠层。与此类似,返回值由compositeSourceOver(overlay)构成的滤镜和被作为参数的CIImage颜色叠层。
/// 颜色叠层滤镜
///
/// - Parameter color: 颜色
/// - Returns: 颜色叠层滤镜
func colorOverlay(color: CIColor) -> Filter {
return { image in
let overLay = colorGenerator(color: color)(image)
return compositeSourceOver(overLay: overLay)(image)
}
}
组合滤镜
到目前为止已经定义了高斯模糊与颜色叠层滤镜,可以先模糊再叠一层颜色。
/// 组合滤镜
func combine() -> Filter {
return { image in
let radius = 5.0
let color = UIColor.red.ciColor
/// 将滤镜应用于图像
let blurredImage = gaussianBlur(radius: radius)(image)
let overlaidImage = colorOverlay(color: color)(blurredImage)
return overlaidImage;
}
}
复合函数
可以将上面两个滤镜调用表达式合并为一体:
colorOverlay(color: color)(gaussianBlur(radius: radius)(image))
但是由于括号错综复杂,这些代码失去了可读性。更好的解决方案是定义运算符来组合滤镜,首先定义一个组合滤镜的函数:
func composeFilters(filter1: @escaping Filter, filter2: @escaping Filter) -> Filter {
return { image in
return filter2(filter1(image))
}
}
composeFilters函数接受两个Filter参数,并返回一个新的Filter滤镜。这个新滤镜接受一个CIImage参数并传递给filter1,再将取得的返回值CIImage传递给filter2。如此便可以定义复合函数:
composeFilters(filter1: gaussianBlur(radius: radius), filter2: colorOverlay(color: color))
为了让代码更具可读性,可以引入运算符。虽然随意定义运算符并不一定能提升代码可读性,但是图像处理库中,滤镜的组合是一个反复被讨论的问题,所以引入运算符极有意义:
precedencegroup FilterPrecedence {
associativity: left//左结合
}
infix operator >>>: FilterPrecedence
func >>>(filter1: @escaping Filter, filter2: @escaping Filter) -> Filter {
return { image in filter2(filter1(image)) }
}
与composeFilters方法相同,现在可以使用运算符达到目的:
gaussianBlur(radius: radius) >>> colorOverlay(color: color)
运算符>>>是左结合的,滤镜将从左到右的顺序被应用到图像上。
组合滤镜运算符是一个符合函数的例子。数学上两个函数f、g构成复合函数被写作f·g,表示的新函数将输入参数x映射到f(g(x))上。除了顺序这恰恰也是>>>运算符所做的:将CIImage参数传递给>>>运算符操作的两个Filter滤镜函数。
理论背景:柯里化
定义接受两个参数的函数的两种方法:
func add1(x: Int, y: Int) -> Int {
return x + y
}
var result1 = add1(x: 1, y: 2)
func add2(x: Int) -> (Int) -> Int {
return { y in x + y }
}
var result2 = add2(x: 1)(2)
方法一将两个参数同时传递给add1;方法二先向函数add2传递一个参数,然后向其返回的函数传递第二个参数。这两个版本完全等价。
add1和add2展示了如何将接受多个参数的函数变为只接受一个参数的函数,这个过程被称为柯里化,将add2称为add1的柯里化版本。
函数柯里化,给了调用者更多的选择,可以用一个、两个……参数来调用。
把定义滤镜的函数进行柯里化,有利于使用<<<运算符进行组合。
讨论
CoreImage框架已经非常成熟,几乎能提供所有需要的功能,尽管如此。这么设计API也有一定的优势:
- 安全:避免为定义的键或强制类型转换而引发的运行时错误。
- 模块化:使用<<<很容易将滤镜进行组合,可以将复杂的滤镜拆分为更小的单元。
- 清晰:即使未使用过CoreImage也能使用这些API来装配滤镜,并不需要知道kCIOutputImageKey等这些键。