ARKit の Face Tracking で顔にマスクをつける for iPhone X - 実践編
前回は概要の説明だったが、今回は Xcode を使用しアプリを作成していく。
Apple 公式のサンプル「Creating Face-Based AR Experiences」のコードをスリムにし機能を絞ったものなので、 英語やコードを読むことが苦にならないのならそちらを読んだ方が良い。
ちなみに、カメラを使用しているため実機でしかアプリが動かないので注意。
顔にマスクをつけるアプリ作成するわけだが全 Blend Shape をジオメトリに適応するのは面倒なので、 用意されている ARSCNFaceGeometry を使用する。
ARSCNFaceGeometry で生成されるジオメトリ
こちらは、顔のジオメトリ情報やテクスチャの UV、 Blend Shape が設定されているので、 アンカーとなるノードに設定し、このジオメトリを設定する。
Face Tracking アプリの振る舞い
振る舞いに関しては ARKit の平面認識とあまり変わらない。
- Face Tracking が使用できるかどうか調べる
- 指で画面操作をしない状態が続くため、画面をロックしないようにする。
- シーンのジオメトリを削除し、Face Tracking 用の顔のジオメトリをシーンに配置する「リセットの設定」をつくる
- 画面表示の際 Face Tracking の設定を行い、ARKit のセッション起動
- 問題なく動作した場合 ARSCNView のデリゲートが顔を検知するので「リセットの設定」からアンカー(ARFaceAnchor)に顔のジオメトリを設置
- フレーム毎に ARSCNView のデリゲートが呼ばれるため、アンカーにある情報をジオメトリに渡す。
- バックグラウンドなど画面表示が消える場合はセッションを止めたり、表示が戻った場合に「リセットの設定」を動かしたりして表示の管理をする。
あとは、セッションのエラー情報表示や、おかしくなった場合にリセットなど元に戻す処理など必要であれば設定する。
今回はエラーの処理を行わない。
以降、Xcode を使用しアプリを作成していく。
(読むのが面倒な場合はサンプルコードへ)
Xcode プロジェクト作成と初期設定
Xcode を起動し新規プロジェクト(Command + Shift + N)から、iOS の Single View App を選択し、 適当なプロジェクト名と Laungage を Swift に設定して作成。
info.plist
アプリでカメラを使用するため ProjectNavigator (Command + 1) から info.plist を選択し、
「Privacy - Camera Usage Description」を追加してカメラ使用に出るアラートの文言を決める。
これを設定しないと起動時に落ちる。
Main.storyboard
Main.storyboard を開き、左の Document Outline にある View Controller の下にある View を選択。
Identity Inspector (Command + Option + 3) の一番上にある Class の UIView を ARSCNView に変更。
ViewController.swift の編集
ViewController.swift を開き、以下を設定。
import ARKit
ViewController に ARKit 用のデリゲート ARSCNViewDelegate, ARSessionDelegate を追加し、 情報を取得する ARSCNView と faceNode、virtualFaceNode の SCNNode を追加
class ViewController: UIViewController, ARSCNViewDelegate, ARSessionDelegate { @IBOutlet var sceneView: ARSCNView! private var faceNode = SCNNode() private var virtualFaceNode = SCNNode() override func viewDidLoad() { ・ ・ ・
Storyboard の ARSCNView と ViewController.swift をつなぐ
Main.storyboard を開き、Assistant Editor (Command + Shift + Enter) で表示し Storyboard の ARSCNView を右クリック+ドラッグで右側のにある ViewController.swift で先ほどの IBOutlet の sceneView と繋ぐ
これで初期設定は完了。
ARKit のカメラを起動する
通常の ARKit 同様に各種設定を行い run を行うだけ。
viewDidLoad
viewDidLoad() に以下のものに変更
override func viewDidLoad() { super.viewDidLoad() // Face Tracking が使えなければ、これ以下の命令を実行を実行しない guard ARFaceTrackingConfiguration.isSupported else { return } // Face Tracking アプリの場合、画面を触らない状況が続くため画面ロックを止める UIApplication.shared.isIdleTimerDisabled = true // ARSCNView と ARSession のデリゲート、周囲の光の設定 sceneView.delegate = self sceneView.session.delegate = self sceneView.automaticallyUpdatesLighting = true // トラッキングの初期化を実行 resetTracking() }
コメント通り、使用できるか調べ、画面ロックを止め、デリゲート等設定して、トラッキング初期化を実行する。
isSupported でそのまま return を返し処理を抜けているので、対応端末以外は画面が黒くなる。 本来はここで対象端末ではない旨を説明するべし。
トラッキングの初期化関数をつくる
didReceiveMemoryWarning() {...} の下あたりに以下の関数を作成
// Face Tracking の設定を行い // オプションにトラッキングのリセットとアンカーを全て削除してセッション開始 func resetTracking() { let configuration = ARFaceTrackingConfiguration() configuration.isLightEstimationEnabled = true sceneView.session.run(configuration, options: [.resetTracking, .removeExistingAnchors]) }
ARFaceTrackingConfiguration で Face Tracking の設定を行い、現実世界の光の取得を許可して、 オプションにトラッキングのリセットとアンカーを全て削除してセッション開始。
画面表示 / 非表示の際の設定
先ほどの関数の上に以下のコードを入れる。
// この ViewController が表示された場合にトラッキングの初期化する override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) resetTracking() } // この ViewController が非表示になった場合にセッションを止める override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) sceneView.session.pause() }
非表示になった場合はセッションを止め、表示の際は先ほど作成した resetTracking() を実行しトラッキングを再開する。
ARKit の初期設定は完了。
ビルドして iPhone X で実行するとカメラのプレビューが表示される。
Face Tracking
Face Tracking の設定を行う。
シリアルキューの設定
後々使うシリアルキューの変数を ViewController クラスで設定する。
private var virtualFaceNode = SCNNode() の下あたりに以下の宣言を書く。
// シリアルキューの設定 private let serialQueue = DispatchQueue(label: "com.test.FaceTracking.serialSceneKitQueue")
viewDidLoad で virtualFaceNode に ARSCNFaceGeometry を設定する
viewDidLoad の resetTracking() 上に以下のコードを設定する
// virtualFaceNode に ARSCNFaceGeometry を設定する let device = sceneView.device! let maskGeometry = ARSCNFaceGeometry(device: device)! maskGeometry.firstMaterial?.diffuse.contents = UIColor.lightGray maskGeometry.firstMaterial?.lightingModel = .physicallyBased virtualFaceNode.geometry = maskGeometry
後ほど設定するデリゲートで参照する virtualFaceNode にマスクのジオメトリである ARSCNFaceGeometry を設定して、 マスクの diffuse をライトグレイ、マテリアルに環境光を適応するためライトモデルを Physically Based にする。
ちなみに、ビルド対象のアクティブスキーマを実機か、Generic OS Device にしないと「let device = sceneView.device!」の箇所でコンパイルエラーになる。
コードを書いている際にビックリマークが気になるならシミュレータ以外を選択すべし。
起点となるノードの初期設定
func resetTracking() {...} の下にトラッキング開始時に起点のノードを初期化する関数を書く。
// Face Tracking の起点となるノードの初期設定 private func setupFaceNodeContent() { // faceNode 以下のチルドノードを消す for child in faceNode.childNodes { child.removeFromParentNode() } // マスクのジオメトリの入った virtualFaceNode をノードに追加する faceNode.addChildNode(virtualFaceNode) }
ARSCNView にマスクを描画する
ARSCNView のデリゲートからアンカーを取得。マスクのノード設置して画面に描画する。
使うデリゲートは2つ。
- トラッキング開始時(マスクが追加された時)
- トラッキング情報が更新された時
トラッキング開始時
先ほど書いた func setupFaceNodeContent() {...} の下に、以下のコードを書く。
// ARNodeTracking 開始 func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { faceNode = node serialQueue.async { self.setupFaceNodeContent() } }
トラッキング開始時に渡されるトラッキングの起点となる node が取得でき、 その node を faceNode へ参照渡しすることで、 faceNode の更新が ARSCNView 内に反映される。
コードでは、ノードを受け取り時 serialQueue.async 内で先ほどの setupFaceNodeContent() を呼び、 参照渡しされている faceNode での処理が ARSCNView で適応されている。
この時点で iPhone X にビルドすると、頭の動きはトラッキングされるが表情を反映されない。
トラッキング情報更新時
以下のコードを先ほどのコードの下へ書き表情の更新を行う。
// ARNodeTracking 更新。ARSCNFaceGeometry の内容を変更する func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) { guard let faceAnchor = anchor as? ARFaceAnchor else { return } let geometry = virtualFaceNode.geometry as! ARSCNFaceGeometry geometry.update(from: faceAnchor.geometry) }
デリゲートの更新情報から、表情の Blend Shape を含むアンカーを取得。 virtualFaceNode のジオメトリとして設定した ARSCNFaceGeometry のメソッド update(from: ARSCNFaceGeometry) に取得したアンカーの状態を渡している。
iPhone X にビルドするとマスクへ表情が適応される。
ARKit でのそのほかの処理
デリゲートでエラーと中断処理を追加する。 今回はエラーや中断の中身は特に書いていないので、必要であれば何か処理を入れる必要がある。
// エラーと中断処理 func session(_ session: ARSession, didFailWithError error: Error) { guard error is ARError else { return } } func sessionWasInterrupted(_ session: ARSession) { } func sessionInterruptionEnded(_ session: ARSession) { DispatchQueue.main.async { // 中断復帰後トラッキングを再開させる self.resetTracking() } }
おまけ1:ホームインジケーターを隠す
以下のコードを書くと、アプリ起動時にホームインジケーターが自動で消える。
override func prefersHomeIndicatorAutoHidden() -> Bool { return true }
おまけ2:顔にテクスチャを貼る
ライトグレイのマスクではスケキヨみたいなので、画面タップから UIImagePickerController から画像を選択し、マスクへテクスチャとして反映させます。
以下、設定項目とコード内容
- info.plist に「Privacy - Photo Library Usage Description」を設定する
- タッチイベントを設定する
- UIImagePickerController の設定とデリゲートからマスクにテクスチャを貼る
info.plist
フォトライブラリを使用するため info.plist を選択し、
「Privacy - Photo Library Usage Description」を追加してカメラ使用に出るアラートの文言を決める。
これを設定しないと起動時に落ちる。
viewDidLoad の修正
viewDidLoad のどこかに以下のコードを書く。
// タップジェスチャ設定を呼び出す self.addTapGesture()
ViewController のエクステンションを書く
ViewController エクステンションなので、 以下のコードを、ViewController の {} の外に書く。
いつも通りの UITapGestureRecognizer 設定と UIImagePickerController の設定。 画像を取得したら virtualFaceNode.geometry?.firstMaterial? に渡している。
extension ViewController: UINavigationControllerDelegate, UIImagePickerControllerDelegate { // タップジェスチャ設定 func addTapGesture(){ let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:))) sceneView.addGestureRecognizer(tapGesture) } // タップジェスチャ動作時の関数 @objc func handleTap(_ gestureRecognize: UIGestureRecognizer) { if (UIImagePickerController.isSourceTypeAvailable(.photoLibrary) != false) { let picker = UIImagePickerController() picker.delegate = self picker.sourceType = .photoLibrary self.present(picker, animated:true, completion:nil) }else{ print("fail") } } // フォトライブラリで画像選択時の処理 @objc func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) { // オリジナルサイズの画像を選択 let pickedImage = info[UIImagePickerControllerOriginalImage] as? UIImage // マスクにテクスチャを反映させる virtualFaceNode.geometry?.firstMaterial?.diffuse.contents = pickedImage // UIImagePickerController を閉じる dismiss(animated: true, completion: nil) } // フォトライブラリでキャンセルタップ時の処理 @objc func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { // UIImagePickerController を閉じる dismiss(animated: true, completion: nil) } }
面倒なので、UIImagePickerController を使用しているが画面が隠れるため、
viewDidAppear と viewWillDisappear の処理が走る。
Apple のサンプルでは Popover を使用しているのでこちらを使用するのが良いと思われる。
また、UIImagePickerController 使用の際、Xcode コンソールに 「PlugInKit Code=13 "query cancelled"」というエラーが出るがARKit で画面が覆われると起こるらしい。
Stack Overflow では Bug なのでは? と言われているが詳細不明。
まとめ
記事自体は長くなってしまったが、プログラム的には 200 行いかないくらいでかけるので、 ARSCNFaceGeometry を使用するなら、お手軽なのではというところ。
ジオメトリとそのモーフデータをつくるのが面倒だが、ARFaceAnchor から Blend Shape を使用するものもそこまで難しないところがよい。
今回のサンプルコード