iOS11から提供されたARKit、 Visionというフレームワークが追加されました。
##ARKitとVision
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を行うという実装を行いました。
Apply high-performance image analysis and computer vision techniques to identify faces, detect features, and classify scenes in images and video.
##実装例
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
は釘バットの部分です。 物体追跡部分にフォーカスするためにアニメーションやラジコンを操作する部分の実装は省略しましたが、ご了承ください。
!