iPhone X前置深度摄像头带来了Animoji和face ID,同时也将3D Face Tracking的接口开放给了开发者。有幸去Cupertino苹果总部参加了iPhone X的封闭开发,本文主要分享一下iPhone X上使用ARKit进行人脸追踪及3D建模的相关内容。

iPhone X前置深度摄像头识别人脸视频

新增接口

ARFaceTrackingConfiguration

ARFaceTrackingConfiguration利用iPhone X前置深度摄像头识别用户的人脸。由于不同的AR体验对iOS设备有不同的硬件要求,所有ARKit配置要求iOS设备至少使用A9及以上处理器,而face tracking更是仅在带有前置深度摄像头的iPhone X上才会有。因此在进行AR配置之前,首先我们需要确认用户设备是否支持我们将要创建的AR体验

ARFaceTrackingConfiguration.isSupported

对于不支持该ARKit配置的设备,提供其它的备选方案或是降级策略也是一种不错的解决方案。然而如果你的app确定ARKit是其核心功能,在info.plist里将ARKit添加到UIRequiredDeviceCapabilities里可以确保你的app只在支持ARKit的设备上可用。
当我们配置使用ARFaceTrackingConfigurationsession会自动添加ARFaceAnchor对象到其anchor list中。每一个face anchor提供了包含脸部位置,方向,拓扑结构,以及表情特征等信息。另外,当我们开启isLightEstimationEnabled设置,ARKit会将检测到的人脸作为灯光探测器以估算出的当前环境光的照射方向及亮度等信息(详见ARDirectionalLightEstimate对象),这样我们可以根据真实的环境光方向及强度去对3D模型进行照射以达到更为逼真的AR效果。

ARFrame

当我们设置为基于人脸的AR(ARFaceTrackingConfiguration),session刷新的frame里除了包含彩色摄像头采集的颜色信息以外(capturedImage),还包含了由深度摄像头采集的深度信息(capturedDepthData)。其结构和iPhone7P后置双摄采集的深度信息一样为AVDepthData。当设置其它AR模式时该属性为nil。在iPhone X上实测效果比7P后置的深度信息更为准确,已经可以很好的区分人像和背景区域。

需注意的是,深度摄像头采样频率和颜色摄像头并不一致,因此ARFrame的capturedDepthData属性也可能是nil。实测下来在帧率60的情况下,每4帧里有1帧包含深度信息。

ARFaceAnchor

前面说过,当我们配置使用ARFaceTrackingConfigurationsession会自动添加ARFaceAnchor对象到其anchor list中。每一个face anchor提供了包含脸部位置,方向,拓扑结构,以及表情特征等信息。比较遗憾的是,当前版本只支持单人脸识别,未来如果ARKit提供多人脸识别后开发者应该也能较快的进行版本升级。

  • 人脸位置和方向

    父类ARAnchor的transform属性以一个4*4矩阵描述了当前人脸在世界坐标系的位置及方向。我们可以使用该矩阵来放置虚拟3D模型以实现贴合到脸部的效果(如果使用SceneKit,会有更便捷的方式来完成虚拟模型的佩戴过程,后面会详述)。该变换矩阵创建了一个“人脸坐标系”以将其它模型放置到人脸的相对位置,其原点在人头中心(鼻子后方几厘米处),且为右手坐标系—x轴正方向为观察者的右方(也就是检测到的人脸的左方),y轴正方向延人头向上,z轴正方向从人脸向外(指向观察者)

  • 人脸拓扑结构 ARFaceGeometry

    ARFaceAnchor的geometry属性封装了人脸具体的拓扑结构信息,包括顶点坐标、纹理坐标、以及三角形索引(实测下来单个人脸包含1220个3D顶点以及2304个三角面片信息,精准度已经相当高了)。

有了这些数据,我们可以实现各种贴合人脸的3D面皮—比如虚拟妆容或者纹身等。我们也可以用其创建人脸的几何形状以完成对虚拟3D模型的遮挡。
如果我们使用SceneKit + Metal做渲染,可以十分方便的通过ARSCNFaceGeometry完成人脸建模,后面会详细说明。
  • 面部表情追踪

    blendShapes属性提供了当前人脸面部表情的一个高阶模型,表示了一系列的面部特征相对于无表情时的偏移系数。听起来也许有些抽象,具体来说,可以看到blendShapes是一个NSDictionary,其key有多种具体的面部表情参数可选,比如ARBlendShapeLocationMouthSmileLeft代表左嘴角微笑程度,而ARBlendShapeLocationMouthSmileRight表示右嘴角的微笑程度。每个key对应的value是一个取值范围为0.0 - 1.0的浮点数,0.0表示中立情况的取值(面无表情时),1.0表示最大程度(比如左嘴角微笑到最大值)。ARKit里提供了51种非常具体的面部表情形变参数,我们可以自行选择采用较多的或者只是采用某几个参数来达成我们的目标,比如,用“张嘴”、“眨左眼”、“眨右眼”来驱动一个卡通人物。

创建人脸AR体验

以上介绍了一下使用ARKit Face Tracking所需要了解的新增接口,下面来详细说明如何搭建一个app以完成人脸AR的真实体验。

创建一个ARKit应用可以选择3种渲染框架,分别是SceneKit,SpriteKit和Metal。对于做一个自拍类的app,SceneKit无疑是一种很好的选择。其接口方便易用,底层使用Metal2渲染,且提供了多种材质以及光照模型,通常情况下无需自定义shader即可完成3D贴脸以及3D挂件的渲染。首先我们需要添加一个ARSCNView,设置好scene以及delegate,在viewWillAppear里添加下面两行代码

ARFaceTrackingConfiguration *configuration = [ARFaceTrackingConfiguration new];
[self.sceneView.session runWithConfiguration:configuration];

这样就创建好了一个ARKit Face Tracking的场景,此时前置摄像头已经开启并实时检测/追踪人脸信息。当检测到人脸之后,我们可以通过delegate更新人脸anchor的函数来同步更新我们自定义的3D面皮或者3D模型。

- (void)renderer:(id <SCNSceneRenderer>)renderer willUpdateNode:(SCNNode *)node forAnchor:(ARAnchor *)anchor;
- (void)renderer:(id <SCNSceneRenderer>)renderer didUpdateNode:(SCNNode *)node forAnchor:(ARAnchor *)anchor;

比如我们要放置一张京剧脸谱贴合到用户脸上,我们可以生成一个脸谱的SCNNode

- (SCNNode *)textureMaskNode
{
if (!_textureMaskNode) {
_textureMaskNode = [self makeFaceGeometry:^(SCNMaterial *material) {
material.fillMode = SCNFillModeFill;
material.diffuse.contents = [UIImage imageNamed:@"maskImage.png"];
} fillMesh:NO];
_textureMaskNode.name = @"textureMask";
}
return _textureMaskNode;
} - (SCNNode*)makeFaceGeometry:(void (^)(SCNMaterial*))materialSetup fillMesh:(BOOL)fillMesh
{
#if TARGET_OS_SIMULATOR
return [SCNNode new];
#else
id<MTLDevice> device = self.sceneView.device; ARSCNFaceGeometry *geometry = [ARSCNFaceGeometry faceGeometryWithDevice:device fillMesh:fillMesh];
SCNMaterial *material = geometry.firstMaterial;
if(material && materialSetup)
materialSetup(material); return [SCNNode nodeWithGeometry:geometry];
#endif
}

注意这个fillMesh参数,如果设置为NO,生成的“蒙皮”眼睛和嘴巴区域是镂空的,反之亦然。模型建好以后,我们需要在face anchor刷新的时候同步更新3D蒙皮的几何信息使其与人脸达到贴合的状态。

- (void)renderer:(id<SCNSceneRenderer>)renderer willUpdateNode:(SCNNode *)node forAnchor:(ARAnchor *)anchor
{
ARFaceAnchor *faceAnchor = (ARFaceAnchor *)anchor;
if (!faceAnchor || ![faceAnchor isKindOfClass:[ARFaceAnchor class]]) {
return;
} if (_needRenderNode) {
[node addChildNode:self.textureMaskNode];
_needRenderNode = NO;
} ARSCNFaceGeometry *faceGeometry = (ARSCNFaceGeometry *)self.textureMaskNode.geometry;
if( faceGeometry && [faceGeometry isKindOfClass:[ARSCNFaceGeometry class]] ) {
[faceGeometry updateFromFaceGeometry:faceAnchor.geometry];
}
}

这里我们是直接将蒙皮node添加到face node作为其childNode,因而不需要对其位置信息做额外处理就能跟随人脸移动。如果是直接加到场景的rootNode上面,还需要同步更新其位置、方向等属性。打上方向光之后,蒙皮显得十分贴合立体。

SCNLight *directional = [SCNLight light];
directional.type = SCNLightTypeDirectional;
directional.color = [UIColor colorWithWhite:1 alpha:1.0];
directional.castsShadow = YES; _directionalLightNode = [SCNNode node];
_directionalLightNode.light = directional;

demo里我们做了一个戏剧变脸效果,当用户遮挡人脸后将其脸谱换掉。实现的原理是当用户人脸检测不到时记一个标志,再次检测到用户人脸时将其3D蒙皮的贴图换掉。比较坑的是,ARKit 检测不到人脸时也并未将其node移除,因此delegate也没有回调

- (void)renderer:(id <SCNSceneRenderer>)renderer didRemoveNode:(SCNNode *)node forAnchor:(ARAnchor *)anchor;

那么如何知道face tracking失败呢?可以通过每一帧刷新的时候遍历查找到ARAnchor,检测其isTrackFace状态。

- (void)session:(ARSession *)session didUpdateFrame:(ARFrame *)frame
{
for (ARAnchor *anchor in frame.anchors) {
if ([anchor isKindOfClass:[ARFaceAnchor class]]) {
ARFaceAnchor *faceAnchor = (ARFaceAnchor *)anchor;
self.isTrackFace = faceAnchor.isTracked;
}
}
}

同样的,我们可以在人脸node上添加其他3D模型(比如3D眼镜)的node使其跟随人脸移动,可以达到非常逼真的效果,SceneKit支持多种格式的模型加载,比如obj、dae等。如果使用的是dae且不是放在bundle里面,需要提前用scntool压缩,模型加载及动画播放所遇到的坑此处不赘述。需要注意的是,当我们给用户戴上3D眼镜或帽子的时候,我们当然是希望模型的后面部分能正确的被用户的脸给挡住以免露出马脚。因此我们需要渲染一个用来遮挡的node并实时更新其几何信息,使用户在头歪向一边的时候3D眼镜的镜架能被人脸正确遮挡。

- (SCNNode *)occlusionMaskNode
{
if (!_occlusionMaskNode) {
_occlusionMaskNode = [self makeFaceGeometry:^(SCNMaterial *material) {
material.colorBufferWriteMask = SCNColorMaskNone;
material.lightingModelName = SCNLightingModelConstant;
material.writesToDepthBuffer = true;
} fillMesh:YES];
_occlusionMaskNode.renderingOrder = -1;
_occlusionMaskNode.name = @"occlusionMask";
}
return _occlusionMaskNode;
}

同样的我们需要在face anchor刷新的时候通过updateFromFaceGeometry:更新其几何信息。需要注意的是,由于ARKit只对人脸区域进行建模,在3D模型设计的时候还需去掉一些不必要的部件:比如眼镜的模型就不需要添加镜脚,因为耳朵部分并没有东西可以去做遮挡。

3D模型设计的时候还需去掉一些不必要的部件效果视频

如果要做类似上面视频中的镜片反射效果,使用SceneKit也十分方便,只需要将镜片的反射贴图(SCNMaterial的reflective属性)映射到cube map即可,支持以下4种设置方案

  1. A horizontal strip image where 6 * image.height == image.width
  2. A vertical strip image where image.height == 6 * image.width
  3. A spherical projection image (latitude/longitude) where 2 * image.height == image.width
  4. A NSArray of 6 images. This array must contain images of the exact same dimensions, in the following order, in a left-handed coordinate system: +X, -X, +Y, -Y, +Z, -Z (or Right, Left, Top, Bottom, Front, Back).

除了人脸的空间位置信息和几何信息,ARKit还提供了十分精细的面部表情形变参数,用来做类似张嘴触发是完全没问题的,我们还可以用其实现一些有趣的效果。比如,根据脸部微笑的程度去替换3D蒙皮的diffuse贴图,使用户笑的时候会出现夸张的效果。

- (UIImage *)meshImageWithBlendShapes:(NSDictionary *)blendShapes
{
if (self.diffuseArray.count == 0)
return nil; NSUInteger _count = self.diffuseArray.count;
NSNumber *smileLeft = blendShapes[ARBlendShapeLocationMouthSmileLeft];
NSNumber *smileRight = blendShapes[ARBlendShapeLocationMouthSmileRight]; CGFloat smileBlend = (smileLeft.floatValue + smileRight.floatValue) / 2;
smileBlend = smileBlend - 0.1;
if (smileBlend < 0.0) smileBlend = 0.0;
NSUInteger index = (NSUInteger)(smileBlend * _count / 0.5);
if (index > _count - 1) {
index = _count - 1;
} return self.diffuseArray[index];
}

将几个脸部表情系数的组合映射到一个具体的分值,可以实现face dance那样有趣的表情模仿。还可以将其映射到3D虚拟人物的形变上以实现animoji的效果,此处开发者们可自行脑洞大开:)

拍照 & 录制

可能是由于SceneKit原本是设计用来做游戏渲染的框架,只提供了一个截屏的接口snapshot,拍照尚可调用,而录制并不是特别方便。如果你计划通过SCNRenderer 的函数

+ (instancetype)rendererWithContext:(nullable EAGLContext *)context options:(nullable NSDictionary *)options;

将其放在OpenGL context里渲染,可以避开视频录制的坑,但也许会遇到更新人脸geometry等其他问题。如果采用默认的Metal方案,设置一个定时器,将snapshot获取到的UIImage转成pixel buffer再进行视频编码,很难做到每秒30帧的同步输出。如果你的app在录制的时候UI非常干净,可以采用系统录屏框架replaykit来进行屏幕录制;如果你想完全掌控每一帧的输出以方便在录制过程中加上水印,可以用SCNRenderer的render函数

- (void)renderAtTime:(CFTimeInterval)time viewport:(CGRect)viewport commandBuffer:(id <MTLCommandBuffer>)commandBuffer passDescriptor:(MTLRenderPassDescriptor *)renderPassDescriptor

将场景渲染到一个id对象中,通过纹理绑定的方式将其转换为CVPixelBufferRef以完成视频编码。某位朋友提醒,可以通过method swizzling的方式直接获取CAMetalLayer的nextDrawable,甚至可以避免上诉方案录制时产生的额外GPU开销,有兴趣的朋友可以尝试一下。

写在末尾

这次能有机会参加Apple的封闭开发且是如此有趣的模块,在没有网络的情况下摸索着做出demo,接触到了最前沿的AR相关技术,对我来说是一份非常宝贵的经历。心怀感恩,踏步前行

iPhone X的更多相关文章

  1. iPhone Anywehre虚拟定位提示“后台服务未启动,请重新安装应用后使用”的解决方法

    问题描述: iPhone越狱了,之后在Cydia中安装Anywhere虚拟定位,但是打开app提示:后台服务未启动,请重新安装应用后使用. 程序无法正常使用... 解决方法: 打开Cydia-已安装, ...

  2. input标签中button在iPhone中圆角的问题

    1.问题 使用H5编写微信页面时,使用<input type="button"/>时,在Android手机中显示正常,但是在iPhone手机中则显示不正常,显示为圆角样 ...

  3. iOS获取iPhone系统等信息和服务器返回空的异常处理

    前言: 在项目中经常会遇到需要获取系统的信息来处理一些特殊的需求和服务端返回为空的处理,写在这里只是笔记一下. 获取设备的信息 NSLog(@"globallyUniqueString=%@ ...

  4. iOS: 在iPhone和Apple Watch之间共享数据: App Groups

    我们可以在iPhone和Apple Watch间通过app groups来共享数据.方法如下: 首先要在dev center添加一个新的 app group: 接下来创建一个新的single view ...

  5. JQ实现判断iPhone、Android设备

    最近做了一版微信宣传页,通过JQ来判断设备,并进行下载 微信内置浏览器对下载链接进行了屏蔽,所以先进行判断,如果是微信内置浏览器,则跳转应用宝链接,如果不是,则判断是iPhone/Adroid/PC ...

  6. ipad和iphone的适配

    关于xib或者storybord下iphone的横竖屏的适配以及ipad的适配 ios8出现了Size Classes,解决了各种屏幕适配的问题,他把屏幕的宽和高分别分成了三种,把屏幕总共分成了九种情 ...

  7. 获取iPhone手机的UDID和设备名称.

    关于设备名称: iPhone的设备名称也可以在手机上面查看到:设置-通用-关于本机-名称(设备名称是可以自己改的) 关于UUID: 什么?用了iPhone这么久你不知道什么叫UDID! UDID 是由 ...

  8. 怎么把电脑的word,txt,pdf等文件拷贝到iPhone手机上

    之前都是用的qq什么的传文件,电脑发送到qq上.今天尝试了一下用itunes把电脑上的文件夹弄到iPhone上. 1.首先,打开电脑的偏好设置,找到共享如图: 打开它,勾选文件共享. 2.把手机和电脑 ...

  9. 手机设计尺寸 - iPhone界面尺寸

    参考网址: http://www.qijishow.com/down/app-index.htm iPhone界面尺寸 设备 分辨率 PPI 状态栏高度 导航栏高度 标签栏高度 iPhone6 plu ...

  10. iphone 尺寸and字体

    iPhone的APP界面一般由四个元素组成,分别是:状态栏.导航栏.主菜单栏以及中间的内容区域 这里取用 640×960 的尺寸设计,那我们就说说在这个尺寸下这些元素的尺寸: 状态栏:就是我们经常说的 ...

随机推荐

  1. CDOJ 1256 二维前缀和处理

    昊昊喜欢运动 他NN 天内会参加MM 种运动(每种运动用一个[1,m][1,m] 的整数表示) 舍友有QQ 个问题 问昊昊第ll 天到第rr 天参加了多少种不同的运动 Input 输入两个数NN , ...

  2. dockerfile 镜像构建

    1.镜像的构建有手动与自动方式,这里我们介绍自动化的构建方式 ,dockerfile常用指令如下 2.构建指令build Usage: docker image build [OPTIONS] PAT ...

  3. matplotlib pyplot 中文显示问题

    import pylab pylab.mpl.rcParams['font.sans-serif'] = ['SimHei'] pylab.mpl.rcParams['axes.unicode_min ...

  4. Python之数据结构:列表

    列表:处理一组有序项目的数据结构 一.基本操作 1.列表运算符 list1=[2,3,4,5,6,7,8] print len(list1) print [1,2]+[3,4] print ['Hi' ...

  5. Dictionary To Dynamic

    原文发布时间为:2012-12-25 -- 来源于本人的百度文章 [由搬家工具导入] public static class DictionaryExt    {        /// <sum ...

  6. 自定义JQuery扩展方法

    ; (function ($, window, document, undefined) { $.getUrlParam = function (name) { var reg = new RegEx ...

  7. [LeetCode] Find Minimum in Rotated Sorted Array 二分搜索

    Suppose a sorted array is rotated at some pivot unknown to you beforehand. (i.e., 0 1 2 4 5 6 7 migh ...

  8. idea 快速生成代码的快捷键

     psvm 加ab键   mian方法快速生成 sout 加tab键   输出打印快捷键  alt+insert      快速生成get  set 方法 itar 生成array for代码块 fo ...

  9. Codeforces 707C. Pythagorean Triples-推公式的数学题

    两道C题题解,能推出来公式简直是无敌. http://codeforces.com/problemset/problem/707/C codeforces707C. Pythagorean Tripl ...

  10. 牛客网 牛客小白月赛1 J.おみやげをまらいました

    J.おみやげをまらいました   链接:https://www.nowcoder.com/acm/contest/85/J来源:牛客网     随便写写.   代码: 1 #include<ios ...