11
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ARKit上で物体追跡

Last updated at Posted at 2017-11-07

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!

11
13
0

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
11
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?