Mercari Engineering Blog

We're the software engineers behind Mercari. Check out our blog to see the tech that powers our marketplace.

iOSのSwiftのネイティブで顔認識して3DのオブジェクトでFacial Animationする話

Mercari Advent Calendar 2018の20日目はメルカリのR4D XRチーム@tarotarokunがお送りします。

はじめに

「mercari R4D」は2017年12月に設立した、社会実装を目的とした研究開発組織です。

R4Dは、研究(Research)と4つのD、設計(Design)・開発(Development)・実装(Deployment)・破壊(Disruption)を意味し、スピーディーな研究開発と社会実装を目的としています。

自分はそのなかでも、VR/AR/MRの領域の研究開発を担当していて、Swiftのネイティブ、Unityなどのゲームエンジンを問わず使用し、その研究にあった開発方法を利用してスピーディーにxR領域の実証実験を行っています。またVRに相性のいいIoT分野の実装も少しやったりしています。

背景

今年はVR/AR/MRのなかでもARが少しづつですが、盛り上がりを見せた年でした。また、iPhone上でVTuber(バーチャルYouTuber)の配信ができるアプリが雨後の竹の子のごとく、たくさんリリースされた年でもありました。

そのなかでも、スマホでの姿勢認識はまだですが、顔認識をして顔の表情を出すことが重要視されているようでした。それらのアプリのほとんどがUnityで顔認識プラグインなどを利用して表情を出していました。

また顔認識プラグインでもiOSの機能を利用せず、独自の画像認識で表情を認識するものが多かったように思います。その理由はiOSの顔認識は、iPhone X以上(デプスカメラが必須)という理由が主だと思われますが、これからのiOS端末はデプスカメラが標準で搭載されるようですので、iOSの機能を利用した方法も表情認識の質を担保する上で重要かと思われます。

UnityでiOSの機能を利用した方法は、Unity公式のブログで紹介されていますので、今回は、iOSのSwiftを利用したネィティブでの方法を今回は紹介しようと思います。

blogs.unity3d.com

カスタムの3Dのオブジェクトを利用しない灰色マスクのAR

まずは、カスタムの3Dオブジェクトを利用しないで、フェイシャルアニメーションができるものを構築してみようと思います。

はじめに ARSCNView を更新するためのクラスを作っていきます。

@available(iOS 11.0, *)
class VirtualContentUpdater: NSObject, ARSCNViewDelegate, ARSessionDelegate {
    //表示 or 更新用
    var virtualFaceNode: VirtualFaceNode? {
        didSet {
            setupFaceNodeContent()
        }
    }
    weak var contentUpdateDelegate: VirtualContentUpdateDelegate?

    //セッションを再起動する必要がないように保持用
    private var faceNode: SCNNode?
    private var sceneView: ARSCNView?
    private var renderEngine: SCNRenderer?

 private let serialQueue = DispatchQueue(label: "com.mercari.serial-queue")

    func setARSCNView(scene: ARSCNView){
        sceneView = scene
        guard let mtlDevice = MTLCreateSystemDefaultDevice() else {
            return
        }
        renderEngine = SCNRenderer(device: mtlDevice, options: nil)
        renderEngine?.scene = scene.scene
    }

    //マスクのセットアップ
    private func setupFaceNodeContent() {
        guard let faceNode = faceNode else { return }

        //全ての子ノードを消去
        for child in faceNode.childNodes {
            child.removeFromParentNode()
        }
        //新しいノードを追加
        if let content = virtualFaceNode {
            faceNode.addChildNode(content)
        }
    }

    // MARK: - ARSCNViewDelegate
    //新しいARアンカーが設置された時に呼び出される
    func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
        faceNode = node
        serialQueue.async {
            self.setupFaceNodeContent()
        }
    }

    //ARアンカーが更新された時に呼び出される
    func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
        guard let faceAnchor = anchor as? ARFaceAnchor else { return }
        virtualFaceNode?.update(withFaceAnchor: faceAnchor) //マスクをアップデートする
    }

    func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
    }

次に、お面クラスMaskクラスを作成をし、MaskのGeometoryをアニメーションさせるクラスを作成します。

import ARKit

@available(iOS 11.0, *)
protocol VirtualFaceContent {
    func update(withFaceAnchor: ARFaceAnchor)
}

@available(iOS 11.0, *)
typealias VirtualFaceNode = VirtualFaceContent & SCNNode

@available(iOS 11.0, *)
class Mask: SCNNode, VirtualFaceContent {

    init(geometry: ARSCNFaceGeometry) {
        let material = geometry.firstMaterial //初期化
        material?.diffuse.contents = UIColor.gray //マスクの色
        material?.lightingModel = .physicallyBased //オブジェクトの照明のモデル

        super.init()
        self.geometry = geometry
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("\(#function) has not been implemented")
    }

    //ARアンカーがアップデートされた時に呼ぶ
    func update(withFaceAnchor anchor: ARFaceAnchor) {
        guard let faceGeometry = geometry as? ARSCNFaceGeometry else { return }
        faceGeometry.update(from: anchor.geometry)
    }
}

そして、ARSCNFaceGeomery メソッドを使ってフェースマスクのジオメトリを作成、および、さきほどの contentUpdater を初期化し、マスクを作成します。

ちなみに、PlayerBaseViewViewController にUIとしておいた ARSCNView です。カメラが起動して、マスクと合成されるViewです。

 let contentUpdater = VirtualContentUpdater()
 func setupAR(){
        playerBaseView.delegate = contentUpdater
        playerBaseView.session.delegate = contentUpdater
        contentUpdater.contentUpdateDelegate = self
        playerBaseView.automaticallyUpdatesLighting = true
        contentUpdater.virtualFaceNode = createFaceNode()
        contentUpdater.setARSCNView(scene: playerBaseView)
    }
  func createFaceNode() -> VirtualFaceNode? {
        guard
            let device = playerBaseView.device,
            let geometry = ARSCNFaceGeometry(device: device) else {
                return nil
        }
        return Mask(geometry: geometry)

    }

Merchan の動画配信部分に実装を入れ込んでみました。


facemask

カスタムの3Dのオブジェクトを利用するAR

Xcodeで利用できるBlendShape(モーフィングしてアニメーションするデータ)付きのデータを作るためには、今のところちょっとしたTIPSが必要です。そのTIPSをここで示しておこうと思います。

BlendShape付きのカスタム3Dのオブジェクトを実装するためには

現在、3Dソフト間で受け渡しのために使われている一般的なアニメーション付きの3DフォーマットはFBXが広く使われていますが、XcodeでFBXは読めないので使えません。

Appleが最近提供しているUSDZフォーマットはXcodeでも使えますが、BlendShape付きのUSDZは今のところ確認できていませんし、他のBlendShape付きファイルからUSDZへコンバートする手段も今のところ確認できていないので、他の手段を考えないといけません。

そこで、XcodeでBlendshape付きの3DオブジェクトであるCollada(.dae)という、元々はSonyさんが開発したフォーマット形式を使います。DAEはいろいろな3Dソフトで対応していますが、きちんと読み込みができるDAEが出力され、少額の投資でコンバートが確認できたUnity用のDAE Exporter Pluginを使いました。

assetstore.unity.com

今回のデータは、UnityでiOSのARフェーストラックキングアニメーションの実装のサンプルで使われているナマケモノ(?)のモデルを出力してみました。

f:id:tarotaro_n:20181204173930p:plain
Unity画面
出力するときに注意なのは、Inspectorのほうで確認できるAvatarの機能があるヒエラルキーを選択しておかないときちんと出力されないことです。

Collada Exporterで出力するための設定は以下になります。今回の出力設定は、Export choiceSelection onlyにするのと blendShapeのチェックボックスをonにすることです。

f:id:tarotaro_n:20181204174555p:plain
出力設定

Xcodeにコンバートした3Dのデータを読み込んでみました。しかし、このままのDAEデータを読み込んで使うと、失敗してアニメーションが表示されないばかりか、3Dオブジェクト自体が表示されないので、SceneKit用の3DフォーマットにXcodeで変換します。

メニューのEditorの Convert Scene Kit scene file format (.scn)を利用してコンバートします。

f:id:tarotaro_n:20181205142546p:plain
Xcodeでの変換

データの準備はOK、ではコードは?

まず、VirtualContentUpdater は、先程のコードのままで大丈夫です。その次にMaskを先程コンバートしたモデルを適応するのと、顔のパーツのパラメータが変わったら、BlendShapeのweightが変わるように設定するメソッドを作成します。

import ARKit

@available(iOS 11.0, *)
class Mask: SCNNode, VirtualFaceContent {
    var faceGeometoryNode: SCNNode?

    init(scene: SCNScene/*geometry: ARSCNFaceGeometry*/) {
        /*let material = geometry.firstMaterial //初期化
        material?.diffuse.contents = UIColor.gray //マスクの色
        material?.lightingModel = .physicallyBased //オブジェクトの照明のモデル
         */
        super.init()
        //self.geometry = geometry
        if let node = scene.rootNode.childNode(withName: "Sloth_Head2", recursively: true) {
            self.faceGeometoryNode = node
        }
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("\(#function) has not been implemented")
    }

    //ARアンカーがアップデートされた時に呼ばれる
    func update(withFaceAnchor anchor: ARFaceAnchor){
        guard let faceNode = self.faceGeometoryNode else {return}
        self.faceGeometoryNode?.simdTransform = anchor.transform
        if let morpher = faceNode.morpher {
            for shape in anchor.blendShapes {
                let blend = shape.value
                morpher.setWeight(CGFloat(blend), forTargetNamed: "blendShape2."+shape.key.rawValue)
            }
        }
    }

    func getBlendShapeIndex(shapeName: String) -> Int {
        guard let faceNode = self.faceGeometoryNode, let morpher = faceNode.morpher else {return -1}
        var cnt = 0
        for target in morpher.targets {
            if target.name == shapeName {
                return cnt
            }
            cnt+=1
        }
        return -1
    }
}

カスタムなモデルを作成するときに顔パーツのblendShapeの名前と、iOSのanchorのキーと同じ名前にして置かないと、anchorとの対応づけが大変になるので、素直にblendShapeのパーツの名前を同じにしておきましょう。

 let contentUpdater = VirtualContentUpdater()
 
 func setupAR(){
        playerBaseView.delegate = contentUpdater
        playerBaseView.session.delegate = contentUpdater
        contentUpdater.contentUpdateDelegate = self
        playerBaseView.automaticallyUpdatesLighting = true
        contentUpdater.virtualFaceNode = createFaceNode()
        contentUpdater.setARSCNView(scene: playerBaseView)
    }

 public func createFaceNode() -> VirtualFaceNode? {
        if let modelScene = SCNScene(named: "facedae/sloth_head_blendshapes.scn") {
            self.playerBaseView.scene = modelScene
            return Mask(scene: modelScene)
        }
        return nil
    }

そして、モデルを生成するところ (createFaceNode()) をscnファイルから生成するように改造します。これで完成です。


charamask

まとめ

3D Facial Animationを実装をしてみて、実装自体は比較的容易にできるのではないかと思います。ただ3Dのソフトなどでもよく起こるデータの持込問題が、iOSでも問題になっているのは非常にめんどくさいことですね。Apple公式(実際には、Pixer主導らしいですが)のアニメーション付きのUSDZのコンバータなどがでてくれると、もう少し楽になるのではないかなぁと期待しています。そしてAppleもARのほうに力を入れていくようなので、そちらも期待したいですね。

PR

このエントリの実装の実験も、R4Dの研究の一部として行われたものでした。このように比較的新しめの技術にも直接メルカリのサービス自体とあまり結びつかなくても、(もちろん結び付けば、それに越したことはないですが)比較的自由に、R4Dの研究の一環として時間がとれ作業できる場が用意されています。そんな環境で仕事したい、自分の技術力でメルカリをテックカンパニーにしていきたいと思った方はぜひ応募してみてください。

mercari.workable.com

明日 21 日目の執筆担当は同じ XR チームの @nkjzm です。引き続きお楽しみください !