开源一个上架 App Store 的相机 App
(点击上方公众号,可快速关注)
来源:伯乐在线 - Hawk0620
如有好文章投稿,请点击 → 这里了解详情
如需转载,发送「转载」二字查看说明
Osho 相机是我独立开发上架的一个相机 App,App Store地址: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。
相关文章:

WIN7下,联想A30T通过USB连接上网
1.手机连接3G信号 2.手机上在设置里 "设置" |"应用程序" |"USB模式" |勾选"网卡模式" 3.连接上Win7,会出现驱动安装程序,手动安装这里的驱动.(驱动在百度网盘:) 4.在设备管理,网络适配器里查看驱动是否安装成…

在bootstrap ace样式框架上修改的后台管理型模板(Tab页后台管理模板)
后台管理模板开始用frameset布局,但是有时候会遮挡比如上面导航或者左边导航的二级三级弹出菜单,因为宽度被限制了,所以有时候就用easyui或者ext的,但是样式不好看,然后看到了bootstrap ace的后台管理模板,…

文件服务器的内存要多少,文件服务器内存要多大
文件服务器内存要多大 内容精选换一换Windows场景中,当把源端服务器迁移到华为云后,目的端服务器C盘的已用空间比对应源端服务器C盘的已用空间大至少1GB,而不是与源端服务器C盘的已用空间一致,这正常吗?正常现象。您可…

好玩的 RAC
UIControl 监听 control 点击 从此告别 addTarget 和 btnClick 1234[[self.loginBtn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(UIButton *btn) {// btn, 即 self.loginBtn// 这里执行点击之后的操作}];UITextField 监听 textField 的 text 改…

机器学习简单代码示例
机器学习简单代码示例 //在gcc-4.7.2下编译通过。 //命令行:g -Wall -ansi -O2 test.cpp -o test #include <iostream> using namespace std; void input(int &oper,const bool meth) {//meth为true则只判断1,为false则判断1或0while(true){ci…

【2007-5】【素数算式】
Description 在下面的算式中每个“#”都表示一个素数数字。##* #————###请编写程序确定这些数字,输出所有的解。Input Output 依次打印输出每一种解,每行表示一种解,格式为:##*# ###。Sample Input Sample Output HINT Sou…
ubuntu服务器版编辑文件,Ubuntu 服务器版 18.04.4 固定 IP 设置
1、输入命令 su 以root用户权限操作2、找到Ubuntu网络配置文件vim /etc/netplan/50-cloud-init.yaml3、输入 i 让文件变成可编辑状态4、修改内容dhcp4: nodhcp4: noaddresses: [192.168.3.50/24]gateway4: 192.168.3.1nameservers:addresses: [8.8.8.8, 8.8.4.4]如图ÿ…

RACCommand 粗解
前言 学习 RAC 的过程中,RACCommand 是我一直很迷惑的点,感觉一直抓不到它的要点,不明白为何要这样使用。曾经想过用别的方法来替代,只要能找到替代的方法,暂时就没必要死磕,结果发现避免不了,那…

将整数拆分为2的幂次方
任意一个正整数都可以用2的幂次方表示,例如:137=2^72^32^0,同时约定次方用括号来表示,即a^b=a(b)。由此可知,137可表 示:2(7)2(3)2(0)。进一步:72^222^0(2^1用2表示)&…

vbs脚本在服务器上虚拟按键,iisvdir.vbs iis虚拟目录管理脚本使用介绍
IIS管理器也是通过调用iisvdir.vbs来实现虚拟目录的创建和删除的。我们可以通过命令行的方式来执行iisvdir.vbs脚本1)创建虚拟目录:cscript c:\windows\system32\iisvdir.vbs [/s server] [/u username /p password] /create [virtualRoot] Alias PhysicalPath2)删除…

Python字典部分源码分析,字典是无序的
1 def clear(self): # real signature unknown; restored from __doc__ 2 """ D.clear() -> None. Remove all items from D. """ 3 pass 1 #练习1、清空字典(置空) 2 li {"key1":"v…

【2011-3】【旋转表格】
Description 小敏是个数学迷,特别擅长加法与除法。老师给他一个问题,有一个22表格,表中有正整数A, B, C 和D,表格和表格值的计算如下:小敏的任务是对给出的表格每次顺时针旋转90度,使旋转后的表格值最大&am…

iOS-FXDanmaku弹幕库介绍、相关技术分享
前言 去年, 2016年, 一大波直播平台在移动端涌出, 直播慢慢步入了人们的视角. 网上如今能够看到各式各样的直播, 如秀场直播、游戏直播、体育直播、娱乐直播等等. 在各种类型的直播中, 弹幕在PC、移动端都几乎成为了标配, 今天在这里主要介绍一下个人开源的iOS弹幕, 以及提前为…

【廖雪峰Python学习笔记】字符串与编码
字符串与编码 三种字符编码 ASCII编码 :计算机由美国人发明,最早只有127个字符编码—— 大小写英文字母、数字和符号Unicode:把中文、日文、韩文等所有语言统一到一套编码中,2-4byte,现代OS和大多数语言都支持utf-8&a…

application
说明: 进行应用级操作.比如:设置提醒框,打开其他应用,显示联网状态,控制状态栏 单例对象, UIApplication *app[UIApplication sharedApplication];常用方法 方法1:设置app图标数字提示信息//设置app图标数字提示信息app.applicationIconBadgeNumber10;//ios8以后需要实现以下方…

博客园2013年4月份第2周源码发布详情
Silk Navigation for ASP.NET源码 2013-4-12 [VS2010]功能介绍:Silk Navigation 微软提供,为构建cross-browser 的Web应用程序,是用ASP.NET MVC3和jQuery的。它是非常简单使用ASP.NET Web窗体构建,从而在一个相当小的,…

iOS 静态库封装
静态库和动态库 静态库和动态库存在形式 静态库:以.a 和 .framework为文件后缀名。动态库:以.tbd(之前叫.dylib) 和 .framework 为文件后缀名。 静态库和动态库的区别 静态库:链接时会被完整的复制到可执行文件中,被多次使用就有多…

【廖雪峰Python学习笔记】list tuple dict set
列表元组字典集合创建l [1, ‘a’, [1, 3], True]t (1, )d {‘key’ : ‘value’}s set([1, 2, 4, 2, 1])索引l[1]t[0]d.get(‘key’)/插入l.insert(1, ‘3’) / l.append(‘4’)/d[‘k1’] ‘v1’s.add(‘9’)修改l[0] 4/d[‘k1’] v/移除l.pop(0)/d.pop(‘k1’)s.remo…

响应式布局简明示例
响应式布局简明示例,响应式布局最好同时也是自适应布局,然后再配合css3媒体查询,来达到完美的响应式布局。css3的媒体查询ie9一下是不支持的,这太遗憾了,不过幸运的是有大神已经为前端屌丝们写好了用于IE9以下的媒体查…

iOS - APP任意push新页面那些事
大家都知道,UINavigationController对象有一个方法pushViewController,用来做视图跳转,也是在iOS开发中常用的页面转换方法之一。大多数APP的结构一般都是,使用一个UITabBarController,每个tab上都是一个UINavigationC…

主攻ASP.NET.4.5 MVC4.0之重生:Entity Framework生成实体类步骤(十三)
1.新建一个ASP.NET MVC 4.0 项目 2.安装Entity Framework Power Tools 3.Entity Framework- -RepositoryReverse Engineer Code First 4.Install-Package EntityFramework or Update-Package EntityFramework PM -Repository Install-Package EntityFramework 已安装“EntityFr…

【廖雪峰Python学习笔记】高阶函数
Higher-order function高阶函数映射过滤算法排序算法高阶函数 变量可指向函数 >>> abs # 函数 <built-in function abs> >>> abs(-0) # 函数调用 0 >>> func abs # 变量可指向函数 >>> func(-9) # 调用变量 调用函数 9函数名…

丁贵才130702010042第二次作业
第二次作业 2.9 设有如下语句,请用相应的谓词公式分别把它们表示出来: (1)有的人喜欢梅花,有的人喜欢菊花,有的人既喜欢梅花有喜欢梅花。 解:定义谓词 P(x):x…

SpringMVC @RequestBody接收Json对象字符串
以前,一直以为在SpringMVC环境中,RequestBody接收的是一个Json对象,一直在调试代码都没有成功,后来发现,其实 RequestBody接收的是一个Json对象的字符串,而不是一个Json对象。然而在ajax请求往往传的都是Js…

【iOS】自定义日期选择器
自定义了一个日期选择器,与大家分享一下,期待宝贵建议。 github下载地址:https://github.com/huahua0809/XHDatePicker 下面只是说明一下怎么用,具体实现请下载代码看看; 如果大家觉得这个分享有所帮助的话,…

【廖雪峰Python学习笔记】函数式编程
Functional Programming高阶函数返回函数匿名函数装饰器偏函数高阶函数 面向过程的程序设计: 把大段代码拆成函数,通过一层层函数调用,可将复杂任务分解成若干简单的任务函数是面向过程的程序设计的基本单位计算机Computer && 计算c…

asp 随机读取ID之Access
我在用ASP连接access 这个桌面数据库的时候,有时候要随机读取ID,传统的办法是: select top 3 * from tablename order by Rnd(ID) 但这样不行,默认还是按ID排序。 解决办法加上timer() 这个函数 select top 3 * from tablenam…

iOS Plist 文件的 增 删 改
一:Plist 文件的创建 Plist 文件作为我们IOS开发的一种数据存储文件,还是经常会用到的。在看《X-code江湖录》的时候,看到了这一点。自己就写了一下,把写的东西分享出来!先看看它的创建,它的创建和我们其他…
Shine Button动画效果 类似Tinder APP的卡片界面
Shine Button动画效果--WCLShineButton 类似太阳动画的button FSPagerView 主要使用UICollectionView实现的优雅的屏幕幻灯片效果,主要用于Banner、产品展示、欢迎引导等。有Swift和OC两个版本。 folding-cell 一个可扩展的具有折叠效果的内容单元格 Cell折叠动效…

【廖雪峰Python学习笔记】面向对象编程OOP
面向对象编程 OOP:Object Oriented Programming程序的基本单元:对象 [ 数据 操作数据的函数] [属性 方法]三大特点:数据封装、继承和多态OPP中的计算机程序: 一系列命令的集合 —— 一组函数顺序执行。函数化大为小࿰…