CoreImage浅谈与使用
本文主要介绍一下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叠加 起来形成强大的自定义效果。
一个 滤镜 是一个对象,有很多输入和输出,并执行一些变换。
一个 滤镜链 是一个链接在一起的滤镜网络,使得一个滤镜的输出可以是另一个滤镜的输入。 image.png
iOS8 之后更是支持自定义 CIFilter,可以定制满足业务需求的复杂效果。
翻译:底层细节都帮你做好了,放心调用API就行了 。
二、内建滤镜的使用
首先介绍一下CoreImage中三个最重要的对象:
-
CIImage
image.png
保存图像数据的类,是一个不可变对象,它表示一个图像数据。CIImage对象,你可以通过UIImage,图像文件或者像素数据来创建,也可以从一个CIFilter对象的输出来获取。
下面有一段官方解释: -
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滤镜可以做如下处理:
image.png
所以一个滤镜的基本使用可以分为四步:
- Create a CIImage :
- Create a CIContext
- Create a CIFilter
- Get the filter output
创建过滤器时,您可以在其上配置许多依赖于您正在使用的过滤器的属性。过滤器为您提供输出图像作为CIImage ,您可以使用CIContext将其转换为UIImage。
三、CPU/GPU的不同选择
CIContext上下文是绘制操作发生的地方,它决定了CoreImage是使用GPU还是CPU来渲染。
image.png image.png上图分别给出了CPU和GPU的创建方式,其中
contextWithOptions
创建GPU方式的上下文没有实时性,虽然渲染是在GPU上执行,但是其输出的image是不能显示的,只有当其被复制回CPU存储器上时,才会被转成一个可被显示的image类型,比如UIImage。该方式处理流程如下图:image.png
对照上图,当使用 Core Image 在 GPU 上渲染图片的时候,先是把图像传递到 GPU 上,然后执行滤镜相关操作。但是当需要生成 CGImage 对象的时候,图像又被复制回 CPU 上。最后要在视图上显示的时候,又返回 GPU 进行渲染。这样在 GPU 和 CPU 之前来回切换,会造成很严重的性能损耗。
如果需要很高的实时性,则需要基于EAGLContext创建上下文,该种方式也是GPU上处理的,代码如下: image.png 处理流程如下图: image.png 这种方式创建的上下文是利用实时渲染的特效,而不是每次操作都产生一个 UIImage,然后再设置到视图上。
核心实现代码:
//获取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用作自动图像增强的滤镜。这些滤镜将会解决在照片中被发现的那些常见问题。 image.png实现代码:
///自动图像增强
-(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 官方教材