假设我们自定义一个OpenGL ES程序来处理图片,那么会有以下几个步骤:
1、初始化OpenGL ES环境,编译、链接顶点着色器和片元着色器;
2、缓存顶点、纹理坐标数据,传送图像数据到GPU;
3、绘制图元到特定的帧缓存;
4、在帧缓存取出绘制的图像。
GPUImageFilter
负责的是第一、二、三步。
GPUImageFramebuffer
负责是第四步。
一、GPUImageFilter解析
GPUImageFilter和响应链的其他元素实现了
GPUImageInput
协议,他们都可以提供纹理参与响应链,或者从响应链的前面接收并处理纹理。响应链的下一个对象是target,响应链可能有多个分支(添加多个targets)。
Filters and other subsequent elements in the chain conform to the GPUImageInput protocol, which lets them take in the supplied or processed texture from the previous link in the chain and do something with it. Objects one step further down the chain are considered targets, and processing can be branched by adding multiple targets to a single output or filter.
1 |
+ (const GLfloat *)textureCoordinatesForRotation:(GPUImageRotationMode)rotationMode; |
outputframebuffer
指定的缓存
usingNextFrameForImageCapture
代表着输出的结果会被用于获取图像,所以在绘制之前要加锁
1 |
if (usingNextFrameForImageCapture) |
glBindTexture(GL_TEXTURE_2D, [firstInputFramebuffer texture]);
1 |
glVertexAttribPointer(filterPositionAttribute, 2, GL_FLOAT, 0, 0, vertices); |
GL_TRIANGLE_STRIP
模式用于绘制三角形带。
这里有介绍
[firstInputFramebuffer unlock];
输入纹理使用完毕,解锁。在调用这个解锁之前必须确定之前已经调用加锁,否则会报错。
GPUImageFramebuffer
使用引用计数来管理缓存,当引用计数小于0的时候会回收缓存。
usingNextFrameForImageCapture
,则会通过GCD信号量来通知仍在等待绘制完成的函数。
1 |
if (usingNextFrameForImageCapture) |
- (void)informTargetsAboutNewFrameAtTime:(CMTime)frameTime;
[self setInputFramebufferForTarget:currentTarget atIndex:textureIndex];
[[self framebufferForOutput] unlock];
[currentTarget newFrameReadyAtTime:frameTime atIndex:textureIndex];
1 |
if (dispatch_semaphore_wait(imageCaptureSemaphore, convertedTimeout) != 0) |
- (void)setInteger:(GLint)newInteger forUniformName:(NSString *)uniformName;
defaultTextureOptions
generateTexture
会创建对应的纹理缓存
generateFramebuffer
会创建对应的帧缓存
CVOpenGLESTextureCache
glTexImage2D()
,这个我们更熟悉的函数来传送CPU图像数据到GPU
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture, 0);
1 |
- (void)activateFramebuffer; |
1 |
[[GPUImageContext sharedFramebufferCache] returnFramebufferToCache:self]; |
newCGImageFromFramebufferContents
函数获取图像数据。
CVPixelBufferGetBaseAddress
和
glReadPixels
都可以获得图像数据,根据iOS版本不同调用不同函数。
A Core Video pixel buffer is an image buffer that holds pixels in main memory. Applications generating frames, compressing or decompressing video, or using Core Image can all make use of Core Video pixel buffers.
Core Video OpenGLES texture caches are used to cache and manage CVOpenGLESTextureRef textures. These texture caches provide you with a way to directly read and write buffers with various pixel formats, such as 420v or BGRA, from GLES.
Core Video OpenGLES textures are texture-based image buffers used for supplying source image data to OpenGL.
GPUImage的四大输入基础类,都可以作为响应链的起点。这些基础类会把图像作为纹理,传给OpenGL ES处理,然后把纹理传递给响应链的下一个对象。
GPUImageVideoCamera
摄像头-视频流
GPUImageStillCamera
摄像头-照相
GPUImagePicture
图片
GPUImageMovie
视频
响应链,先要理解帧缓存的概念,这在
OpenGL ES教程-帧缓存
有提到过。
上一篇介绍的是
GPUImageFramebuffer
和
GPUImageFilter
。
简单回顾一下:
GPUImageFilter
就是用来接收源图像,通过自定义的顶点、片元着色器来渲染新的图像,并在绘制完成后通知响应链的下一个对象。
GPUImageFramebuffer
就是用来管理纹理缓存的格式与读写帧缓存的buffer。
这一篇介绍的是
GPUImageVideoCamera
和
GPUImageView
。
GPUImageVideoCamera是GPUImageOutput的子类,提供来自摄像头的图像数据作为源数据,一般是响应链的源头。
1 |
_captureSession = [[AVCaptureSession alloc] init]; |
AVCaptureVideoDataOutput
是
AVCaptureOutput
的子类,用来处理从摄像头采集的未压缩或者压缩过的图像帧。
captureOutput:didOutputSampleBuffer:fromConnection: delegate
,可以访问图像帧。
1 |
- (void)setSampleBufferDelegate: |
需要注意的是,当一个新的视频图像帧被采集后,它会被传送到output,调用这里设置的delegate。所有的delegate函数会在这个queue中调用。
如果队列被阻塞
,新的图像帧到达后会被自动丢弃(默认alwaysDiscardsLateVideoFrames = YES)。这允许app处理当前的图像帧,不需要去管理不断增加的内存,
因为处理速度跟不上采集的速度,等待处理的图像帧会占用内存,并且不断增大
。
必须使用
同步队列
处理图像帧,保证帧的序列是顺序的。
1 |
if (dispatch_semaphore_wait(frameRenderingSemaphore, DISPATCH_TIME_NOW) != 0) |
YCbCr或Y’CbCr有的时候会被写作:YCBCR或是Y’CBCR,是色彩空间的一种,通常会用于影片中的影像连续处理,或是数字摄影系统中。Y’为颜色的亮度(luma)成分、而CB和CR则为蓝色和红色的浓度偏移量成份。
YUV主要用于优化彩色视频信号的传输,使其向后相容老式黑白电视。与RGB视频信号传输相比,它最大的优点在于只需占用极少的频宽(RGB要求三个独立的视频信号同时传输)。
CbCr 则是在世界数字组织视频标准研制过程中作为ITU - R BT.601 建议的一部分,其实是YUV经过缩放和偏移的翻版。其中Y与YUV 中的Y含义一致,Cb,Cr 同样都指色彩,只是在表示方法上不同而已。在YUV 家族中,YCbCr 是在计算机系统中应用最多的成员,其应用领域很广泛,JPEG、MPEG均采用此格式。一般人们所讲的YUV大多是指YCbCr。YCbCr 有许多取样格式,如4∶4∶4,4∶2∶2,4∶1∶1 和4∶2∶0。
百度百科的介绍
YUV数据格式-图文详解
GPUImage中的YUV
GLProgram *yuvConversionProgram;
将YUV颜色空间转换成RGB颜色空间的GLSL。
CVPixelBufferGetPlaneCount()
返回缓冲区的平面数。
通过
CVOpenGLESTextureCacheCreateTextureFromImage()
创建两个纹理luminanceTextureRef(亮度纹理)和chrominanceTextureRef(色度纹理)。
convertYUVToRGBOutput()
把YUV颜色空间的纹理转换成RGB颜色空间的纹理
顶点着色器-通用
kGPUImageVertexShaderString
片元着色器:
1、
kGPUImageYUVFullRangeConversionForLAFragmentShaderString
2、
kGPUImageYUVVideoRangeConversionForLAFragmentShaderString
区别在不同的格式
video-range (luma=[16,235] chroma=[16,240])
full-range (luma=[0,255] chroma=[1,255])
glActiveTextue 并不是激活纹理单元,而是选择当前活跃的纹理单元。每一个纹理单元都有GL_TEXTURE_1D, 2D, 3D 和 CUBE_MAP。
1 |
glActiveTexture(GL_TEXTURE1); |
GPUImageView是响应链的终点,一般用于显示GPUImage的图像。
如果是kGPUImageFillModePreserveAspectRatio
保持原宽高比,并且图像不超过屏幕。那么以当前屏幕大小为准。
widthScaling = insetRect.size.width / currentViewSize.width;
如果是kGPUImageFillModePreserveAspectRatioAndFill
保持原宽高比,并且图像要铺满整个屏幕。那么图像大小为准。
widthScaling = currentViewSize.height / insetRect.size.height;
imageVertices存放着顶点数据,上面的修改都会存放在这个数组。
1 |
glActiveTexture(GL_TEXTURE4); |
这两行是分别绑定顶点坐标数据和纹理坐标数据。
1 |
glVertexAttribPointer(displayPositionAttribute, 2, GL_FLOAT, 0, 0, imageVertices); |
这两行是设定输入的源图像数据缓存,并且对缓存加锁。
1 |
inputFramebufferForDisplay = newInputFramebuffer; |
在准备好着色器、纹理data、顶点位置坐标和纹理坐标后,就可以调用
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
绘制图像。
GPUImageFilter
就是用来接收源图像,通过自定义的顶点、片元着色器来渲染新的图像,并在绘制完成后通知响应链的下一个对象。
GPUImageFramebuffer
就是用来管理纹理缓存的格式与读写帧缓存的buffer。
GPUImageVideoCamera
是
GPUImageOutput
的子类,提供来自摄像头的图像数据作为源数据,一般是响应链的源头。
GPUImageView
是响应链的终点,一般用于显示GPUImage的图像。
琨君
的
基于GPUImage的实时美颜滤镜
对GPUImage实现美颜滤镜的原理和思路做了详细介绍。
本文以
琨君的代码
为demo,结合前两篇解析,探究美颜过程中的GPUImage实现。
GPUImageFilter
绘制的逻辑。
1 |
- (void)renderToTextureWithVertices:(const GLfloat *)vertices |
下面这部分是核心的绘制逻辑:
glActiveTexture()
是选择纹理单元,
glBindTexture()
是把纹理单元和
firstInputFramebuffer
、
secondInputFramebuffer
管理的纹理内存绑定。
glUniform1i()
告诉GLSL选择的纹理单元是2。
这部分在上一篇介绍也有提到,再详细阐述:
glActiveTexture()
选择的是纹理单元,和
glGenTextures()
返回的数字没有关系,可以在纹理单元2上面绑定纹理12。
glGenTextures()
返回的纹理可以是
GL_TEXTURE_2D
类型也可以是
GL_TEXTURE_CUBE_MAP
类型,取决于
glBindTexture()
第一次绑定纹理的是
GL_TEXTURE_2D
还是
GL_TEXTURE_CUBE_MAP
。
1 |
glActiveTexture(GL_TEXTURE2); |
nextAvailableTextureIndex
用于获取下一个纹理索引
1 |
- (NSInteger)nextAvailableTextureIndex; |
setInputFramebuffer: atIndex:
会根据上面获取的
textureIndex
设置
firstInputFramebuffer
和
secondInputFramebuffer
。如果是
textureIndex = 0
,设置
hasSetFirstTexture
表示已经设置第一个纹理。
GPUImageBeautifyFilter
是
基于GPUImage的实时美颜滤镜
中的美颜滤镜,包括
GPUImageBilateralFilter
、
GPUImageCannyEdgeDetectionFilter
、
GPUImageCombinationFilter
、
GPUImageHSBFilter
。
绘制流程图
GPUImageVideoCamera
捕获摄像头图像
newFrameReadyAtTime: atIndex:
通知
GPUImageBeautifyFilter
;
GPUImageBeautifyFilter
调用
newFrameReadyAtTime: atIndex:
GPUImageBilateralFliter
输入纹理已经准备好;
GPUImageBilateralFliter
绘制图像后在
informTargetsAboutNewFrameAtTime()
,
setInputFramebufferForTarget: atIndex:
GPUImageCombinationFilter
输入纹理,
GPUImageCombinationFilter
纹理已经绘制完毕;
GPUImageBeautifyFilter
调用
newFrameReadyAtTime: atIndex:
GPUImageCannyEdgeDetectionFilter
输入纹理已经准备好;
GPUImageCannyEdgeDetectionFilter
绘制图像后,
GPUImageCombinationFilter
输入纹理;
GPUImageBeautifyFilter
调用
newFrameReadyAtTime: atIndex:
GPUImageCombinationFilter
输入纹理已经准备好;
GPUImageCombinationFilter
判断是否有三个纹理,三个纹理都已经准备好后
GPUImageThreeInputFilter
的绘制函数
renderToTextureWithVertices: textureCoordinates:
,
GPUImageHSBFilter
的输入纹理,
GPUImageHSBFilter
纹理已经绘制完毕;
GPUImageHSBFilter
调用
renderToTextureWithVertices: textureCoordinates:
绘制图像,
GPUImageView
的输入纹理,并通知
GPUImageView
输入纹理已经绘制完毕;
GPUImageView
把输入纹理绘制到自己的帧缓存,然后通过
[self.context presentRenderbuffer:GL_RENDERBUFFER];
显示到
UIView
上。
这次介绍的
GPUImageContext
、
GPUImageFramebufferCache
和
GPUImagePicture
。
GPUImageContext是GPUImage对OpenGL ES上下文的封装,添加了GPUImage相关的上下文,比如说Program的使用缓存,处理队列,CV纹理缓存等。
useAsCurrentContext()
在useAsCurrentContext设置当前上下文的时候,会先判断上下文是否是当前context,不是再设置(为了避免上下文切换的性能消耗,即使设置的上下文是同一个上下文也会消耗性能)
sizeThatFitsWithinATextureForSize()
会调整纹理大小,如果超过最大的纹理,会调整为不超过最大的纹理宽高。
(GLProgram*)programForVertexShaderString:fragmentShaderString:;
- (void)useSharegroup:(EAGLSharegroup *)sharegroup;
- (EAGLContext *)context;
返回OpenGL ES2.0的上下文,同时设置
glDisable(GL_DEPTH_TEST);
,图像处理管道默认不允许使用深度缓存。
GPUImageFramebufferCache是GPUImageFrameBuffer的管理类
- (NSString *)hashForSize: textureOptions:onlyTexture:;
- (void)returnFramebufferToCache:;
回收缓存。根据size、textureOptions和onlyTexture,创建缓存字符串,缓存字符串+当前缓存数量形成framebufferCache缓存的key。(之所以会加上数量,是因为缓存字符串不唯一)
- (void)addFramebufferToActiveImageCaptureList:;
- (void)removeFramebufferFromActiveImageCaptureList:
newCGImageFromFramebufferContents()
读取帧缓存图像数据时,保持GPUImageFramebuffer的引用。并且读取完数据后,在
dataProviderUnlockCallback()
方法释放。
GPUImagePicture是PGUImage的图像处理类,继承GPUImageOutput,一般作为响应链的源头。
pixelSizeOfImage 图像的像素大小。
hasProcessedImage 图像是否已处理。
imageUpdateSemaphore 图像处理的GCD信号量。
- (id)initWithCGImage:smoothlyScaleOutput:
用源图像newImageSource和是否采用mipmaps来初始化GPUImagePicture。
- (BOOL)processImageWithCompletionHandler:;
通知targets处理图像,并在完成后调用complete代码块。在处理开始时,会标记hasProcessedImage为YES,并调用
dispatch_semaphore_wait()
,确定上次处理已经完成,否则取消这次处理。
- (void)addTarget: atTextureLocation:;
添加target到响应链。如果hasProcessedImage为YES,表示图像已经处理完毕,直接设置targets的InputSize,并调用
newFrameReadyAtTime()
通知target。
将GPUImageView设置为self.view,根据face.png,设置GPUImagePicture,然后添加GPUImageTiltShiftFilter到响应链,再把GPUImageView作为响应链的终点,最后调用processImage,开始处理图像。
1 |
GPUImageView *primaryView = [[GPUImageView alloc] initWithFrame:self.view.frame]; |
Mipmap纹理技术是目前解决纹理分辨率与视点距离关系的最有效途径,它会先将图片压缩成很多逐渐缩小的图片,例如一张64 64的图片,会产生64 64,32 32,16 16,8 8,4 4,2 2,1 1的7张图片,当屏幕上需要绘制像素点为20 20 时,程序只是利用 32 32 和 16 16 这两张图片来计算出即将显示为 20 20 大小的一个图片,这比单独利用 32*32 的那张原始片计算出来的图片效果要好得多,速度也更快.
kCGImageAlphaLast:alpha 分量存储在每个像素中的低位,如RGBA。
kCGImageAlphaFirst:alpha 分量存储在每个像素中的高位,如ARGB。
kCGImageAlphaPremultipliedLast:alpha 分量存储在每个像素中的低位,同时颜色分量已经乘以了 alpha 值。
kCGImageAlphaPremultipliedFirst:alpha 分量存储在每个像素中的高位,同时颜色分量已经乘以了 alpha 值。
kCGImageAlphaNoneSkipLast:没有 alpha 分量。如果像素的总大小大于颜色空间中颜色分量数目所需要的空间,则低位将被忽略。
kCGImageAlphaNoneSkipFirst:没有 alpha 分量。如果像素的总大小大于颜色空间中颜色分量数目所需要的空间,则高位将被忽略。