ARKit上で物体追跡

iOS11から提供されたARKit、 Visionというフレームワークが追加されました。

##ARKitとVision
>[ARKit](https://developer.apple.com/arkit/)
>> iOS 11 introduces ARKit, a new framework that allows you to easily create unparalleled augmented reality experiences for iPhone and iPad. By blending digital objects and information with the environment around you, ARKit takes apps beyond the screen, freeing them to interact with the real world in entirely new ways.

このARKitは、**Visual Inertial Odometry**という技術が用いられています。簡単に説明すると、デバイスのモーションセンサーとカメラセンサーを用いてデバイス周りの空間を把握する技術だそうです。今回、現実世界の物体とAR上の３DオブジェクトでinteractionしたかったのでiOS11から追加されたVisionというまた別の画像処理、画像認識系のフレームワークを用いて物体追跡を行い、物体の２D座標位置と3Dオブジェクトの3D座標位置が重なるポイントでinteractionを行うという実装を行いました。

>[Vision](https://developer.apple.com/documentation/vision)
>> Apply high-performance image analysis and computer vision techniques to identify faces, detect features, and classify scenes in images and video.

##実装例
```ViewController.swift
import UIKit
import ARKit
import Vision

class ViewController: UIViewController {
    
  @IBOutlet weak var sceneView: ARSCNView!
  //追跡している部分の上にのせるビュー
  @IBOutlet weak var highlightView: UIView?
  //ハンドラーの宣言
  var visionSequenceHandler = VNSequenceRequestHandler()
  //フレームごとの観測結果を保持しておくグローバル変数
  var lastObservation: VNDetectedObjectObservation?
  
  var flashNode: VirtualObject?

  override func viewDidLoad() {
    super.viewDidLoad()
    highlightView?.frame = .zero
    
    sceneView.delegate = self
    //カメラフレームをデリゲートメソッドから取得するために必要![flash.gif](https://qiita-image-store.s3.amazonaws.com/0/104400/ebf3f695-9c31-db2f-50e5-f04c3199f8a2.gif)

    sceneView.session.delegate = self
    sceneView.scene = SCNScene()
    //3Dオブジェクトをロード
    flashNode = VirtualObject(name: "Flash.dae")
    flashNode?.loadModel()
    
    sceneView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(ViewController.userTapped(with:))))
  }
  
  @objc func userTapped(with gestureRecognizer: UITapGestureRecognizer) {
    highlightView?.frame.size = CGSize(width: 100, height: 100)
    highlightView?.center = gestureRecognizer.location(in: self.view)
    /*
    UIKit上の座標軸からVisionフレームワーク上の座標軸に変換する処理
    UIKit - 左上がOriginでwidth, heightの最大値がデバイスのピクセルのサイズ
    Vision - 左下がOriginでwidth, heightの最大値は共に１
    */
    let originalRect = self.highlightView?.frame ?? .zero
    let t = CGAffineTransform(scaleX: 1.0 / self.view.frame.size.width, y: 1.0 / self.view.frame.size.height)
    let normalizedHighlightImageBoundingBox = originalRect.applying(t)
    guard let fromViewToCameraImageTransform = self.sceneView.session.currentFrame?.displayTransform(for: .portrait, viewportSize: self.sceneView.frame.size).inverted() else { return }
    var trackImageBoundingBoxInImage = normalizedHighlightImageBoundingBox.applying(fromViewToCameraImageTransform)
    trackImageBoundingBoxInImage.origin.y = 1 - trackImageBoundingBoxInImage.origin.y
    //highlightViewのRect内の最初の観測結果をグローバル変数に入れておく処理
    let newObservation = VNDetectedObjectObservation(boundingBox: trackImageBoundingBoxInImage)
    self.lastObservation = newObservation
  }
  
  func handleVisionRequestUpdate(_ request: VNRequest, error: Error?) {
    //ハンドラーが実行されるスレッドがメインスレッドではないのでメインスレッドに戻す。
    DispatchQueue.main.async {
      //新しい観測結果を取得
      guard let newObservation = request.results?.first as? VNDetectedObjectObservation else {
        self.visionSequenceHandler = VNSequenceRequestHandler()
        return
      }
      //観測を保持するグローバル変数に新しい観測結果
      self.lastObservation = newObservation
      //confidenceが低ければ追跡用のビューを隠す
      guard newObservation.confidence >= 0.3 else {
        self.highlightView?.frame = .zero
        return
      }
      /*
    　Visionフレームワーク上の座標軸からUIKit上の座標軸に変換する処理
    　UIKit - 左上がOriginでwidth, heightの最大値がデバイスのピクセルのサイズ
    　Vision - 左下がOriginでwidth, heightの最大値は共に１
    　*/
      var transformedRect = newObservation.boundingBox
      transformedRect.origin.y = 1 - transformedRect.origin.y
      guard let fromCameraImageToViewTransform = self.sceneView.session.currentFrame?.displayTransform(for: .portrait, viewportSize: self.sceneView.frame.size) else { return }
      let normalizedHighlightImageBoundingBox = transformedRect.applying(fromCameraImageToViewTransform)
      let t = CGAffineTransform(scaleX: self.view.frame.size.width, y: self.view.frame.size.height)
      let unnormalizedTrackImageBoundingBox = normalizedHighlightImageBoundingBox.applying(t)
      //highlightViewに新しい観測結果を基にした新しいrectを代入。これによってhighlightViewが新たなるポイントに動いて、ある物体を追いかけて
      いるように見える。
      self.highlightView?.frame = unnormalizedTrackImageBoundingBox
      //highlightViewと衝突した時に3Dオブジェクトが消える。
      self.hitNode(at: self.highlightView!.center, name: "flash") { [weak self] node in
        guard let `self` = self else { return }
        node.removeFromParentNode()
      }
    }
  }
  //２D座標と3Dモデルの衝突判定
  func hitNode(at point: CGPoint, name: String, onSuccess: (SCNNode) -> Void) {
    guard let result = sceneView.hitTest(point).first else { return }
    let node = result.node
    if node.name == name {
      Utility.playSound(scene: sceneView, name: "hitBug.wav")
      Utility.showParticle(scene: sceneView, name: "Explosion", position: node.position)
      onSuccess(node)
    }
  }
  
  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    let configuration = ARWorldTrackingConfiguration()
    //平面検出のために必要
    configuration.planeDetection = .horizontal
    sceneView.session.run(configuration)
  }
  
  override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    sceneView.session.pause()
  }
}

extension ViewController: ARSCNViewDelegate {
    func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
        var node: SCNNode?
        //平面を検知した時にオブジェクトを配置する処理。
        if let planeAnchor = anchor as? ARPlaneAnchor {
            node = SCNNode()
            let flash = flashNode?.clone()
            flash?.position = SCNVector3Make(planeAnchor.center.x, 0.1, planeAnchor.center.z)
            node?.addChildNode(flash!)
        } else {
            print("not plane anchor \(anchor)")
        }
        return node
    }
}

extension ViewController: ARSessionDelegate {
  //最新のカメラフレームが提供されるデリゲートメソッド
  func session(_ session: ARSession, didUpdate frame: ARFrame) {
    //CVPixelBuffer型に変換
    guard let pixelBuffer: CVPixelBuffer = session.currentFrame?.capturedImage,
    let lastObservation = lastObservation else {
      self.visionSequenceHandler = VNSequenceRequestHandler()
      return
    }
    //物体認識リクエストをインスタンス化。グローバル変数に追加した観測とリクエストの結果を受け取るハンドラーを登録
    let request = VNTrackObjectRequest(detectedObjectObservation: lastObservation, completionHandler: self.handleVisionRequestUpdate)
    //早さ重視の場合 - .fast
    //正確さ重視の場合 - .accurate
    request.trackingLevel = .fast
    
    do {
      //リクエストを実行
      try self.visionSequenceHandler.perform([request], on: pixelBuffer)
    } catch {
      print("Throws: \(error)")
    }
  }
}

```
`highlightView`は**釘バット**の部分です。　物体追跡部分にフォーカスするためにアニメーションやラジコンを操作する部分の実装は省略しましたが、ご了承ください。
![flash.gif](https://qiita-image-store.s3.amazonaws.com/0/104400/6a41f1ed-ea72-2e6e-bfeb-425bcdc955a7.gif)![gif_hit.gif](https://qiita-image-store.s3.amazonaws.com/0/104400/7bb4cfbe-78c0-979e-11a6-af5a48937b82.gif)![ollie.gif](https://qiita-image-store.s3.amazonaws.com/0/104400/92b778a8-2619-89f1-702f-24fad106b471.gif)!