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。

开源一个上架 App Store 的相机 App的更多相关文章

  1. Invalid App Store Icon. The App Store Icon in the asset catalog in 'xxx.app' can’t be transparent nor contain an alpha channel.

    1.向appstore上传应用的时候,报了这样一个错误 ERROR ITMS-90717: "Invalid App Store Icon. The App Store Icon in th ...

  2. [App Store Connect帮助]四、添加 App 图标、App 预览和屏幕快照(1)App Store 图标、App 预览和屏幕快照概述

    您可以为您的 App Store 产品页提供有关您 App 的 App Store 图标.三个 App 预览和十张屏幕快照. App Store 图标 您必须提供一个 App Store 图标,用于在 ...

  3. archive后upload to app store时遇到app id不可用的问题

    问题如下图 出现此问题的原因有两种: 1.此app id在AppStore中已经存在,也就是说你使用别人注册的app ID ,  如果是这样,你只能更换app ID 2.此app ID是自己的,突然之 ...

  4. WP8__从windowsphone app store 中根据app id获取应用的相关信息(下载网址及图片id等)

    windows phone 官网应用商店地址 http://www.windowsphone.com/zh-cn/store/featured-apps------------------------ ...

  5. APP Store上架QA&注意事项

    一. App Store上架费用,要多少钱. 这个因产品而异,一般是6000-10000元人民币. 二. App Store上架周期,要多久过. 这个因产品而异,正常的话一周内,如果产品老是出问题,被 ...

  6. IOS 上架到App Store被拒的常见问题总结

    Guideline 2.3.3 - Performance - Accurate Metadata 2017年11月16日 上午12:52 发件人 Apple 2. 3 Performance: Ac ...

  7. [苹果APP上架]ios App Store上架详细教程-一条龙顺滑上架-适合小白

    如何在 2022 年将您的应用提交到 App Store 您正在启动您的第一个应用程序,或者距离上次已经有一段时间了.作者纸飞机@cheng716051来给你讲讲将应用程序提交到 App Store ...

  8. App Store审核被拒的23个理由

    原文地址 iOS 应用提交审核要持续一周或者更久,在提交之前,我们一定要进行「自我审查」,避免被拒.ASO100 为大家收集整理了2015年 App Store 审核被拒的23个理由,并且附上官方拒绝 ...

  9. App 被拒 -- App Store Review Guidelines (2015)中英文对照

    Introduction(简介) We're pleased that you want to invest your talents and time to develop applications ...

随机推荐

  1. 发现AspNet.Core版本控制库Bug一枚,你还想入坑?

    我,博客写作小白一枚,注册账号多年却未曾留下只言片语,在潜水的这些年里从大家的博客中收获了很多新的知识忽觉惶恐心有不安,是时候给大家分享一些我的经验和教训了.嗯嗯,实话告诉大家前面的话的都是来凑字数的 ...

  2. JSON.parse()和JSON.stringify()和eval('(' + result + ')')

    var str = '{"name":"huangxiaojian","age":"23"}' 结果: JSON.par ...

  3. python - dict.setdefault

    index = dict.serdefault(key,default) 尝试往dict中插入新键值key,如果key已存在就原dict不变,否则插入key:defalut:返回值为key在dict中 ...

  4. Python 日志处理(二) 使用正则表达式处理Nginx 日志

    使用正则表达式来处理Nginx 日志 一. 先对单行的日志进行分组正则匹配,返回匹配后的结果(字典格式): from datetime import datetime import re #单行日志 ...

  5. Python学习笔记整理总结【RabbitMQ队列】

    RabbitMQ是消息队列.之前学过的队列queue:线程queue(threading queue),只是多个线程之间进行数据交互.进程queue(processing queue),只是父进程与子 ...

  6. display:box和display:flex填坑之路

    背景分析:最近做移动端项目时,遇到一个常见的需求: 可以滑动的导航,如下图 虽然是很常见的一个布局,但在移动端没有做过,想当然的写下以下的样式,简单描述下: 父元素 width:100%: overf ...

  7. Loadrunner手动编写包含事务、检查点、关联等元素的脚本实例

    一.前言: 本文适合初学者,包含很多细节,包括 二.准备: 1.以虚拟机中的Linux系统作为服务器,开启bugfree服务. 2.以fiddler作为抓包工具,辅助脚本开发. 3.脚本流程:bugf ...

  8. Outlook邮箱配置

    1.在outlook邮箱里启用pop3 2.接收邮件服务器 pop3-mail.outlook.com 3.发送邮件服务器 smtp-mail.outlook.com 4.其他设置 -->发送服 ...

  9. YiShop_商城网站建设应该注意什么

    现在电子商务迅速发展,而专门搭建商城网站的第三方开发商也很多.现在搭建一个商城网站容易,如何运营一个商城网站才是重点.下面就由YiShop说说电子商城网站建设要思考什么呢(1)建设网站的目的是什么首先 ...

  10. JSON序列化时消除空格

    使用 python 序列化时,通常使用 json.dumps()生成 json,但是会在key和value之间默认给你加上一个空格.传参时可能会应为这个空格导致服务端解析失败. 之前做接口测试时,就遇 ...