https://itunes.apple.com/cn/app/osho/id1203312279?mt=8。它支持1:1,4:3,16:9多种分辨率拍摄,滤镜可在取景框的实时预览,拍摄过程可与滤镜实时合成,支持分段拍摄,支持回删等特性。下面先分享分享开发这个 App 的一些心得体会,文末会给出项目的下载地址,阅读本文可能需要一点点 AVFoundation 开发的基础。

1、GLKView和GPUImageVideoCamera

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

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

- (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.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。

Oslo 相机 App的更多相关文章

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

    Osho 相机是我独立开发上架的一个相机 App,App Store地址:https://itunes.apple.com/cn/app/osho/id1203312279?mt=8.它支持1:1,4 ...

  2. Android 指定调用已安装的某个“相机”App

    在做项目时,有这样一个需求:如果我的手机中安装了四个相机软件,那么,在调用系统相机的时候,这四个相机软件都会被列出来,但是其中的两个在拍照完后并不能将拍得的照片返回给我,因此,能不能指定开启一个我已知 ...

  3. iOS开发-实现相机app的方法[转载自官方]

    This brief code example to illustrates how you can capture video and convert the frames you get to U ...

  4. 在一个老外微信PM的眼中,中国移动App UI那些事儿

    本文编译自Dan Grover的博客,他现在是腾讯微信的产品经理.以下是他从旧金山搬到广州后的近半年时间里,在试用过微信微博等中国主流移动App后,总结出的中美App在设计理念上的差异,并对中国移动A ...

  5. Android相机使用(系统相机、自定义相机、大图片处理)

    本博文主要是介绍了android上使用相机进行拍照并显示的两种方式,并且由于涉及到要把拍到的照片显示出来,该例子也会涉及到Android加载大图片时候的处理(避免OOM),还有简要提一下有些人Surf ...

  6. 毕业设计--天气预报App

    9月中旬,开始动手做我的毕业设计了,之前一直在纠结做啥,后来想想,既然是做毕业设计,那就大胆地做点自己没接触过的东西吧.然后网上查找资料得知做天气预报需要用到开放的API,而且要用那种现在还在维护的, ...

  7. Android调用系统相机、自己定义相机、处理大图片

    Android调用系统相机和自己定义相机实例 本博文主要是介绍了android上使用相机进行拍照并显示的两种方式,而且因为涉及到要把拍到的照片显示出来,该样例也会涉及到Android载入大图片时候的处 ...

  8. Android - 和其他APP交互 - 获得activity的返回值

    启用另一个activity不一定是单向的.也可以启用另一个activity并且获得返回值.要获得返回值的话,调用startActivityForResult()(而不是startActivity()) ...

  9. 【踩坑速记】MIUI系统BUG,调用系统相机拍照可能会带给你的一系列坑,将拍照适配方案进行到底!

    一.写在前面 前几天也是分享了一些学习必备干货(还没关注的,赶紧入坑:传送门),也好久没有与大家探讨技术方案了,心里也是挺痒痒的,这不,一有点闲暇之时,就迫不及待把最近测出来的坑分享给大家. 提起An ...

随机推荐

  1. Java奇淫巧技之Lombok

    http://blog.csdn.net/ghsau/article/details/52334762

  2. 推文《阿里凑单算法首次公开!基于Graph Embedding的打包购商品挖掘系统解析》笔记

    推文<阿里凑单算法首次公开!基于Graph Embedding的打包购商品挖掘系统解析>笔记 从17年5月份开始接触Graph Embedding,学术论文读了很多,但是一直不清楚这技术是 ...

  3. 你应该知道的Linux历史

    说道linux的历史不得不说的就是unix,我们的linux就是类unix系统: 1969年第一台unix系统 贝尔实验室 1970年C语言诞生,对unix内核重新编写 system V(AT& ...

  4. SQLSERVER 免费对比数据库结构和数据的工具支持:SQL Server 2012, SQL Server 2008 and SQL Server 2005

    New xSQL Schema Compare - version 5 Compare the schemas of two SQL Server databases, review differen ...

  5. 浅谈压缩感知(十五):感知矩阵之spark常数

    在压缩感知中,有一些用来评价感知矩阵(非测量矩阵)的指标,如常见的RIP等,除了RIP之外,spark常数也能够用来衡量能否成为合适的感知矩阵. 0.相关概念与符号 1.零空间条件NULL Space ...

  6. beanshell

    http://www.beanshell.org/download.html

  7. AS打开速度慢,AS项目导入慢,新建项目导入慢

    1.AS打开速度慢 在Android Studio的bin目录里,打开idea.prooperties文件,添加disable.android.first.run=true 再次打开时,启动加快 2. ...

  8. Timer 与 TimerTask 示例

    , 1000);// 1秒后执行 然后每隔1秒 执行一次 ); ); timer.cancel();//停止任务(程序停止) } } /** * 启动刷新滚动数据的定时器 */public void ...

  9. VO、DTO与领域模型的概念

    业务对象模型(也叫领域模型 domain model)是描述业务用例实现的对象模型.它是对业务角色和业务实体之间应该如何联系和协作以执行业务的一种抽象.业务对象模型从业务角色内部的观点定义了业务用例. ...

  10. 微信小程序 - 配置普通二维码跳小程序

    普通二维码跳小程序规则: https://developers.weixin.qq.com/miniprogram/introduction/qrcode.html#%E5%8A%9F%E8%83%B ...