15
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

updated at

ARKit上で物体追跡

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

ARKitとVision

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上の3DオブジェクトでinteractionしたかったのでiOS11から追加されたVisionというまた別の画像処理、画像認識系のフレームワークを用いて物体追跡を行い、物体の2D座標位置と3Dオブジェクトの3D座標位置が重なるポイントでinteractionを行うという実装を行いました。

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の最大値は共に1
    */
    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の最大値は共に1
     */
      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()
      }
    }
  }
  //2D座標と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.gifgif_hit.gifollie.gif!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
15
Help us understand the problem. What are the problem?