GPUImage源码阅读笔记(一)
GUPImage是一个使用GPU加速的图像视频处理框架。最新的GPUImage已经是第三版GPUImage3。GPUImage1是用Objective-C实现,用OpenGL ES 2.0来渲染,可以运行在iOS,Mac-OS等平台。GPUImage2是用swift实现,用OpenGL ES 2.0来渲染,可以运行在iOS,Mac-OS等平台和Linux等平台。GPUImage3用swift实现,但采用了苹果新出的图像渲染框架Metal来渲染,可以运行在iOS,Mac-OS等平台以及硬件可支持的Linux等平台。
因为swift和Metal的相继出现,GPUImage也很久没更新,甚至最新提出的issue也没有修复,作者可能更专注于新的版本迭代,这一切都可以理解,但同样如果还在使用GPUImage1的开发人员可能就要自己多做些工作了。但同样对于初学者来说对GPUImage1的学习却不失为一个学习底层图像视频处理加速方面知识的一个入门途径。本篇延续之前的源码阅读笔记风格,还是从一个简单典型的应用来分析整个流程中GPUImage的作用过程。
首先下载下来GPUImage的源码,

可看出源码分为实例和框架两部分,实例分为iOS和Mac两部分,iOS实例部分中也分为很多类型的应用,现在就从一个简单典型的应用部分FilterShowcase开始分析。

这个demo主要实现的是选择自带滤镜对摄像头采集的内容进行实时处理(上面是效果图)。
下面先来看一下这个demo的结构:

首页是一个自带的滤镜选择列表,对应的controller为ShowcaseFilterListController

选择后进入上面效果图对应的页面,对应的controller为ShowcaseFilterViewController。
再往下边Frameworks的目录下有一个GPUImage的子工程。

这个子工程其实就是GPUImage的框架部分。GPUImage.h头文件中导入了框架所有头文件以供用户通过导入这个头文件引用框架。GLProgram类负责openGL ES的着色器管理。GPUImageContext类用来管理openGL ES的上下文,工作队列,以及用这个类来调用着色器,相当于整个框架的上下文管理类。GPUImageFramebuffer用来管理要操作的纹理缓存和帧缓存,GPUImageFramebufferCache用来管理缓冲区。Sources部分是输入部分,用来输入要处理的对象,其中有摄像头的采集,静态图片输入,视频输入等。Pipeline部分只有一个类GPUImageFilterPipeline顾名思义用来将多个滤镜依次连接在一个管道内。Filters部分为滤镜实现和管理部分,也包含一些自带滤镜。Outputs部分为最终输出部分,包括在view上的显示输出,写入一个文件等。
下面开始重点分析GPUImage的作用过程:
这个是ShowcaseFilterViewController的初始化方法:

设置了上个页面选择的滤镜类型。

在xib中将根视图设置为GPUImageView类型。
进入到viewDidLoad方法中:

在viewDidLoad方法中创建滤镜。

- (void)setupFilter这个方法比较长大约1500+行代码,折叠处理后是以上结构,下面开始逐行分析:
videoCamera = [[GPUImageVideoCamera alloc] initWithSessionPreset:AVCaptureSessionPreset1920x1080 cameraPosition:AVCaptureDevicePositionBack]表示创建一个相机用来采集图像。GPUImageVideoCamera是框架Sources部分中使用AVFoundation自定义的一个相机。
videoCamera.outputImageOrientation = UIInterfaceOrientationPortrait表示竖屏采集。
展开switch条件分支:

可看到每个分支内容是根据不同的滤镜设置页面title,如果滤镜可调节设置滤镜滑块最大最小值和当前值,并初始化对应滤镜。以一个常见的亮度滤镜为例:

亮度滤镜由框架自带的GPUImageBrightnessFilter实现。
再继续看下边的if-else分支:

每个选择主要是配置整个图像处理的管道,以亮度滤镜为例:

前边部分:

所以全过程就是将videoCamera作为图像处理的源头,通过[videoCamera addTarget:filter]将亮度滤镜作为管道中videoCamera处理后的下一个环节,滤镜处理结束后再通过[filter addTarget:filterView]将GPUImageView *类型的self.view做为管道的再下一个环节,在这里也就是最后一个环节了,结果就是通过亮度滤镜将摄像头采集到的图像滤镜处理后在GPUImageView *类型的self.view上实时显示出来。
最后

摄像头开启采集处理。
下面就以亮度滤镜为例分析整个图像处理过程:
整个流程梳理下来看,步骤是这样的:
第一步,GPUImageVideoCamera作为图像处理的第一个环节,负责图像采集
第二步,GPUImageBrightnessFilter作为图像处理的第二个环节,接收第一步采集的图像,并根据设置的参数进行亮度滤镜处理
第三步,GPUImageView作为第三个环节,接收第二步处理结果并在视图上进行实时渲染。
可以看出整个流程要涉及到AVFoundation,CoreGraphics,CoreVideo,CoreMedia,openGL ES等部分,下面就每一步和每一步涉及的系统框架进行分析。
首先,初始化GPUImageVideoCamera相机

点击进入GPUImageVideoCamera类:




这个方法主要是基于AVFoundation创建一个摄像头采集管理的类,

cameraProcessingQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0);audioProcessingQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW,0);创建相机和麦克风的处理队列,frameRenderingSemaphore = dispatch_semaphore_create(1);创建了一个信号量,用于管理GPUImageVideoCamera对图像的处理(后面详细介绍)。

_frameRate = 0设置帧率,_runBenchmark = NO用来做日志处理,capturePaused = NO表示捕获暂停,outputRotation = kGPUImageNoRotation; internalRotation = kGPUImageNoRotation;设置方向模式,captureAsYUV = YES设置以YUV的格式采集,_preferredConversion = kColorConversion709;设置颜色格式转换时运算矩阵(后面详细讲)。
下面这部分为创建自定义相机的过程:

这块为得到AVCaptureDevice *类型的采集设备,方法是上面这样的,通过获取devices数组,再对其遍历得到对应的采集设备,不是通过开发者直接创建,由于传进来的cameraPosition是AVCaptureDevicePositionBack,所以获取到的也是相对应的后摄像头采集设备。

创建采集图像的会话,并开始配置会话。

创建采集设备的输入。

创建采集设备的输出。
综上所述,使用AVFoundation框架创建的相机包含四部分,AVCaptureDevice *_inputCamera可认为是相机的管理类也可以将其想象为一个相机,AVCaptureSession*_captureSession是会话连接,可以将其想象为把镜头采集的视频传入成为后台数据中间的传输线路,AVCaptureDeviceInput *videoInput为相机的采集器,可以将其想象为镜头,AVCaptureVideoDataOutput *videoOutput负责输出相机采集的数据,可以将其想起想象为相机自带的洗印设备。上边的比喻不一定完全确切,但总体来说这部分其实就是AVFoundation通过对相机硬件的抽象,从而提供给开发者简洁的接口。

判断条件中:

这个判断方法表示设备是否支持将一个image的纹理绑定为一个openGL ES的处理纹理缓存(下文对基于这个判断有不同处理方式,后面有具体详解)。
captureAsYUV上文有设置过YES,如果满足这个条件,

如果输出数据videoOutput支持kCVPixelFormatType_420YpCbCr8BiPlanarFullRange格式输出,则设置supportsFullYUVRange为YES。对于CoreVideo pixel format type constants的解释,参看详情

根据supportsFullYUVRange值设置数据输出videoOutput的编解码配置,并设置isFullYUVRange的值,isFullYUVRange表示videoOutput支持kCVPixelFormatType_420YpCbCr8BiPlanarFullRange的输出格式。
接下来会做一些openGL ES的处理:


表明所有openGL ES的操作都在这个[GPUImageContext sharedContextQueue]队列中完成。
继续展开折叠:


将当前openGL ES的EAGLContext*类型上下文设置为GPUImage的openGL ES上下文。

根据isFullYUVRange的不同值选择不同的颜色格式转换着色器。


这部分是根据传入的shading创建顶点着色器和片元着色器。




可看出顶点着色器是kGPUImageVertexShaderString同一个的,区别在于片元着色器,isFullYUVRange为YES时选择kGPUImageYUVFullRangeConversionForLAFragmentShaderString,反之选择kGPUImageYUVVideoRangeConversionForLAFragmentShaderString。

这一部分是建立着色器和应用之间的变量传输管道,举例说明:


这一步是为着色器程序program绑定一块缓冲区,第一个参数传入着色器程序标示,第二个参数传入缓冲区数字标示(这里很巧妙用数组attributes的变量position下标,下标存储了缓冲区数字标示,元素存储了名字),第三个参数将缓冲区命名为position。
接着:


将缓冲区的数字标示传到本类以供下文调用(应用就是通过这个标示将变量传给着色器的对应缓冲区,下边还有具体讲解)。
接下来



调用着色器程序。

将顶点数据和纹理数据设置为enable状态以供后边调用。
就此这一步的openGL ES任务完成,可看出这一步主要是设置好主色器,配置好着色器和应用之间的数据传输,运行着色器,总之做好一切准备工作,磨刀霍霍就等着数据进来对其处理了。


这一步又是对相机的处理,将videoOutput数据输入的代理设置为本类(会实现相关代理方法接收采集数据,下文会具体分析),并将其连入到会话_captureSession中。
_captureSession标示目前的摄像头采集水平这里是AVCaptureSessionPreset1920x1080,也即1080p的视频流,同时并提交配置。
就此这个相机的初始化完成,可以看出这个初始化方法主要做了两件事,一件是创建相机,另外一件是设置相关的openGL ES工作部分。
在VC中调用[videoCamera startCameraCapture]就可以开始视频采集了。
接下来分析处理数据的过程:
前面设置了相机输出数据的代理是本类,所以本类也实现了相关的代理方法:

代理方法中第一个参数captureOutput为采集设备(AVFoundation不仅可以用来创建自定义相机,也可以创建录音设备,由于本篇侧重于图像处理,所以前边没提录音这块)的数据输出设备,第二个参数sampleBuffer是相机的输出的采样缓存数据,第三个参数connection表示AVCaptureSession会话系统的一个端到端的连接。
所以这种情况下必然进入第三个else分支:

首先这里使用了在初始化方法中设置的信号量frameRenderingSemaphore,如果没资源就return退出方法,如果有资源信号量减一继续。

如果VC中没有使用人脸识别hook的话,接着在GPUImageContext的全局队列contextQueue中执行方法[self processVideoSampleBuffer:sampleBuffer]。在这个方法处理结束后发送信号dispatch_semaphore_signal(frameRenderingSemaphore)。
所以全过程看出信号量主要是针对这个方法[self processVideoSampleBuffer:sampleBuffer]进行管理,如果对一个单位的采样处理结束有资源接着处理下一个,否则就丢弃这个采样。
这个方法也就是将YUV格式的采样转化为RGB格式,下面具体分析:

CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent()是记录一个起始时间点用以后边计算帧率。

展开第一个选择支:

如果获取到的CVBuffer不空的进入选择,再根据注释解释的选择和isFullYUVRange变量设置_preferredConversion。
CMTime currentTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);设置最近的sampleBuffer的时间戳。
[GPUImageContext useImageProcessingContext]设置当前opengl es 的上下文context。
再继续展开下一个选择分支:

YUV中Y表示亮度,UV表示色彩,所以准备两个CVBuffer:

继续展开下一个选择分支:



设置纹理的长和宽。
glActiveTexture(GL_TEXTURE4)设置激活纹理单元。Y-plane 选择当前活跃纹理单元为当前纹理单元。

如果本设备支持红色纹理,则调用err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, [[GPUImageContext sharedImageProcessingContext] coreVideoTextureCache], cameraFrame, NULL, GL_TEXTURE_2D, GL_LUMINANCE, bufferWidth, bufferHeight, GL_LUMINANCE, GL_UNSIGNED_BYTE, 0, &luminanceTextureRef)从cameraFrame中创建出Y平面的纹理,这个函数GL_LUMINANCE参数为亮度,luminanceTextureRef为新创建的纹理地址引用,也就是上文声明的亮度纹理。

接下来这部分都是openGL ES的API,如注释所示,就是将亮度纹理传入到着色器对应的缓存中,yuvConversionLuminanceTextureUniform = [yuvConversionProgram uniformIndex:@"luminanceTexture"]在本类的初始化方法中设置过,前文提到过。
接下来这部分:

同上面原理一样,将UV色彩纹理传入着色器程序中。
接下来[self convertYUVToRGBOutput]这个方法就是将YUV格式转化为RGB的过程。


总体就是调用openGL ES的一系列方法,运行前边创建的着色器程序将输入的YUV格式的纹理转化为RGB格式的纹理。

这里设置旋转方向的长和宽。
接着调用方法[self updateTargetsForVideoCameraUsingCacheTextureAtWidth:rotatedImageBufferWidth height:rotatedImageBufferHeight time:currentTime]将处理过的纹理传给管道中的下一个环节。

展开第二个循环:

可以看出最终是调用GPUImageInput协议的[currentTarget newFrameReadyAtTime:currentTime atIndex:textureIndexOfTarget]方法将接力棒传递到下一个处理环节,在这个例子里其实就是GPUImageBrightnessFilter亮度滤镜。
综上,GPUImageVideoCamera这部分全流程就结束了。下边还有两个环节GPUImageBrightnessFilter和GPUImageView的处理,且听下回分解。