1. 程式人生 > >《iPhone X ARKit Face Tracking》

《iPhone X ARKit Face Tracking》

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的裝置上可用。
當我們配置使用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模型設計的時候還需去掉一些不必要的部件:比如眼鏡的模型就不需要新增鏡腳,因為耳朵部分並沒有東西可以去做遮擋。

如果要做類似上面視訊中的鏡片反射效果,使用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相關技術,對我來說是一份非常寶貴的經歷。心懷感恩,踏步前行。

更多精彩內容歡迎關注騰訊 Bugly的微信公眾賬號:

這裡寫圖片描述

騰訊 Bugly是一款專為移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的情況以及解決方案。智慧合併功能幫助開發同學把每天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響使用者數最多的崩潰,精準定位功能幫助開發同學定位到出問題的程式碼行,實時上報可以在釋出後快速的瞭解應用的質量情況,適配最新的 iOS, Android 官方作業系統,鵝廠的工程師都在使用,快來加入我們吧!