ARKitを用いてあれこれする機会があったので
備忘録代わりにまとめておきます。
バージョン情報
Xcode10.0
Swift4.2
準備
StoryboardにUIViewを貼り付けてクラスをARSCNViewに設定します。
カメラを使うので忘れずにInfo.plist
に
Privacy - Camera Usage Description
を追加しておきましょう。
実装
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を設定しています。
viewWillAppear
でsceneView.session.run()
をコールして顔のトラッキングを開始します。
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
rightNode
とleftNode
を生成しname
を振ってfaceNode
に追加します。
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor)
は顔の動きが観測された毎に呼ばれるメソッドです。
ここでマスクの位置情報と先ほど追加したNodeの位置情報を更新します。
分かりやすくするため、地点に目印を表示するカスタムNodeクラスを作成する。
// 円型のノード
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)
}
}
円型のノードを追加するカスタムクラスを追加
// 始点と終点から距離を計算して返す
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の中から先ほど追加したrightNode
とleftNode
を取り出す。
ARFaceAnchor
のgeometryからVertexIDを元に位置情報を取得します。
rightNode
とleftNode
が取得できたらdistanceMeasurement
で地点間の距離を計算してdistanceLabel
に表記します。
ビルド
最後に
ものさしで測りながら精度を確かめたところ大体0.7くらいの誤差以内で観測できていました。
なかなかの精度!
以下ViewController
のソースです。
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)
}
}