Session 719: Core Image: Performance, Prototyping, and Python
相信絕大多數 iOS 開發者對 Core Image 都不陌生,作為系統標配的、異常強大的影像處理庫,在絕大多數場景下都能滿足 App 的影像處理需求。而且,目前 Core Image 已經支援在 iOS 上做自定義 filter,頗有趕超 GPUImage 的態勢(GPUImage 是目前 iOS 做影像處理事實上的標杆)。加上 iOS12 蘋果打算 deprecate OpenGL 和 OpenGL ES, 推廣 Metal。那和 Metal 聯絡緊密的 Core Image 無疑更有勝算。
這個 Session 講的內容主要包括三個部分:
- Core Image 新的效能 API;
- 在 Core Image 體系上快速搭建濾鏡原型;
- 在 Core Image 體系上應用機器學習;
1. Core Image 新的效能 API
中間快取
在講中間快取之前,需要先複習一下 Core Image。在 Core Image 中,我們能夠對影像鏈式的執行 Filter,如下圖所示:

通過 Filter 的組合,我們可以實現一些複雜的影像處理效果。建立 Filter 鏈的方法可以參考如下程式碼( 節選自 Core Image Programming Guide):
func applyFilterChain(to image: CIImage) ->
CIImage {
// The CIPhotoEffectInstant filter takes only an input image let colorFilter = CIFilter(name: "CIPhotoEffectProcess", withInputParameters: [kCIInputImageKey: image])! // Pass the result of the color filter into the Bloom filter // and set its parameters for a glowy effect. let bloomImage = colorFilter.outputImage!.applyingFilter("CIBloom", withInputParameters: [ kCIInputRadiusKey: 10.0, kCIInputIntensityKey: 1.0 ]) // imageByCroppingToRect is a convenience method for // creating the CICrop filter and accessing its outputImage. let cropRect = CGRect(x: 350, y: 350, width: 150, height: 150) let croppedImage = bloomImage.cropping(to: cropRect) return croppedImage
}複製程式碼
整個過程很直觀,我們將圖片喂到第一個 Filter,然後得到第一個 Filter 的 outputImage
,然後再把該物件喂到第二個 Filter……以此類推建立 Filter 鏈。
Core Image 的 Lazy
值得注意的一點是,當上述程式碼執行時,影像處理並沒有發生,只是 Core Image 內部進行了一些關係的建立,只有當影像需要被渲染的時候,才會實際去執行各個 Filter 的影像處理過程。
因為有 Lazy 的特性,所以 Core Image 上最重要的一個優化就是 “自動連線(Filter Concatenation)”, 因為最終影像處理的過程都發生在所有 Filter 成鏈之後。所以 Core Image 可以將鏈式的多個 Filter 合併 成一個來執行,省去不必要的開銷。如下圖所示:

中間快取
現在回過頭來看這樣一個場景:

三個 Filter,第一個計算很耗時,而第三個的引數可以讓使用者手動調節。這意味著每次使用者調節後都需要重新計算這三個 Filter。但其實前兩份 Filter 的引數是不變的,也就是說前兩個 Filter 的運算過程和結果都是不隨著使用者調整第三個 Filter 的引數改變而改變的。這裡重複的計算是否有可能進行優化呢?
我們很容易就想到,我們只需要把前兩次運算的結果 cache 下來就可以了,如下圖所示:

但是上文提到,Core Image 會把 Filter 鏈自動合併為一個 Filter,我們如何訪問中間結果呢?
蘋果在 iOS12的 Core Image 中,給 CIImage
新增了一箇中間快取的屬性(insertingIntermediate
), 來解決這個問題,如下圖所示:

我們希望儲存第二個 Filter 的結果,只需要在第二個 Filter 的 outputImage
呼叫 insertingIntermediate()
來生成一個新的 CIImage
傳到後面的流程即可。這樣第三個 Filter 的引數調整就不會導致前兩個 Filter 的重複計算。
怎麼做的呢? 其實就是自動合併的邏輯會根據 insertingIntermediate
進行調整。如下圖所示:

Core Image 的 CIContext
可以設定是否要開啟 cacheIntermediate
, 但這次新增的 insertingIntermediate
有更高的優先順序。具體一些使用上的建議可以參考下圖:

Kernal 語言的新特性
兩種模式
iOS 上支援自定義 Filter,自定義 Filter 使用 Kernal 語言進行開發(一種類似 GLSL 的指令碼語言)。目前一共有兩種開發 CIKernel 程式的模式:

第一種是傳統的基於 CIKernal 開發語言進行編寫,然後編譯成 Metal 或者 GLSL 的方式,第二種是直接使用 Metal Shading 語言進行開發,然後在 build 期間就生成二進位制的庫,執行階段 load 之後直接轉換為 GPU 的指令。
目前因為蘋果主推 MPS(Metal Performance Shader), 所以方式一已經被標記為 deprecated。
按組讀寫
使用 Metal 來開發 CIKernel 的優勢:
- 支援半精度浮點數;
- 支援按組讀寫(Group read &
Group write);
半精度浮點是純運算性質方面的優化,在 A11 晶片上運算更快,而且因為用到的暫存器小所以也有較大的優化空間。
接下來重點介紹一下按組讀寫。
假設我們對左圖紅框畫素做一個3×3的卷積運算,結果為存入右邊的綠色框。顯而易見,對於每個新的畫素,都需要讀取輸入影像9次畫素值。

但如果是按組讀寫,如下圖所示。我們一次性讀取16個畫素來計算並寫入右邊的四個畫素,那我們整個過程中寫了4次,讀取了16次。每個新畫素平均需讀取的數量為4,比上述的單畫素需要9次顯著降低。

按組讀寫的原理是很簡單的,接下來介紹一下如果我們有一個之前使用 CIKernal Language 開發的 kernal,如果修改使其能夠使用按組讀寫這樣高速的優化。
假設我們的 kernal 如下圖所示:

第一步,轉換為 metal:

第二步, 改造為按組讀寫的模式。核心就是使用了 s.gatherX
來實現。

在使用了按組讀寫和半浮點經典的優化後,基本都可以得到2倍的效能提升。
2. 在 Core Image 體系上快速搭建濾鏡原型
一般來講,一個濾鏡典型的研發流程是首先在電腦上進行快速原型的測試,之後再移植到生產環境,電腦上有大量的工具(OpenCV、SciPy、Numpy、Tensorflow 等等)來進行快速原型開發,而生產環境的技術棧卻是 Core Image, vImage,Metal等完全不同的技術架構棧,這往往會導致一個問題:在電腦上原型測得好好的,結果到手機上效果卻撲街了。
蘋果為了解決這個問題,釋出了一個神器 —— PyCoreImage。

初次看到這個名詞是不是感到非常穿越? 但其實很明顯,就是可以在 Python 中呼叫 Core Image 的介面。
我們在 prototype 的時候使用 Python + PyCoreImage 這樣的方式,那就最大程度的模擬了真實的執行環境,基本上移植到手機上效果也不會打折。而且最關鍵的是,只要學一個框架就好了啊,多的學不完啊!!!!
在使用 PyCoreImage 時,最關鍵是要了解 PyObjc 的用法,PyObjc 在 OS X 10.5 釋出,實現了在 Python 可以呼叫 Objective-C 的程式碼,其中最主要的轉換規則就是冒號變下劃線,具體可以參考圖中的例子。

說回 PyCoreImage,其中的原理其實大概也可以想到,如下圖所示,PyCoreImage 通過 PyObjC 和 macOS 的 Core Image 進行互動,並將結果輸出回 NumPy。

下圖中的程式碼首先匯入了一個圖片,然後對其應用高斯模糊的 Filter,然後將結果輸出到變數 bar
中。

剩下的更多關於 PyCoreImage 的用法可以參考 Session 的 ppt,這裡不再贅述。
3. 在 Core Image 體系上應用機器學習
影像處理和計算機發展至今,已經大量通過使用機器學習和深度學習來提升演算法的效果。Core Image 也對機器學習提供了非常有好的支援。
CoreML Filter
Core Image 現在可以直接將圖片 apply 到一個 CoreML 的模型裡,相對於給 Core Image 的 Filter 連結上了深度學習的能力。
iOS12 中的 Core Image 提供了 CICoreMLModelFilter
類來將 CoreML 的 model 封裝成 Core Image 能夠識別的 Filter 格式。
下圖是一個 ML 領域的經典應用的例子(風格遷移)

不過現在在網上還完全搜不到 CICoreMLModelFilter
,(大霧
資料填補
對於機器學習而言,訓練集的完整性、覆蓋度能夠很大程度上決定最後模型的精確程度。但是現實情況是,我們往往沒有那麼多訓練集,在這樣的情況下,學術界一般都採用對現有訓練集的圖片進行相應的變化來起到擴充資料集的作用。這類技術統稱資料填補(Data Augmentation
Core Image 對於這類任務天生支援的很好,支援包括以下幾種型別的變化:
- 影像外觀;
- 影像噪聲;
- 幾何變換;
以下是幾種使用 Core Image 的不同 Filter 來將一張圖變多多張訓練圖片的例子:

小結
這個 Session 帶來的內容總體來說還是激動人心的,雖然有的同學可能覺得比較小,沒有那種顛覆式的創新,但對於從事影像領域工作的同學而言,毫無疑問這幾個工作都給人一種 mind opener 的感覺,切實的反應了蘋果對於多媒體、使用者體驗這兩個領域非常超前的思考。