If you're new here, you may want to subscribe to my
RSS feed or follow me on
Twitter. Thanks for visiting!
跟着這個教程,你會經過實際動手的經驗來學習Core Image技術,親身體驗如何應用一些不一樣的濾鏡來實時地產生各類神奇的效果。 Core Image是一個很強大的框架。它可讓你簡單地應用各類濾鏡來處理圖像,好比修改鮮豔程度, 色澤, 或者曝光。 它利用GPU(或者CPU,取決於客戶)來很是快速、甚至實時地處理圖像數據和視頻的幀。多個Core Image濾鏡能夠疊加在一塊兒,從而能夠一次性地產生多重濾鏡效果。這種多重濾鏡的優勢在於它能夠生成一個改進的濾鏡,從而一次性的處理圖像達到目標效果,而不是對同一個圖像順序地屢次應用單個濾鏡。每個濾鏡都有屬於它本身的參數。這些參數和濾鏡信息,好比功能、輸入參數等均可以經過程序來查詢。用戶也能夠來查詢系統從而獲得當前可用的濾鏡信息。到目前爲止,Mac上只有一部分Core Image濾鏡能夠在iOS上使用。可是隨着這些可以使用濾鏡的數目愈來愈多,API能夠用來發現新的濾鏡屬性。
Core Image 總覽
開始以前,讓咱們談談Core Image框架中最重要的幾個類:
- CIContext. 全部圖像處理都是在一個CIContext 中完成的,這很像是一個Core Image處理器或是OpenGL的上下文。
- CIImage. 這個類保存圖像數據。它能夠從UIImage、圖像文件、或者是像素數據中構造出來。
- CIFilter. 濾鏡類包含一個字典結構,對各類濾鏡定義了屬於他們各自的屬性。濾鏡有不少種,好比鮮豔程度濾鏡,色彩反轉濾鏡,剪裁濾鏡等等。
在新建一個項目過程當中,你會依次用到這些類。
讓咱們開始吧
打開Xcode, 用iOSApplicationSingle View Application 模板建立一個項目。輸入CoreImageFun做爲產品的名字。選擇iPhone做爲設備類型,而且確保只勾選Use Storyboards和Use Automatic Reference Counting兩個選項。 首先,讓咱們導入Core Image框架。在Mac上,這個過程是QuartzCore框架的一部分;可是在iOS上,這個是單獨的一個框架。在左側文件導航欄中,進入項目文件夾。選擇Build Phases標籤頁,擴展Link Binaries和Library group, 點擊「+」來添加按鈕;找到CoreImage框架而且雙擊完成添加。 第二步,下載
教程資源,把其中的image.png添加到項目中,咱們的建立設置就完成了。 以後,打開MainStoryboard.storyboard, 把圖像視圖拖拽到視圖控制器中,並把它的模式設定爲Aspect Fit使得它的位置和維度近似以下圖所示:

同時,打開Assistant Editor,確保編輯器顯示ViewController.h, 並從UIImageView拖拽到@interface如下。把Connection設置到指向Outlet, 並命名爲imageView,以後點擊Connect進行鏈接。 編譯運行來確保到目前爲止每一步都是正確的。若是一切正常,你將會看到一個空屏幕。這時,咱們的初始化設置就完成了,下面咱們就進入Core Image部分!
基本的圖像濾鏡
做爲第一個嘗試,咱們先簡單的讓圖像經過一個CIFilter 以後顯示在屏幕上。每一次當咱們想應用一個CIFilter的時候都要有如下四個步驟:
- 建立一個 CIImage 對象: CIImage 有以下的初始化方法: imageWithURL:, imageWithData:, imageWithCVPixelBuffer:, 和 imageWithBitmapData:bytesPerRow:size:format:colorSpace:。可是大多數時候你只會常常用到imageWithURL。
- 建立一個 CIContext: 一個 CIContext 能夠是基於CPU或是GPU的。它能夠被重用,因此你不用每次都建立一個。可是當輸出CIImage對象的時候你至少必定會須要一個CIContext。
- 建立一個CIFilter: 當你建立濾鏡的時候,你能夠在上面配置必定數量的屬性。具體的屬性取決於你所要用的濾鏡。
- 輸出濾鏡:這個濾鏡會輸出一個圖像成爲CIImage。 你能夠用CIContext把它轉化爲一個UIImage ,具體過程以下。
讓咱們看看這是如何實現的。把下面的代碼加入到viewDidLoad中的ViewController.m裏面。
// 1
NSString *filePath =
[[NSBundle mainBundle] pathForResource:@"image" ofType:@"png"];
NSURL *fileNameAndPath = [NSURL fileURLWithPath:filePath];
// 2
CIImage *beginImage =
[CIImage imageWithContentsOfURL:fileNameAndPath];
// 3
CIFilter *filter = [CIFilter filterWithName:@"CISepiaTone"
keysAndValues: kCIInputImageKey, beginImage,
@"inputIntensity", @0.8, nil];
CIImage *outputImage = [filter outputImage];
// 4
UIImage *newImage = [UIImage imageWithCIImage:outputImage];
self.imageView.image = newImage; |
讓咱們依次看看這些代碼都作了什麼事情
- 前兩行建立了一個NSURL 對象, 包含指向圖形文件的路徑。
- 下面,用imageWithContentsOfURL方法建立CIImage。
- 以後,建立CIFilter對象。一個 CIFilter 構造函數有兩個輸入,分別是濾鏡的名字,還有規定了濾鏡屬性的鍵值和取值的字典。 每個濾鏡會有它本身惟一的鍵值和一組有效的取值。CISepiaTone 濾鏡只能選兩個值: KCIInputImageKey (一個CIImage) 和 @」inputIntensity」。 後者是一個封裝成NSNumber (用新的文字型語法)的浮點小數,取值在0和1 之間。大部分的濾鏡有默認值,只有CIImage是個例外。你必須提供一個值給它,由於它沒有默認值。從濾鏡中導出CIImage很簡單,只須要用outputImage方法。
- 一旦你有了導出的 CIImage,你就能夠把它轉化爲一個 UIImage。 在新的iOS6中,UIImage 方法+ imageWithCIImage方法能夠實現從CIImage 到UIImage 到轉化。一旦轉化完成,咱們就可讓UIImage 顯示在以前添加的圖像視圖裏。
編輯運行項目,你將會看到你的圖片以下圖通常,已經被墨色調濾鏡處理過。恭喜你,你已經成功掌握並運用了CIImage和CIFilters。
把它放在上下文中
在進行下一步以前,有一個優化的方法很實用。我前面提到過,你須要一個CIContext來進行CIFilter,可是在上面的例子中咱們沒有提到這個對象。由於咱們調用的UIImage方法(imageWithCIImage)已經自動地爲咱們完成了這個步驟。它生成了一個CIContext而且用它來處理圖像的過濾。這使得調用Core Image的接口變得很簡單。 可是,有一個主要的問題是,它的每次調用都會生成一個CIContext。CIContext原本是能夠重用以便提升性能和效率的。好比下面咱們要談到的例子,若是你想用滑動條來選擇過濾參數取值,每次改變濾鏡參數都會自動生成一個CIContext, 使得性能很是差。 讓咱們想個好辦法搞定這個問題。刪除你以前添加到viewDidLoad裏面的代碼,用下面的代碼取而代之:
CIImage *beginImage =
[CIImage imageWithContentsOfURL:fileNameAndPath];
// 1
CIContext *context = [CIContext contextWithOptions:nil];
CIFilter *filter = [CIFilter filterWithName:@"CISepiaTone"
keysAndValues: kCIInputImageKey, beginImage,
@"inputIntensity", @0.8, nil];
CIImage *outputImage = [filter outputImage];
// 2
CGImageRef cgimg =
[context createCGImage:outputImage fromRect:[outputImage extent]];
// 3
UIImage *newImage = [UIImage imageWithCGImage:cgimg];
self.imageView.image = newImage;
// 4
CGImageRelease(cgimg); |
再讓我逐步解釋一下這部分代碼
- 在這部分代碼中,你建立了CIContext對象。CIContext 構造函數的輸入是一個NSDictionary。 它規定了各類選項,包括顏色格式以及內容是否應該運行在CPU或是GPU上。對於這個應用程序,默認值是能夠用的。因此你只須要傳入nil做爲參數就行了。
- 在這裏你用上下文對象裏的一個方法來畫一個CGImage。 調用上下文中的createCGImage:fromRect:和提供的CIImage能夠生成一個CGImageRef。
- 下面,你用UIImage + imageWithCGImage,從CGImage中建立一個UIImage。
- 最後,開放 CGImageRef接口。 CGImage 是一個C接口,即便有ARC,也須要你本身來作內存管理。
編譯運行,確保正常工做。 在這個例子中,添加CIContext的建立 和你本身來建立的區別不大。可是在下一部分中,你將會看到當你實現動態改變濾鏡參數的時候的重大性能差異。
改變濾鏡的取值
上面能夠看到,Core Image濾鏡很好用,可是這些只是很是初級的應用。讓咱們添加一個滑動條使得咱們可以實時動態地調整圖像設置。 打開MainStoryboard.storyboard, 拖拽一個滑動條到圖像窗口的下部 (以下圖)。

確保Assistant Editor 是可見的而且顯示ViewController.h。 控制@interface下的滑動條。把Connection設置到Action,把名字設置成amountSliderValueChanged, 把Event設置成Value Changed,接下來讓咱們把滑動條鏈接到輸出。再一次控制@interface下的滑動條, 可是這一次把Connection設置到Outlet,把名字設置成amountSlider, 以後點擊 Connect。 每一次滑動條改變位置,你須要從新用新的值進行圖像過濾。可是你必定不想每次都重作整個過程,那將會很是的低效。你其實只須要在你的類中改變一小部分,從而使得你已經在viewDidLoad方法中建立的對象還能繼續被使用。最重要的一步是在任何須要被用到的地方屢次重用CIContext。若是你每次都從新建立它,你的程序將會很是地慢。另外一步優化是你能夠保存CIFilter和存有初始圖像的CIImage。對每個輸出你都須要生成一個新的CIFilter,可是每次初始用到的圖像始終是同一個。 你須要添加一些實例變量來完成這個任務。 把下面的3個實例變量添加到ViewController.m裏你本身的@implementation中。
@implementation ViewController {
CIContext *context;
CIFilter *filter;
CIImage *beginImage;
} |
而且, 改變viewDidLoad方法中的變量, 使得他們調用實例變量,而不是聲明新的本地變量:
beginImage = [CIImage imageWithContentsOfURL:fileNameAndPath];
context = [CIContext contextWithOptions:nil];
filter = [CIFilter filterWithName:@"CISepiaTone"
keysAndValues:kCIInputImageKey, beginImage, @"inputIntensity",
@0.8, nil]; |
如今,你將實現changeValue方法來實現改變CIFilter 字典中@」inputIntensity」鍵值的功能。在咱們實現了這個改變以後,你還須要重複以下一些步驟:
- 從CIFilter 中獲得CIImage
- 把CIImage轉化成 CGImageRef.
- 把CGImageRef 轉化成UIImage, 在圖像視圖中顯示出來。
因此用以下的部分替換amountSliderValueChanged方法:
- (IBAction)amountSliderValueChanged:(UISlider *)slider {
float slideValue = slider.value;
[filter setValue:@(slideValue)
forKey:@"inputIntensity"];
CIImage *outputImage = [filter outputImage];
CGImageRef cgimg = [context createCGImage:outputImage
fromRect:[outputImage extent]];
UIImage *newImage = [UIImage imageWithCGImage:cgimg];
self.imageView.image = newImage;
CGImageRelease(cgimg);
} |
你將會注意到,在方法定義中,你已經把變量類型從(id)轉化成了(UISlider *)。 你知道你只會用這個方法來從你的UISlider中得到數據,因此你能夠作這個轉變,不會影響其餘的部分。若是咱們保持變量類型爲(id)不變, 則必須把它轉化爲UISlider,不然下一行將會報錯。確保頭文件中的聲明也作了相應修改。 你能夠從滑動條中獲取浮點數。滑動條有相應的默認設置 – 最小值0,最大值0,默認值0.5。這恰好是這個CIFilter的合理取值,簡直太方便了! CIFilter 有相應的方法能夠任由咱們在字典中設置不一樣鍵值的取值。在這裏你只須要把@」inputIntensity」 鍵設置成一個NSNumber 對象,它的取值是你從滑動條上獲得的任意浮點數。 代碼的其餘部分應該看上去很像,由於都是遵循和viewDidLoad方法一樣的邏輯。你將會反覆重用這些代碼。從如今開始,你將用changeSlider方法來爲UIImageView提供CIFilter輸出。 編譯運行,你將會獲得一個能夠實時改變圖片墨色調數值的滑動條!
從相冊中讀取照片
既然你如今能夠改變濾鏡的取值, 真正有趣的東西纔剛剛開始。若是你不想要這幅花朵的圖像怎麼辦呢?讓咱們創建一個UIImagePickerController, 使得你能夠任意從相冊中選取圖片讀取到你的項目中來進行任意修改。你須要建立一個按鈕來打開相冊視圖。因此打開ViewController.xib,把一個按鈕拖拽到滑動條的右下方,且命名爲「相冊」(Photo Album)

確保Assistant Editor 是可見的而且顯示ViewController.h。 控制@interface下的按鈕。把Connection設置到Action,把名字設置成loadPhoto, 把Event設置成Touch Up Inside,以後點擊Connect。 接下來切換到ViewController.m,實現loadPhoto方法以下:
- (IBAction)loadPhoto:(id)sender {
UIImagePickerController *pickerC =
[[UIImagePickerController alloc] init];
pickerC.delegate = self;
[self presentViewController:pickerC animated:YES completion:nil];
} |
第一行代碼實例化一個新的UIImagePickerController。以後,你設置圖像選取代理爲ViewController。在這裏,你將會看到一個警告消息。你須要把ViewController設置爲UIImagePickerControllerDelegate和UINaviationControllerDelegate,而且在代理協議下實現全部的方法。 仍是在ViewController.m中,改變類型拓展以下:
@interface ViewController () <UIImagePickerControllerDelegate, UINavigationBarDelegate>
@end |
如今實現下面的兩個方法:
- (void)imagePickerController:(UIImagePickerController *)picker
didFinishPickingMediaWithInfo:(NSDictionary *)info {
[self dismissViewControllerAnimated:YES completion:nil];
NSLog(@"%@", info);
}
- (void)imagePickerControllerDidCancel:
(UIImagePickerController *)picker {
[self dismissViewControllerAnimated:YES completion:nil];
} |
在兩個方法裏,你摒除了UIPickerController,而用新的代理來完成相應的功能。若是你在代理中沒有相應的實現,那你就只有一直瞪着圖像選擇器發呆了。第一個方法是不完整的, 它只是一個標誌位,用來註銷所選圖像的信息。imagePickerControllerDidCancel方法用來清除PickerController。 編譯運行,按那個「相冊」(Photo Album)按鈕, 圖像選擇器就會跳出來,顯示你相冊裏全部的照片。若是你在模擬器上運行, 可能就不會看到任何照片。在模擬器或者沒有照相機的設備上,你能夠用Safari瀏覽器保存照片到你的相冊。打開Safari瀏覽器, 找到一個圖片, 按住過一會,就會彈出一個對話框讓你保存圖片。下次你運行你的應用程序,就會看到這個圖片了。 下面是在當你選定一個圖片以後,控制檯中應該顯示的信息(會根據所選圖片內容相應有所不一樣):
2012-09-20 17:30:52.561 CoreImageFun[3766:c07] {
UIImagePickerControllerMediaType = "public.image";
UIImagePickerControllerOriginalImage = "";
UIImagePickerControllerReferenceURL = "assets-library://asset/asset.JPG?
id=253312C6-A454-45B4-A9DA-649126A76CA5&ext=JPG";
}
注意,在字典中有一個字段就是專門爲被選擇的原始圖片而設置的。這個字段就正是你須要取出而且過濾的! 既然咱們已經知道怎麼選取一個圖片,那麼咱們怎麼設置CIImage beganImage來調用這個圖片呢? 簡單!只須要以下修改代理的方法:
- (void)imagePickerController:(UIImagePickerController *)picker
didFinishPickingMediaWithInfo:(NSDictionary *)info {
[self dismissViewControllerAnimated:YES completion:nil];
UIImage *gotImage =
[info objectForKey:UIImagePickerControllerOriginalImage];
beginImage = [CIImage imageWithCGImage:gotImage.CGImage];
[filter setValue:beginImage forKey:kCIInputImageKey];
[self amountSliderValueChanged:self.amountSlider];
} |
你須要從你選擇的圖片中建立一個新的CIImage。在UIImagePickerControllerOriginalImage鍵值是個常數的狀況下,你能夠經過尋找字典中的取值獲得圖片的 UIImage 代理。注意最好用一個常數,而不是一個硬編碼的字符串,由於Apple能夠在將來改變鍵的名字。從UIImagePickerController代理協議參考中你能夠找到全部的常數鍵。 你須要轉化這些成爲一個 CIImage,可是並無一個方法能夠把一個 UIImage轉化成一個CIImage。然而你有[CIImage imageWithCGImage:] 方法。它能夠經過調用UIImage.CGImage來從UIImage中獲得CIImage。那麼你徹底能夠作同樣的事情! 因而你設置濾鏡字典中的相應鍵,使得導入的圖片正是你剛剛常見的CIImage。 最後一行可能看起來會很奇怪。還記得我是怎麼闡述changeView的代碼是用最新的值來運行濾鏡,而且根據運行結果更新圖像視圖的嗎? 你須要再作一遍這個工做,因此你只須要調用一遍changeValue方法。即便滑動條的值沒有改變,你仍然可使用哪一個方法的代碼來完成這個工做。 你能夠拆開那部分代碼造成單獨的方法。並且隨着你作的事情愈來愈複雜,你也但願用這種方式儘可能避免混淆。可是就當前這個問題而言,你的目的只是想用changeValue方法,因此你傳入amountSlider,獲得正確的值就行了。 編譯運行,你如今就能夠編輯更新你相冊裏的任意圖片或照片了。

在把你的圖片作了墨色調處理以後,怎麼保持它呢。你能夠截屏,可是你沒那麼土!讓咱們學學如何保存處理後的圖片到你的相冊裏。
保存到相冊
爲了保存到相冊,你須要一個AssetsLibrary框架。進入到項目容器裏,選擇Build Phases標籤頁,擴展Link Binaries和Library group, 點擊「+」來添加按鈕。找到AssetsLibrary框架,選擇進行添加。 以後把下面的#import 內容添加到ViewController.m的頂部。
#import <AssetsLibrary/AssetsLibrary.h> |
你須要明白一件事情,那就是當你保存一張照片到相冊的時候,即便你退出了這個應用,這個過程仍然能夠繼續。 這點可能會致使一些問題,由於GPU在當你切換應用的時候會中止當前的工做。若是照片尚未保存完畢就退出了程序,那可能之後就找不到這個要保存的照片了。 對於這個問題的解決方法是利用CPU的CIRendering上下文。然而默認設備是GPU,並且GPU比CPU快不少。因此你其實能夠建立第二個CIContext,只爲了保存這個圖片。 讓咱們添加一個新按鈕來實現對當前編輯照片的保存。打開MainStoryboard, 添加一個新按鈕,標記爲「保存」(Save to Album)。

以後把這個按鈕鏈接到一個新的savePhoto方法, 就像你剛作完的過程同樣。以後切換到ViewController.m 而且按照以下代碼實現這個方法:
- (IBAction)savePhoto:(id)sender {
// 1
CIImage *saveToSave = [filter outputImage];
// 2
CIContext *softwareContext = [CIContext
contextWithOptions:@{kCIContextUseSoftwareRenderer : @(YES)} ];
// 3
CGImageRef cgImg = [softwareContext createCGImage:saveToSave
fromRect:[saveToSave extent]];
// 4
ALAssetsLibrary* library = [[ALAssetsLibrary alloc] init];
[library writeImageToSavedPhotosAlbum:cgImg
metadata:[saveToSave properties]
completionBlock:^(NSURL *assetURL, NSError *error) {
// 5
CGImageRelease(cgImg);
}];
} |
在這段代碼中:
- 從濾鏡中獲得CIImage輸出
- 建立一個新的、基於軟件的CIContext
- 生成CGImageRef.
- 保存CGImageRef 到圖片庫
- 釋放CGImage。最後一步在回調部分發生,使得只有在完成以後纔會用到它。
編譯而且在真正的設備上運行這個應用,你就能夠永久保存你想要的圖片到相冊裏。
圖像元數據怎麼處理呢?
讓咱們簡單談談圖像的元數據。移動電話上拍攝的圖像文件有一系列的數據相關聯,好比GPS座標,圖像格式,圖像朝向等等。具體來講,圖像的朝向是你須要保存的數據。加載原始圖像到CIImage,轉化爲CGImage, 進而轉化爲UIImage的過程去除掉了原始圖像的元數據。爲了保存圖像的朝向,你須要記錄而且恢復這些相關圖像信息到UIImage。你能夠經過添加一個新的私有實例變量到ViewController.m當中來達到這個目的。
@implementation ViewController {
CIContext *context;
CIFilter *filter;
CIImage *beginImage;
UIImageOrientation orientation; // New!
} |
下一步,當從相冊里加載原始圖像的時候,能夠經過imagePickerController: didFinishPickingMediaWithInfo方法設定相應的元數據值。把下面幾行代碼加入到 「beginImage = [CIImage imageWithCGImage:gotImage.CGImage]」 這一行代碼的前面:
orientation = gotImage.imageOrientation; |
最終,改變amountSliderChanged中的代碼,建立imageView對象中設定的UIImage:
UIImage *newImage = [UIImage imageWithCGImage:cgimg scale:1.0 orientation:orientation]; |
如今,若是你用非默認的朝向照一張照片, 這個朝向信息將會被保存下來。
還有其餘什麼濾鏡能夠用嗎?
CIFilter 接口在Mac OS上有130個濾鏡,外加能夠定製濾鏡的能力。 在iOS6中,有93個或更多;可是目前還不能實如今iOS平臺上對濾鏡的定製。但願之後能夠作到。 爲了找到可用的濾鏡信息,你能夠利用 [CIFilter filterNamesInCategory:kCICategoryBuiltIn] 方法。 這個方法會返回一列可用濾鏡的名字。並且,每個濾鏡都有一個屬性方法來返回一個包含濾鏡信息的字典結構。這些信息包括濾鏡的名字,濾鏡的分類,濾鏡的輸入以及輸入的默認值和可接受的值範圍。 讓咱們爲你的類整理出一個方法。調用這個方法能夠在日誌文件中打印出全部可用濾鏡信息。把下面這個方法加入到viewDidLoad的上面:
-(void)logAllFilters {
NSArray *properties = [CIFilter filterNamesInCategory:
kCICategoryBuiltIn];
NSLog(@"%@", properties);
for (NSString *filterName in properties) {
CIFilter *fltr = [CIFilter filterWithName:filterName];
NSLog(@"%@", [fltr attributes]);
}
} |
這個方法從filterNamesInCategory方法中獲取可用濾鏡的名字,先打印名字,以後對於在列表上的每個名字,建立一個相應的濾鏡,而且記錄該濾鏡中的屬性字典。以後在viewDidLoad的底部調用下面這個方法:
你將會在輸出中看到下面的內容:

天啊,簡直有太多的濾鏡了!
更復雜的濾鏡鏈
既然咱們已經學習了iOS6平臺上全部可用的濾鏡, 咱們能夠進一步看看如何建立一個更復雜的濾鏡鏈。爲了達到這個目的,咱們須要建立一個專門的方法來處理CIImage。它將導入CIImage,過濾處理,以後返回一個CIImage。添加以下的方法:
-(CIImage *)oldPhoto:(CIImage *)img withAmount:(float)intensity { // 1 CIFilter *sepia = [CIFilter filterWithName:@"CISepiaTone"]; [sepia setValue:img forKey:kCIInputImageKey]; [sepia setValue:@(intensity) forKey:@"inputIntensity"]; // 2 CIFilter *random = [CIFilter filterWithName:@"CIRandomGenerator"]; // 3 CIFilter *lighten = [CIFilter filterWithName:@"CIColorControls"]; [lighten setValue:random.outputImage forKey:kCIInputImageKey]; [lighten setValue:@(1 - intensity) forKey:@"inputBrightness"]; [lighten setValue:@0.0 forKey:@"inputSaturation"]; // 4 CIImage *croppedImage = [lighten.outputImage imageByCroppingToRect:[beginImage extent] |