開源一個上架 App Store 的相機 App

WerYWI 7年前發布 | 10K 次閱讀 開源 iOS開發 移動開發

1、GLKView和GPUImageVideoCamera

一開始取景框的預覽我是基于 GLKView 做的,GLKView 是蘋果對 OpenGL 的封裝,我們可以使用它的回調函數 -glkView:drawInRect: 進行對處理后的 samplebuffer 渲染的工作( samplebuffer 是在相機回調 didOutputSampleBuffer 產生的),附上當初簡版代碼:

- (CIImage *)renderImageInRect:(CGRect)rect {
    CMSampleBufferRef sampleBuffer = _sampleBufferHolder.sampleBuffer;
 
    if (sampleBuffer != nil) {
        UIImage *originImage = [selfimageFromSamplePlanerPixelBuffer:sampleBuffer];
        if (originImage) {
          if (self.filterName && self.filterName.length > 0) {
 
              GPUImageOutput<GPUImageInput> *filter;
                if ([self.filterTypeisEqual: @"1"]) {
                    Class class = NSClassFromString(self.filterName);
                    filter = [[class alloc]init];
                } else {
                    NSBundle *bundle = [NSBundlebundleForClass:self.class];
                    NSURL *filterAmaro = [NSURLfileURLWithPath:[bundlepathForResource:self.filterNameofType:@"acv"]];
                    filter = [[GPUImageToneCurveFilter alloc]initWithACVURL:filterAmaro];
                }
                [filterforceProcessingAtSize:originImage.size];
                GPUImagePicture *pic = [[GPUImagePicture alloc]initWithImage:originImage];
                [picaddTarget:filter];
                [filteruseNextFrameForImageCapture];
                [filteraddTarget:self.gpuImageView];
                [picprocessImage];              
                UIImage *filterImage = [filterimageFromCurrentFramebuffer];
                //UIImage *filterImage = [filter imageByFilteringImage:originImage];
 
                _CIImage = [[CIImage alloc]initWithCGImage:filterImage.CGImageoptions:nil];
            } else {
            _CIImage = [CIImageimageWithCVPixelBuffer:CMSampleBufferGetImageBuffer(sampleBuffer)];
        }
    }  
    CIImage *image = _CIImage;
 
    if (image != nil) {
        image = [imageimageByApplyingTransform:self.preferredCIImageTransform];
 
        if (self.scaleAndResizeCIImageAutomatically) {
          image = [selfscaleAndResizeCIImage:imageforRect:rect];
        }
    }
 
    return image;
}
 
- (void)glkView:(GLKView *)viewdrawInRect:(CGRect)rect {
    @autoreleasepool {
        rect = CGRectMultiply(rect, self.contentScaleFactor);
        glClearColor(0, 0, 0, 0);
        glClear(GL_COLOR_BUFFER_BIT);
 
        CIImage *image = [selfrenderImageInRect:rect];
 
        if (image != nil) {
            [_context.CIContextdrawImage:imageinRect:rectfromRect:image.extent];
        }
    }
}
 

這樣的實現在低端機器上取景框會有明顯的卡頓,而且 ViewController 上的列表幾乎無法滑動,雖然手勢倒是還可以支持。 因為要實現分段拍攝與回刪等功能,采用這種方式的初衷是期望更高度的自定義,而不去使用 GPUImageVideoCamera , 畢竟我得在 AVCaptureVideoDataOutputSampleBufferDelegate , AVCaptureAudioDataOutputSampleBufferDelegate 這兩個回調做文章,為了滿足需求,所以得在不侵入 GPUImage 源代碼的前提下點功夫。

怎么樣才能在不破壞 GPUImageVideoCamera 的代碼呢?我想到兩個方法,第一個是創建一個類,然后把 GPUImageVideoCamera 里的代碼拷貝過來,這么做簡單粗暴,缺點是若以后 GPUImage 升級了,代碼維護起來是個小災難;再來說說第二個方法——繼承,繼承是個挺優雅的行為,可它的麻煩在于獲取不到私有變量,好在有強大的 runtime,解決了這個棘手的問題。下面是用 runtime 獲取私有變量:

- (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 調用私有方法:

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 的處理代碼:

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 屬性設置如下:

_videoInput.transform = CGAffineTransformRotate(_videoConfiguration.affineTransform, -M_PI_2);
 

經過這兩次方向的處理,合成的小視頻終于方向正常了。此處為簡版的合成視頻代碼:

CIImage *image = [[CIImage alloc]initWithCGImage:img.CGImageoptions:nil];
CVPixelBufferLockBaseAddress(pixelBuffer, 0);
[self.context.CIContextrender:imagetoCVPixelBuffer: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 是個挺有意思的過程,在其中邂逅不少優秀開源代碼,向開源代碼學習,才能避免自己總是寫出一成不變的代碼。最

 

 

 

 本文由用戶 WerYWI 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!