ARKit2.0が凄い。あなたの見ている方向を記録、可視化するデモ

# はじめに
[aptpod Advent Calendar 2018](https://qiita.com/advent-calendar/2018/aptpod) 21日目を担当させて頂きます上野と申します。　
普段はiOSやAndroidのスマートフォン、macOSやWindowsのようなPC、ネイティブアプリ系の開発をメインで行なっていて、テクニカルな所の調査もしたりしてます。

今回弊社[aptpod](https://www.aptpod.co.jp/index.html)のアドベントカレンダーに参加させていただくので
せっかくなので普段アプリではどんなことををやっているのー？
そう言えば最近こんな技術出たねーといったことが一度に見る事が記事にしたいと思います。

## 今回フォーカスする点
aptpodでは[intdash](https://www.aptpod.co.jp/products/intdash/)と呼ばれるプラットフォームを開発していて、
その中で[M2M(Machine-to-Machine)](https://ja.wikipedia.org/wiki/%E3%83%9E%E3%82%B7%E3%83%B3%E3%83%84%E3%83%BC%E3%83%9E%E3%82%B7%E3%83%B3)双方間でセンサーやGPS、カメラで得た映像の様なデータを計測・収集、伝送するような事を行なっています。

そのデータを計測・収集するLoggerと言う役割がiPhoneではお手軽に出来てしまうんですが
今回お見せしたいものがARKit2.0で出来る「**Eye-Tracking**」です。
この記事ではiPhoneで収集できる ***目の向き*** と ***顔の向き*** を元に実際に ***どこを見ているのか*** のデータを収集し、可視化、収集したデータをリプレイ再生する所までのデモアプリを開発していきたいと思います。

## [サンプル](https://developer.apple.com/documentation/arkit/creating_face-based_ar_experiences)をみてみる
Appleの方で、顔系のARKitのサンプルアプリのプロジェクトを公開していたので確認してみます。
<img width="200" alt="Screen Shot 2018-12-20 at 18.31.32.png" src="https://qiita-image-store.s3.amazonaws.com/0/310001/cc153f75-e62f-5c40-4a32-1a813fcb48d3.png"> <img width="200" alt="Screen Shot 2018-12-20 at 18.31.25.png" src="https://qiita-image-store.s3.amazonaws.com/0/310001/30c947a8-2855-4a91-231b-c3e7bedf81ae.png">
<img width="200" alt="Screen Shot 2018-12-20 at 18.18.57.png" src="https://qiita-image-store.s3.amazonaws.com/0/310001/df6f5e7b-7c7f-cd36-2bf0-59da5c2df31a.png"> <img width="200" alt="Screen Shot 2018-12-20 at 18.19.38.png" src="https://qiita-image-store.s3.amazonaws.com/0/310001/72a8b363-5b67-3f44-afa7-5c98213938ea.png">
、、、すごい。笑

このサンプルだけでもかなりの機能が確認出来ますね。
ビルド及び動作にはXcodeプロジェクトに ***ARKitFrameworkの追加*** が必要だったの
と ***iPhoneX以降のFaceIDが利用できる端末*** でかつ ***iOS12以降*** である必要があります。

しれっとモザイクをかけていてメインではないので深くは語りませんが
下記コードの様に[CIFilter](https://developer.apple.com/documentation/coreimage/cifilter)で出力する画像に対してフィルターを簡単にかけれます。([※参考](http://appleengine.hatenablog.com/entry/advent20171215))

```swift:ViewController.swift
    /// - Tag: ARFaceGeometryUpdate
    func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
        // ARKit 設定時にカメラからの画像が空で渡されるのでその場合は処理しない
        guard let cuptureImage = sceneView.session.currentFrame?.capturedImage else {
            return
        }
        
        // PixelBuffer を CIImage に変換しフィルターをかける
        let ciImage = CIImage.init(cvPixelBuffer: cuptureImage)
        let filter:CIFilter = CIFilter(name: "CIPixellate")!
        filter.setValue(ciImage, forKey: kCIInputImageKey)
        // モザイクのブロックの大きさ
        filter.setValue(15, forKey: "inputScale")
        
        //　CIImage を CGImage に変換して背景に適応
        //　カメラ画像はホーム右のランドスケープの状態で画像が渡されるため、CGImagePropertyOrientation(rawValue: 6) でポートレートで正しい向きに表示されるよう変換
        let context = CIContext()
        let result = filter.outputImage!.oriented(CGImagePropertyOrientation(rawValue: 6)!)
        if let cgImage = context.createCGImage(result, from: result.extent) {
            sceneView.scene.background.contents = cgImage
        }
        
        guard anchor == currentFaceAnchor,
            let contentNode = selectedContentController.contentNode,
            contentNode.parent == node
            else { return }
        
        selectedContentController.renderer(renderer, didUpdate: contentNode, for: anchor)
    }

```

サンプルから読み解くと [ARSCNView](https://developer.apple.com/documentation/arkit/arscnview) の中に session([ARSession](https://developer.apple.com/documentation/arkit/arsession))を持っていて、
ARSessionにDelegateだったりRunScriptが存在する様ですね。

```swift:ViewController.swift
    /// - Tag: ARFaceTrackingSetup
    func resetTracking() {
        guard ARFaceTrackingConfiguration.isSupported else { return }
        let configuration = ARFaceTrackingConfiguration()
        configuration.isLightEstimationEnabled = true
        sceneView.session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
    }
```

# 実際にデモアプリを作りながらデータを可視化してみる

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">ARKit2の視線・見ている方向を可視化するデモ <a href="https://twitter.com/hashtag/ARKit?src=hash&amp;ref_src=twsrc%5Etfw">#ARKit</a> <a href="https://twitter.com/hashtag/ARKit2?src=hash&amp;ref_src=twsrc%5Etfw">#ARKit2</a> <a href="https://twitter.com/hashtag/iOS?src=hash&amp;ref_src=twsrc%5Etfw">#iOS</a> <a href="https://twitter.com/hashtag/AR?src=hash&amp;ref_src=twsrc%5Etfw">#AR</a> <a href="https://twitter.com/hashtag/%E3%83%87%E3%83%A2?src=hash&amp;ref_src=twsrc%5Etfw">#デモ</a> <a href="https://t.co/AbJoDgcM63">pic.twitter.com/AbJoDgcM63</a></p>&mdash; aptueno (@aptueno) <a href="https://twitter.com/aptueno/status/1075684833878130688?ref_src=twsrc%5Etfw">2018年12月20日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

ARKitで一般に公開されているものはサンプル同様[SceneKit](https://developer.apple.com/documentation/scenekit)を利用して3D空間上に表示しているものが多いので
今回は平面上で見れる様にしたいと思います。
※割とiOSアプリ開発で一般的な事は省略しながら進めますのでご了承ください。

## 取り敢えず目のデータを取得してみる

今回はサンプルの様に顔や3Dオブジェクトを描画することはしないので ***Single View App*** でプロジェクトを開発します。
![スクリーンショット 2018-12-08 13.53.56.png](https://qiita-image-store.s3.amazonaws.com/0/310001/aea54e6a-6083-19da-f2e1-597f4b6678dc.png)

プロジェクトを作り終えたら描画はしませんがカメラは利用するので ***Info.plist*** にカメラのプライバシー宣言をしましょう。
![スクリーンショット 2018-12-08 13.56.22.png](https://qiita-image-store.s3.amazonaws.com/0/310001/8f599e23-f967-45b0-dfbf-3042faad1fa8.png)

サンプルアプリ同様、 ***ARKit.framework*** の追加も忘れずに。
![スクリーンショット 2018-12-17 23.35.19.png](https://qiita-image-store.s3.amazonaws.com/0/310001/fdd8ee9d-9e10-2137-6a45-61e9e615d64d.png)

これで準備は完了でさっそくコードを書いていきます。
デフォルトで作られる ***ViewController.swift***　に書いていきます。

```swift:ViewController.swift
import UIKit
import ARKit

class ViewController: UIViewController  {
    // ARSession
    let session = ARSession()
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.        
        self.session.delegate = self
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)        
        // "Reset" to run the AR session for the first time.
        resetTracking()
    }
}

//MARK:- ARSession
extension ViewController: ARSessionDelegate {
    
    func resetTracking() {
        guard ARFaceTrackingConfiguration.isSupported else { return }
        let configuration = ARFaceTrackingConfiguration()
        configuration.isLightEstimationEnabled = true
        self.session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
    }
    
    //MARK:- ARSessionDelegate    
    func session(_ session: ARSession, didUpdate frame: ARFrame) {
        frame.anchors.forEach { anchor in
            guard #available(iOS 12.0, *), let faceAnchor = anchor as? ARFaceAnchor else { return }
         
            // FaceAnchorから左、右目の位置や向きが取得可能。
            let left = faceAnchor.leftEyeTransform
            print("left:\(left)")
            let right = faceAnchor.rightEyeTransform
            print("right:\(right)")
        }
    }

    func session(_ session: ARSession, didFailWithError error: Error) {}
}
```

いざ、実行。

![スクリーンショット 2018-12-17 23.58.57.png](https://qiita-image-store.s3.amazonaws.com/0/310001/f6dee4cb-d954-55fb-df83-95b261620dee.png)

なんと言う事でしょう、、たった数十行のコードで取得が可能な様です。
通常カメラアプリを作る際はデバイスがなんだの、レイヤー、サンプルがなんだのと
設定が必要ですがたったこれだけで動くとは驚きです。

## 取得したデータについて

取得した左目や右目の情報([leftEyeTransform](https://developer.apple.com/documentation/arkit/arfaceanchor/2968191-lefteyetransform))は[simd_float4x4](https://developer.apple.com/documentation/simd/simd_float4x4)で取得ができます。
<img src="https://docs-assets.developer.apple.com/published/ec412af25c/208d8c19-43cb-4440-bd35-12b0572e3926.png" width=50%>
3D系の開発経験がない人からしたら「なんなんだこれは、、、」と思うかもしれませんが
3Dに置ける ***座標、向き*** 等の情報でサンプルアプリでは
下記の様に左目、右目のNodeにそれらを入れることで向きや位置を設定しています。

```swift:TransformVisualization.swift
 func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
      guard #available(iOS 12.0, *), let faceAnchor = anchor as? ARFaceAnchor else { return }
        
      rightEyeNode.simdTransform = faceAnchor.rightEyeTransform
      leftEyeNode.simdTransform = faceAnchor.leftEyeTransform
 }
```

## 取得したデータを加工する

取得したデータの各情報や3Dに関する事も書いてみたいですが今回はデモを作る事に専念するので
深くは書きませんがある程度利用できるデータの加工を行います。

まず取得出来たtransformからに置ける角度を取得します。

```swift:角度の取得
  let leftX = faceAnchor.leftEyeTransform.columns.2.x
```

ここで取得できた値は[ラジアン(弧度法)](https://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%B8%E3%82%A2%E3%83%B3)値です。一般的にラジアンの値で角度を表現する人はいませんから[度(度数法)](https://ja.wikipedia.org/wiki/%E5%BA%A6_(%E8%A7%92%E5%BA%A6))数値に変換します。

```swift:FloatingPointExtension.swift
  extension FloatingPoint {  
      var degreesToRadians: Self { return self * .pi / 180 }
      var radiansToDegrees: Self { return self * 180 / .pi }
  }
```

```swift:ViewController.swift
  let leftX = faceAnchor.leftEyeTransform.columns.2.x
  self.leftEyeXLabel.text = String.init(format: "x:%0.2f degrees", leftX.radiansToDegrees)
```

ですが、実はこれだけではダメなんです。
この ***leftEyeTransform*** で取得出来る角度の値は ***ローカル座標系*** 
(この例で言うと顔を正面からみた時の目の見ている角度) なので
実際に ***見ている方向を得る*** には ***顔の角度情報を足してあげる*** と良いでしょう。

3D空間における各座標系については詳しくは説明しませんが
良い記事があったので気になる方は下記の記事等を見てみてください。
[・ARKitのための3D数学](https://qiita.com/k-boy/items/775633fe3fd6da9c5fb6p)

顔の角度情報は ***leftEyeTransform*** を取得した ***faceAnchor*** の ***transform*** から取得出来ますが

```swift:ViewController.swift
  let leftX = faceAnchor.transform.columns.2.x + faceAnchor.leftEyeTransform.columns.2.x
  self.leftEyeXLabel.text = String.init(format: "x:%0.2f degrees", leftX.radiansToDegrees)
```

今回の可視化は3Dではなく平面的に表示しようと考えているので少し変えようと思います。

```swift
  var faceNode = SCNNode()
  var faceTargetNode = SCNNode()

  override func viewDidLoad() {
      super.viewDidLoad()
      // Do any additional setup after loading the view, typically from a nib.
               
      // Face Target
      self.faceTargetNode.position = SCNVector3(x: 0, y: 0, z: 1)
      self.faceNode.addChildNode(self.faceTargetNode)
  }
```

上記コードが何をやっているかと言うと親のノードに対してZ軸をずらした子ノードを追加しています。
![スクリーンショット 2018-12-19 23.43.43.png](https://qiita-image-store.s3.amazonaws.com/0/310001/c8bb4049-c4d1-9601-5a6a-90d213c2e7a5.png)
3D上ではこんな感じ。
この親ノードの角度を動かすと子ノードの位置が親ノードの動いた角度分動きます。
![角度変える.png](https://qiita-image-store.s3.amazonaws.com/0/310001/0d9e589c-5e41-3285-8093-935ba24396c2.png)
学生の頃、数学で習った人も多いかと思いますが２点間の位置が分かれば角度がわかります。
<img src="https://camo.qiitausercontent.com/8db072be3424d7e564e0a1fa2e4112dbfa7255ee/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f34333638382f61356638383738312d386165362d363032362d393731342d6137343834626530363365372e706e67"> [←　画像をお借りした場所：2点間の距離と角度と座標の求め方](https://qiita.com/Hoshi_7/items/d04936883ff3eb1eed2d)



```swift:２点間の角度を求める
func radians(v1: Float, v12: Float, v2: Float, v22: Float) -> Float {
    return atan2f(v22 - v12, v2 - v1)
}
```

上記の処理を利用して平面上の角度を求めるとこんな感じ。

```swift:ViewController.swift
  func session(_ session: ARSession, didUpdate frame: ARFrame) {
      frame.anchors.forEach { anchor in
          guard #available(iOS 12.0, *), let faceAnchor = anchor as? ARFaceAnchor else { return }

      // Face Node
      self.faceNode.simdTransform = faceAnchor.transform

      // Left Radians
      let leftX = radians(v1: self.faceNode.worldPosition.z, 
                         v12: self.faceNode.worldPosition.x, 
                          v2: self.faceTargetNode.worldPosition.z, 
                         v22: self.faceTargetNode.worldPosition.x) 
            + faceAnchor.leftEyeTransform.columns.2.x
      self.leftEyeXLabel.text = String.init(format: "x:%0.2f degrees", leftX.radiansToDegrees)
      let leftY = faceAnchor.transform.columns.2.y + faceAnchor.leftEyeTransform.columns.2.y
      self.leftEyeYLabel.text = String.init(format: "y:%0.2f degrees", leftY.radiansToDegrees)
            
      // Right Radians
      let rightX = radians(v1: self.faceNode.worldPosition.z, 
                          v12: self.faceNode.worldPosition.x, 
                           v2: self.faceTargetNode.worldPosition.z, 
                          v22: self.faceTargetNode.worldPosition.x)
              + faceAnchor.rightEyeTransform.columns.2.x
      self.rightEyeXLabel.text = String.init(format: "x:%0.2f degrees", rightX.radiansToDegrees)
      let rightY = faceAnchor.transform.columns.2.y + faceAnchor.rightEyeTransform.columns.2.y
      self.rightEyeYLabel.text = String.init(format: "y:%0.2f degrees", rightY.radiansToDegrees)
  }
```

## データを可視化する
データの可視化は度数で値を入力しアニメーションする様にしました。
<img height="500" alt="Screen Shot 2018-12-20 at 0.52.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/310001/93fe2019-635b-0c71-8c56-764d177bfcc8.png">
[CAShapeLayer](https://developer.apple.com/documentation/quartzcore/cashapelayer)と[UIBezierPath](https://developer.apple.com/documentation/uikit/uibezierpath)を利用しマスクレイヤーを作成し扇型をつくっています。
コードが汚くなってしまったのとデザインに関してはここには記述しませんが割と簡単に作成する事ができます。

```swift
var degrees: CGFloat = 0 {
  didSet {
    UIView.animate(withDuration: 0) {
      self.directionView.transform = CGAffineTransform.init(rotationAngle: (self.offsetDegrees+self.degrees).degreesToRadians)
    }
  }
}
```
回転するアニメーションは加工した度数値で rotationAngle するだけ良くて簡単ですね。

## 録画・リプレイ機能をつけてみる

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">ARKit2視線可視化デモ - リプレイまで <a href="https://twitter.com/hashtag/ARKit2?src=hash&amp;ref_src=twsrc%5Etfw">#ARKit2</a> <a href="https://twitter.com/hashtag/Demo?src=hash&amp;ref_src=twsrc%5Etfw">#Demo</a> <a href="https://t.co/m2ND1u81mC">pic.twitter.com/m2ND1u81mC</a></p>&mdash; aptueno (@aptueno) <a href="https://twitter.com/aptueno/status/1075687641226432513?ref_src=twsrc%5Etfw">2018年12月20日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

可視化はできましたが視線情報は自分で確認しづらいので録画・リプレイ機能をつけてみたいと思います。

```swift:計測したデータを保持しておく
struct LineOfSightUnit {
    var timeStamp: TimeInterval
    var faceEyeTransform: simd_float4x4
    var leftEyeTransform: simd_float4x4
    var rightEyeTransform: simd_float4x4
}
var lastLineOfSightObj: LineOfSightUnit?
var baseTime: TimeInterval? = nil
var recordedList: [LineOfSightUnit] = [LineOfSightUnit]()

func startRecording() {
    self.recordedList.removeAll()
    self.baseTime = nil
    self.isRecording = true
}
    
func stopRecording() {
    self.isRecording = false
}

func session(_ session: ARSession, didUpdate frame: ARFrame) {
    frame.anchors.forEach { anchor in
        guard #available(iOS 12.0, *), let faceAnchor = anchor as? ARFaceAnchor else { return }
            
        // Keep Time.
        let date = Date()
            
        let obj = LineOfSightUnit.init(
        timeStamp: date.timeIntervalSince1970,
        faceEyeTransform: faceAnchor.transform,
        leftEyeTransform: faceAnchor.leftEyeTransform,
        rightEyeTransform: faceAnchor.rightEyeTransform)
        self.lastLineOfSightObj = obj
        if self.isRecording {
           if self.baseTime == nil {
              self.baseTime = date.timeIntervalSince1970
           }
           self.recordedList.append(obj)
           self.unitStatusLabel.text = "\(self.recordedList.count.commaString) units saved"
        }

        ...
}
```

まずはこんな感じで出力されたデータを保持しておきます。

```swift
func startReplay() {
    self.isReplay = true
    var delay: TimeInterval = 0
    DispatchQueue.global().async {
        while(self.isReplay) {
            guard self.isReplay, let baseTime = self.baseTime, self.recordedList.count > 0, self.isRunning else {
                Thread.sleep(forTimeInterval: self.REPLAY_THREAD_INTERVAL)
                continue
            }
            let obj = self.recordedList[self.replayCnt]
            DispatchQueue.main.async {
                // Current Time
                self.currentTimeLabel.text = (obj.timeStamp - baseTime).timeString
                // Units Status
                self.unitStatusLabel.text = "\(self.replayCnt.commaString) / \(self.recordedList.count.commaString) units"
                // Face Transform
                self.faceNode.simdTransform = obj.faceEyeTransform

                // Left Transform
                let leftX = self.radians(v1: self.faceNode.worldPosition.z, v12: self.faceNode.worldPosition.x, v2: self.faceTargetNode.worldPosition.z, v22: self.faceTargetNode.worldPosition.x) + obj.leftEyeTransform.columns.2.x
                    self.leftEyeXLabel.text = String.init(format: "x:%0.2f degrees", leftX.radiansToDegrees)
                    self.leftEyeXView.degrees = CGFloat(leftX.radiansToDegrees)
                // 各ラベルやデザインにデータを入れる処理
                ... 

                self.replayCnt += 1
                if self.replayCnt >= self.recordedList.count {
                    self.replayCnt = 0
                    delay = self.REPLAY_THREAD_INTERVAL
                } else {
                    delay = self.recordedList[self.replayCnt].timeStamp - obj.timeStamp
                }
                Thread.sleep(forTimeInterval: delay)
            }
        }
    }
}
    
func stopReplay() {
    self.isReplay = false
    self.replayCnt = 0
}
```

後はリプレイ様の関数を呼び出して、フラグがたっている間別スレッドで貯めたデータをループして再生します。
スレッドでは次のユニットのタイムスタンプとの差分の時間でDelayを行い、プレイヤーっぽいものを作ってみました。

## まとめ
ここまでで全てしっかりとは解説できてないですが今回作成したデモについてざっくりとした解説は以上です。
座標の計算方法や、その他の処理は他にもやり方はあるとは思いますがARKit自体がとても簡単なので試す価値ありです。
今回作成したデモはgithubで公開しますので興味ある方は動かしてみてください。

[・iOS-VisualizeLineOfSight-Demo](https://github.com/aptpod/iOS-VisualizeLineOfSight-Demo)

またARKitのアドベントカレンダーもある様なので興味ある方はみてもいいかもしれません。
[・ARKit Advent Calendar 2018](https://qiita.com/advent-calendar/2018/arkit)