Swift函數(shù)式編程二(封裝Core Image)

代碼地址

前言

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等這些鍵。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末点弯,一起剝皮案震驚了整個(gè)濱河市扇调,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌抢肛,老刑警劉巖狼钮,帶你破解...
    沈念sama閱讀 212,383評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異捡絮,居然都是意外死亡熬芜,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門福稳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)涎拉,“玉大人,你說(shuō)我怎么就攤上這事的圆」呐。” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 157,852評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵越妈,是天一觀的道長(zhǎng)季俩。 經(jīng)常有香客問(wèn)我,道長(zhǎng)梅掠,這世上最難降的妖魔是什么酌住? 我笑而不...
    開(kāi)封第一講書人閱讀 56,621評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮阎抒,結(jié)果婚禮上酪我,老公的妹妹穿的比我還像新娘。我一直安慰自己挠蛉,他們只是感情好祭示,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著谴古,像睡著了一般质涛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上掰担,一...
    開(kāi)封第一講書人閱讀 49,929評(píng)論 1 290
  • 那天汇陆,我揣著相機(jī)與錄音,去河邊找鬼带饱。 笑死毡代,一個(gè)胖子當(dāng)著我的面吹牛阅羹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播教寂,決...
    沈念sama閱讀 39,076評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼捏鱼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了酪耕?” 一聲冷哼從身側(cè)響起导梆,我...
    開(kāi)封第一講書人閱讀 37,803評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎迂烁,沒(méi)想到半個(gè)月后看尼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,265評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡盟步,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評(píng)論 2 327
  • 正文 我和宋清朗相戀三年藏斩,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片却盘。...
    茶點(diǎn)故事閱讀 38,716評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡狰域,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出谷炸,到底是詐尸還是另有隱情北专,我是刑警寧澤,帶...
    沈念sama閱讀 34,395評(píng)論 4 333
  • 正文 年R本政府宣布旬陡,位于F島的核電站,受9級(jí)特大地震影響语婴,放射性物質(zhì)發(fā)生泄漏描孟。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評(píng)論 3 316
  • 文/蒙蒙 一砰左、第九天 我趴在偏房一處隱蔽的房頂上張望匿醒。 院中可真熱鬧,春花似錦缠导、人聲如沸廉羔。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,798評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)憋他。三九已至,卻和暖如春髓削,著一層夾襖步出監(jiān)牢的瞬間竹挡,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,027評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工立膛, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留揪罕,地道東北人梯码。 一個(gè)月前我還...
    沈念sama閱讀 46,488評(píng)論 2 361
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像好啰,于是被迫代替她去往敵國(guó)和親轩娶。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容