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. Qt OpenGL:学习现代3D图形编程之四,透视投影浅析

    一.非真实的世界 与之前几篇文章不同的是,这里要画12个三角形,这个12个三角形构造一个方形棱柱(这里为长方体).棱柱的每个四边形表面由两个三角形组成.这两个三角形其中的一条边重合,而且它们的六个顶点 ...

  2. 在Android上山寨了一个Ios9的LivePhotos,放Github上了

    9月10号的凌晨上演了一场IT界的春晚,相信很多果粉(恩,如果你指坚果,那我也没办法了,是在下输了)都熬夜看了吧,看完打算去医院割肾了吧.在发布会上发布了游戏机 Apple TV,更大的砧板 Ipad ...

  3. android 桌面小工具(Widget)开发教程

    刚学做了哥Widget,感觉不错哦,先来秀下效果(用朋友手机截的图) 这个Widget会每隔5秒钟自动切换内容和图片,图片最好使用小图,大图会导致你手机桌面(UI)线程卡顿 教程开始: 1.首先创建一 ...

  4. 简单实现http proxy server怎么实现?

    原文:https://blog.csdn.net/dolphin98629/article/details/54599850 简单实现http proxy server怎么实现?

  5. Java-JUC(六):创建线程的4种方式

    Java创建线程的4种方式: Java使用Thread类代表线程,所有线程对象都必须是Thread类或者其子类的实例.Java可以用以下4种方式来创建线程: 1)继承Thread类创建线程: 2)实现 ...

  6. capwap学习笔记——capwap的前世今生(转)

    公司要做AP和AC,从今天开始学习capwap. 1 capwap的前世今生 1.1 胖AP.瘦AP.AC 传统的WLAN网络都是为企业或家庭内少量移动用户的接入而组建的.因此,只需要一个无线路由器就 ...

  7. windows vs2017环境下编译webkit 2

    WebKit在Windows上 内容 安装开发工具 设置Git存储库 设置支持工具 构建WebKit 安装Cygwin(可选) 得到一个崩溃日志 本指南提供了用于构建WebKit的指令在Windows ...

  8. Android LazyList 从网络获取图片并缓存

    原演示地址 本文内容 环境 演示 LazyList 从网络获取图片并缓存 参考资料 本文是 Github 上的一个演示,通过网络获取歌手专辑的缩略图,并显示在 ListView 控件中.该演示具备将缩 ...

  9. Format Conditions按条件显示表格记录

    标记特定记录 DevExpress强大得确实让人觉得它别具一格!现在,我有这样一个需求,把一个表中某字段为False的记录标记出来.下面是效果(某字段的可见性为否): 实现方式 如果是以前,我写个代码 ...

  10. asp.net 常用于客户端注册的机器信息

    项目需要:根据客户端信息去获取用户登录信息 1.根据客户端信息,并查询数据库是否有匹配.如果没有则重新插入客户端信息: 2.根据客户端的设置提交用户登录信息,用户登录成功后,查询以前是否有过配置信息, ...