GPUImage作为iOS相当老牌的图片处理三方库已经有些日子了(2013年发布第一个版本),至今甚至感觉要离我们慢慢远去(2015年更新了最后一个release)。可能现在分享这个稍微有点晚,再加上落影大神早已发布过此类文章,但是还是想从自己的角度来分享一下对其的理解和想法。
本文集所有内容皆为原创,严禁转载。
正如之前两章中对GPUImage的理解,它的处理流程就像一个链或者管道,官方叫法为Pipeline。有管道就一定存在源头,也就是使用GPUImage时需要做的第一件事就是把源头准备好,才能进行接下来的滤镜也好调整也好的各种操作,最后把结果从管道中导出。所以理解GPUImage对输入源到底做了哪些事情对于理解整个处理过程十分重要。因为之前使用GPUImage基本是针对静态图片的处理,所以本章就针对静态图片作为输入源的具体操作做介绍。可能之后马上也会涉及到视频输入源,那这些就之后再补充。
上篇文章中官方文档有提到,GPUImage提供了四种可以作为输入源的资源类型,再加上我补充的一种。其中GPUImagePicture、GPUImageRawDataInput这两种可作为静态图片资源类型的输入源初始化类型。但在具体说明者两个类之前,要先重点了解一下GPUImage中所有可作为输入源类型的父类:GPUImageOutput。
GPUImageOutput
此类的介绍在该类的头文件注释中,大概意思是:图片或者视频帧可通过继承了GPUImageOutput的资源对象进行加载,资源对象把图片或者视频帧加载到OpenGL ES的纹理中,处理完成后再把这些纹理传递处理管道中的下一个对象。
简单的说,就是继承了这个类的子类对象都可以:1.纹理信息传递给下一位;2.纹理信息导出成图片。因此,在此类中的实现就差不多分为三部分:1.自身数据的管理;2.数据传递对象的管理。3.导出图片。以下根据这三点分别介绍相应方法及属性。
1.自身数据的管理:
//这个就是所有管道中每个节点传递的内容。GPUImage或者说OpenGL ES处理图片或者视频过程中,会将需要处理的图像内容放置到Buffer中处理。Framebuffer顾名思义,就是用来渲染单张图片或者一帧图像内容的对象,也就是传说中的FBO在GPUImage中的对应。可以看出,在OpenGL ES中也把其对象化,但本身基于C的api限制,在使用的时候并不能体现面向对象的特征,因此GPUImage把它严格意义上的变成了一个对象类型。我对FrameBuffer的理解是一个带有图片原始内容(texture)+各种纹理参数的类。
GPUImageFramebuffer *outputFramebuffer;
说到FrameBuffer其实还需要单独一章来说明其具体作用和GPUImage对此类对象的特殊管理方式,那就放到之后,现在先把它理解成一个GPUImageOutput类型的对象A必定有一个outputFramebuffer对象的属性,A从管道的上一个节点得到数据后进行处理完的数据内容会存在自身的outputFramebuffer中,然后再把outputFramebuffer传递给下一个节点。
GPUImageOutput中并没有涉及到outputFramebuffer属性的初始化部分,原因在于针对不同的数据内容再加上上面提到的GPUImage对frameBuffer的特殊管理,outputFramebuffer的初始化部分都在GPUImageOutput的各个子类中进行。
//这个方法就是用来把自身的outputFramebuffer传递给下一个目标节点,也就是方法中的target。另外可以注意到方法还有第二个参数inputTextureIndex,简单理解就是,每个节点的输入源不一定只有一个。简单的处理流程一般都为一张图片加一个滤镜输出,但要实现对于把两张或者多张图片进行合并后输出这种需要两个以上输入源的需求时就需要增加这个参数来。
- (void)setInputFramebufferForTarget:(id)target atIndex:(NSInteger)inputTextureIndex;
{
[target setInputFramebuffer:[self framebufferForOutput] atIndex:inputTextureIndex];
}
2.数据传递对象的管理
//targets用于记录自身对象添加过的目标节点,所以对于一个管道来说,并不是只有一条路,每个节点都可以产生多个分支。例如,一张图片作为输入源,可以分别进行加马赛克和图片锐化操作,最终导出两张图片。
//targetTextureIndices 用于记录targets数组中对应下标的某个目标节点的输入顺序。这个顺序对于需要多个输入源的目标节点非常重要,决定了这个目标节点自己的frameBuffer的各种样式还有处理后的效果。值的范围在对应目标节点需要的输入源个数内,例如target0对象需要2个输入源,那targetTextureIndices中对应下标的元素值为0或者1。
NSMutableArray *targets, *targetTextureIndices;
//字面意思:当前目标对象是否要忽略。忽略的意思就是在处理管道中要不要选择不做这个目标对象的处理。
@property(readwrite, nonatomic) BOOL shouldIgnoreUpdatesToThisTarget;
//以下两个方法就是用来设置下一个目标节点的方法。如果下个target需要两个输入源,那么这两个输入源需按顺序调用addTarget,先add的对象的textureLocation为0,另一个则为1。方法中有两个操作比较重要:1.调用- (void)setInputFramebufferForTarget:(id)target atIndex:(NSInteger)inputTextureIndex;方法,在添加时就把自己所属的frameBuffer传递给了刚添加好的target。2.把添加的target以及自己在这个target中所占的下标保存管理。再会发现,这些操作都在一个定义好的队列中实现,如下下:
- (void)addTarget:(id)newTarget;
- (void)addTarget:(id)newTarget atTextureLocation:(NSInteger)textureLocation;
在GPUImageContext有对这个自定义队列进行初始化的操作,可以看到,当前运行的硬件环境决定了这个队列是串行还是并发。所有的任务都是同步的形式添加到队列中,目的是保证整个管道每一步的操作都能按顺序执行,这样才能保证管道中每个target得到并处理的内容都是存在且正确的。
_contextQueue = dispatch_queue_create("com.sunsetlakesoftware.GPUImage.openGLESContextQueue", GPUImageDefaultQueueAttribute());
dispatch_queue_attr_t GPUImageDefaultQueueAttribute(void)
{
#if TARGET_OS_IPHONE
if ([[[UIDevice currentDevice] systemVersion] compare:@"9.0" options:NSNumericSearch] != NSOrderedAscending)
{
return dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_DEFAULT, 0);
}
#endif
return nil;
}
//有添加就有移除,当当前管道中某些target的处理效果不需要或者需要更改的时候,你可以把这些节点从它的上一站移除,这个移除的操作就需要它的上一节点对象调用以下方法实现。如果作为中间节点,还需要把自己所添加的target移除。就像火车中的中间一截车厢,需要把自己与上一截和下一截的链接都去掉,才可以完全移除。当然在ARC下,全部销毁的时候不需要做这些移除工作。
- (void)removeTarget:(id)targetToRemove;
- (void)removeAllTargets;
3.导出图片
GPUImageOutput类其中一个非常独一无二的作用就是可以直接从该类处理完成后的数据内容中生成并导出图片。但是GPUImageOutput毕竟只是一个共有的基类,包括上方说到两点以及导出图片的具体方法的实现部分并没有在本类中体现,由其子类重写。
- (void)useNextFrameForImageCapture;
{
}
- (CGImageRef)newCGImageFromCurrentlyProcessedOutput;
{
return nil;
}
不过在- (CGImageRef)newCGImageByFilteringCGImage:(CGImageRef)imageToFilter方法中,告诉了大家如何实现导出图片的具体步骤:
1.其中stillImageSource是一个输入源对象,把自己添加到当前target是肯定要做的;
2.调用当前将要导出图片的target的processImage之前一定要先调用useNextFrameForImageCapture方法;
3.调用当前将要导出图片的target的processImage方法。
[self useNextFrameForImageCapture];
[stillImageSource addTarget:(id)self];
[stillImageSource processImage];
CGImageRef processedImage = [self newCGImageFromCurrentlyProcessedOutput];
以上是GPUImageOutput类中的重要三个部分的介绍,但是像中间涉及到的frameBuffer、处理队列以及未提到的其他属性和方法并未描述完全,完全是因为作者的设计思路太狡猾,把几乎图像处理过程中会涉及到的点都进行了封装和准备。这些内容详细介绍同样放之后补上,如果有疑问或对我以上的理解有异议,欢迎私信讨论。