GUPImage是一個使用GPU加速的圖像視頻處理框架瘾蛋。最新的GPUImage已經是第三版GPUImage3刨仑。GPUImage1是用Objective-C實現,用OpenGL ES 2.0來渲染栋齿,可以運行在iOS纤虽,Mac-OS等平臺。GPUImage2是用swift實現公给,用OpenGL ES 2.0來渲染,可以運行在iOS腿准,Mac-OS等平臺和Linux等平臺。GPUImage3用swift實現灾前,但采用了蘋果新出的圖像渲染框架Metal來渲染扑浸,可以運行在iOS础嫡,Mac-OS等平臺以及硬件可支持的Linux等平臺。
因為swift和Metal的相繼出現,GPUImage也很久沒更新,甚至最新提出的issue也沒有修復闽瓢,作者可能更專注于新的版本迭代,這一切都可以理解,但同樣如果還在使用GPUImage1的開發(fā)人員可能就要自己多做些工作了销钝。但同樣對于初學者來說對GPUImage1的學習卻不失為一個學習底層圖像視頻處理加速方面知識的一個入門途徑纵装。本篇延續(xù)之前的源碼閱讀筆記風格,還是從一個簡單典型的應用來分析整個流程中GPUImage的作用過程。
首先下載下來GPUImage的源碼瓶籽,
可看出源碼分為實例和框架兩部分俏险,實例分為iOS和Mac兩部分,iOS實例部分中也分為很多類型的應用种蘸,現在就從一個簡單典型的應用部分FilterShowcase開始分析顶猜。
這個demo主要實現的是選擇自帶濾鏡對攝像頭采集的內容進行實時處理(上面是效果圖)挠日。
下面先來看一下這個demo的結構:
首頁是一個自帶的濾鏡選擇列表,對應的controller為ShowcaseFilterListController
選擇后進入上面效果圖對應的頁面庇麦,對應的controller為ShowcaseFilterViewController航棱。
再往下邊Frameworks的目錄下有一個GPUImage的子工程秕豫。
這個子工程其實就是GPUImage的框架部分。GPUImage.h頭文件中導入了框架所有頭文件以供用戶通過導入這個頭文件引用框架饵隙。GLProgram類負責openGL ES的著色器管理驶俊。GPUImageContext類用來管理openGL ES的上下文胚膊,工作隊列药版,以及用這個類來調用著色器,相當于整個框架的上下文管理類还栓。GPUImageFramebuffer用來管理要操作的紋理緩存和幀緩存碌廓,GPUImageFramebufferCache用來管理緩沖區(qū)。Sources部分是輸入部分剩盒,用來輸入要處理的對象氓皱,其中有攝像頭的采集,靜態(tài)圖片輸入,視頻輸入等垢揩。Pipeline部分只有一個類GPUImageFilterPipeline顧名思義用來將多個濾鏡依次連接在一個管道內贪惹。Filters部分為濾鏡實現和管理部分订咸,也包含一些自帶濾鏡蜻懦。Outputs部分為最終輸出部分眼坏,包括在view上的顯示輸出,寫入一個文件等葵第。
下面開始重點分析GPUImage的作用過程:
這個是ShowcaseFilterViewController的初始化方法:
設置了上個頁面選擇的濾鏡類型。
在xib中將根視圖設置為GPUImageView類型砰苍。
進入到viewDidLoad方法中:
在viewDidLoad方法中創(chuàng)建濾鏡掂为。
- (void)setupFilter這個方法比較長大約1500+行代碼,折疊處理后是以上結構殴边,下面開始逐行分析:
videoCamera = [[GPUImageVideoCamera alloc] initWithSessionPreset:AVCaptureSessionPreset1920x1080 cameraPosition:AVCaptureDevicePositionBack]表示創(chuàng)建一個相機用來采集圖像糊渊。GPUImageVideoCamera是框架Sources部分中使用AVFoundation自定義的一個相機右核。
videoCamera.outputImageOrientation = UIInterfaceOrientationPortrait表示豎屏采集。
展開switch條件分支:
可看到每個分支內容是根據不同的濾鏡設置頁面title渺绒,如果濾鏡可調節(jié)設置濾鏡滑塊最大最小值和當前值蒙兰,并初始化對應濾鏡。以一個常見的亮度濾鏡為例:
亮度濾鏡由框架自帶的GPUImageBrightnessFilter實現芒篷。
再繼續(xù)看下邊的if-else分支:
每個選擇主要是配置整個圖像處理的管道搜变,以亮度濾鏡為例:
前邊部分:
所以全過程就是將videoCamera作為圖像處理的源頭,通過[videoCamera addTarget:filter]將亮度濾鏡作為管道中videoCamera處理后的下一個環(huán)節(jié)针炉,濾鏡處理結束后再通過[filter addTarget:filterView]將GPUImageView *類型的self.view做為管道的再下一個環(huán)節(jié)挠他,在這里也就是最后一個環(huán)節(jié)了,結果就是通過亮度濾鏡將攝像頭采集到的圖像濾鏡處理后在GPUImageView *類型的self.view上實時顯示出來篡帕。
最后
攝像頭開啟采集處理殖侵。
下面就以亮度濾鏡為例分析整個圖像處理過程:
整個流程梳理下來看贸呢,步驟是這樣的:
第一步,GPUImageVideoCamera作為圖像處理的第一個環(huán)節(jié)拢军,負責圖像采集
第二步楞陷,GPUImageBrightnessFilter作為圖像處理的第二個環(huán)節(jié),接收第一步采集的圖像茉唉,并根據設置的參數進行亮度濾鏡處理
第三步固蛾,GPUImageView作為第三個環(huán)節(jié),接收第二步處理結果并在視圖上進行實時渲染度陆。
可以看出整個流程要涉及到AVFoundation艾凯,CoreGraphics,CoreVideo懂傀,CoreMedia趾诗,openGL ES等部分,下面就每一步和每一步涉及的系統(tǒng)框架進行分析蹬蚁。
首先恃泪,初始化GPUImageVideoCamera相機
點擊進入GPUImageVideoCamera類:
這個方法主要是基于AVFoundation創(chuàng)建一個攝像頭采集管理的類,
cameraProcessingQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0);audioProcessingQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW,0);創(chuàng)建相機和麥克風的處理隊列犀斋,frameRenderingSemaphore = dispatch_semaphore_create(1);創(chuàng)建了一個信號量贝乎,用于管理GPUImageVideoCamera對圖像的處理(后面詳細介紹)。
_frameRate = 0設置幀率闪水,_runBenchmark = NO用來做日志處理糕非,capturePaused = NO表示捕獲暫停蒙具,outputRotation = kGPUImageNoRotation; internalRotation = kGPUImageNoRotation;設置方向模式球榆,captureAsYUV = YES設置以YUV的格式采集,_preferredConversion = kColorConversion709;設置顏色格式轉換時運算矩陣(后面詳細講)禁筏。
下面這部分為創(chuàng)建自定義相機的過程:
這塊為得到AVCaptureDevice *類型的采集設備持钉,方法是上面這樣的,通過獲取devices數組篱昔,再對其遍歷得到對應的采集設備每强,不是通過開發(fā)者直接創(chuàng)建,由于傳進來的cameraPosition是AVCaptureDevicePositionBack州刽,所以獲取到的也是相對應的后攝像頭采集設備空执。
創(chuàng)建采集圖像的會話,并開始配置會話穗椅。
創(chuàng)建采集設備的輸入辨绊。
創(chuàng)建采集設備的輸出。
綜上所述匹表,使用AVFoundation框架創(chuàng)建的相機包含四部分门坷,AVCaptureDevice *_inputCamera可認為是相機的管理類也可以將其想象為一個相機宣鄙,AVCaptureSession*_captureSession是會話連接,可以將其想象為把鏡頭采集的視頻傳入成為后臺數據中間的傳輸線路默蚌,AVCaptureDeviceInput *videoInput為相機的采集器冻晤,可以將其想象為鏡頭,AVCaptureVideoDataOutput *videoOutput負責輸出相機采集的數據绸吸,可以將其想起想象為相機自帶的洗印設備鼻弧。上邊的比喻不一定完全確切,但總體來說這部分其實就是AVFoundation通過對相機硬件的抽象惯裕,從而提供給開發(fā)者簡潔的接口温数。
判斷條件中:
這個判斷方法表示設備是否支持將一個image的紋理綁定為一個openGL ES的處理紋理緩存(下文對基于這個判斷有不同處理方式,后面有具體詳解)蜻势。
captureAsYUV上文有設置過YES撑刺,如果滿足這個條件,
如果輸出數據videoOutput支持kCVPixelFormatType_420YpCbCr8BiPlanarFullRange格式輸出握玛,則設置supportsFullYUVRange為YES够傍。對于CoreVideo pixel format type constants的解釋,參看詳情
根據supportsFullYUVRange值設置數據輸出videoOutput的編解碼配置挠铲,并設置isFullYUVRange的值冕屯,isFullYUVRange表示videoOutput支持kCVPixelFormatType_420YpCbCr8BiPlanarFullRange的輸出格式。
接下來會做一些openGL ES的處理:
表明所有openGL ES的操作都在這個[GPUImageContext sharedContextQueue]隊列中完成拂苹。
繼續(xù)展開折疊:
將當前openGL ES的EAGLContext*類型上下文設置為GPUImage的openGL ES上下文安聘。
根據isFullYUVRange的不同值選擇不同的顏色格式轉換著色器。
這部分是根據傳入的shading創(chuàng)建頂點著色器和片元著色器瓢棒。
可看出頂點著色器是kGPUImageVertexShaderString同一個的浴韭,區(qū)別在于片元著色器,isFullYUVRange為YES時選擇kGPUImageYUVFullRangeConversionForLAFragmentShaderString脯宿,反之選擇kGPUImageYUVVideoRangeConversionForLAFragmentShaderString念颈。
這一部分是建立著色器和應用之間的變量傳輸管道,舉例說明:
這一步是為著色器程序program綁定一塊緩沖區(qū)连霉,第一個參數傳入著色器程序標示榴芳,第二個參數傳入緩沖區(qū)數字標示(這里很巧妙用數組attributes的變量position下標,下標存儲了緩沖區(qū)數字標示跺撼,元素存儲了名字)窟感,第三個參數將緩沖區(qū)命名為position。
接著:
將緩沖區(qū)的數字標示傳到本類以供下文調用(應用就是通過這個標示將變量傳給著色器的對應緩沖區(qū)歉井,下邊還有具體講解)柿祈。
接下來
調用著色器程序。
將頂點數據和紋理數據設置為enable狀態(tài)以供后邊調用。
就此這一步的openGL ES任務完成谍夭,可看出這一步主要是設置好主色器黑滴,配置好著色器和應用之間的數據傳輸,運行著色器紧索,總之做好一切準備工作袁辈,磨刀霍霍就等著數據進來對其處理了。
這一步又是對相機的處理珠漂,將videoOutput數據輸入的代理設置為本類(會實現相關代理方法接收采集數據晚缩,下文會具體分析),并將其連入到會話_captureSession中媳危。
_captureSession標示目前的攝像頭采集水平這里是AVCaptureSessionPreset1920x1080荞彼,也即1080p的視頻流,同時并提交配置待笑。
就此這個相機的初始化完成鸣皂,可以看出這個初始化方法主要做了兩件事,一件是創(chuàng)建相機暮蹂,另外一件是設置相關的openGL ES工作部分寞缝。
在VC中調用[videoCamera startCameraCapture]就可以開始視頻采集了。
接下來分析處理數據的過程:
前面設置了相機輸出數據的代理是本類仰泻,所以本類也實現了相關的代理方法:
代理方法中第一個參數captureOutput為采集設備(AVFoundation不僅可以用來創(chuàng)建自定義相機荆陆,也可以創(chuàng)建錄音設備,由于本篇側重于圖像處理集侯,所以前邊沒提錄音這塊)的數據輸出設備被啼,第二個參數sampleBuffer是相機的輸出的采樣緩存數據,第三個參數connection表示AVCaptureSession會話系統(tǒng)的一個端到端的連接棠枉。
所以這種情況下必然進入第三個else分支:
首先這里使用了在初始化方法中設置的信號量frameRenderingSemaphore浓体,如果沒資源就return退出方法,如果有資源信號量減一繼續(xù)术健。
如果VC中沒有使用人臉識別hook的話汹碱,接著在GPUImageContext的全局隊列contextQueue中執(zhí)行方法[self processVideoSampleBuffer:sampleBuffer]粘衬。在這個方法處理結束后發(fā)送信號dispatch_semaphore_signal(frameRenderingSemaphore)荞估。
所以全過程看出信號量主要是針對這個方法[self processVideoSampleBuffer:sampleBuffer]進行管理,如果對一個單位的采樣處理結束有資源接著處理下一個稚新,否則就丟棄這個采樣勘伺。
這個方法也就是將YUV格式的采樣轉化為RGB格式,下面具體分析:
CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent()是記錄一個起始時間點用以后邊計算幀率褂删。
展開第一個選擇支:
如果獲取到的CVBuffer不空的進入選擇飞醉,再根據注釋解釋的選擇和isFullYUVRange變量設置_preferredConversion。
CMTime currentTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);設置最近的sampleBuffer的時間戳。
[GPUImageContext useImageProcessingContext]設置當前opengl es 的上下文context缅帘。
再繼續(xù)展開下一個選擇分支:
YUV中Y表示亮度轴术,UV表示色彩,所以準備兩個CVBuffer:
繼續(xù)展開下一個選擇分支:
設置紋理的長和寬钦无。
glActiveTexture(GL_TEXTURE4)設置激活紋理單元逗栽。Y-plane 選擇當前活躍紋理單元為當前紋理單元。
如果本設備支持紅色紋理失暂,則調用err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, [[GPUImageContext sharedImageProcessingContext] coreVideoTextureCache], cameraFrame, NULL, GL_TEXTURE_2D, GL_LUMINANCE, bufferWidth, bufferHeight, GL_LUMINANCE, GL_UNSIGNED_BYTE, 0, &luminanceTextureRef)從cameraFrame中創(chuàng)建出Y平面的紋理彼宠,這個函數GL_LUMINANCE參數為亮度,luminanceTextureRef為新創(chuàng)建的紋理地址引用弟塞,也就是上文聲明的亮度紋理凭峡。
接下來這部分都是openGL ES的API,如注釋所示决记,就是將亮度紋理傳入到著色器對應的緩存中摧冀,yuvConversionLuminanceTextureUniform = [yuvConversionProgram uniformIndex:@"luminanceTexture"]在本類的初始化方法中設置過,前文提到過系宫。
接下來這部分:
同上面原理一樣按价,將UV色彩紋理傳入著色器程序中。
接下來[self convertYUVToRGBOutput]這個方法就是將YUV格式轉化為RGB的過程笙瑟。
總體就是調用openGL ES的一系列方法楼镐,運行前邊創(chuàng)建的著色器程序將輸入的YUV格式的紋理轉化為RGB格式的紋理。
這里設置旋轉方向的長和寬往枷。
接著調用方法[self updateTargetsForVideoCameraUsingCacheTextureAtWidth:rotatedImageBufferWidth? height:rotatedImageBufferHeight? time:currentTime]將處理過的紋理傳給管道中的下一個環(huán)節(jié)框产。
展開第二個循環(huán):
可以看出最終是調用GPUImageInput協(xié)議的[currentTarget newFrameReadyAtTime:currentTime atIndex:textureIndexOfTarget]方法將接力棒傳遞到下一個處理環(huán)節(jié),在這個例子里其實就是GPUImageBrightnessFilter亮度濾鏡错洁。
綜上秉宿,GPUImageVideoCamera這部分全流程就結束了。下邊還有兩個環(huán)節(jié)GPUImageBrightnessFilter和GPUImageView的處理屯碴,且聽下回分解描睦。