2018-07-29

前言 ? GPUImage是iOS上一個基于OpenGL進行圖像處理的開源框架,內(nèi)置大量濾鏡,架構(gòu)靈活,可以在其基礎(chǔ)上很輕松地實現(xiàn)各種圖像處理功能。本文主要向大家分享一下項目的核心架構(gòu)、源碼解讀及使用心得。 ? GPUImage有哪些特性 ? 1.豐富的輸入組件 攝像頭、圖片、視頻、OpenGL紋理、二進制數(shù)據(jù)、UIElement(UIView, CALayer) ? 2.大量現(xiàn)成的內(nèi)置濾鏡(4大類) 1). 顏色類(亮度、色度、飽和度、對比度、曲線、白平衡...) 2). 圖像類(仿射變換、裁剪、高斯模糊、毛玻璃效果...) 3). 顏色混合類(差異混合、alpha混合、遮罩混合...) 4). 效果類(像素化、素描效果、壓花效果、球形玻璃效果...) ? 3.豐富的輸出組件 UIView、視頻文件、GPU紋理、二進制數(shù)據(jù) ? 4.靈活的濾鏡鏈 濾鏡效果之間可以相互串聯(lián)、并聯(lián),調(diào)用管理相當(dāng)靈活。 ? 5.接口易用 濾鏡和OpenGL資源的創(chuàng)建及使用都做了統(tǒng)一的封裝,簡單易用,并且內(nèi)置了一個cache模塊實現(xiàn)了framebuffer的復(fù)用。 ? 6.線程管理 OpenGLContext不是多線程安全的,GPUImage創(chuàng)建了專門的contextQueue,所有的濾鏡都會扔到統(tǒng)一的線程中處理。 ? 7.輕松實現(xiàn)自定義濾鏡效果 繼承GPUImageFilter自動獲得上面全部特性,無需關(guān)注上下文的環(huán)境搭建,專注于效果的核心算法實現(xiàn)即可。 基本用法 // 獲取一張圖片 UIImage *inputImage = [UIImage imageNamed:@"sample.jpg"]; // 創(chuàng)建圖片輸入組件GPUImagePicture *sourcePicture = [[GPUImagePicture alloc] initWithImage:inputImage smoothlyScaleOutput:YES];? // 創(chuàng)建素描濾鏡 GPUImageSketchFilter *customFilter = [[GPUImageSketchFilter alloc] init]; // 把素描濾鏡串聯(lián)在圖片輸入組件之后 [sourcePicture addTarget:customFilter]; // 創(chuàng)建ImageView輸出組件GPUImageView *imageView = [[GPUImageView alloc] initWithFrame:mainScreenFrame]; [self.view addSubView:imageView]; // 把ImageView輸出組件串在濾鏡鏈末尾[customFilter addTarget:imageView]; // 調(diào)用圖片輸入組件的process方法,渲染結(jié)果就會繪制到imageView上[sourcePicture processImage]; ? 效果如圖: ? 整個框架的目錄結(jié)構(gòu) ? ? 核心架構(gòu) ? ? 基本上每個濾鏡都繼承自GPUImageFilter; 而GPUImageFilter作為整套框架的核心; 接收一個GPUImageFrameBuffer輸入; 調(diào)用GLProgram渲染處理; 輸出一個GPUImageFrameBuffer; 把輸出的GPUImageFrameBuffer傳給通過targets屬性關(guān)聯(lián)的下級濾鏡; 直到傳遞至最終的輸出組件; 核心架構(gòu)可以整體劃分為三塊:輸入、濾鏡處理、輸出 接下來我們就深入源碼,看看GPUImage是如何獲取數(shù)據(jù)、傳遞數(shù)據(jù)、處理數(shù)據(jù)和輸出數(shù)據(jù)的 ? 獲取數(shù)據(jù) ? GPUImage提供了多種不同的輸入組件,但是無論是哪種輸入源,獲取數(shù)據(jù)的本質(zhì)都是把圖像數(shù)據(jù)轉(zhuǎn)換成OpenGL紋理。這里就以視頻拍攝組件(GPUImageVideoCamera)為例,來講講GPUImage是如何把每幀采樣數(shù)據(jù)傳入到GPU的。 ? GPUImageVideoCamera里大部分代碼都是對攝像頭的調(diào)用管理,不了解的同學(xué)可以去學(xué)習(xí)一下AVFoundation(傳送門)。攝像頭拍攝過程中每一幀都會有一個數(shù)據(jù)回調(diào),在GPUImageVideoCamera中對應(yīng)的處理回調(diào)的方法為: ? - (void)processVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer; ? iOS的每一幀攝像頭采樣數(shù)據(jù)都會封裝成CMSampleBufferRef; CMSampleBufferRef除了包含圖像數(shù)據(jù)、還包含一些格式信息、圖像寬高、時間戳等額外屬性; 攝像頭默認的采樣格式為YUV420,關(guān)于YUV格式大家可以自行搜索學(xué)習(xí)一下(傳送門): ? YUV420按照數(shù)據(jù)的存儲方式又可以細分成若干種格式,這里主要是kCVPixelFormatType_420YpCbCr8BiPlanarFullRange和kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange兩種; ? 兩種格式都是planar類型的存儲方式,y數(shù)據(jù)和uv數(shù)據(jù)分開放在兩個plane中; 這樣的數(shù)據(jù)沒法直接傳給GPU去用,GPUImageVideoCamera把兩個plane的數(shù)據(jù)分別取出: ? - (void)processVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer { ? ?// 一大坨的代碼用于獲取采樣數(shù)據(jù)的基本屬性(寬、高、格式等等) ? ?...... ? ?if ([GPUImageContext supportsFastTextureUpload] && captureAsYUV) { ? ? ? ?CVOpenGLESTextureRef luminanceTextureRef = NULL; ? ? ? ?CVOpenGLESTextureRef chrominanceTextureRef = NULL; ? ? ? ?if (CVPixelBufferGetPlaneCount(cameraFrame) > 0) // Check for YUV planar inputs to do RGB conversion ? ? ? ?{ ?? ? ? ? ? ?...... // 從cameraFrame的plane-0提取y通道的數(shù)據(jù),填充到luminanceTextureRef ? ? ? ? ? ?glActiveTexture(GL_TEXTURE4); ? ? ? ? ? ?err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, [[GPUImageContext sharedImageProcessingContext] coreVideoTextureCache], cameraFrame, NULL, GL_TEXTURE_2D, GL_LUMINANCE, bufferWidth, bufferHeight, GL_LUMINANCE, GL_UNSIGNED_BYTE, 0, &luminanceTextureRef); ? ? ? ? ? ?...... ? ? ? ? ? ? ? ? ? ? ? ?// 從cameraFrame的plane-1提取uv通道的數(shù)據(jù),填充到chrominanceTextureRef ? ? ? ? ? ?glActiveTexture(GL_TEXTURE5); ? ? ? ? ? ?err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, [[GPUImageContext sharedImageProcessingContext] coreVideoTextureCache], cameraFrame, NULL, GL_TEXTURE_2D, GL_LUMINANCE_ALPHA, bufferWidth/2, bufferHeight/2, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, 1, &chrominanceTextureRef); ? ? ? ? ? ?...... ? ? ? ? ? ? ?? ? ? ? ? ?// 把luminance和chrominance作為2個獨立的紋理傳入GPU ? ? ? ? ? ?[self convertYUVToRGBOutput]; ? ? ? ? ? ?...... ? ? ? ?} ? ?} else { ?? ? ? ?...... ? ?} ? } ? 注意CVOpenGLESTextureCacheCreateTextureFromImage中對于internalFormat的設(shè)置; 通常我們創(chuàng)建一般紋理的時候都會設(shè)成GL_RGBA,傳入的圖像數(shù)據(jù)也會是rgba格式的; 而這里y數(shù)據(jù)因為只包含一個通道,所以設(shè)成了GL_LUMINANCE(灰度圖); uv數(shù)據(jù)則包含2個通道,所以設(shè)成了GL_LUMINANCE_ALPHA(帶alpha的灰度圖); 另外uv紋理的寬高只設(shè)成了圖像寬高的一半,這是因為yuv420中,每個相鄰的2x2格子共用一份uv數(shù)據(jù); 數(shù)據(jù)傳到GPU紋理后,再通過一個顏色轉(zhuǎn)換(yuv->rgb)的shader(shader是OpenGL可編程著色器,可以理解為GPU側(cè)的代碼,關(guān)于shader需要一些OpenGL編程基礎(chǔ)(傳送門)),繪制到目標(biāo)紋理: ? ?// fullrange varying highp vec2 textureCoordinate; uniform sampler2D luminanceTexture; uniform sampler2D chrominanceTexture; uniform mediump mat3 colorConversionMatrix; void main() { ? ? mediump vec3 yuv; ? ? lowp vec3 rgb; ? ? yuv.x = texture2D(luminanceTexture, textureCoordinate).r; ? ? yuv.yz = texture2D(chrominanceTexture, textureCoordinate).ra - vec2(0.5, 0.5); ? ? rgb = colorConversionMatrix * yuv; ? ? gl_FragColor = vec4(rgb, 1); } ?// videorange varying highp vec2 textureCoordinate; uniform sampler2D luminanceTexture; uniform sampler2D chrominanceTexture; uniform mediump mat3 colorConversionMatrix; void main() { ? ? mediump vec3 yuv; ? ? lowp vec3 rgb; ? ? yuv.x = texture2D(luminanceTexture, textureCoordinate).r - (16.0/255.0); ? ? yuv.yz = texture2D(chrominanceTexture, textureCoordinate).ra - vec2(0.5, 0.5); ? ? rgb = colorConversionMatrix * yuv; ? ? gl_FragColor = vec4(rgb, 1); } ? 注意yuv420fullrange和yuv420videorange的數(shù)值范圍是不同的,因此轉(zhuǎn)換公式也不同,這里會有2個顏色轉(zhuǎn)換shader,根據(jù)實際的采樣格式選擇正確的shader; 渲染輸出到目標(biāo)紋理后就得到一個轉(zhuǎn)換成rgb格式的GPU紋理,完成了獲取輸入數(shù)據(jù)的工作; ? 傳遞數(shù)據(jù) ? GPUImage的圖像處理過程,被設(shè)計成了濾鏡鏈的形式;輸入組件、效果濾鏡、輸出組件串聯(lián)在一起,每次推動渲染的時候,輸入數(shù)據(jù)就會按順序傳遞,經(jīng)過處理,最終輸出。 ? ? GPUImage設(shè)計了一個GPUImageInput協(xié)議,定義了GPUImageFilter之間傳入數(shù)據(jù)的方法: ? - (void)setInputFramebuffer:(GPUImageFramebuffer *)newInputFramebuffer atIndex:(NSInteger)textureIndex { ? ?firstInputFramebuffer = newInputFramebuffer; ? ?[firstInputFramebuffer lock]; } ? firstInputFramebuffer屬性用來保存輸入紋理; GPUImageFilter作為單輸入濾鏡基類遵守了GPUImageInput協(xié)議,GPUImage還提供了GPUImageTwoInputFilter, GPUImageThreeInputFilter等多輸入filter的基類。 ? 這里還有一個很重要的入口方法用于推動數(shù)據(jù)流轉(zhuǎn): ? - (void)newFrameReadyAtTime:(CMTime)frameTime atIndex:(NSInteger)textureIndex { ? ?...... ? ? ? ?[self renderToTextureWithVertices:imageVertices textureCoordinates:[[self class] textureCoordinatesForRotation:inputRotation]]; ? ?[self informTargetsAboutNewFrameAtTime:frameTime]; } ? 每個濾鏡都是由這個入口方法開始啟動,這個方法包含2個調(diào)用 1). 首先調(diào)用render方法進行效果渲染 2). 調(diào)用informTargets方法將渲染結(jié)果推到下級濾鏡 ? GPUImageFilter繼承自GPUImageOutput,定義了輸出數(shù)據(jù),向后傳遞的方法: ? - (void)notifyTargetsAboutNewOutputTexture; ? 但是這里比較奇怪的是濾鏡鏈的傳遞實際并沒有用notifyTargets方法,而是用了前面提到的informTargets方法: ? - (void)informTargetsAboutNewFrameAtTime:(CMTime)frameTime { ? ?...... ? ? ? ?// Get all targets the framebuffer so they can grab a lock on it ? ?for (id currentTarget in targets) { ? ? ? ?if (currentTarget != self.targetToIgnoreForUpdates) { ? ? ? ? ? ?NSInteger indexOfObject = [targets indexOfObject:currentTarget]; ? ? ? ? ? ?NSInteger textureIndex = [[targetTextureIndices objectAtIndex:indexOfObject] integerValue]; ? ? ? ? ? ?[self setInputFramebufferForTarget:currentTarget atIndex:textureIndex]; ? ? ? ? ? ?[currentTarget setInputSize:[self outputFrameSize] atIndex:textureIndex]; ? ? ? ?} ? ?} ? ? ? ?...... ? ? ? ?// Trigger processing last, so that our unlock comes first in serial execution, avoiding the need for a callback ? ?for (id currentTarget in targets) { ? ? ? ?if (currentTarget != self.targetToIgnoreForUpdates) { ? ? ? ? ? ?NSInteger indexOfObject = [targets indexOfObject:currentTarget]; ? ? ? ? ? ?NSInteger textureIndex = [[targetTextureIndices objectAtIndex:indexOfObject] integerValue]; ? ? ? ? ? ?[currentTarget newFrameReadyAtTime:frameTime atIndex:textureIndex]; ? ? ? ?} ? ?} } ? GPUImageOutput定義了一個targets屬性來保存下一級濾鏡,這里可以注意到targets是個數(shù)組,因此濾鏡鏈也支持并聯(lián)結(jié)構(gòu)??梢钥吹竭@個方法主要做了2件事情: 1). 對每個target調(diào)用setInputFramebuffer方法把自己的渲染結(jié)果傳給下級濾鏡作為輸入 2). 對每個target調(diào)用newFrameReadyAtTime方法推動下級濾鏡啟動渲染 濾鏡之間通過targets屬性相互銜接串在一起,完成了數(shù)據(jù)傳遞工作。 ? ? 處理數(shù)據(jù) ? 前面提到的renderToTextureWithVertices:方法便是每個濾鏡必經(jīng)的渲染入口。 每個濾鏡都可以設(shè)置自己的shader,重寫該渲染方法,實現(xiàn)自己的效果: ? - (void)renderToTextureWithVertices:(const GLfloat *)vertices textureCoordinates:(const GLfloat *)textureCoordinates { ? ?...... ? ?[GPUImageContext setActiveShaderProgram:filterProgram]; ? ?outputFramebuffer = [[GPUImageContext sharedFramebufferCache] fetchFramebufferForSize:[self sizeOfFBO] textureOptions:self.outputTextureOptions onlyTexture:NO]; ? ?[outputFramebuffer activateFramebuffer]; ? ?...... ? ?[self setUniformsForProgramAtIndex:0]; ? ? ? ?glClearColor(backgroundColorRed, backgroundColorGreen, backgroundColorBlue, backgroundColorAlpha); ? ?glClear(GL_COLOR_BUFFER_BIT); ? ?glActiveTexture(GL_TEXTURE2); ? ?glBindTexture(GL_TEXTURE_2D, [firstInputFramebuffer texture]); ? ?glUniform1i(filterInputTextureUniform, 2); ? ?glVertexAttribPointer(filterPositionAttribute, 2, GL_FLOAT, 0, 0, vertices); ? ?glVertexAttribPointer(filterTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, textureCoordinates); ? ? ? ?glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); ? ? ? ?...... } ? 上面這個是GPUImageFilter的默認方法,大致做了這么幾件事情: 1). 向frameBufferCache申請一個outputFrameBuffer 2). 將申請得到的outputFrameBuffer激活并設(shè)為渲染對象 3). glClear清除畫布 4). 設(shè)置輸入紋理 5). 傳入頂點 6). 傳入紋理坐標(biāo) 7). 調(diào)用繪制方法 ? 再來看看GPUImageFilter使用的默認shader: ? ?// vertex shader attribute vec4 position; attribute vec4 inputTextureCoordinate; varying vec2 textureCoordinate; void main() { ? ? gl_Position = position; ? ? textureCoordinate = inputTextureCoordinate.xy; } ?// fragment shader varying highp vec2 textureCoordinate; uniform sampler2D inputImageTexture; void main() { ? ? gl_FragColor = texture2D(inputImageTexture, textureCoordinate); } ? 這個shader實際上啥也沒做,VertexShader(頂點著色器)就是把傳入的頂點坐標(biāo)和紋理坐標(biāo)原樣傳給FragmentShader,F(xiàn)ragmentShader(片段著色器)就是從紋理取出原始色值直接輸出,最終效果就是把圖片原樣渲染到畫面。 ? 輸出數(shù)據(jù) ? 比較常用的主要是GPUImageView和GPUImageMovieWriter。 GPUImageView繼承自UIView,用于實時預(yù)覽,用法非常簡單 1). 創(chuàng)建GPUImageView 2). 串入濾鏡鏈 3). 插到視圖里去 UIView的contentMode、hidden、backgroundColor等屬性都可以正常使用 里面比較關(guān)鍵的方法主要有這么2個: ? // 申明自己的CALayer為CAEAGLLayer+ (Class)layerClass ?{ ? ?return [CAEAGLLayer class]; } - (void)createDisplayFramebuffer { ? ?[GPUImageContext useImageProcessingContext]; ? ? ? ?glGenFramebuffers(1, &displayFramebuffer); ? ?glBindFramebuffer(GL_FRAMEBUFFER, displayFramebuffer); ? ?glGenRenderbuffers(1, &displayRenderbuffer); ? ?glBindRenderbuffer(GL_RENDERBUFFER, displayRenderbuffer); ? ?[[[GPUImageContext sharedImageProcessingContext] context] renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer*)self.layer]; ? ?GLint backingWidth, backingHeight; ? ?glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &backingWidth); ? ?glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &backingHeight); ? ? ? ?...... ? ?glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, displayRenderbuffer); ? ?...... } ? 創(chuàng)建frameBuffer和renderBuffer時把renderBuffer和CALayer關(guān)聯(lián)在一起; 這是iOS內(nèi)建的一種GPU渲染輸出的聯(lián)動方法; 這樣newFrameReadyAtTime渲染過后畫面就會輸出到CALayer。 ? GPUImageMovieWriter主要用于將視頻輸出到磁盤; 里面大量的代碼都是在設(shè)置和使用AVAssetWriter,不了解的同學(xué)還是得去看AVFoundation; 這里主要是重寫了newFrameReadyAtTime:方法: ? - (void)newFrameReadyAtTime:(CMTime)frameTime atIndex:(NSInteger)textureIndex { ? ?...... ? ?GPUImageFramebuffer *inputFramebufferForBlock = firstInputFramebuffer; ? ?glFinish(); ? ?runAsynchronouslyOnContextQueue(_movieWriterContext, ^{ ? ? ? ?...... ? ? ? ? ? ? ? ?// Render the frame with swizzled colors, so that they can be uploaded quickly as BGRA frames ? ? ? ?[_movieWriterContext useAsCurrentContext]; ? ? ? ?[self renderAtInternalSizeUsingFramebuffer:inputFramebufferForBlock]; ? ? ? ? ? ? ? ?CVPixelBufferRef pixel_buffer = NULL; ? ? ? ? ? ? ? ?if ([GPUImageContext supportsFastTextureUpload]) { ? ? ? ? ? ?pixel_buffer = renderTarget; ? ? ? ? ? ?CVPixelBufferLockBaseAddress(pixel_buffer, 0); ? ? ? ?} else { ? ? ? ? ? ?CVReturn status = CVPixelBufferPoolCreatePixelBuffer (NULL, [assetWriterPixelBufferInput pixelBufferPool], &pixel_buffer); ? ? ? ? ? ?if ((pixel_buffer == NULL) || (status != kCVReturnSuccess)) { ? ? ? ? ? ? ? ?CVPixelBufferRelease(pixel_buffer); ? ? ? ? ? ? ? ?return; ? ? ? ? ? ?} else { ? ? ? ? ? ? ? ?CVPixelBufferLockBaseAddress(pixel_buffer, 0); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?GLubyte *pixelBufferData = (GLubyte *)CVPixelBufferGetBaseAddress(pixel_buffer); ? ? ? ? ? ? ? ?glReadPixels(0, 0, videoSize.width, videoSize.height, GL_RGBA, GL_UNSIGNED_BYTE, pixelBufferData); ? ? ? ? ? ?} ? ? ? ?} ? ? ? ? ? ? ? ?...... ?? ? ? ?[assetWriterPixelBufferInput appendPixelBuffer:pixel_buffer]; ?? ? ? ?...... ? ?}); } ? 這里有幾個地方值得注意: 1). 在取數(shù)據(jù)之前先調(diào)了一下glFinish,CPU和GPU之間是類似于client-server的關(guān)系,CPU側(cè)調(diào)用OpenGL命令后并不是同步等待OpenGL完成渲染再繼續(xù)執(zhí)行的,而glFinish命令可以確保OpenGL把隊列中的命令都渲染完再繼續(xù)執(zhí)行,這樣可以保證后面取到的數(shù)據(jù)是正確的當(dāng)次渲染結(jié)果。 2). 取數(shù)據(jù)時用了supportsFastTextureUpload判斷,這是個從iOS5開始支持的一種CVOpenGLESTextureCacheRef和CVImageBufferRef的映射(映射的創(chuàng)建可以參看獲取數(shù)據(jù)中的 CVOpenGLESTextureCacheCreateTextureFromImage),通過這個映射可以直接拿到CVPixelBufferRef而不需要再用glReadPixel來讀取數(shù)據(jù),這樣性能更好。 最后歸納一下本文涉及到的知識點 ? 1.?AVFoundation 攝像頭調(diào)用、輸出視頻都會用到AVFoundation 2.?YUV420 視頻采集的數(shù)據(jù)格式 3.?OpenGL shader GPU的可編程著色器 4.?CAEAGLLayer iOS內(nèi)建的GPU到屏幕的聯(lián)動方法 5.?fastTextureUpload iOS5開始支持的一種CVOpenGLESTextureCacheRef和CVImageBufferRef的映射 ?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容