1、GLKView和GPUImageVideoCamera
一開始取景框的預覽我是基於 GLKView 做的,GLKView 是蘋果對 OpenGL
的封裝,我們可以使用它的回撥函式 -glkView:drawInRect:
進行對處理後的 samplebuffer
渲染的工作(samplebuffer
是在相機回撥 didOutputSampleBuffer
產生的),附上當初簡版程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
- (CIImage *)renderImageInRect:(CGRect)rect { CMSampleBufferRef sampleBuffer = _sampleBufferHolder.sampleBuffer; if (sampleBuffer != nil) { UIImage *originImage = [self imageFromSamplePlanerPixelBuffer:sampleBuffer]; if (originImage) { if (self.filterName && self.filterName.length > 0) { GPUImageOutput<GPUImageInput> *filter; if ([self.filterType isEqual: @"1"]) { Class class = NSClassFromString(self.filterName); filter = [[class alloc] init]; } else { NSBundle *bundle = [NSBundle bundleForClass:self.class]; NSURL *filterAmaro = [NSURL fileURLWithPath:[bundle pathForResource:self.filterName ofType:@"acv"]]; filter = [[GPUImageToneCurveFilter alloc] initWithACVURL:filterAmaro]; } [filter forceProcessingAtSize:originImage.size]; GPUImagePicture *pic = [[GPUImagePicture alloc] initWithImage:originImage]; [pic addTarget:filter]; [filter useNextFrameForImageCapture]; [filter addTarget:self.gpuImageView]; [pic processImage]; UIImage *filterImage = [filter imageFromCurrentFramebuffer]; //UIImage *filterImage = [filter imageByFilteringImage:originImage]; _CIImage = [[CIImage alloc] initWithCGImage:filterImage.CGImage options:nil]; } else { _CIImage = [CIImage imageWithCVPixelBuffer:CMSampleBufferGetImageBuffer(sampleBuffer)]; } } CIImage *image = _CIImage; if (image != nil) { image = [image imageByApplyingTransform:self.preferredCIImageTransform]; if (self.scaleAndResizeCIImageAutomatically) { image = [self scaleAndResizeCIImage:image forRect:rect]; } } return image; } - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect { @autoreleasepool { rect = CGRectMultiply(rect, self.contentScaleFactor); glClearColor(0, 0, 0, 0); glClear(GL_COLOR_BUFFER_BIT); CIImage *image = [self renderImageInRect:rect]; if (image != nil) { [_context.CIContext drawImage:image inRect:rect fromRect:image.extent]; } } } |
這樣的實現在低端機器上取景框會有明顯的卡頓,而且 ViewController 上的列表幾乎無法滑動,雖然手勢倒是還可以支援。 因為要實現分段拍攝與回刪等功能,採用這種方式的初衷是期望更高度的自定義,而不去使用 GPUImageVideoCamera
, 畢竟我得在 AVCaptureVideoDataOutputSampleBufferDelegate
, AVCaptureAudioDataOutputSampleBufferDelegate
這兩個回撥做文章,為了滿足需求,所以得在不侵入 GPUImage
原始碼的前提下點功夫。
怎麼樣才能在不破壞 GPUImageVideoCamera
的程式碼呢?我想到兩個方法,第一個是建立一個類,然後把 GPUImageVideoCamera
裡的程式碼拷貝過來,這麼做簡單粗暴,缺點是若以後 GPUImage
升級了,程式碼維護起來是個小災難;再來說說第二個方法——繼承,繼承是個挺優雅的行為,可它的麻煩在於獲取不到私有變數,好在有強大的 runtime,解決了這個棘手的問題。下面是用 runtime 獲取私有變數:
1 2 3 4 5 6 |
- (AVCaptureAudioDataOutput *)gpuAudioOutput { Ivar var = class_getInstanceVariable([super class], "audioOutput"); id nameVar = object_getIvar(self, var); return nameVar; } |
至此取景框實現了濾鏡的渲染並保證了列表的滑動幀率。
2、實時合成以及 GPUImage 的 outputImageOrientation
顧名思義,outputImageOrientation
屬性和影像方向有關的。GPUImage
的這個屬性是對不同裝置的在取景框的影像方向做過優化的,但這個優化會與 videoOrientation 產生衝突,它會導致切換攝像頭導致影像方向不對,也會造成拍攝完之後的視訊方向不對。 最後的解決辦法是確保攝像頭輸出的影像方向正確,所以將其設定為 UIInterfaceOrientationPortrait
,而不對 videoOrientation
進行設定,剩下的問題就是怎樣處理拍攝完成之後視訊的方向。
先來看看視訊的實時合成,因為這裡包含了對使用者合成的 CVPixelBufferRef
資源處理。還是使用繼承的方式繼承 GPUImageView
,其中使用了 runtime 呼叫私有方法:
1 2 3 4 5 6 7 8 9 |
SEL s = NSSelectorFromString(@"textureCoordinatesForRotation:"); IMP imp = [[GPUImageView class] methodForSelector:s]; GLfloat *(*func)(id, SEL, GPUImageRotationMode) = (void *)imp; GLfloat *result = [GPUImageView class] ? func([GPUImageView class], s, inputRotation) : nil; ...... glVertexAttribPointer(self.gpuDisplayTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, result); |
直奔重點——CVPixelBufferRef
的處理,將 renderTarget 轉換為 CGImageRef 物件,再使用 UIGraphics 獲得經 CGAffineTransform
處理過方向的 UIImage,此時 UIImage 的方向並不是正常的方向,而是旋轉過90度的圖片,這麼做的目的是為 videoInput 的 transform 屬性埋下伏筆。下面是 CVPixelBufferRef 的處理程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
int width = self.gpuInputFramebufferForDisplay.size.width; int height = self.gpuInputFramebufferForDisplay.size.height; renderTarget = self.gpuInputFramebufferForDisplay.gpuBufferRef; NSUInteger paddedWidthOfImage = CVPixelBufferGetBytesPerRow(renderTarget) / 4.0; NSUInteger paddedBytesForImage = paddedWidthOfImage * (int)height * 4; glFinish(); CVPixelBufferLockBaseAddress(renderTarget, 0); GLubyte *data = (GLubyte *)CVPixelBufferGetBaseAddress(renderTarget); CGDataProviderRef ref = CGDataProviderCreateWithData(NULL, data, paddedBytesForImage, NULL); CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB(); CGImageRef iref = CGImageCreate((int)width, (int)height, 8, 32, CVPixelBufferGetBytesPerRow(renderTarget), colorspace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst, ref, NULL, NO, kCGRenderingIntentDefault); UIGraphicsBeginImageContext(CGSizeMake(height, width)); CGContextRef cgcontext = UIGraphicsGetCurrentContext(); CGAffineTransform transform = CGAffineTransformIdentity; transform = CGAffineTransformMakeTranslation(height / 2.0, width / 2.0); transform = CGAffineTransformRotate(transform, M_PI_2); transform = CGAffineTransformScale(transform, 1.0, -1.0); CGContextConcatCTM(cgcontext, transform); CGContextSetBlendMode(cgcontext, kCGBlendModeCopy); CGContextDrawImage(cgcontext, CGRectMake(0.0, 0.0, width, height), iref); UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); self.img = image; CFRelease(ref); CFRelease(colorspace); CGImageRelease(iref); CVPixelBufferUnlockBaseAddress(renderTarget, 0); |
而 videoInput 的 transform 屬性設定如下:
1 2 |
_videoInput.transform = CGAffineTransformRotate(_videoConfiguration.affineTransform, -M_PI_2); |
經過這兩次方向的處理,合成的小視訊終於方向正常了。此處為簡版的合成視訊程式碼:
1 2 3 4 5 6 |
CIImage *image = [[CIImage alloc] initWithCGImage:img.CGImage options:nil]; CVPixelBufferLockBaseAddress(pixelBuffer, 0); [self.context.CIContext render:image toCVPixelBuffer:pixelBuffer]; ... [_videoPixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:bufferTimestamp] |
可以看到關鍵點還是在於上面繼承自 GPUImageView
這個類獲取到的 renderTarget 屬性,它應該即是取景框實時預覽的結果,我在最初的合成中是使用 sampleBuffer 轉 UIImage,再通過 GPUImage 新增濾鏡,最後將 UIImage 再轉 CIImage,這麼做導致拍攝時會卡。當時我幾乎想放棄了,甚至想採用拍好後再加濾鏡的方式繞過去,最後這些不純粹的方法都被我 ban 掉了。
既然濾鏡可以在取景框實時渲染,我想到了 GPUImageView
可能有料。在閱讀過 GPUImage 的諸多原始碼後,終於在 GPUImageFramebuffer.m
找到了一個叫 renderTarget 的屬性。至此,合成的功能也告一段落。
3、關於濾鏡
這裡主要分享個有意思的過程。App 裡有三種型別的濾鏡。基於 glsl 的、直接使用 acv 的以及直接使用 lookuptable 的。lookuptable 其實也是 photoshop 可匯出的一種圖片,但一般的軟體都會對其加密,下面簡單提下我是如何反編譯“借用”某軟體的部分濾鏡吧。使用 Hopper Disassembler 軟體進行反編譯,然後通過某些關鍵字的搜尋,幸運地找到了下圖的一個方法名。
reverse 只能說這麼多了….在開原始碼裡我已將這一類敏感的濾鏡剔除了。
小結
開發相機 App 是個挺有意思的過程,在其中邂逅不少優秀開原始碼,向開原始碼學習,才能避免自己總是寫出一成不變的程式碼。最後附上專案的開源地址 https://github.com/hawk0620/ZPCamera,希望能夠幫到有需要的朋友,也歡迎 star 和 pull request。
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!