iPhone X
iPhone X前置深度摄像头带来了Animoji和face ID,同时也将3D Face Tracking的接口开放给了开发者。有幸去Cupertino苹果总部参加了iPhone X的封闭开发,本文主要分享一下iPhone X上使用ARKit进行人脸追踪及3D建模的相关内容。
新增接口
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的设备上可用。
当我们配置使用ARFaceTrackingConfiguration
,session
会自动添加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
前面说过,当我们配置使用ARFaceTrackingConfiguration
,session
会自动添加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模型设计的时候还需去掉一些不必要的部件:比如眼镜的模型就不需要添加镜脚,因为耳朵部分并没有东西可以去做遮挡。
如果要做类似上面视频中的镜片反射效果,使用SceneKit也十分方便,只需要将镜片的反射贴图(SCNMaterial的reflective属性)映射到cube map即可,支持以下4种设置方案
- A horizontal strip image where
6 * image.height == image.width
- A vertical strip image where
image.height == 6 * image.width
- A spherical projection image (latitude/longitude) where
2 * image.height == image.width
- 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相关技术,对我来说是一份非常宝贵的经历。心怀感恩,踏步前行