GPUImage作为iOS相当老牌的图片处理三方库已经有些日子了(2013年发布第一个版本),至今甚至感觉要离我们慢慢远去(2015年更新了最后一个release)。可能现在分享这个稍微有点晚,再加上落影大神早已发布过此类文章,但是还是想从自己的角度来分享一下对其的理解和想法。
本文集所有内容皆为原创,严禁转载。
之前我把GPUImage处理过程比做管道,上一篇内容说明了在管道中“流动的载体”GPUImageFramebuffer。这一篇就从管道的具体源头入手。GPUImage提供了五种输入源类:GPUImagePicture、GPUImageRawDataInput、GPUImageUIElement、GPUImageMovie、GPUImageVideoCamera。他们是GPUImageOutput的子类且是在整个处理过程中唯一不需要遵循GPUImageInput协议的类,这个比较好理解,如果是GPUImageOutput的子类的对象的话就可以在管道中把自己的内容给到下一个节点对象;只有遵循了GPUImageInput协议的类的对象的话才可以接收到上一个节点对象传来的内容并进行处理。因此,作为输入源并不需要接收其他节点传递来的内容,只需要单向传递出去即可。以下就对这五个输入源逐一说明:
GPUImagePicture
这是我最常用到的类,因为实现的处理都是针对静态图片的。GPUImagePicture一共有五个初始化方法,不过最终实现都是完全一致的,都会得到CGImage对象并且载入到纹理当中。即所有初始化方法最后都会调用下方这个初始化方法:
- (id)initWithCGImage:(CGImageRef)newImageSource smoothlyScaleOutput:(BOOL)smoothlyScaleOutput;
·成员变量
在初始化过程中此变量用于存储图片的实际大小,但是!如果图片的实际大小大于设备GPU提供的纹理存储的最大空间时,就会对得到一个最大且合适的图片大小。
CGSize pixelSizeOfImage;
看名字就知道和- (void)processImage方法肯定有关系。初始化时为NO。这是用来控制初始化整个处理链时每个节点对象调用addTarget方法时不需要做具体的处理操作,只有等真正在操作过程中时才会实现。
BOOL hasProcessedImage;
semaphore对象用作处理多线程中的执行顺序问题,不同Group,semaphore的颗粒度更小。这个变量在初始化方法中被初始化,在- (BOOL)processImageWithCompletionHandler:(void (^)(void))completion用到,具体作用是防止多次调用造成的数据错乱。
dispatch_semaphore_t imageUpdateSemaphore;
·初始化
1.得到图片对象的大小,如果宽高有一个为0的话就会进到断言中。此处有多一个判断,注释测意思是如果图片的大小超过纹理的最大尺寸的话就需要重新设置图片大小并对图片进行压缩处理。
// For now, deal with images larger than the maximum texture size by resizing to be within that limit
CGSize scaledImageSizeToFitOnGPU = [GPUImageContext sizeThatFitsWithinATextureForSize:pixelSizeOfImage];
2.shouldSmoothlyScaleOutput属性之前我一直不明白具体作用,了解完mipmap技术后才有所理解。shouldSmoothlyScaleOutput默认及通过前不带此参数的初始化方法时都为NO,此时不会对纹理进行mipmap处理并存储。如果为YES的话就要保证图片的宽高为2的倍数。
if (self.shouldSmoothlyScaleOutput)
{
// In order to use mipmaps, you need to provide power-of-two textures, so convert to the next largest power of two and stretch to fill
CGFloat powerClosestToWidth = ceil(log2(pixelSizeOfImage.width));
CGFloat powerClosestToHeight = ceil(log2(pixelSizeOfImage.height));
pixelSizeToUseForTexture = CGSizeMake(pow(2.0, powerClosestToWidth), pow(2.0, powerClosestToHeight));
shouldRedrawUsingCoreGraphics = YES;
}
这里引用纹理映射Mipmap,解释一下Mipmap:
Mipmap是一个功能强大的纹理技术,它可以提高渲染的性能以及提升场景的视觉质量。它可以用来解决使用一般的纹理贴图会出现的两个常见的问题:
闪烁,当屏幕上被渲染物体的表面与它所应用的纹理图像相比显得非常小时,就会出现闪烁。尤其当相机和物体在移动的时候,这种负面效果更容易被看到。
性能问题。加载了大量的纹理数据之后,还要对其进行过滤处理(缩小),在屏幕上显示的只是一小部分。纹理越大,所造成的性能影响就越大。
Mipmap就可以解决上面那两个问题。当加载纹理的时候,不单单是加载一个纹理,而是加载一系列从大到小的纹理当mipmapped纹理状态中。然后OpenGl会根据给定的几何图像的大小选择最合适的纹理。Mipmap是把纹理按照2的倍数进行缩放,直到图像为1x1的大小,然后把这些图都存储起来,当要使用的就选择一个合适的图像。这会增加一些额外的内存。在正方形的纹理贴图中使用mipmap技术,大概要比原先多出三分之一的内存空间。
3.如果图片大小没问题且shouldSmoothlyScaleOutput为NO,那么接下来需要判断图片对象是否满足GL的存储配置,通过获取CGImage的各个属性来与标准配置进行比较,如果有一项不满足,那就需要重绘图片生成新的CGImage对象。
4.重绘操作具体实现如下。先开辟一段图片数据存储空间,将这一段内存地址给到imageData。重绘完成后即可得一段存储了将要使用到的图片数据的地址。如果不需要重绘,则直接可以通过方法将CGImage对象存储到内存地址中。
// For resized or incompatible image: redraw
imageData = (GLubyte *) calloc(1, (int)pixelSizeToUseForTexture.width * (int)pixelSizeToUseForTexture.height * 4);
CGColorSpaceRef genericRGBColorspace = CGColorSpaceCreateDeviceRGB();
CGContextRef imageContext = CGBitmapContextCreate(imageData, (size_t)pixelSizeToUseForTexture.width, (size_t)pixelSizeToUseForTexture.height, 8, (size_t)pixelSizeToUseForTexture.width * 4, genericRGBColorspace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
// CGContextSetBlendMode(imageContext, kCGBlendModeCopy); // From Technical Q&A QA1708: http://developer.apple.com/library/ios/#qa/qa1708/_index.html
CGContextDrawImage(imageContext, CGRectMake(0.0, 0.0, pixelSizeToUseForTexture.width, pixelSizeToUseForTexture.height), newImageSource);
CGContextRelease(imageContext);
CGColorSpaceRelease(genericRGBColorspace);
5.最后就是写入到纹理的操作了(这里会涉及到使用封装好的串行队列以及当前的EAGLContext对象,这两就之后单独一篇详细说明)。
首先先取到自身的outputFramebuffer;
如果需要使用mipmap的话需要单独设置纹理参数;
图片数据写入纹理;
如果需要使用mipmap的话就要在图片写入纹理后生成mipmap;
glBindTexture(GL_TEXTURE_2D, [outputFramebuffer texture]);
if (self.shouldSmoothlyScaleOutput)
{
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
}
// no need to use self.outputTextureOptions here since pictures need this texture formats and type
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (int)pixelSizeToUseForTexture.width, (int)pixelSizeToUseForTexture.height, 0, format, GL_UNSIGNED_BYTE, imageData);
if (self.shouldSmoothlyScaleOutput)
{
glGenerateMipmap(GL_TEXTURE_2D);
}
glBindTexture(GL_TEXTURE_2D, 0);
6.最后切勿忘了释放过程中创建的CoreGraphic、CoreFundation框架下的对象:
free(imageData);
CFRelease(dataFromImageDataProvider);
·Image rendering
这个方法在处理过程中将会调用十分频繁。在整个处理链搭建好之后,如果想要将处理后的最终结果显示在GPUImageView上或者导出,更或是从中间的filter节点直接导出图片之前都需要执行以下方法。顾名思义,这个方法的作用是告诉输入源开始处理传入的图片。查看具体实现代码会发现,实际的操作就相当于把输入源所拥有的图片内容及参数传递给链中的下一个或者多个节点节点。
- (void)processImage
下图就是一个多分支的处理链,Filter1处理后可导出只具有Filter1效果的图片;Filter2处理后将会显示在GPUImageView对象上,Filter3处理完后会继续传递给Filter5进行下一步渲染。每个箭头相当于导火索,那么打火机就是调用GPUImagePicture的- (void)processImage方法。
- (void)processImage其实只是调用了不需要block回调的- (BOOL)processImageWithCompletionHandler:(void (^)(void))completion方法。通过for循环遍历已经添加好的targets,例如上图中的Filter1、Filter2、Filter3、Filter4。要知道这个for循环是在一个定义好的异步串行队列中执行,并且在for循环之后使用了semaphore,这就意味着整个操作执行全部完成后才会触发completion回调。
for (idcurrentTarget in targets)
{
//获取当前target添加的位置,这与Filter有关,例如如果是两个或者两个以上输入源的Filter,每个输入源的添加顺序就决定着对应的处理顺序从而影响最终效果,这在之后专门针对Filter文章中做介绍。
NSInteger indexOfObject = [targets indexOfObject:currentTarget];
NSInteger textureIndexOfTarget = [[targetTextureIndices objectAtIndex:indexOfObject] integerValue];
[currentTarget setCurrentlyReceivingMonochromeInput:NO];
//将自身的FrameBuffer的纹理大小传递,从而下一个target生成或者取到相同大小的纹理用作处理后的内容的存储。
[currentTarget setInputSize:pixelSizeOfImage atIndex:textureIndexOfTarget];
//将自身存储着已经处理好的内容的FrameBuffer传递,如果是输入源的话内容也就是原图片数据。
[currentTarget setInputFramebuffer:outputFramebuffer atIndex:textureIndexOfTarget];
[currentTarget newFrameReadyAtTime:kCMTimeIndefinite atIndex:textureIndexOfTarget];
}
·更简便的方法得到经过Filter处理的图片
GPUImagePicture暴露了一个可以更方便得到处理后的图片的方法,但是前提还是需要搭建整个处理链,方便之处是不需要担心因为漏写了一些导出图片必须要写的代码导致的崩溃问题。第一个入参是Filter对象,需传入需要经过处理的Filter链的最后一个Filter。第二个参数是携带到处图片对象的block回调。
- (void)processImageUpToFilter:(GPUImageOutput*)finalFilterInChain withCompletionHandler:(void (^)(UIImage *processedImage))block;
{
[finalFilterInChain useNextFrameForImageCapture];
[self processImageWithCompletionHandler:^{
UIImage *imageFromFilter = [finalFilterInChain imageFromCurrentFramebuffer];
block(imageFromFilter);
}];
}
GPUImageRawDataInput
能使用这个类也是需求需要,主要操作内容其实和GPUImagePicture一致都是将图片数据导入,区别就是GPUImagePicture可以直接使用诸如UIImage或者CGImage格式的图片对象进行导入,而GPUImageRawDataInput是将图片的二进制数据作为内容载入到纹理。通过代码有两种办法(我只知道这两种,可能还有其他办法)可以使图片对象转化为二机制数据内容:1.UIImage->NSData->bytes;2.CoreGraphic重绘UIImage保存至内存,bytes指向这段内存地址。这种方式加载图片数据一般很少会用到,没人想多绕个弯子去实现GPUImagePicture就能实现的效果。但是如果想在图片载入前对图片进行一些操作或者是想载入CoreGraphic绘制的内容的话,使用GPUImageRawDataInput就可以直接将处理后的数据载入,过程中不需要再生成图片对象。
这个类有两个与GPUImagePicture类相同的公有变量,就不做重复解释。初始化方法一共有三个,最终都是调用最后一个方法,先重点介绍一下这个方法的四个入参:
- (id)initWithBytes:(GLubyte *)bytesToUpload size:(CGSize)imageSize pixelFormat:(GPUPixelFormat)pixelFormat type:(GPUPixelType)pixelType;
bytesToUpload:
GLubyte是OpenGL数据类型,无符号单字节整型,包含数值从0 到 255。
图片对象转化为二进制相当于用二进制数据存储了图片中每个像素点的RGB或者RGBA值,像素的单独颜色值范围是0到255,所以比如某个像素点的R的内容存储就是最小GLubyte单位长度。那么GLubyte *可以理解为指向存放二进制数据的内存地址指针类型。
最终这个参数会在调用OpenGL的glTexImage2D函数时最为最后一个参数被使用,最后一个参数为:pixels 指定内存中指向图像数据的指针。
imageSize:
输入的二进制数据内容的图片大小。
简单理解,将图片看成由二维的二进制数据构成的数组,第一行为图片中最上方一行的像素数据,依次类推。图片的二进制数据内容在内存中占用连续的一段长度(存储的最简单情况假设),也就是一维的存储形式。那么根据这些数据并不能知道原始图片的大小,也就意味着不知道图片的第一行像素数据长度是多少。
因此这个参数的其中一个作用是在调用OpenGL的glTexImage2D函数进行写入纹理时说明原始图片的宽高以确定纹理图像的宽高。另一个作用是上一章讲的GPUImageRawDataInput同样需要获取自身Framebuffer时使用。
pixelFormat:
这个参数主要作用是作为glTexImage2D函数的第三个参数,虽然枚举中有四种类型,但在实际使用GPUImageRawDataInput初始化时其实只用到了两种:GPUPixelFormatRGBA、GPUPixelFormatRGB。从字面上理解就是初始化的图片数据是否带alpha通道。
OpenGL的glTexImage2D函数的第三个参数的解释是:internalformat 指定纹理中的颜色组件。可选的值有GL_ALPHA,GL_RGB,GL_RGBA,GL_LUMINANCE, GL_LUMINANCE_ALPHA 等几种。这几个可选值与GPUPixelFormat是有对应关系的。
因此在选择这个参数的内容时并不需要过多考虑,只有两种选择:有透明度、没透明度。
对了,GPUImageRawDataInput头文件最上方有一行注释,pixelFormat参数默认情况下为GPUPixelFormatBGRA:
// The default format for input bytes is GPUPixelFormatBGRA, unless specified with pixelFormat:
pixelType:
这个参数同样是作为glTexImage2D函数的其中一个参数使用的:type 指定像素数据的数据类型。大致可以理解为二进制数据的存储精度。
GPUPixelType只有两个枚举项,以下也做简单介绍:
同样的,最上方注释说明pixelType默认情况下为GPUPixelTypeUByte:
// The default type for input bytes is GPUPixelTypeUByte, unless specified with pixelType:
以下待补充
GPUImageUIElement
GPUImageMovie
GPUImageVideoCamera
参考: