這篇文章會為初學者介紹一下 Core Image,一個 OS X 和 iOS 的圖像處理框架。
如果你想跟著本文中的代碼學習,你可以在 GitHub 上下載示例工程。示例工程是一個 iOS 應用程序,列出了系統提供的大量圖像濾鏡以供選擇,並提供了一個用戶界面用來調整參數並觀察效果。
雖然示例代碼是用 Swift 寫的 iOS 程序,不過實現概念很容易轉換到 Objective-C 和 OS X.
基本概念
說到 Core Image,我們首先需要介紹幾個基本的概念。
一個濾鏡是一個對象,有很多輸入和輸出,並執行一些變換。例如,模糊濾鏡可能需要輸入圖像和一個模糊半徑來產生適當的模糊後的輸出圖像。
一個濾鏡圖表是一個鏈接在一起的濾鏡網絡 (無回路有向圖),使得一個濾鏡的輸出可以是另一個濾鏡的輸入。以這種方式,可以實現精心制作的效果。我們將在下面看到如何連接濾鏡來創建一個復古的拍照效果。
熟悉 Core Image API
有了上述的這些概念,我們可以開始探索 Core Image 的圖像濾鏡細節了。
Core Image 架構
Core Image 有一個插件架構,這意味著它允許用戶編寫自定義的濾鏡並與系統提供的濾鏡集成來擴展其功能。我們在這篇文章中不會用到 Core Image 的可擴展性;我提到它只是因為它影響到了框架的 API。
Core Image 是用來最大化利用其所運行之上的硬件的。每個濾鏡實際上的實現,即內核,是由一個 GLSL (即 OpenGL 的著色語言) 的子集來書寫的。當多個濾鏡連接成一個濾鏡圖表,Core Image 便把內核串在一起來構建一個可在 GPU 上運行的高效程序。
只要有可能,Core Image 都會把工作延遲。通常情況下,直到濾鏡圖表的最後一個濾鏡的輸出被請求之前都不會發生分配或處理。
為了完成工作,Core Image 需要一個稱為上下文 (context) 的對象。這個上下文是框架真正工作的地方,它需要分配必要的內存,並編譯和運行濾鏡內核來執行圖像處理。建立一個上下文是非常昂貴的,所以你會經常想創建一個反復使用的上下文。接下來我們將看到如何創建一個上下文。
查詢可用的濾鏡
Core Image 濾鏡是按名字創建的。要獲得系統濾鏡的列表,我們要向 Core Image 的 kCICategoryBuiltIn 類別請求得到濾鏡的名字:
let filterNames = CIFilter.filterNamesInCategory(kCICategoryBuiltIn) as [String]
iOS 上可用的濾鏡列表非常接近於 OS X 上可用濾鏡的一個子集。在 OS X 上有 169 個內置濾鏡,在 iOS 上有 127 個。
通過名字創建一個濾鏡
現在,我們有了可用濾鏡的列表,我們就可以創建和使用濾鏡了。例如,要創建一個高斯模糊濾鏡,我們傳給 CIFilter 初始化方法相應的名稱就可以了:
let blurFilter = CIFilter(named:"CIGaussianBlur")
設置濾鏡參數
由於 Core Image 的插件結構,大多數濾鏡屬性並不是直接設置的,而是通過鍵值編碼(KVC)設置。例如,要設置模糊濾鏡的模糊半徑,我們使用 KVC 來設置 inputRadius 屬性:
blurFilter.setValue(10.0 forKey:"inputRadius")
由於這種方法需要 AnyObject? (即 Objective-C 裡的 id)作為其參數值,它不是類型安全的。因此,設置濾鏡參數需要謹慎一些,確保你傳值的類型是正確的。
查詢濾鏡屬性
為了知道一個濾鏡提供什麼樣的輸入和輸出參數,我們就可以分別獲取 inputKeys 和 outputKeys 數組。它們都返回 NSString 的數組。
要獲取每個參數的詳細信息,我們可以看看由濾鏡提供的 attributes 字典。每個輸入和輸出參數名映射到它自己的字典裡,描述了它是什麼樣的參數,如果有的話還會給出它的最大值和最小值。例如,下面是 CIColorControls 濾鏡對應的 inputBrightness 參數字典:
inputBrightness = { CIAttributeClass = NSNumber; CIAttributeDefault = 0; CIAttributeIdentity = 0; CIAttributeMin = -1; CIAttributeSliderMax = 1; CIAttributeSliderMin = -1; CIAttributeType = CIAttributeTypeScalar; };
對於數值參數,該字典會包含 kCIAttributeSliderMin 和 kCIAttributeSliderMax 鍵,來限制期望的輸入域。大多數參數還包含一個 kCIAttributeDefault 關鍵字,映射到該參數的默認值。
圖片濾鏡實戰
圖像濾鏡的工作由三部分組成:構建和配置濾鏡圖表,發送等待濾鏡處理的圖像,得到濾鏡處理後的圖像。下面的部分對此進行了詳細描述。
構建一個濾鏡圖表
構建一個濾鏡圖表由這幾個部分組成:實例化我們需要的濾鏡,設置它們的參數,把它們連接起來以便該圖像數據按順序傳過每個濾鏡。
在本節中,我們將創建一個用來制作 19 世紀錫版照風格圖像的濾鏡圖表。我們將兩個效果鏈在一起來達到這種效果:同時去飽和以及染色調的黑白濾鏡,和一個暗角濾鏡來創建一個有陰影效果的加框圖片。
用 Quartz Composer,來做 Core Image 濾鏡圖表的原型非常有用,可以從蘋果開發者網站下載。下面,我們整理了所需的照片濾鏡,把黑白濾鏡和暗角濾鏡串在一起:
一旦達到了我們滿意的效果,我們可以重新在代碼裡創建濾鏡圖表:
let sepiaColor = CIColor(red: 0.76, green: 0.65, blue: 0.54) let monochromeFilter = CIFilter(name: "CIColorMonochrome", withInputParameters: ["inputColor" : sepiaColor, "inputIntensity" : 1.0]) monochromeFilter.setValue(inputImage, forKey: "inputImage") let vignetteFilter = CIFilter(name: "CIVignette", withInputParameters: ["inputRadius" : 1.75, "inputIntensity" : 1.0]) vignetteFilter.setValue(monochromeFilter.outputImage, forKey: "inputImage") let outputImage = vignetteFilter.outputImage
需要注意的是黑白濾鏡的輸出圖像變為暗角濾鏡的輸入圖像。這將導致暗角效果要應用到黑白圖像上。還要注意的是,我們可以在初始化中指定參數,而不一定需要用 KVC 單獨設置它們。
創建輸入圖像
Core Image 濾鏡要求其輸入圖像是 CIImage 類型。而對於 iOS 的程序員來說這可能會有一點不尋常,因為他們更習慣用 UIImage,但這個區別是值得的。一個 CIImage 實例實際上比 UIImage 更全面,因為 CIImage 可以無限大。當然,我們不能存儲無限的圖像在內存中,但在概念上,這意味著你可以從 2D 平面上的任意區域獲取圖像數據,並得到一個有意義的結果。
所有我們在本文中使用的圖像都是有限的,而且也可以很容易從一個 UIImage 來創建一個 CIImage。事實上,這只需要一行代碼:
let inputImage = CIImage(image: uiImage)
也有很方便的初始化方法直接從圖像數據或文件 URL 來創建 CIImage。
一旦我們有了一個 CIImage,我們就可以通過設置濾鏡的 inputImage 參數來將其設置為濾鏡的輸入圖像:
filter.setValue(inputImage, forKey:"inputImage")
得到一個濾鏡處理後的圖片
濾鏡都有一個名為 outputImage 的屬性。正如你可能已經猜到的一樣,它是 CIImage 類型的。那麼,我們如何實現從一個 CIImage 創建 UIImage 這樣一個反向操作?好了,雖然我們到此已經花了所有的時間建立一個濾鏡圖表,現在是調用 CIContext 的力量來實際的做圖像濾鏡處理工作的時候了。
創建一個上下文最簡單的方法是給它的構造方法傳一個 nil 字典:
let ciContext = CIContext(options: nil)
為了得到一個濾鏡處理過的圖像,我們需要 CIContext 從輸出圖像的一個矩形內創建一個 CGImage,傳入輸入圖像的范圍(bounds):
let cgImage = ciContext.createCGImage(filter.outputImage, fromRect: inputImage.extent())
我們使用輸入圖像大小的原因是,輸出圖像通常和輸入圖像具有不同的尺寸比。例如,一個模糊圖像由於采樣超出了輸入圖像的邊緣,圍繞在其邊界外還會有一些額外的像素。
現在,我們可以從這個新創建的 CGImage 來創建一個 UIImage 了:
let uiImage = UIImage(CGImage: cgImage)
直接從一個 CIImage 創建 UIImage 也是可以的,但這種方法有點讓人郁悶:如果你試圖在一個 UIImageView 上顯示這樣的圖像,其 contentMode 屬性將被忽略。使用過渡的 CGImage 則需要一個額外的步驟,但可以省去這一煩惱。
用 OpenGL 來提高性能
用 CPU 來繪制一個 CGImage 是非常耗時和浪費的,它只將結果回傳給 UIKit 來做合成。我們更希望能夠在屏幕上繪制應用濾鏡後的圖像,而不必去 Core Graphics 裡繞一圈。幸運的是,由於 OpenGL 和 Core Image 的可互操作性,我們可以這麼做。
要 OpenGL 上下文和 Core Image 上下文之間共享資源,我們需要用一個稍微不同的方式來創建我們的 CIContext:
let eaglContext = EAGLContext(API: .OpenGLES2) let ciContext = CIContext(EAGLContext: context)
在這裡,我們用 OpenGL ES 2.0 的功能集創建了一個 EAGLContext。這個 GL 上下文可以用作一個 GLKView 的背襯上下文或用來繪制成一個 CAEAGLLayer。示例代碼使用這種技術來有效地繪制圖像。
當一個 CIContext 具有了關聯 GL 的上下文,濾鏡處理後的圖像就可用 OpenGL 來繪制,像如下這樣調用方法:
ciContext.drawImage(filter.outputImage, inRect: outputBounds, fromRect: inputBounds)
與以前一樣,fromRect 參數是用濾鏡處理後的圖像的坐標空間來繪制的圖像的一部分。這個 inRect 參數是 GL 上下文的坐標空間的矩形應用到需要繪制圖像上。如果你想保持圖像的長寬比,你可能需要做一些數學計算來得到適當的 inRect。
強制在 CPU 上做濾鏡操作
只要有可能,Core Image 將在 GPU 上執行濾鏡操作。然而,它確實有回滾到 CPU 上執行的可能。濾鏡操作在 CPU 上完成可具有更好的精確度,因為 GPU 經常在浮點計算上以失真換得更快的速度。在創建一個上下文時,你可以通過設置 kCIContextUseSoftwareRenderer 關鍵字的值為 true 來強制 Core Image 在 CPU 上運行。
你可以通過在 Xcode 中設置計劃配置(scheme configuration)裡的 CI_PRINT_TREE 環境變量為 1 來決定用 CPU 還是 GPU 來渲染。這將導致每次一個濾鏡處理圖像被渲染的時候 Core Image 都會打印診斷信息。此設置用來檢查合成圖像濾鏡樹也很有用。
示例應用一覽
本文的示例代碼是一個 iPhone 應用程序,展示了 iOS 裡大量的各式 Core Image 圖像濾鏡。
為濾鏡參數創建一個 GUI
為了盡可能多的演示各種濾鏡,示例應用程序利用了 Core Image 的內省特點生成了一個界面,用於控制它支持的濾鏡參數:
示例應用程序只限於單一的圖像輸入以及零個或多個數值輸入的濾鏡。也有一些有趣的濾鏡不屬於這一類(特別是那些合成和轉換濾鏡)。即便如此,該應用程序仍然很好的概述了 Core Image 支持的功能。
對於每個濾鏡的輸入參數,都有一個滑動條可以用於配置參數的最小值和最大值,其值被設置為默認值。當滑動條的值發生變化時,它把改變後的值傳給它的 delegate,一個持有 CIFilter 引用的 UIImageView 子類。
使用內建的照片濾鏡
除了許多其他的內置濾鏡,示例應用程序還展示了 iOS 7 中引入的照片濾鏡。這些濾鏡沒有我們可以調整的參數,但它們值得被囊括進來,因為它們展示了如何在 iOS 中模擬照片應用程序的效果:
結論
這篇文章簡要介紹了 Core Image 這個高性能的圖像處理框架。我們一直在試圖在如此簡短的形式內盡可能多的展示這個框架的功能。你現在已經學會了如何實例化和串聯 Core Image 的濾鏡,在濾鏡圖表傳入和輸出圖像,以及調整參數來獲得想要的結果。你還學習了如何訪問系統提供的照片濾鏡,用以模擬在 iOS 上的照片應用程序的行為。
現在你知道了足夠多的東西來寫你自己的照片編輯應用程序了。隨著更多的一些探索,你就可以寫自己的濾鏡了,利用你的 Mac 或 iPhone 的神奇的力量來執行以前無法想象的效果。快去動手做吧!
參考
Core Image Reference Collection 是 Core Image 的權威文檔集。
Core Image Filter Reference 包含了 Core Image 提供的圖像濾鏡的完整列表,以及用法示例。
如果想要寫更函數式風格的 Core Image 代碼,可以看看 Florian Kluger 在 objccn.io 話題 #16 裡的文章。
話題 #21 下的更多文章
原文 An Introduction to Core Image