开源一个上架 App Store 的相机 App

Osho 相机是我独立开发上架的一个相机 App,App Store地址:点我。它支持1:1,4:3,16:9多种分辨率拍摄,滤镜可在取景框的实时预览,拍摄过程可与滤镜实时合成,支持分段拍摄,支持回删等特性。下面先分享分享开发这个 App 的一些心得体会,文末会给出项目的下载地址,阅读本文可能需要一点点 AVFoundation 开发的基础。

1、GLKView和GPUImageVideoCamera

一开始取景框的预览我是基于 GLKView 做的,GLKView 是苹果对OpenGL的封装,我们可以使用它的回调函数-glkView:drawInRect:进行对处理后的samplebuffer渲染的工作(samplebuffer是在相机回调didOutputSampleBuffer产生的),附上当初简版代码:

Objective-C

- (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];

}

}

}

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{

CMSampleBufferRefsampleBuffer=_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"]){

Classclass=NSClassFromString(self.filterName);

filter=[[classalloc]init];

}else{

NSBundle*bundle=[NSBundlebundleForClass:self.class];

NSURL*filterAmaro=[NSURLfileURLWithPath:[bundlepathForResource:self.filterNameofType:@"acv"]];

filter=[[GPUImageToneCurveFilteralloc]initWithACVURL:filterAmaro];

}

[filterforceProcessingAtSize:originImage.size];

GPUImagePicture*pic=[[GPUImagePicturealloc]initWithImage:originImage];

[picaddTarget:filter];

[filteruseNextFrameForImageCapture];

[filteraddTarget:self.gpuImageView];

[picprocessImage];

UIImage*filterImage=[filterimageFromCurrentFramebuffer];

//UIImage *filterImage = [filter imageByFilteringImage:originImage];

_CIImage=[[CIImagealloc]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];

}

}

returnimage;

}

-(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 获取私有变量:

Objective-C

- (AVCaptureAudioDataOutput *)gpuAudioOutput {

Ivar var = class_getInstanceVariable([super class], "audioOutput");

id nameVar = object_getIvar(self, var);

return nameVar;

}

1

2

3

4

5

6-(AVCaptureAudioDataOutput*)gpuAudioOutput{

Ivarvar=class_getInstanceVariable([superclass],"audioOutput");

idnameVar=object_getIvar(self,var);

returnnameVar;

}

至此取景框实现了滤镜的渲染并保证了列表的滑动帧率。

2、实时合成以及 GPUImage 的 outputImageOrientation

顾名思义,outputImageOrientation属性和图像方向有关的。GPUImage的这个属性是对不同设备的在取景框的图像方向做过优化的,但这个优化会与 videoOrientation 产生冲突,它会导致切换摄像头导致图像方向不对,也会造成拍摄完之后的视频方向不对。 最后的解决办法是确保摄像头输出的图像方向正确,所以将其设置为UIInterfaceOrientationPortrait,而不对videoOrientation进行设置,剩下的问题就是怎样处理拍摄完成之后视频的方向。

先来看看视频的实时合成,因为这里包含了对用户合成的CVPixelBufferRef资源处理。还是使用继承的方式继承GPUImageView,其中使用了 runtime 调用私有方法:

Objective-C

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);

1

2

3

4

5

6

7

8

9SELs=NSSelectorFromString(@"textureCoordinatesForRotation:");

IMPimp=[[GPUImageViewclass]methodForSelector:s];

GLfloat*(*func)(id,SEL,GPUImageRotationMode)=(void*)imp;

GLfloat*result=[GPUImageViewclass]?func([GPUImageViewclass],s,inputRotation): nil;

......

glVertexAttribPointer(self.gpuDisplayTextureCoordinateAttribute,2,GL_FLOAT,0,0,result);

直奔重点——CVPixelBufferRef的处理,将 renderTarget 转换为 CGImageRef 对象,再使用 UIGraphics 获得经CGAffineTransform处理过方向的 UIImage,此时 UIImage 的方向并不是正常的方向,而是旋转过90度的图片,这么做的目的是为 videoInput 的 transform 属性埋下伏笔。下面是 CVPixelBufferRef 的处理代码:

Objective-C

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);

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

34intwidth=self.gpuInputFramebufferForDisplay.size.width;

intheight=self.gpuInputFramebufferForDisplay.size.height;

renderTarget=self.gpuInputFramebufferForDisplay.gpuBufferRef;

NSUIntegerpaddedWidthOfImage=CVPixelBufferGetBytesPerRow(renderTarget)/4.0;

NSUIntegerpaddedBytesForImage=paddedWidthOfImage*(int)height*4;

glFinish();

CVPixelBufferLockBaseAddress(renderTarget,0);

GLubyte*data=(GLubyte*)CVPixelBufferGetBaseAddress(renderTarget);

CGDataProviderRefref=CGDataProviderCreateWithData(NULL,data,paddedBytesForImage,NULL);

CGColorSpaceRefcolorspace=CGColorSpaceCreateDeviceRGB();

CGImageRefiref=CGImageCreate((int)width,(int)height,8,32,CVPixelBufferGetBytesPerRow(renderTarget),colorspace,kCGBitmapByteOrder32Little|kCGImageAlphaPremultipliedFirst,ref,NULL,NO,kCGRenderingIntentDefault);

UIGraphicsBeginImageContext(CGSizeMake(height,width));

CGContextRefcgcontext=UIGraphicsGetCurrentContext();

CGAffineTransformtransform=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 属性设置如下:

Objective-C

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

1

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

经过这两次方向的处理,合成的小视频终于方向正常了。此处为简版的合成视频代码:

Objective-C

CIImage *image = [[CIImage alloc] initWithCGImage:img.CGImage options:nil];

CVPixelBufferLockBaseAddress(pixelBuffer, 0);

[self.context.CIContext render:image toCVPixelBuffer:pixelBuffer];

...

[_videoPixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:bufferTimestamp]

1

2

3

4

5

6CIImage*image=[[CIImagealloc]initWithCGImage:img.CGImageoptions:nil];

CVPixelBufferLockBaseAddress(pixelBuffer,0);

[self.context.CIContextrender:imagetoCVPixelBuffer:pixelBuffer];

...

[_videoPixelBufferAdaptorappendPixelBuffer:pixelBufferwithPresentationTime: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。


原文地址:http://ios.jobbole.com/92926/ 

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容