GPUImage(五):五種類型輸入源

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



參考:

1. OpenGL學習筆記4:紋理

2.glTexImage2D的詳細說明



最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容