老驥伏櫪价脾,志在千里
前記
最近一直在研究圖像處理方面拂到,既上一篇iOS Quart2D繪圖之UIImage簡(jiǎn)單使用后帅刊,就一直在學(xué)習(xí)關(guān)于CoreImage
圖像濾鏡處理走芋。中間也看了不少文章,也得到了不少幫助绩蜻,下面就結(jié)合這些知識(shí)和我自己的認(rèn)識(shí)铣墨,記錄一下,方便自己办绝,方便他人
- 作用:對(duì)圖像進(jìn)行濾鏡操作伊约,比如模糊、顏色改變孕蝉、銳化屡律、人臉識(shí)別等。
-
Core Graphics
對(duì)比:基于Quartz 2D
繪圖引擎的繪圖API
降淮,通過(guò)它可以進(jìn)行繪圖功能超埋、常用的剪切裁剪合成等搏讶。
簡(jiǎn)介
Core Image
是一個(gè)很強(qiáng)大的框架。它可以讓你簡(jiǎn)單地應(yīng)用各種濾鏡來(lái)處理圖像霍殴,比如修改鮮艷程度, 色澤, 或者曝光媒惕。 它利用GPU
(或者CPU
)來(lái)非常快速来庭、甚至實(shí)時(shí)地處理圖像數(shù)據(jù)和視頻的幀妒蔚。并且隱藏了底層圖形處理的所有細(xì)節(jié),通過(guò)提供的API就能簡(jiǎn)單的使用了月弛,無(wú)須關(guān)心OpenGL
或者OpenGL ES
是如何充分利用GPU
的能力的肴盏,也不需要你知道GCD
在其中發(fā)揮了怎樣的作用,Core Image
處理了全部的細(xì)節(jié)
實(shí)現(xiàn)方式
Core Image
濾鏡需要一副輸入圖像(生成圖像的濾鏡除外)以及一些定制濾鏡行為的參數(shù)帽衙。被請(qǐng)求時(shí)菜皂,Core Image
將濾鏡應(yīng)用于輸入圖像,并提供一副輸出圖像厉萝。在應(yīng)用濾鏡方面恍飘,Core Image
的效率極高:僅當(dāng)輸出圖像被請(qǐng)求時(shí)才應(yīng)用濾鏡,而不是在指定時(shí)就應(yīng)用它們谴垫;另外常侣,Core Image
盡可能將濾鏡合并,以最大限度地減少應(yīng)用濾鏡的計(jì)算量弹渔。
涉及API
-
CIImage
:這是一個(gè)模型對(duì)象,它保存能構(gòu)建圖像的數(shù)據(jù)溯祸,可以是圖像的Data
肢专,可以是一個(gè)文件,也可以是CIFilter
輸出的對(duì)象焦辅。 -
CIContext
:上下文博杖,是框架真正工作的地方,它需要分配必要的內(nèi)存筷登,并編譯和運(yùn)行濾鏡內(nèi)核來(lái)執(zhí)行圖像處理剃根。建立一個(gè)上下文是非常昂貴的,所以你會(huì)經(jīng)常想創(chuàng)建一個(gè)反復(fù)使用的上下文前方。 -
CIFilter
:濾鏡對(duì)象狈醉,主要是對(duì)圖像進(jìn)行處理的類(lèi)。通過(guò)設(shè)置一些鍵值來(lái)控制濾鏡的具體效果
注:Core Image
和Core Graphics
使用的是左下原點(diǎn)坐標(biāo)
到此有一個(gè)疑問(wèn)惠险?就是蘋(píng)果怎么會(huì)弄出這么多image
苗傅,比如CIImage
、UIImage
班巩、CGImageRef
渣慕,有什么區(qū)別呢?為了弄清這個(gè)問(wèn)題,我也特別搜尋了一番逊桦,下面也記錄一下
①UIImage
:管理圖片數(shù)據(jù)眨猎,主要用來(lái)展現(xiàn),Image
對(duì)象并沒(méi)有提供直接訪問(wèn)相關(guān)的圖片數(shù)據(jù)的操作, 因此你總是通過(guò)已經(jīng)存在的圖片數(shù)據(jù)來(lái)創(chuàng)建它
②CGImage
:是基于像素的矩陣强经,每個(gè)點(diǎn)都對(duì)應(yīng)了圖片中點(diǎn)的像素信息
③CIImage
:包含了創(chuàng)建圖片的所有必要的數(shù)據(jù)睡陪,但其本身沒(méi)有渲染成圖片,它代表的是圖像數(shù)據(jù)或者生成圖像數(shù)據(jù)的流程(如濾鏡)夕凝。擁有與之關(guān)聯(lián)的圖片數(shù)據(jù), 但本質(zhì)上并不是一張圖片宝穗,你可以CIImage
對(duì)象作為一個(gè)圖片的"配方"。CIImage
對(duì)象擁有生成一張圖片所具備的所有信息码秉,但Core Image
并不會(huì)真正的去渲染一張圖片, 除非被要求這么做逮矛。
使用方式
- 1 .
CIImage
創(chuàng)建,在使用濾鏡之前转砖,你必須要先有一個(gè)CIImage
對(duì)象须鼎,在擁有該對(duì)象后結(jié)合CIFilter
才能實(shí)現(xiàn)我們的濾鏡效果。這里需要注意的是府蔗,如果直接使用image.cIImage
晋控,那么很遺憾的告訴你,你將得到一個(gè)nil
姓赤,哈哈
如下:
原因在
UIImage
的API
中有介紹// returns underlying CIImage or nil if CGImageRef based
赡译,應(yīng)該是說(shuō)圖片可能不是基于CIImage
而創(chuàng)建的正確的方式為
//得到CIImage
CIImage *inputCIImage = [[CIImage alloc] initWithImage:_image];
- 2 .
CIFilter
創(chuàng)建方式大概有下面三種
+(nullable CIFilter *) filterWithName:(NSString *) name
+(nullable CIFilter *)filterWithName:(NSString *)name
keysAndValues:key0, ... NS_REQUIRES_NIL_TERMINATION NS_SWIFT_UNAVAILABLE("");
+(nullable CIFilter *)filterWithName:(NSString *)name
withInputParameters:(nullable NSDictionary<NSString *,id> *)params NS_AVAILABLE(10_10, 8_0);
方法上都差不多,只是后面兩個(gè)在初始化的時(shí)候加入了一些鍵值不铆,在API
文檔中蝌焚,可以查到很多鍵值,這里需要說(shuō)明下誓斥,鍵值kCIInputImageKey
是我們必須要設(shè)置的只洒,這是為我們的濾鏡對(duì)象設(shè)置輸入圖像,圖像的值為CIImage
對(duì)象劳坑,方法如下
[_filter setValue:inputCIImage forKey:kCIInputImageKey];
方法中的name
就是我們需要用的濾鏡效果毕谴,具體效果,可以在官網(wǎng)上面進(jìn)行查詢距芬,如下
下面涝开,我們以沖印效果為例,沖印屬于
CICategoryColorEffect
中的CIPhotoEffectProcess
//創(chuàng)建濾鏡對(duì)象
CIFilter *ciFilter = [CIFilter filterWithName:@"CIPhotoEffectProcess" keysAndValues:kCIInputImageKey,ciImage, nil];
大概效果如下
注意:
1蔑穴、在設(shè)置鍵值的時(shí)候忠寻,我們需要有選擇性的進(jìn)行設(shè)置,具體怎么選擇呢存和?
比如上面的沖印效果奕剃,在官方文檔是這么展示的
只有一個(gè)必須輸入的
inputImage
衷旅,因此不需要其它參數(shù)就可以實(shí)現(xiàn)又比如高斯模糊
CIGaussianBlur
,在官方文檔中纵朋,是這么展示的
如果我們需要控制其模糊半徑柿顶,可以這么設(shè)置
[ciFilter setValue:@(20.f) forKey:@"inputRadius"];
2、CIFilter
并不是線程安全的操软,這意味著 一個(gè) CIFilter
對(duì)象不能在多個(gè)線程間共享嘁锯。如果你的操作是多線程的,每個(gè)線程都必須創(chuàng)建自己的 CIFilter
對(duì)象聂薪,而CIContext
和CIImage
對(duì)象都是不可修改的, 意味著它們可以在線程之間安全的共享家乘。多個(gè)線程可以使用同樣的GPU
或者CPU
的CIContext
對(duì)象來(lái)渲染CIImage
對(duì)象
在CIFilter
類(lèi)中,還有一些其他函數(shù)藏澳,可能是我們需要用到的仁锯,這里也簡(jiǎn)單說(shuō)明下
//輸入的鍵值信息
NSArray<NSString *> *inputKeys;
//輸出的鍵值信息
NSArray<NSString *> *outputKeys;
//返回濾鏡的屬性描述信息
NSDictionary<NSString *,id> *attributes;
//將所有輸入鍵值的值設(shè)為默認(rèn)值(曾經(jīng)亂用,導(dǎo)致我的濾鏡效果完全沒(méi)有任何反應(yīng)翔悠,差點(diǎn)懷疑人生...)
- (void)setDefaults;
//根據(jù)濾鏡的key查找其下面的所以子類(lèi)效果
+ (NSArray<NSString *> *)filterNamesInCategory:(nullable NSString *)category
- 3 .
CIContext
在創(chuàng)建結(jié)果圖片的時(shí)候需要用到业崖,剛開(kāi)始用的時(shí)候,出于好奇用了兩種不同的方法來(lái)返回結(jié)果蓄愁,本以為....我會(huì)有一個(gè)方式獲取不到處理后的結(jié)果双炕,然而大跌眼鏡,居然有....
CIImage *outPutImage = [ciFilter outputImage];
//獲取上下文
CIContext *context = [CIContext contextWithOptions:nil];
CGImageRef cgImage = [context createCGImage:outPutImage fromRect:outPutImage.extent];
UIImage *filter_image = [UIImage imageWithCGImage:cgImage];
CGImageRelease(cgImage);
// UIImage *filter_image = [UIImage imageWithCIImage:outPutImage];
就是上面屏蔽的代碼部分imageWithCIImage
撮抓,這就使我納悶了妇斤,于是猜測(cè)并查閱資料,原來(lái)在調(diào)用該方法的時(shí)候丹拯,其實(shí)是隱式的聲明了CIContext
趟济,這樣看來(lái),哇咽笼!好簡(jiǎn)單,省了我一堆代碼戚炫,然而剑刑,這卻引起另外的問(wèn)題了,就是每次都會(huì)重新創(chuàng)建一個(gè) CIContext
双肤,然而 CIContext
的代價(jià)是非常高的施掏。并且,CIContext
和 CIImage
對(duì)象是不可變的茅糜,在線程之間共享這些對(duì)象是安全的七芭。所以多個(gè)線程可以使用同一個(gè) GPU
或者 CPU
CIContext
對(duì)象來(lái)渲染 CIImage
對(duì)象。所以我們不應(yīng)該使用 imageWithCIImage
來(lái)生成UIImage
蔑赘,而應(yīng)該用上述另外一種方式來(lái)獲取結(jié)果圖像狸驳。
Core Image 效率
Core Image
在處理圖像的時(shí)候预明,可以有兩種選擇GPU
、CPU
耙箍,在Context
中可以對(duì)其進(jìn)行設(shè)置撰糠,通過(guò)設(shè)置鍵值,這里的鍵值為kCIContextUseSoftwareRenderer
辩昆,默認(rèn)情況下阅酪,是為GPU
處理方式,如果將其設(shè)置為YES
汁针,則為CPU
處理
如下
//CPU處理
CIContext *context = [CIContext contextWithOptions:[NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:kCIContextUseSoftwareRenderer]];
如果通過(guò)GPU
的話术辐,速度就會(huì)更快,利用了GPU
硬件的并行優(yōu)勢(shì)施无,可以使用 OpenGLES
或者Metal
來(lái)渲染圖像辉词,這種方式CPU
完全沒(méi)有負(fù)擔(dān),應(yīng)用程序的運(yùn)行循環(huán)不會(huì)受到圖像渲染的影響帆精。但是也有個(gè)問(wèn)題较屿,就是如果APP
運(yùn)行到后臺(tái)的時(shí)候,GPU
就會(huì)停止處理卓练,等回到前臺(tái)的時(shí)候又繼續(xù)隘蝎,而如果采取CPU
來(lái)處理的話,就不會(huì)出現(xiàn)這么一種情況襟企,在前面的圖中嘱么,我們可以看到CPU
是采用GCD
的方式來(lái)對(duì)圖像進(jìn)行渲染。所以在使用的時(shí)候顽悼,還是需要分情況曼振,如果是處理復(fù)雜的操作,比如高斯模糊這樣的蔚龙,建議還是用GPU
來(lái)處理冰评,可以節(jié)省CPU
的開(kāi)銷(xiāo),如果在后臺(tái)還需要操作的話木羹,可以使用CPU
來(lái)操作甲雅。
//
CIImage *outPutImage = [ciFilter outputImage];
//獲取上下文
CIContext *context = [CIContext contextWithOptions:nil];
CGImageRef cgImage = [context createCGImage:outPutImage fromRect:outPutImage.extent];
UIImage *filter_image = [UIImage imageWithCGImage:cgImage];
CGImageRelease(cgImage);
上面的這段代碼是通過(guò)GPU
的方式來(lái)處理圖像,然后得到結(jié)果UIImage
坑填,最后再賦值給UIImageView
抛人。
分析下這個(gè)過(guò)程:
1、將圖像上傳到GPU
脐瑰,然后進(jìn)行濾鏡處理
2妖枚、得到CGImageRef cgImage
的時(shí)候,又將圖像復(fù)制到了CPU
上
3苍在、在賦值給UIImageView
進(jìn)行顯示的時(shí)候绝页,又需要通過(guò)GPU
處理位圖數(shù)據(jù)荠商,進(jìn)行渲染
這樣的話,我們就在GPU
-CPU
-GPU
上循環(huán)操作抒寂,在性能上肯定是有一定的損耗的结啼,那么為了避免這種問(wèn)題,我們?cè)撨@怎么辦呢屈芜?
查看API
郊愧,我們可以看到有這么一個(gè)函數(shù)
+ (CIContext *)contextWithEAGLContext:(EAGLContext *)eaglContext
EAGLContext
:是基于OpenGL ES
的上下文
通過(guò)上面的函數(shù),我們通過(guò)OpenGL ES
的上下文創(chuàng)建的Core Image
的上下文就可以實(shí)時(shí)渲染了井佑,并且渲染圖像的過(guò)程始終在 GPU
上進(jìn)行属铁,但是要顯示圖像,又該怎么辦呢躬翁?如果還是用UIImageView
的話焦蘑,那么勢(shì)必會(huì)回到CPU
上,這里盒发,我們可以用GLKView
例嘱,一個(gè)屬于GLKIT
中的類(lèi),通過(guò)GLKView
和其屬性@property (nonatomic, retain) EAGLContext *context
來(lái)將圖像繪制出來(lái)宁舰,這樣的話拼卵,就能保證我們的濾鏡,一直在GPU
上進(jìn)行蛮艰,大大的提高效率腋腮。
針對(duì)該方案,我自定義了一個(gè)類(lèi)似UIImageView
的類(lèi)FilterImageView
//FilterImageView.h
#import <GLKit/GLKit.h>
@interface FilterImageView : GLKView
@property (nonatomic,strong) UIImage *image;
@property (nonatomic,strong) CIFilter *filter;
@end
.m
文件核心代碼
//FilterImageView.m
- (id)initWithFrame:(CGRect)frame
{
EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
self = [super initWithFrame:frame context:context];
if (self) {
_ciContext = [CIContext contextWithEAGLContext:context options:[NSDictionary dictionaryWithObject:[NSNumber numberWithBool:NO] forKey:kCIContextUseSoftwareRenderer]];
//超出父視圖 進(jìn)行剪切
self.clipsToBounds = YES;
}
return self;
}
- (void)drawRect:(CGRect)rect
{
if (_ciContext && _image) {
//得到CIImage
CIImage *inputCIImage = [[CIImage alloc] initWithImage:_image];
CGRect inRect = [self imageBoundsForContentModeWithFromRect:inputCIImage.extent
toRect:CGRectMake(0, 0, self.drawableWidth, self.drawableHeight)];
if (_filter) {
[_filter setValue:inputCIImage forKey:kCIInputImageKey];
//根據(jù)filter得到輸出圖像
if (_filter.outputImage) {
//渲染開(kāi)始
[_ciContext drawImage:_filter.outputImage
inRect:inRect
fromRect:inputCIImage.extent];
}
}else{
[_ciContext drawImage:inputCIImage
inRect:inRect
fromRect:inputCIImage.extent];
}
}
}
如此之后壤蚜,我們就能提高濾鏡的效率即寡,特別是一些復(fù)雜的。
關(guān)于濾鏡袜刷,能寫(xiě)的就只要這么多了聪富,在學(xué)習(xí)中,也確實(shí)發(fā)現(xiàn)這是一個(gè)好東西著蟹,可以做很多炫酷的東西出來(lái)善涨,為此,特意做了一個(gè)簡(jiǎn)單的Demo草则,目前還未完善,希望各位勿噴蟹漓。
參考文章
- iOS中圖形圖像處理第一部分:位圖圖像原圖修改 主要是講解圖像的基本原理和修改圖像原理
- 官方文檔 關(guān)于所有濾鏡的效果展示和簡(jiǎn)單使用
- swift講解 CoreImage 用swift講解的關(guān)于濾鏡的炕横,講的非常詳細(xì)。
- Core Image 你需要了解的那些事~ 講的很詳細(xì)的一篇