GPUImage作為iOS相當(dāng)老牌的圖片處理三方庫已經(jīng)有些日子了(2013年發(fā)布第一個版本)耘擂,至今甚至感覺要離我們慢慢遠(yuǎn)去(2015年更新了最后一個release)”嘟龋可能現(xiàn)在分享這個稍微有點晚,再加上落影大神早已發(fā)布過此類文章伤锚,但是還是想從自己的角度來分享一下對其的理解和想法酣栈。
本文集所有內(nèi)容皆為原創(chuàng)险胰,嚴(yán)禁轉(zhuǎn)載矿筝。
之前我把GPUImage處理過程比做管道,上一篇內(nèi)容說明了在管道中“流動的載體”GPUImageFramebuffer窖维。這一篇就從管道的具體源頭入手。GPUImage提供了五種輸入源類:GPUImagePicture铸史、GPUImageRawDataInput、GPUImageUIElement沛贪、GPUImageMovie利赋、GPUImageVideoCamera。他們是GPUImageOutput的子類且是在整個處理過程中唯一不需要遵循GPUImageInput協(xié)議的類猩系,這個比較好理解媚送,如果是GPUImageOutput的子類的對象的話就可以在管道中把自己的內(nèi)容給到下一個節(jié)點對象;只有遵循了GPUImageInput協(xié)議的類的對象的話才可以接收到上一個節(jié)點對象傳來的內(nèi)容并進(jìn)行處理寇甸。因此塘偎,作為輸入源并不需要接收其他節(jié)點傳遞來的內(nèi)容,只需要單向傳遞出去即可拿霉。以下就對這五個輸入源逐一說明:
GPUImagePicture
這是我最常用到的類吟秩,因為實現(xiàn)的處理都是針對靜態(tài)圖片的。GPUImagePicture一共有五個初始化方法绽淘,不過最終實現(xiàn)都是完全一致的涵防,都會得到CGImage對象并且載入到紋理當(dāng)中。即所有初始化方法最后都會調(diào)用下方這個初始化方法:
- (id)initWithCGImage:(CGImageRef)newImageSource smoothlyScaleOutput:(BOOL)smoothlyScaleOutput;
·成員變量
在初始化過程中此變量用于存儲圖片的實際大小沪铭,但是壮池!如果圖片的實際大小大于設(shè)備GPU提供的紋理存儲的最大空間時,就會對得到一個最大且合適的圖片大小杀怠。
CGSize pixelSizeOfImage; ? ?
看名字就知道和- (void)processImage方法肯定有關(guān)系椰憋。初始化時為NO。這是用來控制初始化整個處理鏈時每個節(jié)點對象調(diào)用addTarget方法時不需要做具體的處理操作赔退,只有等真正在操作過程中時才會實現(xiàn)橙依。
BOOL hasProcessedImage;
semaphore對象用作處理多線程中的執(zhí)行順序問題,不同Group,semaphore的顆粒度更小票编。這個變量在初始化方法中被初始化褪储,在- (BOOL)processImageWithCompletionHandler:(void (^)(void))completion用到,具體作用是防止多次調(diào)用造成的數(shù)據(jù)錯亂慧域。
dispatch_semaphore_t imageUpdateSemaphore;
·初始化
1.得到圖片對象的大小鲤竹,如果寬高有一個為0的話就會進(jìn)到斷言中。此處有多一個判斷昔榴,注釋測意思是如果圖片的大小超過紋理的最大尺寸的話就需要重新設(shè)置圖片大小并對圖片進(jìn)行壓縮處理辛藻。
// 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技術(shù)后才有所理解互订。shouldSmoothlyScaleOutput默認(rèn)及通過前不帶此參數(shù)的初始化方法時都為NO吱肌,此時不會對紋理進(jìn)行mipmap處理并存儲氮墨。如果為YES的話就要保證圖片的寬高為2的倍數(shù)规揪。
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是一個功能強大的紋理技術(shù)奸忽,它可以提高渲染的性能以及提升場景的視覺質(zhì)量栗菜。它可以用來解決使用一般的紋理貼圖會出現(xiàn)的兩個常見的問題:
閃爍苛萎,當(dāng)屏幕上被渲染物體的表面與它所應(yīng)用的紋理圖像相比顯得非常小時腌歉,就會出現(xiàn)閃爍翘盖。尤其當(dāng)相機和物體在移動的時候馍驯,這種負(fù)面效果更容易被看到。
性能問題狂打。加載了大量的紋理數(shù)據(jù)之后趴乡,還要對其進(jìn)行過濾處理(縮辛滥蟆)惦辛,在屏幕上顯示的只是一小部分胖齐。紋理越大市怎,所造成的性能影響就越大。
Mipmap就可以解決上面那兩個問題干像。當(dāng)加載紋理的時候,不單單是加載一個紋理速客,而是加載一系列從大到小的紋理當(dāng)mipmapped紋理狀態(tài)中溺职。然后OpenGl會根據(jù)給定的幾何圖像的大小選擇最合適的紋理浪耘。Mipmap是把紋理按照2的倍數(shù)進(jìn)行縮放七冲,直到圖像為1x1的大小上渴,然后把這些圖都存儲起來,當(dāng)要使用的就選擇一個合適的圖像掘鄙。這會增加一些額外的內(nèi)存。在正方形的紋理貼圖中使用mipmap技術(shù)收津,大概要比原先多出三分之一的內(nèi)存空間朋截。
3.如果圖片大小沒問題且shouldSmoothlyScaleOutput為NO部服,那么接下來需要判斷圖片對象是否滿足GL的存儲配置廓八,通過獲取CGImage的各個屬性來與標(biāo)準(zhǔn)配置進(jìn)行比較剧蹂,如果有一項不滿足宠叼,那就需要重繪圖片生成新的CGImage對象冒冬。
4.重繪操作具體實現(xiàn)如下摩渺。先開辟一段圖片數(shù)據(jù)存儲空間横侦,將這一段內(nèi)存地址給到imageData枉侧。重繪完成后即可得一段存儲了將要使用到的圖片數(shù)據(jù)的地址棵逊。如果不需要重繪银酗,則直接可以通過方法將CGImage對象存儲到內(nèi)存地址中。
// 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.最后就是寫入到紋理的操作了(這里會涉及到使用封裝好的串行隊列以及當(dāng)前的EAGLContext對象锯蛀,這兩就之后單獨一篇詳細(xì)說明)旁涤。
? ? 首先先取到自身的outputFramebuffer劈愚;
? ? 如果需要使用mipmap的話需要單獨設(shè)置紋理參數(shù)菌羽;
? ? 圖片數(shù)據(jù)寫入紋理注祖;
? ? 如果需要使用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.最后切勿忘了釋放過程中創(chuàng)建的CoreGraphic罩缴、CoreFundation框架下的對象:
free(imageData);
CFRelease(dataFromImageDataProvider);
·Image rendering
這個方法在處理過程中將會調(diào)用十分頻繁箫章。在整個處理鏈搭建好之后,如果想要將處理后的最終結(jié)果顯示在GPUImageView上或者導(dǎo)出稚叹,更或是從中間的filter節(jié)點直接導(dǎo)出圖片之前都需要執(zhí)行以下方法扒袖。顧名思義季率,這個方法的作用是告訴輸入源開始處理傳入的圖片飒泻。查看具體實現(xiàn)代碼會發(fā)現(xiàn)泞遗,實際的操作就相當(dāng)于把輸入源所擁有的圖片內(nèi)容及參數(shù)傳遞給鏈中的下一個或者多個節(jié)點節(jié)點汹买。
- (void)processImage
下圖就是一個多分支的處理鏈晦毙,F(xiàn)ilter1處理后可導(dǎo)出只具有Filter1效果的圖片见妒;Filter2處理后將會顯示在GPUImageView對象上纵潦,F(xiàn)ilter3處理完后會繼續(xù)傳遞給Filter5進(jìn)行下一步渲染返敬。每個箭頭相當(dāng)于導(dǎo)火索寥院,那么打火機就是調(diào)用GPUImagePicture的- (void)processImage方法秸谢。
- (void)processImage其實只是調(diào)用了不需要block回調(diào)的- (BOOL)processImageWithCompletionHandler:(void (^)(void))completion方法塑煎。通過for循環(huán)遍歷已經(jīng)添加好的targets最铁,例如上圖中的Filter1冷尉、Filter2、Filter3、Filter4膊夹。要知道這個for循環(huán)是在一個定義好的異步串行隊列中執(zhí)行割疾,并且在for循環(huán)之后使用了semaphore宏榕,這就意味著整個操作執(zhí)行全部完成后才會觸發(fā)completion回調(diào)麻昼。
for (idcurrentTarget in targets)
{
//獲取當(dāng)前target添加的位置,這與Filter有關(guān)叉抡,例如如果是兩個或者兩個以上輸入源的Filter答毫,每個輸入源的添加順序就決定著對應(yīng)的處理順序從而影響最終效果褥民,這在之后專門針對Filter文章中做介紹。
? ? NSInteger indexOfObject = [targets indexOfObject:currentTarget]; ?
? ? NSInteger textureIndexOfTarget = [[targetTextureIndices objectAtIndex:indexOfObject] integerValue];
? ? [currentTarget setCurrentlyReceivingMonochromeInput:NO];
//將自身的FrameBuffer的紋理大小傳遞洗搂,從而下一個target生成或者取到相同大小的紋理用作處理后的內(nèi)容的存儲消返。
? ? [currentTarget setInputSize:pixelSizeOfImage atIndex:textureIndexOfTarget];
//將自身存儲著已經(jīng)處理好的內(nèi)容的FrameBuffer傳遞,如果是輸入源的話內(nèi)容也就是原圖片數(shù)據(jù)耘拇。
? ? [currentTarget setInputFramebuffer:outputFramebuffer atIndex:textureIndexOfTarget];
? ? [currentTarget newFrameReadyAtTime:kCMTimeIndefinite atIndex:textureIndexOfTarget];
}
·更簡便的方法得到經(jīng)過Filter處理的圖片
GPUImagePicture暴露了一個可以更方便得到處理后的圖片的方法撵颊,但是前提還是需要搭建整個處理鏈,方便之處是不需要擔(dān)心因為漏寫了一些導(dǎo)出圖片必須要寫的代碼導(dǎo)致的崩潰問題惫叛。第一個入?yún)⑹荈ilter對象倡勇,需傳入需要經(jīng)過處理的Filter鏈的最后一個Filter嘉涌。第二個參數(shù)是攜帶到處圖片對象的block回調(diào)题篷。
- (void)processImageUpToFilter:(GPUImageOutput*)finalFilterInChain withCompletionHandler:(void (^)(UIImage *processedImage))block;
{
[finalFilterInChain useNextFrameForImageCapture];
[self processImageWithCompletionHandler:^{
UIImage *imageFromFilter = [finalFilterInChain imageFromCurrentFramebuffer];
block(imageFromFilter);
}];
}
GPUImageRawDataInput
能使用這個類也是需求需要,主要操作內(nèi)容其實和GPUImagePicture一致都是將圖片數(shù)據(jù)導(dǎo)入,區(qū)別就是GPUImagePicture可以直接使用諸如UIImage或者CGImage格式的圖片對象進(jìn)行導(dǎo)入呈昔,而GPUImageRawDataInput是將圖片的二進(jìn)制數(shù)據(jù)作為內(nèi)容載入到紋理郭宝。通過代碼有兩種辦法(我只知道這兩種育特,可能還有其他辦法)可以使圖片對象轉(zhuǎn)化為二機制數(shù)據(jù)內(nèi)容:1.UIImage->NSData->bytes;2.CoreGraphic重繪UIImage保存至內(nèi)存,bytes指向這段內(nèi)存地址。這種方式加載圖片數(shù)據(jù)一般很少會用到霸妹,沒人想多繞個彎子去實現(xiàn)GPUImagePicture就能實現(xiàn)的效果。但是如果想在圖片載入前對圖片進(jìn)行一些操作或者是想載入CoreGraphic繪制的內(nèi)容的話,使用GPUImageRawDataInput就可以直接將處理后的數(shù)據(jù)載入蚣常,過程中不需要再生成圖片對象。
這個類有兩個與GPUImagePicture類相同的公有變量,就不做重復(fù)解釋拒秘。初始化方法一共有三個羹应,最終都是調(diào)用最后一個方法裸违,先重點介紹一下這個方法的四個入?yún)ⅲ?/p>
- (id)initWithBytes:(GLubyte *)bytesToUpload size:(CGSize)imageSize pixelFormat:(GPUPixelFormat)pixelFormat type:(GPUPixelType)pixelType;
bytesToUpload:
GLubyte是OpenGL數(shù)據(jù)類型料饥,無符號單字節(jié)整型,包含數(shù)值從0 到 255。
圖片對象轉(zhuǎn)化為二進(jìn)制相當(dāng)于用二進(jìn)制數(shù)據(jù)存儲了圖片中每個像素點的RGB或者RGBA值,像素的單獨顏色值范圍是0到255问芬,所以比如某個像素點的R的內(nèi)容存儲就是最小GLubyte單位長度骑歹。那么GLubyte *可以理解為指向存放二進(jìn)制數(shù)據(jù)的內(nèi)存地址指針類型衰琐。
最終這個參數(shù)會在調(diào)用OpenGL的glTexImage2D函數(shù)時最為最后一個參數(shù)被使用狗热,最后一個參數(shù)為:pixels 指定內(nèi)存中指向圖像數(shù)據(jù)的指針光羞。
imageSize:
輸入的二進(jìn)制數(shù)據(jù)內(nèi)容的圖片大小局服。
簡單理解竞穷,將圖片看成由二維的二進(jìn)制數(shù)據(jù)構(gòu)成的數(shù)組,第一行為圖片中最上方一行的像素數(shù)據(jù),依次類推于颖。圖片的二進(jìn)制數(shù)據(jù)內(nèi)容在內(nèi)存中占用連續(xù)的一段長度(存儲的最簡單情況假設(shè))竟块,也就是一維的存儲形式。那么根據(jù)這些數(shù)據(jù)并不能知道原始圖片的大小,也就意味著不知道圖片的第一行像素數(shù)據(jù)長度是多少煎娇。
因此這個參數(shù)的其中一個作用是在調(diào)用OpenGL的glTexImage2D函數(shù)進(jìn)行寫入紋理時說明原始圖片的寬高以確定紋理圖像的寬高。另一個作用是上一章講的GPUImageRawDataInput同樣需要獲取自身Framebuffer時使用票髓。
pixelFormat:
這個參數(shù)主要作用是作為glTexImage2D函數(shù)的第三個參數(shù)攀涵,雖然枚舉中有四種類型,但在實際使用GPUImageRawDataInput初始化時其實只用到了兩種:GPUPixelFormatRGBA洽沟、GPUPixelFormatRGB汁果。從字面上理解就是初始化的圖片數(shù)據(jù)是否帶alpha通道。
OpenGL的glTexImage2D函數(shù)的第三個參數(shù)的解釋是:internalformat 指定紋理中的顏色組件玲躯【莸拢可選的值有GL_ALPHA,GL_RGB,GL_RGBA,GL_LUMINANCE, GL_LUMINANCE_ALPHA 等幾種鳄乏。這幾個可選值與GPUPixelFormat是有對應(yīng)關(guān)系的。
因此在選擇這個參數(shù)的內(nèi)容時并不需要過多考慮棘利,只有兩種選擇:有透明度橱野、沒透明度。
對了善玫,GPUImageRawDataInput頭文件最上方有一行注釋水援,pixelFormat參數(shù)默認(rèn)情況下為GPUPixelFormatBGRA:
// The default format for input bytes is GPUPixelFormatBGRA, unless specified with pixelFormat:
pixelType:
這個參數(shù)同樣是作為glTexImage2D函數(shù)的其中一個參數(shù)使用的:type 指定像素數(shù)據(jù)的數(shù)據(jù)類型。大致可以理解為二進(jìn)制數(shù)據(jù)的存儲精度茅郎。
GPUPixelType只有兩個枚舉項蜗元,以下也做簡單介紹:
同樣的,最上方注釋說明pixelType默認(rèn)情況下為GPUPixelTypeUByte:
// The default type for input bytes is GPUPixelTypeUByte, unless specified with pixelType:
以下待補充
GPUImageUIElement
GPUImageMovie
GPUImageVideoCamera
參考: