本文主要介紹一下CoreImage的圖像處理框架的應用以及我在使用過程中的坑點。本文提供一個簡易的Demo,對于CoreImage不了解的,可以借助demo快速上手。CoreImage是前一段時間了解的,現在記錄一下,供大家參考。
概覽
本文主要從一下幾個方面來介紹:
1.CoreImage概念
2.內建濾鏡的使用
3.CPU/GPU的不同選擇方案
4.人臉檢測
5.自動增強濾鏡
6.自定義濾鏡
7.注意點
一、CoreImage的概念
Core Image一個圖像處理和分析技術,同時也提供了對視頻圖像實時處理的技術。iOS5中新加入的一個框架,里面提供了強大高效的圖像處理功能,用來對基于像素的圖像進行操作與分析。還提供了很多強大的濾鏡,可以實現你想要的效果,它的處理數據基于CoreGraphics,CoreVideo,和Image I/O框架,既可以使用GPU也可以使用CPU的渲染路徑。
CoreImage封裝了底層圖形處理的實現細節,你不必關心OpenGL和OpenGL ES是如何利用GPU的,也不必知曉GCD是如何利用多核進行處理的。
其內置了很多強大的濾鏡(Filter) (目前數量超過了190種), 這些Filter 提供了各種各樣的效果, 并且還可以通過 濾鏡鏈 將各種效果的 Filter疊加 起來形成強大的自定義效果。
一個 濾鏡 是一個對象,有很多輸入和輸出,并執行一些變換。
一個 濾鏡鏈 是一個鏈接在一起的濾鏡網絡,使得一個濾鏡的輸出可以是另一個濾鏡的輸入。
iOS8 之后更是支持自定義 CIFilter,可以定制滿足業務需求的復雜效果。
翻譯:底層細節都幫你做好了,放心調用API就行了 。
二、內建濾鏡的使用
首先介紹一下CoreImage中三個最重要的對象:
-
CIImage
保存圖像數據的類,是一個不可變對象,它表示一個圖像數據。CIImage對象,你可以通過UIImage,圖像文件或者像素數據來創建,也可以從一個CIFilter對象的輸出來獲取。
下面有一段官方解釋:image.png -
CIFilter
表示應用的濾鏡,這是框架對圖片屬性進行細節處理的類。它對所有的像素進行操作,用一些鍵-值設置來決定具體操作的程度。
image.png
每個源圖像的像素由CISampler對象提取(簡單地取樣器sampler)。
顧名思義,采樣器sampler檢索圖像的樣本,并將其提供給內核。過濾器創建者為每個源圖像提供一個采樣器。過濾器客戶端不需要知道有關采樣器的任何信息。
過濾器創建者在內核中定義每塊像素圖像處理計算,Core Image確定是否使用GPU或CPU執行計算。 Core Image根據設備功能使用Metal,OpenGL或OpenGL ES實現圖像的處理。 CIContex
表示上下文,也是實現對圖像處理的具體對象。可以基于CPU或者GPU ,用于繪制渲染,可以從其中取得圖片的信息。
代碼展示:
- (UIImage *)addEffect:(NSString *)filtername fromImage:(UIImage *)image{
///note 1
// CIImage * image1 = [image CIImage];
// NSLog(@"%@",image1);
//因為: UIImage 對象可能不是基于 CIImage 創建的(由 imageWithCIImage: 生成的),這樣就無法獲取到 CIImage 對象
//解決方法一:
// NSString * path = [[NSBundle mainBundle] pathForResource:@"tu.jpg" ofType:nil];
// UIImage * tempImage = [UIImage imageWithCIImage:[CIImage imageWithContentsOfURL:[NSURL fileURLWithPath:path]]];
// CIImage * tempCIimg = [tempImage CIImage];
// NSLog(@"%@",tempCIimg);
//解決方法2
CIImage * ciimage = [[CIImage alloc] initWithImage:image];
CIFilter * filter = [CIFilter filterWithName:filtername];
[filter setValue:ciimage forKey:kCIInputImageKey];
// 已有的值不改變, 其他的設為默認值
[filter setDefaults];
//渲染并輸出CIImage
CIImage * outimage = [filter outputImage];
//UIImage * newImage = [UIImage imageWithCIImage:outimage]; //每次創建都會開辟新的CIContext上下文,耗費空間
// 獲取繪制上下文
CIContext * context = [CIContext contextWithOptions:nil];//(GPU上創建)
//self.context; //
//創建CGImage
CGImageRef cgimage = [context createCGImage:outimage fromRect:[outimage extent]];
UIImage * newImage = [UIImage imageWithCGImage:cgimage];
CGImageRelease(cgimage);
return newImage;
}
上方有一個注意點:UIImage 對象可能不是基于 CIImage 創建的(由 imageWithCIImage: 生成的),這樣就無法獲取到 CIImage 對象。正確寫法如代碼中的解決方法1 和 解決方法2.
該方法是傳入兩個參數(一張圖片、濾鏡的名稱),具體濾鏡有哪些可以看官方文檔,也可以自己通過下面方法輸出:
// 打印濾鏡名稱
// `kCICategoryBuiltIn`內置; `kCICategoryColorEffect`色彩
- (void)showFilters {
NSArray *filterNames = [CIFilter filterNamesInCategory:kCICategoryColorEffect];
for (NSString *filterName in filterNames) {
NSLog(@"%@", filterName);
// CIFilter *filter = [CIFilter filterWithName:filterName];
// NSDictionary *attributes = filter.attributes;
// NSLog(@"%@", attributes); // 查看屬性
}
}
例如一個CIMotionBlur濾鏡可以做如下處理:所以一個濾鏡的基本使用可以分為四步:
- Create a CIImage :
- Create a CIContext
- Create a CIFilter
- Get the filter output
創建過濾器時,您可以在其上配置許多依賴于您正在使用的過濾器的屬性。過濾器為您提供輸出圖像作為CIImage ,您可以使用CIContext將其轉換為UIImage。
三、CPU/GPU的不同選擇
CIContext上下文是繪制操作發生的地方,它決定了CoreImage是使用GPU還是CPU來渲染。
上圖分別給出了CPU和GPU的創建方式,其中
contextWithOptions
創建GPU方式的上下文沒有實時性,雖然渲染是在GPU上執行,但是其輸出的image是不能顯示的,只有當其被復制回CPU存儲器上時,才會被轉成一個可被顯示的image類型,比如UIImage。該方式處理流程如下圖:對照上圖,當使用 Core Image 在 GPU 上渲染圖片的時候,先是把圖像傳遞到 GPU 上,然后執行濾鏡相關操作。但是當需要生成 CGImage 對象的時候,圖像又被復制回 CPU 上。最后要在視圖上顯示的時候,又返回 GPU 進行渲染。這樣在 GPU 和 CPU 之前來回切換,會造成很嚴重的性能損耗。
如果需要很高的實時性,則需要基于EAGLContext創建上下文,該種方式也是GPU上處理的,代碼如下:
核心實現代碼:
//獲取openGLES渲染環境
EAGLContext * context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
//初始化GLKview 并制定openGLES渲染環境 + 綁定
_showView = [[GLKView alloc] initWithFrame:frame context:context];
//A default implementation for views that draw their content using OpenGL ES.
/*
Binds the context and drawable. This needs to be called when the currently bound framebuffer
has been changed during the draw method.
*/
[_showView bindDrawable];
//添加進圖層
[self addSubview:_showView];
//創建上下文CIContext - GPU方式 :但是必須在主線程
_context = [CIContext contextWithEAGLContext:context options:@{kCIContextWorkingColorSpace:[NSNull null]}];
然后再使用context進行繪制:
[_context drawImage:ciimage inRect:CGRectMake(0, 0, viewSize.width*scale, viewSize.height*scale) fromRect:[ciimage extent]];
具體詳細實現過程可以看:demo中的GLESvc
和GLESView
實現。
兩種方式的對比
GPU方式:處理速度更快,因為利用了 GPU 硬件的并行優勢。可以使用 OpenGLES 或者 Metal 來渲染圖像,這種方式CPU完全沒有負擔,但是 GPU 受限于硬件紋理尺寸,當 App 切換到后臺狀態時 GPU 處理會被打斷。
CPU方式:會采用GCD對圖像進行渲染處理,這保證了CPU方式比較可靠,并且更容易使用,可以在后臺實現渲染過程。
四、人臉檢測
CIDetecror是Core Image框架中提供的一個識別類,包括對人臉、形狀、條碼、文本的識別。
人臉識別功能不單單可以對人臉進行獲取,還可以獲取眼睛和嘴等面部特征信息。但是CIDetector不包括面紋編碼提取, 只能判斷是不是人臉,而不能判斷這張人臉是誰的。
實現代碼:
-(void)detector:(CIImage *)image{
//CIContext * context = [CIContext contextWithOptions:nil];
NSDictionary * param = [NSDictionary dictionaryWithObject:CIDetectorAccuracyLow forKey:CIDetectorAccuracy];
CIDetector * faceDetector = [CIDetector detectorOfType:CIDetectorTypeFace context:_context options:param];
NSArray * detectResult = [faceDetector featuresInImage:image];
printf("count: %lu \n",(unsigned long)detectResult.count);
if (detectResult.count == 0) {
self.resultView.hidden = YES;
return;
}
self.resultView.hidden = NO;
for (CIFaceFeature * feature in detectResult) {
[UIView animateWithDuration:5/60.0f animations:^{
self.face.frame = CGRectMake(feature.bounds.origin.x/scaleValue, feature.bounds.origin.y/scaleValue, feature.bounds.size.width/scaleValue, feature.bounds.size.height/scaleValue);
if (feature.hasLeftEyePosition) {
self.leftEye.center = CGPointMake(feature.leftEyePosition.x/scaleValue, feature.leftEyePosition.y/scaleValue);
}
if (feature.hasRightEyePosition) {
self.rightEye.center = CGPointMake(feature.rightEyePosition.x/scaleValue, feature.rightEyePosition.y/scaleValue);
}
if (feature.hasMouthPosition) {
self.mouth.center = CGPointMake(feature.mouthPosition.x/scaleValue, feature.mouthPosition.y/scaleValue);
}
///note: UI坐標系 和 CoreImage坐標系不一樣:左下角為原點
}];
//_resultView.transform = CGAffineTransformMakeScale(1, -1);
}
}
此處有一個注意點,UI坐標系 和 CoreImage坐標系不一樣,CoreImage坐標系左下角為原點,UI坐標系左上角為圓點。
五、自動增強濾鏡
CoreImage的自動增強特征分析了圖像的直方圖,人臉區域內容和元數據屬性。接下來它將返回一個CIFilter對象的數組,每個CIFilter的輸入參數已經被設置好了,這些設置能夠自動去改善被分析的圖像。這種也是常見的自動美顏方式。下表列出了CoreImage用作自動圖像增強的濾鏡。這些濾鏡將會解決在照片中被發現的那些常見問題。實現代碼:
///自動圖像增強
-(UIImage *)autoAdjust:(CIImage *)image{
id orientationProperty = [[image properties] valueForKey:(__bridge id)kCGImagePropertyOrientation];
NSDictionary *options = nil;
if (orientationProperty) {
options = @{CIDetectorImageOrientation : orientationProperty};
//用于設置識別方向,值是一個從1 ~ 8的整型的NSNumber。如果值存在,檢測將會基于這個方向進行,但返回的特征仍然是基于這些圖像的。
}
NSArray *adjustments = [image autoAdjustmentFiltersWithOptions:options];
for (CIFilter *filter in adjustments) {
[filter setValue:image forKey:kCIInputImageKey];
image = filter.outputImage;
}
CIContext * context = [CIContext contextWithOptions:nil];//(GPU上創建) //self.context;
//創建CGImage
CGImageRef cgimage = [context createCGImage:image fromRect:[image extent]];
UIImage * newImage = [UIImage imageWithCGImage:cgimage];
return newImage;
}
六、自定義濾鏡
什么時候需要自定義濾鏡?
1 對于一種表達效果,我們使用了多種濾鏡,并且后續還會繼續使用這種效果。
2 對于一些高級的、apple并沒有提供的一些效果,需要對算法進行封裝的。
封裝方法:
1可以基于已存在的濾鏡來子類化一個CIFilter,還可以描述具有多個濾鏡鏈的配方
2用屬性來聲明濾鏡的輸入參數,屬性名必須以input為前綴,例如:inputImage
3可用重寫setDefaults方法來設置默認參數。 在iOS中,CIFilter被創建后會自動調用該方法
4需要重寫outputImage方法
例如我這邊有一個對視頻每一幀添加水印的方法:
-(void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection{
CVPixelBufferRef pixelBuffer = (CVPixelBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
CIImage *ciimage = [CIImage imageWithCVPixelBuffer:pixelBuffer];
if (_witchBtn.isOn) {
//[self.filter setValue:ciimage forKey:kCIInputImageKey];
//ciimage = [_filter outputImage];
// CLColorInvertFilter * customeFilter = [[CLColorInvertFilter alloc] init];
// customeFilter.inputImage = ciimage;
// ciimage = [customeFilter outputImage];
//自定義添加水印
_customerFilter = [[HZChromaKeyFilter alloc] initWithInputImage:[UIImage imageNamed:@"tu.jpg"] backgroundImage:ciimage];
ciimage = _customerFilter.outputImage;
}
[self.gpuView drawCIImage:ciimage];
}
而具體的HZChromaKeyFilter
類的實現:
@interface HZChromaKeyFilter : CIFilter
-(instancetype)initWithInputImage:(UIImage *)image
backgroundImage:(CIImage *)bgImage;
@property (nonatomic,readwrite,strong) UIImage *inputFilterImage;
@property (nonatomic,readwrite,strong) CIImage *backgroundImage;
@end
@implementation HZChromaKeyFilter
-(instancetype)initWithInputImage:(UIImage *)image backgroundImage:(CIImage *)bgImage{
self=[super init];
if (!self) {
return nil;
}
self.inputFilterImage=image;
self.backgroundImage=bgImage;
return self;
}
static int angle = 0;
-(CIImage *)outputImage{
CIImage *myImage = [[CIImage alloc] initWithImage:self.inputFilterImage];
//位移
CIImage * tempImage = myImage;//[scaleFilter outputImage];
CGSize extsz1 = self.backgroundImage.extent.size;
CGSize extsz2 = tempImage.extent.size;
CGAffineTransform transform = CGAffineTransformMakeTranslation(extsz1.width-extsz2.width -100, extsz2.height+100);
transform = CGAffineTransformRotate(transform, M_PI*2*(angle/360.0));
angle ++;
if (angle == 360) {
angle = 0;
}
CIFilter * transformFilter = [CIFilter filterWithName:@"CIAffineTransform"];
[transformFilter setValue:tempImage forKey:@"inputImage"];
[transformFilter setValue:[NSValue valueWithCGAffineTransform:transform] forKey:@"inputTransform"];
CIImage *backgroundCIImage = self.backgroundImage; //[[CIImage alloc] initWithImage:self.backgroundImage];
CIImage *resulImage = [[CIFilter filterWithName:@"CISourceOverCompositing" keysAndValues:kCIInputImageKey,transformFilter.outputImage,
kCIInputBackgroundImageKey,backgroundCIImage,nil]
valueForKey:kCIOutputImageKey];
return resulImage;
}
具體可以參考demo中的HZChromaKeyFilter.h
實現。
七、注意點
- 不要每次渲染都去創建一個CIConcext,上下文中保存了大量的狀態信息,重用會更加高效
- 當使用GPU的上下文時,應當避免使用CoreAnimation。如果希望同時使用它們,則應該使用CPU上下文。 涉及GPU的處理應該放到主線程來完成。
- 避免CPU和GPU之間進行沒必要的紋理切換
- 保證圖像不要超過CPU和GPU的限制。
參考文獻:
1 官方教材