iOS 开发每天分享优质文章自鉴

Swift函数式编程二(封装Core Image)

2019-02-26  本文已影响0人  酒茶白开水

代码地址

前言

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也有一定的优势:

上一篇下一篇

猜你喜欢

热点阅读