伴隨著這強大的圖像過濾功能,Core Image 包含的 APIs 有臉部偵測 (face detection),圖像自動增強 (automatic image enhancements) 及透過混合使用多重濾鏡製作出與別不同的濾鏡效果。
在本教程中將會向你介紹 Core Image 的基本功能,並展示一些優良實作和簡單技巧去提升表現。
範例App
在本教程中,我們會透過建立一個簡單的 App 為圖像加入濾鏡。開始之前,先準備一些圖像並保存在一個文件夾之內。
首先,在 Xcode 中使用 Single View Application 模板去創建一個新的 iOS 程式:
把專案命名為 Core Image
並選擇 Swift
為程式語言。然後前往 Main.storyboard
及新增一個 UIImageView
。接著加入 Auto Layout 約束條件 (constraints) 使圖像能因應螢幕尺寸和方向的改變作出自動調整:
打開準備好的圖片並加入到專案的 asset catalog 中 (例如 Assets.xcassets),讓圖像可以在 image view 展示出來。
然後打開 ViewController.swift
並跟著以下的程式碼作出更改:
import UIKit
import CoreImage
class ViewController: UIViewController {
@IBOutlet var imageView: UIImageView?
override func viewDidLoad() {
//TODO: filter the image
}
}
要使用 Core Image 就需要先滙入Core Image框架 (import CoreImage
),這樣框架內的類別都能在 ViewController.swift
內使用。以上的程式碼,我們同時創建了一個 UIImageView
outlet 去連接早前加入的圖像。
現在回到 Main.storyboard
並把 outlet 與 image view 連結起來。
最後,選取 image view 並前往屬性檢閱器,為加入的圖像設定圖像屬性。
這就完成了專案組態的設定。接下來就開始吧!
深入探討 Core Image
Core Image 透過三種類別在 iOS 裡支援圖像處理。第一個是 CIFilter
類別,它可以為圖像套用預製濾鏡。
下一步是把圖像濾鏡添加至 App, 然後以下列的程式碼來更新 viewDidLoad()
:
override func viewDidLoad() {
guard let image = imageView?.image, cgimg = image.CGImage else {
print("imageView doesn't have an image!")
return
}
let coreImage = CIImage(CGImage: cgimg)
let filter = CIFilter(name: "CISepiaTone")
filter?.setValue(coreImage, forKey: kCIInputImageKey)
filter?.setValue(0.5, forKey: kCIInputIntensityKey)
if let output = filter?.valueForKey(kCIOutputImageKey) as? CIImage {
let filteredImage = UIImage(CIImage: output)
imageView?.image = filteredImage
}
else {
print("image filtering failed")
}
}
讓我們把這些程式碼逐句去了解一下:
第2行: 第一個 guard
是確保 imageView 的圖像是存在的。第二個 guard
把 imageView 的圖像(若它是存在)轉換成 CGImage (下文會再作詳談).
第7行: 這個加入了一個叫做 coreImage
的變數。Core Image 在CIImage
上執行而非 UIImage
。為了符合這個條件,所有 UIImage 物件需要在使用濾鏡前轉換成 CIImage
。
第9行: Core Image 內置多款濾鏡去支援 App 的圖像處理。這一行建立了一個名為 CISepiaTone
的 CIFilter
。所有的濾鏡名稱都是預定好的,你可以在這個 參考文獻 檢視一下。
第10行: 此行是設定濾鏡的輸入圖像。
第11行: 此行指定圖像所應用的濾鏡強度為50%。
第13行: 這一行是呼叫 Core Image 為圖像使用濾鏡,然後安全地解開及釋出結果。若圖像能適當地建立,在 if
斜述的程式碼便可執行。
第14行: 為使圖像能在 UIImageView
顯示出來,所以使用這行的程式碼把圖像轉換為 UIImage
。
第15行: 這一行把 imageView 的圖像設定成新的 UIImage。
第19行: 若果濾鏡不能有效地使用,這一行才會被執行。
現在有了使用 Core Image 的體驗,若果你對 Swift 有基本概念和認識的話,相信為圖像應用濾鏡並不是很困難。但對於新手來說,或許會對圖像在 UIImage
和 CIImage
之間作出轉換產生困惑。
你或許已經注意到 UIImage
是來自 CIImage
屬性。但為何我們要作出轉換呢?其中一個原因是這個屬性的值是 nil
。 Matt 在 StackOverflow 曾提及到,除非 UIImage 已預存了CIImage (例如由 imageWithCIImage: 所產生),否則 UIImage 的 CIImage 值一般也是 nil
的。
要解決這個問題,我們首先把 UIImage 物件轉換成 CGImage。然後再把 CGImage 物件轉成 CIImage,這樣我們就可以在 Core Image APIs 裡使用得到。當應用了濾鏡,我們再次把釋出來的 CIImage 轉換為 UIImage,並在 UIImageView 顯示出來。
接著下來可以執行 App (command + R) 作簡單測試,看看添加了一層棕褐色調的圖像效果。試一試隨意調整輸入值並留意有什麼變化。
提升圖像處理效能
到目前為止,程式碼對於較小的圖像過濾都有不錯的表現。但是若果要使用大型圖像則需要優化這些程式碼,畢竟為使用者提供流暢及更優質的使用體驗是很重要的。但剛才我們所使用的程式碼在這個範疇上的表現卻不太理想。
在範例 app 使用較小圖像顯然得到不錯的效果。但實際上,若果在此程式碼使用大型圖像,裝置的主線程會被阻塞。而阻塞主線程的事是作為開發者需要避免發生,以免減低 App 的回應性。接下來我們一起探討如何改良這些程式碼吧!
最簡單且有效的解決方法是利用裝置上的 GPU。每一台 iPhone 都同時具有 GPU 和 CPU 兩款處理器。而 GPU 善於處理複雜的圖形工作,例如圖像處理。現時我們的程式碼都是單靠在 CPU 上運行,所以圖像處理的效能亦受到影響,因此我們需要把它更換為 GPU。
第一步,我們需要透過 CIContext
去優化程式碼。透過使用 CIContext 類別可以讓開發者指定使用 GPU 還是 CPU 為執行程式碼的處理器。以下是一個實作舉例:
let openGLContext = EAGLContext(API: .OpenGLES3)
let context = CIContext(EAGLContext: openGLContext)
但是只是建立一個簡單的內容並不足夠,我們需要指示 Core Image 去執行,而這裡則有點複雜。我們先修改程式碼如下:
override func viewDidLoad() {
guard let image = imageView?.image, cgimg = image.CGImage else {
print("imageView doesn't have an image!")
return
}
let openGLContext = EAGLContext(API: .OpenGLES2)
let context = CIContext(EAGLContext: openGLContext)
let coreImage = CIImage(CGImage: cgimg)
let filter = CIFilter(name: "CISepiaTone")
filter?.setValue(coreImage, forKey: kCIInputImageKey)
filter?.setValue(1, forKey: kCIInputIntensityKey)
if let output = filter?.valueForKey(kCIOutputImageKey) as? CIImage {
let cgimgresult = context.createCGImage(output, fromRect: output.extent)
let result = UIImage(CGImage: cgimgresult)
imageView?.image = result
}
}
不是太困難,對嗎?我把作出改動的程式碼以黃色背景標示出來。接下來明說一下:
第7行: 此行是建立一個新的 OpenGL ES context。OpenGL ES 能讓 GPU 直接存取,是一個高速圖形處理的 API。我們需要利用 GPU 提升圖像表現。所以,這裡我們會新增一個 OpenGL ES 2 的 context。若果你的 App 將會使用於這些支援 OpenGL ES 3 的 最新裝置 ,請把相關的值更改為 .OpenGLES3
。
第8行: 因我們不能直接使用 OpenGL ES context 為圖像加入濾鏡,我們需要在新的 OpenGL ES context 加入 CIContext
。這樣我們才能使用到 GPU 和得到更佳的效能。
第17行: 當我們建立了 CIContext 物件,圖像的濾鏡處理會有稍微不同。首先,我們在 context 建立 CGImage 圖像。而 fromRect
參數是用於設定圖像的大小。在我們所寫的程式碼中,我們只在原有圖像上使用了 extent
(意思指圖像大小),這樣就可以釋出跟原圖大小相同的圖像。
就是這樣!現在你的 App 已經可以透過 GPU 而非 CPU 為圖像使用濾鏡。雖然你可能還沒有注意到效能瞬間已經得到提升,但一定能感受到這個方法比起使用 CPU 更加快速。若果把裝置轉為橫向 (landscape),你會發現圖像顯示的質素比起之前得到很大的改善。
使用多重濾鏡
Core Image 擁有大量預製濾鏡。但假若這些都不能滿足所需,其中一個解決方法是建立自訂 Core Image kernel,不過這是超出這個教程的範圍。然而,我將會向大家講解如何使用多重濾鏡,只要得到合適的配對,應該能得到與期望相近的濾鏡效果。
以下的實作示範了同時套用棕褐色濾鏡和增加光亮度濾鏡所得出來的效果:
override func viewDidLoad() {
guard let image = imageView?.image, cgimg = image.CGImage else {
print("imageView doesn't have an image!")
return
}
let openGLContext = EAGLContext(API: .OpenGLES2)
let context = CIContext(EAGLContext: openGLContext!)
let coreImage = CIImage(CGImage: cgimg)
let sepiaFilter = CIFilter(name: "CISepiaTone")
sepiaFilter?.setValue(coreImage, forKey: kCIInputImageKey)
sepiaFilter?.setValue(1, forKey: kCIInputIntensityKey)
if let sepiaOutput = sepiaFilter?.valueForKey(kCIOutputImageKey) as? CIImage {
let exposureFilter = CIFilter(name: "CIExposureAdjust")
exposureFilter?.setValue(sepiaOutput, forKey: kCIInputImageKey)
exposureFilter?.setValue(1, forKey: kCIInputEVKey)
if let exposureOutput = exposureFilter?.valueForKey(kCIOutputImageKey) as? CIImage {
let output = context.createCGImage(exposureOutput, fromRect: exposureOutput.extent)
let result = UIImage(CGImage: output)
imageView?.image = result
}
}
}
如你所看到,在 Core Image 使用多重濾鏡是很簡單直接的,而我們也只不過是把一個濾鏡輸出來的圖像再輸入至另一個濾鏡,重叠使用以達至特別的效果。我們使用的是 kCIInputEVKey
而非 kCIInputIntensityKey
,因為這些濾鏡不是使用 intensity 參數,而是使用 EV 參數。有關更多資訊,可以到這個 參考文件。
現在再次執行 App,你將會得到像這樣子的圖像效果:
結語
在本文中,對 Core Image 有了基本的認識和如何使用它強大的功能去為你的 App 加入優質的濾鏡圖像。然而 Core Image APIs 是一個很大的課題,還有很多內容在這個教程是沒有涉及到的。希望你在這個教程裡得到了愉快的學習體驗!
作為參考,可以到GitHub下載完整的Xcode專案。
原文:Core Image Introduction: Applying Image Filters to Photos