代碼地址
前言
Core Image是一個(gè)強(qiáng)大的圖像處理框架私爷,但是API略顯笨拙盗痒。它的API是弱類型的腔稀,通過(guò)鍵值編碼(KVC)來(lái)配置圖像濾鏡(Filter)的隧哮,在使用參數(shù)的類型或名字時(shí),都使用字符串來(lái)進(jìn)行表示,這十分容易出錯(cuò)稽犁,極有可能導(dǎo)致運(yùn)行時(shí)錯(cuò)誤焰望。因此打算利用類型來(lái)規(guī)避這些問(wèn)題,最終實(shí)現(xiàn)一組類型安全且高度模塊化的API缭付。
濾鏡類型
CIFilter是Core Image的核心類之一柿估,用于創(chuàng)建圖片濾鏡循未。幾乎總是通過(guò)kCIInputImageKey鍵提供輸入圖像來(lái)實(shí)例化CIFilter陷猫,再通過(guò)kCIOutputImageKey鍵取回處理后的圖像。取回的結(jié)果可以作為下一個(gè)濾鏡的輸入值的妖。嘗試分裝應(yīng)用這些鍵值對(duì)的細(xì)節(jié)绣檬,從而提供一個(gè)強(qiáng)類型的API。于是將Filter定義為一個(gè)函數(shù)嫂粟,該函數(shù)接受一個(gè)圖像作為參數(shù)并返回一個(gè)新圖像娇未。將在這個(gè)類型的基礎(chǔ)上進(jìn)行后續(xù)的構(gòu)建:
/// 圖片濾鏡
typealias Filter = (CIImage) -> CIImage
構(gòu)建濾鏡
已經(jīng)定義了Filter類型,于是就可以定義函數(shù)來(lái)構(gòu)建特定的濾鏡了星虹。這些函數(shù)接受特定濾鏡所需的參數(shù)后零抬,構(gòu)造并返回一個(gè)Filter類型的值。
1宽涌、高斯模糊
高斯模糊很簡(jiǎn)單只需要半徑這一個(gè)參數(shù)平夜。這個(gè)函數(shù)返回一個(gè)新函數(shù),新函數(shù)接受CIImage類型的參數(shù)并返回一個(gè)新的CIImage對(duì)象卸亮。返回值符合之前定義的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、顏色疊加
創(chuàng)建一個(gè)可以在圖像上覆蓋純色疊層的濾鏡兼贸,CoreImage不包括這樣的濾鏡段直,但是可以用已經(jīng)存在的濾鏡來(lái)組成。
將使用兩個(gè)基礎(chǔ)濾鏡:顏色生成(CIConstantColorGenerator)與覆蓋合成(CISourceOverCompositing)溶诞。
下面這段代碼與高斯模糊相似鸯檬,有一個(gè)區(qū)別就是顏色生成不檢測(cè)輸入圖像。因此不需要給函數(shù)中的圖像參數(shù)命名螺垢,用_來(lái)強(qiáng)調(diào)參數(shù)被忽略喧务。
/// 顏色生成濾鏡
///
/// - 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)
}
}
最后創(chuàng)建顏色疊層濾鏡,首先調(diào)用colorGenerator函數(shù)返回一個(gè)濾鏡Filter讯蒲,再執(zhí)行濾鏡得到一個(gè)CIIMage新疊層痊土。與此類似,返回值由compositeSourceOver(overlay)構(gòu)成的濾鏡和被作為參數(shù)的CIImage顏色疊層墨林。
/// 顏色疊層濾鏡
///
/// - Parameter color: 顏色
/// - Returns: 顏色疊層濾鏡
func colorOverlay(color: CIColor) -> Filter {
return { image in
let overLay = colorGenerator(color: color)(image)
return compositeSourceOver(overLay: overLay)(image)
}
}
組合濾鏡
到目前為止已經(jīng)定義了高斯模糊與顏色疊層濾鏡赁酝,可以先模糊再疊一層顏色犯祠。
/// 組合濾鏡
func combine() -> Filter {
return { image in
let radius = 5.0
let color = UIColor.red.ciColor
/// 將濾鏡應(yīng)用于圖像
let blurredImage = gaussianBlur(radius: radius)(image)
let overlaidImage = colorOverlay(color: color)(blurredImage)
return overlaidImage;
}
}
復(fù)合函數(shù)
可以將上面兩個(gè)濾鏡調(diào)用表達(dá)式合并為一體:
colorOverlay(color: color)(gaussianBlur(radius: radius)(image))
但是由于括號(hào)錯(cuò)綜復(fù)雜,這些代碼失去了可讀性酌呆。更好的解決方案是定義運(yùn)算符來(lái)組合濾鏡衡载,首先定義一個(gè)組合濾鏡的函數(shù):
func composeFilters(filter1: @escaping Filter, filter2: @escaping Filter) -> Filter {
return { image in
return filter2(filter1(image))
}
}
composeFilters函數(shù)接受兩個(gè)Filter參數(shù),并返回一個(gè)新的Filter濾鏡隙袁。這個(gè)新濾鏡接受一個(gè)CIImage參數(shù)并傳遞給filter1痰娱,再將取得的返回值CIImage傳遞給filter2。如此便可以定義復(fù)合函數(shù):
composeFilters(filter1: gaussianBlur(radius: radius), filter2: colorOverlay(color: color))
為了讓代碼更具可讀性菩收,可以引入運(yùn)算符梨睁。雖然隨意定義運(yùn)算符并不一定能提升代碼可讀性,但是圖像處理庫(kù)中娜饵,濾鏡的組合是一個(gè)反復(fù)被討論的問(wèn)題坡贺,所以引入運(yùn)算符極有意義:
precedencegroup FilterPrecedence {
associativity: left//左結(jié)合
}
infix operator >>>: FilterPrecedence
func >>>(filter1: @escaping Filter, filter2: @escaping Filter) -> Filter {
return { image in filter2(filter1(image)) }
}
與composeFilters方法相同,現(xiàn)在可以使用運(yùn)算符達(dá)到目的:
gaussianBlur(radius: radius) >>> colorOverlay(color: color)
運(yùn)算符>>>是左結(jié)合的箱舞,濾鏡將從左到右的順序被應(yīng)用到圖像上遍坟。
組合濾鏡運(yùn)算符是一個(gè)符合函數(shù)的例子。數(shù)學(xué)上兩個(gè)函數(shù)f晴股、g構(gòu)成復(fù)合函數(shù)被寫作f·g愿伴,表示的新函數(shù)將輸入?yún)?shù)x映射到f(g(x))上。除了順序這恰恰也是>>>運(yùn)算符所做的:將CIImage參數(shù)傳遞給>>>運(yùn)算符操作的兩個(gè)Filter濾鏡函數(shù)队魏。
理論背景:柯里化
定義接受兩個(gè)參數(shù)的函數(shù)的兩種方法:
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)
方法一將兩個(gè)參數(shù)同時(shí)傳遞給add1公般;方法二先向函數(shù)add2傳遞一個(gè)參數(shù),然后向其返回的函數(shù)傳遞第二個(gè)參數(shù)胡桨。這兩個(gè)版本完全等價(jià)。
add1和add2展示了如何將接受多個(gè)參數(shù)的函數(shù)變?yōu)橹唤邮芤粋€(gè)參數(shù)的函數(shù)刽虹,這個(gè)過(guò)程被稱為柯里化,將add2稱為add1的柯里化版本。
函數(shù)柯里化,給了調(diào)用者更多的選擇哆料,可以用一個(gè)、兩個(gè)……參數(shù)來(lái)調(diào)用典阵。
把定義濾鏡的函數(shù)進(jìn)行柯里化蹋半,有利于使用<<<運(yùn)算符進(jìn)行組合。
討論
CoreImage框架已經(jīng)非常成熟,幾乎能提供所有需要的功能,盡管如此。這么設(shè)計(jì)API也有一定的優(yōu)勢(shì):
- 安全:避免為定義的鍵或強(qiáng)制類型轉(zhuǎn)換而引發(fā)的運(yùn)行時(shí)錯(cuò)誤。
- 模塊化:使用<<<很容易將濾鏡進(jìn)行組合,可以將復(fù)雜的濾鏡拆分為更小的單元压固。
- 清晰:即使未使用過(guò)CoreImage也能使用這些API來(lái)裝配濾鏡阎毅,并不需要知道kCIOutputImageKey等這些鍵。