update: 2011/12/31
reference: Beginning Core Image in iOS 5 Tutorial | Ray Wenderlich
A. 前言
1. Core Image 是一個強大的框架(framework) , 可以讓你輕易的在影像上應用濾鏡功
能(filters), 例如更改自然飽和度(vibrance), 色彩(hue) 或曝光時間(exposure). 它使用
GPU(或 CPU, 使用者可自行定義)來處理影像資料, 並且速度非常快. 快到足以處理即
時的影格(video frames).
2. Core Image 濾鏡能夠一次堆疊(stacked) 多個效果應用到圖片或影格上. 當多個濾鏡
堆疊在一起時, 它們是效率高的因為它們建立了更改後(modified)的單一濾鏡應用到
影像上, 取代了同時對影像作一個接著一個的濾鏡處理.
3. 每個濾鏡都有它自己的參數, 並且可以在程式碼中查詢到有關濾鏡本身, 它的用途以
及輸入參數的相關資訊. 這個系統也可以查尋到有哪些濾鏡可供使用. 目前, 只有在
Mac 上可用的 Core Image 濾鏡的部分集合在 iOS 上是可以使用的. 然而, 當越來越
多的濾鏡變成可用之時, 就可以使用此 API 發現到新的濾鏡屬性.
4. 在這個導覽裡, 你會獲得對 Core Image 實際動手做的體驗. 我們將應用一些不同的
濾鏡, 而你將會發現在影像上即時應用極酷的效果是如此的容易.
---------------------------------------------------------------------------------------------
B. Core Image 概觀
在我們開始之前, 先來討論一些 Core Image 框架中最重要的類別.
1. CIContext
所有 core image 中的處理, 都是在 CIContext 裡完成的. 這有點類似 Core Graphics
或 OpenGL 的 context.
2. CIImage
這個類別持有(hold)影像資料. 它能夠由以下所建立: UIImage, 影像檔, 或像素
(pixel)資料.
3. CIFilter
這個濾鏡類別有一個字典(dictionary)用來定義所描繪特定濾鏡的特性. 濾鏡的例子:
自然飽和度(vibrance)濾鏡, 色彩反轉(color inversion)濾鏡, 裁剪(cropping)濾鏡
.... 等等.
當建立好專案時, 我們將會開始使用這些類別.
---------------------------------------------------------------------------------------------
C. 開始新專案
1. 開啟 Xcode 建立新專案: Xcode > File > New > New Project... >
iOS > Application > Single View Application > Next
Product Name: CITest
Device Family: iPhone
Use Storyboard: unchecked
Use Automatic Reference Counting: checked
Include Unit Tests: unchecked
2. 首先, 第一件事是添加 Core Image 框架. 在 Mac 上它是 QuartzCore 框架的一部分,
但是在 iOS 上它是獨立的框架.
project container > Targets(project name) > Build Phases >
Link Binaries with Library > press the +
Search CoreI > double-click on CoreImage framework > Add
和 sound 檔案放到 Resources 的群組裡. 你的專案沒有那個群組, 所以如果你想要
的話(非必需), control-click 已加入的 image.png 檔案, 選擇
New Group from Selection, 接著 click 新產生的 folder 並將名稱改成 Resources.
4. 現在我們要將狀態列(status bar)隱藏, 必需在 .plist 檔案增加一個 key 和一行 code,
並且在 .xib 檔案更改一個設定來完成.
a. 首先是 .plist 檔案. 開啓 Supporting Files 群組下的 CITest-Info.plist 檔案, 在任何
空白的地方按下 control-click 選擇 Add Row,
把 Key 改成 Status bar is initally hidden, Value 設為 YES.
inspector 將 Status Bar 的值設為 None.
5. 在 .xib 檔案中, 從 objects panel 拖拉一個 UIImageView 到 View 物件裡.
位置與尺寸大約符合以下的圖示:
control-drag 到 @interface 下方的位置, 將 Connection 設為 Outlet, Name 設為
imgV, 按下 Connect.
application:didFinishLaunchingWithOptions: 方法裡(在 return YES 之前)
[[UIApplication sharedApplication] setStatusBarHidden:YES];
8. 執行 project, 你應該會看到一個完全灰色的顯示畫面並且沒有狀態列.
初始的設定已完成, 開始要進入 Core Image 了.
D. 影像濾鏡基礎
1. 我們將簡單地經由 CIFilter 執行影像並顯示在螢幕上作為開始.
每次我們要將 CIFilters 應用到影像上, 需要做四件事:
a. 建立 CIImage 物件
CIImage 有下列的初始化方法: imageWithURL, imageWithData,
imageWithCVPixelBuffer 與
imageWithBitmapData:bytesPerRow:size:format:colorSpace.
你大部份的時間很可能都是使用 imageWithURL.
b. 建立 CIContext
CIContext 能夠是以 CPU 或 GPU 作為根基的.
c. 建立 CIFilter
當你建立濾鏡(filter)時, 需要依照你使用濾鏡的不同來設定數個屬性.
d. 取得 filter output
filter 會提供你一個輸出的影像作為 CIImage - 你可以使用 CIContext 將它
(CIImage)轉換為 UIImage , 你會在接下來的步驟看到.
2. 來看看它是如何運作的, 將以下的程式碼加到 ViewController.m 的 viewDidLoad:.
// 建立一個持有影像檔路徑的 NSURL 物件
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"image"
ofType:@"png"];
NSURL *fileNameAndPath = [NSURL fileURLWithPath:filePath];
// 建立 CIImage 與 CIContext:
// 1. 利用 imageWithContentsOfURL 建立 CIImage
// 2. CIContext 建構子利用 NSDictionary 來詳細說明有哪些選項可用, 包含色彩
// 格式, 以及 context 是要在 CPU 或 GPU 上執行. 目前這個 app, 使用預設值
// 即可, 所以傳入 nil 作為參數.
CIImage *beginImage = [CIImage imageWithContentsOfURL:fileNameAndPath];
CIContext *context = [CIContext contextWithOptions:nil];
// 下一步, 建立 CIFilter 物件. CIFilter 建構子夾帶著濾鏡的名稱, 以及一個用來詳細
// 說明此濾鏡的 keys 跟 values 的字典. 每一個濾鏡都有自己獨特的 keys 和有效的
// values 集合.
//
// CISepiaTone(深褐色調)濾鏡的字典帶著二個參數值, 其 Key 為:
// KCIInputImageKey (CIImage) 與
// @"inputIntensity" (a float value, wrapped in an NSNumber, between 0 and 1).
//
// 在這裡, 我們設定 @"inputIntensity" 的值為 0.8. 大部份的濾鏡參數都有預設值,
// 會在無提供值的情況下使用. 有一個例外, 就是 CIImage, 因為沒有預設值,
// 所以必須提供該值.
CIFilter *filter = [CIFilter filterWithName:@"CISepiaTone" keysAndValues:
kCIInputImageKey, beginImage,
@"inputIntensity", [NSNumber numberWithFloat:0.8], nil];
// 從 CIFilter 的 outputImage 屬性, 可以輕易的取得 CIImage
CIImage *outputImage = [filter outputImage];
// 一旦我們已經有了輸出的 CIImage, 我們需要將它轉變成 CGImage
// (這樣才可轉成 UIImage 或直接畫到銀幕上). 這是需要 context 進來處理的地方.
// 在 context 上配合所提供的 CIImage 呼叫 createCGImage:fromRect: 方法,
// 會產生 CGImageRef. 然後, 使用 CGImageRef 呼叫 imageWithCGImage 建構子,
// 可以產生 UIImage.
CGImageRef cgimg = [context createCGImage:outputImage
fromRect:[outputImage extent]];
UIImage *newImg = [UIImage imageWithCGImage:cgimg];
// 已經轉成 UIImage 後, 我們就可以在先前新增好的 UIImageView 上顯示.
[imgV setImage:newImg];
// 釋放 CGImageRef 的記憶體
CGImageRelease(cgimg);
3. 編譯並執行專案, 你就可以看到經由深褐色調(sepia tone)濾鏡處理過的影像結果.
恭喜, 你已經成功地 使用 CIImag 與 CIFilters 了!
E. 更改濾鏡的設定值
1. 很棒, 但這只是你能夠用 Core Image 濾鏡做到的效果的起始而已. 讓我們加入一個 slider 並設定好, 然後我們就可以即時地調整影像的設定.
2. 開啓 ViewController.xib 檔案, 拖拉一個 Slider 到 UIImageView 的下方. 確認
Assistant Editor 是開啓的並顯示 ViewController.h 檔案, 從 slider control-drag
到 @interface 的下方. 將 Connection 設為: Action, Name 設為: changeValue,
確認 Event 是設為: Value Changed, 按下 Connect.
3. 同樣地, 將 slider 連結到 outlet.
再一次從 slider control-drag 到 @interface 下方, 但這次將 Connection 設為: Outlet
Name 設為: amountSlider , 按下 Connect.
4. 每次當我們移動 slider 時, 就需要重新建立部分的處理步驟. 然而, 我們不想要重做
整個過程, 那是非常沒有效率的並且會花費很多時間. 我們需要在類別中更改一些東
西, 才能夠保持住( hold 住) 在 viewDidLoad 方法裡, 某些我們建立的物件.
5. 我們所要做的最重要事情, 是每當需要使用 CIContext 時, 可以重復使用它
(CIContext). 假如每次都重新建立 CIContext , 程式會執行的非常慢.
其它我們能夠保持住的東西是 CIFilter 和最初持有影像的 CIImage.
每一次的影像輸出, 我們都需要新的 CIImage, 但是剛開始的影像是保持不變的.
6. 我們需要新增一些實體變數來完成這個任務.
在 ViewController.m 私有的 @implementation 加入以下三個實體變數.
@implementation ViewController {
//@add
CIContext *context;
CIFilter *filter;
CIImage *beginImage;
}
7. 也在 viewDidLoad 方法裡, 更改這些變數, 讓它們使用實體變數取代
宣告新的區域變數.
//CIImage *beginImage = [CIImage imageWithContentsOfURL:fileNameAndPath];
beginImage = [CIImage imageWithContentsOfURL:fileNameAndPath];
//CIContext *context = [CIContext contextWithOptions:nil];
context = [CIContext contextWithOptions:nil];
//CIFilter *filter = [CIFilter filterWithName:@"CISepiaTone" keysAndValues: kCIInputImageKey, beginImage, @"inputIntensity", [NSNumber numberWithFloat:0.8], nil];
filter = [CIFilter filterWithName:@"CISepiaTone" keysAndValues: kCIInputImageKey, beginImage, @"inputIntensity", [NSNumber numberWithFloat:0.8], nil];
8. 現在開始實作 changeValue: 方法. 在這個方法中, 我們所要做的是去改變 CIFilter
字典中 @"inputIntensity" key 的值. 一旦改變了這個值, 就需要去重複一些步驟:
a. 從 CIFilter 取得輸出的 CIImage.
b. 將 CIImage 轉成 CGImageRef.
c. 將 CGImageRef 轉成 UIImage, 並於 image view 上顯示出來.
9. 所以在 ViewController.m 中, 修改 changeValue: 方法如下所示.
- (IBAction)changeValue:(UISlider *)sender {
//@add
float slideValue = [sender value];
[filter setValue:[NSNumber numberWithFloat:slideValue]
forKey:@"inputIntensity"];
CIImage *outputImage = [filter outputImage];
CGImageRef cgimg = [context createCGImage:outputImage
fromRect:[outputImage extent]];
UIImage *newImg = [UIImage imageWithCGImage:cgimg];
[imgV setImage:newImg];
CGImageRelease(cgimg);
}
10. 你會注意到在此方法的定義中, 我們將變數形態由 (id)sender 改成
(UISlider *)sender. 我們知道將只會使用此方法來取得 UISlider 的值, 所以我們
能夠進一步的做這樣的更改. 如果保留成 (id) 的形態, 就必須轉型(cast)成
UISlider *, 不然的話接下一行就會丟出一個 error. 請確認在 ViewController.h 中,
此方法的宣告改成: (以符合在這裡作的更改)
- (IBAction)changeValue:(UISlider *)sender;
我們從 slider 可以得到 float value. Slider 設定的預設值: 最小 0, 最大 1,
預設 0.5. 這也是適合 CIFilter 的設定值範圍, 多麼方便啊!
CIFilter 所擁有的方法可允許我們在其字典中, 對不同的 key 設定 value 值.
在這裡, 我們從 slider 取得 float value, 設定成 @"inputIntensity" key 的
NSNumber 物件 value.
程式碼的其餘部分應該看起來很熟悉, 它是依循與 viewDidLoad 方法相同的邏輯.
我們將會一再地使用這段程式碼. 從現在開始, 我們將使用 changeSlider 方法來描繪
從 CIFilter 輸出到 UIImageView 的效果.
編譯並執行, 你應該會有一個功能性的活動式 slider, 可以即時的改變影像的
深褐色(sepia)值.
F. 從相簿取得相片
1. 現在我們可以在 app 的執行中直接更改濾鏡的設定值, 事情開始變得真正有趣了!但是, 如果這張花朵的照片不是我們所關心的? 那就來設定一個 UIPickerController,
這樣就可以從相簿取得相片到程式裡, 供我們把玩.
2. 我們需要建立一個 button 用來將相簿的 view 帶上來, 所以開啓 ViewController.xib
檔案並拖拉一個 button 到 slider 的右邊.
3. 然後, 確認 Assistant Editor 是開啓的並顯示 ViewController.h 檔案, 從 button
control-drag 到 @interface 下方的位置, 將 Connection 設為 Action, Name 設為
loadPhoto, 確認 Event 設為 Touch Up Inside, 按下 Connect.
4. 我們需要先將 ViewController 設為符合(conform)
UIImagePickerControllerDelegate 與 UINaviationControllerDelegate, 並且實作
該 delegates protocol 的方法. 在 ViewController.h 檔案中, 更改如下:
@interface ViewController : UIViewController <UIImagePickerControllerDelegate, UINavigationControllerDelegate>
5. 下一步, 切換到 ViewController.m 檔案, 實作 loadPhoto: 方法, 如下所示:
- (IBAction)loadPhoto:(id)sender {
//@add
UIImagePickerController *pickerC =
[[UIImagePickerController alloc] init];
pickerC.delegate = self;
[self presentModalViewController:pickerC animated:YES];
}
第一行的程式碼, 實體化一個新的 UIImagePickerController. 然後將這個 image picker
的 delegate 設為 self (ViewController).
接著, 實作以下二個方法:
//@add
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
[self dismissModalViewControllerAnimated:YES];
NSLog(@"%@", info);
}
//@add
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
[self dismissModalViewControllerAnimated:YES];
}
在這二個方法, 我們讓 UIPickerController 離開了(dismiss). 這就是 delegate 的工作,
假如沒有在這邊處理的話, 我們只能盯著螢幕上永不消失的 image picker!
第一個方法尚未完成, 它只是對所選取的影像註銷(log out)某些訊息的持有者
(placeholder). 而第二個: cancel 方法, 僅僅清除(rid)了 picker controller, 完成了.
6. 編譯並執行, 然後 tap 按鈕就會帶出從相簿擷取照片的 image picker.
2011-12-31 10:40:32.715 CITest[2208:707] {
UIImagePickerControllerMediaType = "public.image";
UIImagePickerControllerOriginalImage = "<UIImage: 0x10f760>";
UIImagePickerControllerReferenceURL = "assets-library://asset/asset.JPG?id=1C4BFBF9-1C90-472A-900B-3AB04DF1EE07&ext=JPG";
}
注意到它有使用者所選的原始照片(original image)的字典(dictionary)入口(entry).
這就是我們所要取出來做濾鏡的.
8. 現在, 我們已經找到選取照片的辦法, 如何去設定 beginImage(type: CIImage)
去使用它呢? 只要更改 ViewController.m 檔案裡的 delegate 方法如下即可:
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
[self dismissModalViewControllerAnimated:YES];
//@add
UIImage *gotImage = [info objectForKey:UIImagePickerControllerOriginalImage];
beginImage = [CIImage imageWithCGImage:gotImage.CGImage];
[filter setValue:beginImage forKey:kCIInputImageKey];
[self changeValue:amountSlider];
}
UIImagePickerControllerOriginalImage key 常數取得的值來得到照片的 UIImage.
注意, 最好使用常數定義來取代直接在程式碼中輸入字串, 因為 Apple 在未來有可能
更改 key 的名稱. 完整的 key 常數清單, 請查閱 UIImagePickerController Delegate
Protocol Reference.
接著, 我們需要將 UIImage 轉成 CIImage, 但是沒有方法可以將 UIImage 轉成
CIImage. 不過, 有 [CIImage imageWithCGImage:] 這個方法. 因此可以藉由 UIImage
呼叫 UIImage.CGImage 間接來取得 CIImage, 正好如此!
然後在濾鏡的字典中設定 kCIInputImageKey key 的 value 為選取的相片: beginImage.
最後一行看起來有點古怪. 還記得在 changeValue: 方法裡, 使用 slider 最新的數值
來執行濾鏡並依此更新 image view 的結果?
是的, 我們必需再次這麼做, 只要呼叫 changeValue: 方法. 即使 slider 的值沒有變動,
仍然可以使用這個方法來讓工作完成.
編譯並執行, 你將可以從相簿來選取照片並作濾鏡處理.
那是少數人會去做的事. 因此接下來, 來學學如何將照片存回相簿.
---------------------------------------------------------------------------------------------
G. 存回相簿
1. 有件事你必須知道, 當你將照片存回相簿時, 它是一個持續性的過程, 甚至在離開 app 後都可以繼續進行.
當 GPU 停止時, 這可能會是個問題, 不管怎樣, 當我們切換到別的 app 時, 它仍然
在處理中. 假設照片沒有完成儲存, 當我們回到相簿時, 將不會看到它.
解決辦法是使用 CPU 的 CIRendering context. 預設是使用 GPU, 所以我們需要做
一點更改來讓 CIContext 使用 CPU. 使用 CPU 的另外一個優點是對於 texture size
沒有限制 (GPU 是有限制的, 可使用 inputImageMaximumSize 與
-outputImageMaximumSize 來查詢目前的設備). GPU 提供較佳的效能, 因此對於
相片而言, 較常使用 CPU (單一的影像是較串流的影像來得容易); 對於影片而言,
GPU 是較佳的選擇.
2. 在 context 中使用 CPU, 必須在 CIContext 初始化時, 加入 options 參數. 記得嗎?
之前是傳入 nil. 好的, 在 ViewController.m 檔案中的 viewDidLoad: 方法修改如下:
//context = [CIContext contextWithOptions:nil];
//@update
context = [CIContext contextWithOptions:[NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:kCIContextUseSoftwareRenderer]];
恭喜, 現在你是使用 CPU 去執行 CI 計算. 然而請注意, software rendering 在模擬器
上是不會運作的.
3. 幫 app 新增一個按鈕, 來讓我們將目前所修改的照片儲存起來. 開啓
ViewController.xib 檔案, 拖拉一個 button, 並連結到一個新的 savePhoto: 方法,
就如如之前所做的.
4. 先幫專案加入 AssetsLibrary framework, 這是用來存取被影像應用程式所管理的
照片或影片. 在 ViewController.m 檔案加入以下的 #import
//@add
#import <AssetsLibrary/AssetsLibrary.h>
- (IBAction)savePhoto:(id)sender {
CIImage *saveToSave = [filter outputImage];
CGImageRef cgImg = [context createCGImage:saveToSave fromRect:[saveToSave extent]];
ALAssetsLibrary* library = [[ALAssetsLibrary alloc] init];
[library writeImageToSavedPhotosAlbum:cgImg
metadata:[saveToSave properties]
completionBlock:^(NSURL *assetURL, NSError *error) {
CGImageRelease(cgImg);
}];
}
在這段程式碼區塊中, 作了以下的處理:
a. 從 filter 取得 CIImage 的輸出.
b. 產生 CGImageRef.
c. 將 CGImageRef 儲存到相簿裡.
d. 釋放 CGImage. 這最後一個步驟是一個 callback block, 當處理完畢時才會觸發.
6. 編譯並執行 app, 現在你就可以將完美的照片儲存到相簿裡, 並永久保存 !
---------------------------------------------------------------------------------------------
H. Image Metadata 是什麼?
1. 來談一下 image metadata. 從移動式電話取得的影像, 會包含多樣的資料, 例如:
GPS 座標, 影像格式, 與定位. 當我們將影像檔儲存到相簿時, 會想要保存這些屬性.
2. 這些 metadata 是隨著 CIImage 關聯在一起的, 且能夠藉由 properties 方法來存取到.
在之前的程式碼裡, 藉由傳入 CIImage metadata 到 ALAssetsLibrary 的儲存函式中.
[library writeImageToSavedPhotosAlbum:cgImg
metadata:[saveToSave properties]
completionBlock:^(NSURL *assetURL, NSError *error) {
CGImageRelease(cgImg);
}];
I. 還有哪些濾鏡可以使用?
1. 在 CIFilter API 中, 有 130 個濾鏡可以在 Mac OS 上使用, 再加上能夠建立客制化的
濾鏡. 而在 iOS 平台上, 目前只有 48 個或更多可用, 原因是濾鏡才剛開始被加入.
目前沒有方式可以在 iOS 平台上建立客制化的濾鏡(應該是指 Core Image), 但是
有可能未來會提供.
2. 為了要查出有哪些濾鏡可用, 可以使用
[CIFilter filterNamesInCategory:kCICategoryBuiltIn] 方法. 這個方法會回傳一個
包含濾鏡名稱的陣列. 另外, 每個濾鏡都有一個 attributes 方法, 會回傳一個
有關此濾鏡資訊的字典(dictionary). 資訊包含了: 濾鏡的名稱, 濾鏡接受輸入的種類,
預設及可接受的輸入值, 以及濾鏡的類型.
3. 讓我們將它們組成一個類別方法, 可用來列印出所有目前可用的濾鏡訊息到 log.
將下面這個類別方法加到 ViewController.m 檔案中 viewDidLoad: 方法的上方.
-(void)logAllFilters { NSArray *properties = [CIFilter filterNamesInCategory: kCICategoryBuiltIn]; NSLog(@"%@", properties); for (NSString *filterName in properties) { CIFilter *fltr = [CIFilter filterWithName:filterName]; NSLog(@"%@", [fltr attributes]); } }這個方法, 簡單地從 filterNamesInCategory 方法得到整個濾鏡陣列.
首先列印出濾鏡名稱的清單. 然後針對每個濾鏡名稱, 建立該濾鏡並且從該濾鏡
列印出屬性字典(attributes dictionary).
4. 接著, 在 viewDidLoad: 方法的最後呼叫它.
[self logAllFilters];
輸出結果如下:
2011-12-31 20:58:55.435 CITest[2959:707] (
CIAdditionCompositing,
CIAffineTransform,
CICheckerboardGenerator,
CIColorBlendMode,
....
)
2011-12-31 20:58:55.440 CITest[2959:707] {
CIAttributeFilterCategories = (
CICategoryCompositeOperation,
CICategoryVideo,
CICategoryStillImage,
CICategoryInterlaced,
CICategoryNonSquarePixels,
CICategoryHighDynamicRange,
CICategoryBuiltIn
);
CIAttributeFilterDisplayName = Addition;
CIAttributeFilterName = CIAdditionCompositing;
inputBackgroundImage = {
CIAttributeClass = CIImage;
CIAttributeType = CIAttributeTypeImage;
};
inputImage = {
CIAttributeClass = CIImage;
CIAttributeType = CIAttributeTypeImage;
};
}
....