LoginSignup
6

More than 5 years have passed since last update.

ARKitを使ったFaceTrackingで地点間の距離を測る

Last updated at Posted at 2018-10-30

ARKitを用いてあれこれする機会があったので
備忘録代わりにまとめておきます。

バージョン情報

Xcode10.0
Swift4.2

準備

StoryboardにUIViewを貼り付けてクラスをARSCNViewに設定します。
スクリーンショット 2018-10-30 10.23.15.png

カメラを使うので忘れずにInfo.plist
Privacy - Camera Usage Descriptionを追加しておきましょう。
スクリーンショット 2018-10-30 11.04.03.png

実装

ViewController.swift
import ARKit
import SceneKit

class ViewController: UIViewController {
    @IBOutlet weak var sceneView: ARSCNView!
    @IBOutlet weak var distanceLabel: UILabel!
    let rightVertexID = 244
    let leftVertexID  = 824

    override func viewDidLoad() {
        super.viewDidLoad()
        guard ARFaceTrackingConfiguration.isSupported else { fatalError() }
        sceneView.delegate = self
        sceneView.scene.background.contents = UIColor.black
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        let configuration = ARFaceTrackingConfiguration()
        sceneView.session.run(configuration)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        sceneView.session.pause()
    }
}
  • 観測地点のVertexIDは予め調べておく。(*顔面に全部で1220個のIDが振られている)

ここではrightVertexIDに右の口角leftVertexIDに左の口角のIDを設定しています。
viewWillAppearsceneView.session.run()をコールして顔のトラッキングを開始します。

ViewController.swift
extension ViewController: ARSCNViewDelegate {
    // 初回マスクがレンダリングされるとき
    func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
        guard let device = MTLCreateSystemDefaultDevice() else {
            return nil
        }
        let faceGeometry = ARSCNFaceGeometry(device: device)
        let faceNode = SCNNode(geometry: faceGeometry)
        faceNode.geometry?.firstMaterial?.fillMode = .lines

        let rightNode = SphereNode(with: .red)
        rightNode.name = "right"
        faceNode.addChildNode(rightNode)
        let leftNode = SphereNode(with: .blue)
        leftNode.name = "left"
        faceNode.addChildNode(leftNode)

        return faceNode
    }

    // マスクが更新されるとき
    func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
        guard let faceAnchor = anchor as? ARFaceAnchor, let faceGeometry = node.geometry as? ARSCNFaceGeometry else { return }

        faceGeometry.update(from: faceAnchor.geometry)
        updateFeatures(for: node, using: faceAnchor)
    }

sceneView.sceneのデリゲートを受け取ります。

初回レンダリング時に呼ばれるメソッド内で観測するためのNodeを追加します。
func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode

rightNodeleftNodeを生成しnameを振ってfaceNodeに追加します。

func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor)は顔の動きが観測された毎に呼ばれるメソッドです。
ここでマスクの位置情報と先ほど追加したNodeの位置情報を更新します。

分かりやすくするため、地点に目印を表示するカスタムNodeクラスを作成する。

SphereNode.swift
// 円型のノード
class SphereNode: SCNNode {
    init(with color: UIColor = .white) {
        super.init()
        let sphere = SCNSphere(radius: 0.003)
        let material = SCNMaterial()
        material.diffuse.contents = color
        sphere.materials = [material]
        sphere.segmentCount = 16
        geometry = sphere
    }

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

    // 線の位置を更新する
    func updatePosition(for vectors: [vector_float3]) {
        let newPos = vectors.reduce(vector_float3(), +) / Float(vectors.count)
        position = SCNVector3(newPos)
    }
}

円型のノードを追加するカスタムクラスを追加

ViewConroller.swift
// 始点と終点から距離を計算して返す
    func distanceMeasurement(startPosition: SCNVector3, endPosition: SCNVector3) -> Double {
        let position = SCNVector3Make(endPosition.x - startPosition.x, endPosition.y - startPosition.y, endPosition.z - startPosition.z)
        let distance = sqrt(position.x * position.x + position.y * position.y + position.z * position.z)
        return Double(distance * 100.0)
    }

    // 顔に貼り付けたオブジェクト位置の更新
    func updateFeatures(for node: SCNNode, using anchor: ARFaceAnchor) {
        guard let rightNode = node.childNode(withName: "right", recursively: true) as? SphereNode,
        let leftNode = node.childNode(withName: "left", recursively: true) as? SphereNode else {
            return
        }
        let rightVertex = anchor.geometry.vertices[rightVertexID]
        rightNode.updatePosition(for: [rightVertex])

        let leftVertex = anchor.geometry.vertices[leftVertexID]
        leftNode.updatePosition(for: [leftVertex])

        DispatchQueue.main.async {
            let distance = String(format: "%.2f",
                                  self.distanceMeasurement(startPosition: rightNode.position, endPosition: leftNode.position))
            self.distanceLabel.text = distance + "cm"
        }
    }

updateFeatures()の中でnodeのChildNodeの中から先ほど追加したrightNodeleftNodeを取り出す。
ARFaceAnchorのgeometryからVertexIDを元に位置情報を取得します。
rightNodeleftNodeが取得できたらdistanceMeasurementで地点間の距離を計算してdistanceLabelに表記します。

ビルド

口をすぼめた時と広げた時の様子
Screen Shot 2018-10-30 at 11.06.51.png
Screen Shot 2018-10-30 at 11.07.06.png

最後に

ものさしで測りながら精度を確かめたところ大体0.7くらいの誤差以内で観測できていました。
なかなかの精度!

以下ViewControllerのソースです。

ViewController.swift
import UIKit
import ARKit
import SceneKit

class ViewController: UIViewController {

    @IBOutlet weak var sceneView: ARSCNView!
    @IBOutlet weak var distanceLabel: UILabel!

    let rightVertexID = 244
    let leftVertexID  = 824

    override func viewDidLoad() {
        super.viewDidLoad()
        guard ARFaceTrackingConfiguration.isSupported else { fatalError() }
        sceneView.delegate = self
        sceneView.scene.background.contents = UIColor.black
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        let configuration = ARFaceTrackingConfiguration()
        sceneView.session.run(configuration)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        sceneView.session.pause()
    }

    // 始点と終点から距離を計算して返す
    func distanceMeasurement(startPosition: SCNVector3, endPosition: SCNVector3) -> Double {
        let position = SCNVector3Make(endPosition.x - startPosition.x, endPosition.y - startPosition.y, endPosition.z - startPosition.z)
        let distance = sqrt(position.x * position.x + position.y * position.y + position.z * position.z)
        return Double(distance * 100.0)
    }

    // 顔に貼り付けたオブジェクト位置の更新
    func updateFeatures(for node: SCNNode, using anchor: ARFaceAnchor) {
        guard let rightNode = node.childNode(withName: "right", recursively: true) as? SphereNode,
        let leftNode = node.childNode(withName: "left", recursively: true) as? SphereNode else {
            return
        }
        let rightVertex = anchor.geometry.vertices[rightVertexID]
        rightNode.updatePosition(for: [rightVertex])

        let leftVertex = anchor.geometry.vertices[leftVertexID]
        leftNode.updatePosition(for: [leftVertex])

        DispatchQueue.main.async {
            let distance = String(format: "%.2f",
                                  self.distanceMeasurement(startPosition: rightNode.position, endPosition: leftNode.position))
            self.distanceLabel.text = distance + "cm"
        }
    }
}

extension ViewController: ARSCNViewDelegate {

    // 最初にマスクがレンダリングされるとき
    func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
        guard let device = MTLCreateSystemDefaultDevice() else {
            return nil
        }
        let faceGeometry = ARSCNFaceGeometry(device: device)
        let faceNode = SCNNode(geometry: faceGeometry)
        faceNode.geometry?.firstMaterial?.fillMode = .lines

        let rightNode = SphereNode(with: .red)
        rightNode.name = "right"
        faceNode.addChildNode(rightNode)
        let leftNode = SphereNode(with: .blue)
        leftNode.name = "left"
        faceNode.addChildNode(leftNode)

        return faceNode
    }

    // マスクが更新されるとき
    func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
        guard let faceAnchor = anchor as? ARFaceAnchor, let faceGeometry = node.geometry as? ARSCNFaceGeometry else { return }

        faceGeometry.update(from: faceAnchor.geometry)
        updateFeatures(for: node, using: faceAnchor)
    }
}

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6