说到图像处理,就不得不提在iOS开发领域一直十分火热的GPUImage框架,它是一个针对iOS和Mac平台的开源图像处理框架,其内置124种滤镜,支持自定义滤镜,支持摄像头数据、静态图片和视频的图像处理,在Github上Star数量19.3k,Fork数量4.5k。GPUImage发展到现在,其作者已发布了3个大版本,使用的编程语言和渲染引擎也有区别。分别是:
1、GPUImage(使用Objective-C编写,基于OpenGLES封装)
2、GPUImage 2(使用Swift 3编写,基于OpenGLES封装)
3、GPUImage 3(使用Swift 4编写,基于Metal封装)
因Android和iOS均支持OpenGLES渲染引擎,但Android不支持Metal渲染引擎,为了保证跨平台特性,我主要对于GPUImage的第一版做了详细的研究和整理(第二版的Swift3语言不稳定,需要额外的学习成本)。
GPUImage框架从功能方面来看,其主要组成部分有:
1、iOS和Mac相关的应用层组件
2、OpenGLES上下文管理组件
3、OpenGLES着色器程序管理组件
4、OpenGLES帧缓存和纹理管理组件
5、滤镜基类和四大类别的具体滤镜
应用层组件和上下文管理组件与平台关系较大,因此重点关注的内容为后面3个组成部分。
着色器管理组件对于顶点着色器和片段着色器的编译、链接和使用进行了封装,开发者可以通过传入着色器代码来自定义着色器程序。
OpenGLES中的帧缓存和纹理是贯穿整个图像处理流程的灵魂部分,该组件也对其进行了很好的封装,可通过管理类对象来进行帧缓存和纹理的管理和操作。同时为了最大程度地节省渲染引擎资源,还使用了缓冲池来对于管理类对象进行管理。在具体的滤镜使用完底层资源后,缓冲池会将其对应的管理类对象进行回收,并在后续渲染时提供给有需求的滤镜进行使用。缓冲池和管理类对象的关系如下图所示:
GPUImage框架很巧妙地使用了Objective-C语言的特性,声明了GPUImageInput协议,规定了接收内容输入的类需要实现哪些方法;同时抽象出了GPUImageOutput这个专门用于内容输出的基类。
滤镜基类通过继承了GPUImageOutput基类,同时又遵循了GPUImageInput协议,从而拥有了既能接收外部渲染结果,又能输出渲染结果的功能。
滤镜基类分为两种:一种是基础滤镜GPUImageFilter,80%的基础滤镜都是以此为基础进行实现的;另一种是高级滤镜GPUImageFilterGroup,用于定制一些特殊的图像处理功能。
基础滤镜GPUImageFilter可以说是GPUImage框架的核心部分,默认接收单个外部渲染结果,输出单个渲染结果。基础滤镜与之前提到的输出基类和输入协议之间的关系如下图所示:
在GPUImageFilter的基础上也发展出了能接收多个外部渲染结果和内部能根据不同的着色器程序进行多次渲染的滤镜,但不管怎么样,输出结果都只有1个,如下图所示:
GPUImageFilterGroup用于封装多个滤镜的相互组合,从而生成一个结构复杂的高级滤镜。作为基类,它对于内部多个滤镜之间的相互关系没有限制,具体实现逻辑由子类进行自定义,高级滤镜与之前提到的输出基类和输入协议之间的关系如下图所示:
在讲解滤镜实现原理之前,需要先了解一个概念:在OpenGLES中,如果在帧缓存上附加一个纹理对象,可以将渲染后的结果写在该纹理中。利用这个特性,每个滤镜都可以将图像处理后的结果写在一个特定的纹理上。具体的滤镜会按照特定的功能,对片段着色器的代码进行定制,以达到图像处理的效果。
之前说到滤镜是同时支持渲染结果的输入和输出的(渲染结果一般指OpenGLES帧缓存和纹理的管理类对象,后面统一称为帧缓存对象),那么单输入滤镜就可以从接收到的帧缓存对象A中获取纹理id,然后从缓冲池中获取一个用于输出的帧缓存对象B,执行完渲染操作后,得到的结果会写在用于输出的帧缓存对象B所封装的纹理中,再将这个帧缓存对象B输出给下一个遵循了GPUImageInput协议的对象,就完成了单次的滤镜处理操作。具体流程如下图所示:

多输入滤镜继承自单输入滤镜,以基类形式对外提供。多输入滤镜在接收到帧缓存对象时,需要等到所有用于输入的帧缓存对象都接收完毕,才会进行渲染操作,通常会在片段着色器中同时操作多个纹理(输入个数由子类决定)。该类滤镜常用于实现多个图像的混合效果。具体流程如下图所示:

多着色器滤镜继承自单输入滤镜,以基类形式对外提供。多着色器滤镜内部会初始化多个着色器程序(着色器程序个数由子类决定),进行多次渲染操作,接收单个帧缓存对象的输入,输出单个帧缓存对象。具体流程如下图所示:
(1) 之前有说到,GPUImage框架利用Objective-C语言的特性,声明了GPUImageInput协议,规定了接收内容输入的类需要实现哪些方法;同时抽象出了GPUImageOutput这个专门用于内容输出的基类,但是输出的目的地有条件限制,那就是必须是遵循了GPUImageInput协议的实例对象。
(2) 滤镜基类通过继承了GPUImageOutput基类,同时又遵循了GPUImageInput协议,从而拥有了既能接收外部渲染结果输入,又能输出渲染结果的功能。
(3) 每个具有特定功能的滤镜都会继承滤镜基类,在这样的设定下,滤镜之间可以传递已经处理好的帧缓存对象,每个滤镜都可以作为接收者,接收上一个滤镜处理好的帧缓存对象,同时可以将自己处理后的帧缓存对象进行输出,作为下一个滤镜的输入内容。
(4) GPUImage框架中许多复杂滤镜都是通过这种滤镜之间传递帧缓存对象的操作来实现的,在前一个滤镜渲染完成后会自动触发下一个滤镜的渲染操作,直到滤镜链上的最后一个滤镜完成渲染为止。
给摄像头实时采集到的数据添加滤镜时,需要涉及 CVPixelBufferRef 的处理。
(1) 摄像头采集到的画面数据会通过系统委托方法给出,类型是 CMSampleBufferRef。
(2) 从 CMSampleBufferRef 提取 CVPixelBufferRef,再转换成 OpenGLES 纹理。
(3) 获取到纹理后,就可以开始滤镜处理的流程。
(4) 图像处理完成后,不再使用系统提供的 previewLayer 来展示画面了,需要滤镜链中的最后一个节点,负责将最终的画面展示在屏幕上。