こちらはiOS Advent Calendarの8日目の記事です。
はじめに
1年以上前くらいにTensorFlow利用でオブジェクトトラッキング+AR表示検証用アプリをこしらえたことがあり、リアルタイムで頑張ろうとすると3,4秒に1回しか画面が更新されないレベルのカックカクで、0.5秒に1回画面のキャプチャ読み込みするくらいじゃないと重たくて使い物にならなかった記憶。
今iOSで標準準備された機械学習のライブラリを使うと、どれだけ楽に同じ要件の代物を実現できるのか?
サンプルのアプリにちょっと手を加えて、ちょっとした電脳ハック感を体験してみましょう。
作るもの
- カメラ映像を表示
- 映像に機械学習済みのトラッキング対象が含まれていたら、該当の名前を表示
- 名前はAR表示
手順
サンプルアプリのダウンロード
以下より。
Recognizing Objects in Live Capture | Apple Developer
カメラ映像を差し替える
AR表示するにはモーションセンサによる奥行きなどの情報も併せて取得している映像が必要なので、Visionで検知する先のカメラ映像を差し替えましょう。
AVFoundationを除去
これを、
import UIKit
import AVFoundation
import Vision
こうしましょう。継承先も同じく。
import UIKit
import SpriteKit
import ARKit
import Vision
AVFoundationから呼び出していたもろもろを除去
これを、
class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate {
var bufferSize: CGSize = .zero
var rootLayer: CALayer! = nil
@IBOutlet weak var previewView: UIView!
private let session = AVCaptureSession()
private var previewLayer: AVCaptureVideoPreviewLayer! = nil
private let videoDataOutput = AVCaptureVideoDataOutput()
private let videoDataOutputQueue = DispatchQueue(label: "VideoDataOutput", qos: .userInitiated, attributes: [], autoreleaseFrequency: .workItem)
こうします。
class ViewController: UIViewController, ARSKViewDelegate, ARSessionDelegate {
var bufferSize: CGSize = .zero
@IBOutlet weak private var previewView: ARSKView!
private let videoDataOutputQueue = DispatchQueue(label: "VideoDataOutput", qos: .userInitiated, attributes: [], autoreleaseFrequency: .workItem)
なお、この際にMain.storyboardにてpreviewViewはUIViewからARSKViewへ差し替える必要があるので、もとのPreview Viewは1回削除してARSKViewを追加し、IBOutletの関連付けをし直します。
そうすると、以下のようになります。
エラーをつぶす
setupAVCapture()
周辺にて怒られ始めると思うので、以下のようにARSKViewのセットアップもろもろの処理に差し替える。
func setupAVCapture() {
previewView.delegate = self
previewView.session.delegate = self
}
func startCaptureSession() {
let configuration = ARWorldTrackingConfiguration()
previewView.session.run(configuration)
}
不要なメソッド削除
teardownAVCapture()
はAVFoundation使用時のカメラ映像で逼迫するメモリ解放用メソッドなので削除。
また、captureOutput(_:didOutput:from:)
メソッドはARSKViewにて同等の役割を果たせるデリゲートメソッドに差し替える形に実装し直すので、ばっさり削除。
そして、継承先のVisionObjectRecognitionViewControllerにてrootLayer削除で怒られてる箇所であるsetupLayers()
メソッドについても、そもそも描画先がCALayerではなくなるのでこちらも削除。
使用するデリゲートメソッドを準備
ViewControllerへ以下を追記して、のちほど継承先で中身を実装。
// MARK: - ARSKViewDelegate, ARSessionDelegate
func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? {
return nil
}
func session(_ session: ARSession, didUpdate frame: ARFrame) {
}
オブジェクトトラッキングを実行するデリゲートメソッドを実装
まず、使用するプロパティを定義。
private var currentBuffer: CVPixelBuffer?
private var requests = [VNRequest]()
映像が更新されるたびに呼ばれるsession(_:didUpdate:)
のメソッドで、以下のように実装。
override func session(_ session: ARSession, didUpdate frame: ARFrame) {
guard currentBuffer == nil, case .normal = frame.camera.trackingState else {
return
}
self.currentBuffer = frame.capturedImage
let exifOrientation = exifOrientationFromDeviceOrientation()
let imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: currentBuffer!, orientation: exifOrientation, options: [:])
do {
defer { self.currentBuffer = nil }
try imageRequestHandler.perform(self.requests)
} catch {
print(error)
}
}
VNImageRequestHandler(cvPixelBuffer: currentBuffer!, orientation: exifOrientation, options: [:])
にてその瞬間のキャプチャと画面向きを設定して、try imageRequestHandler.perform(self.requests)
であらかじめ機械学習済みのモデル(サンプルアプリでいうところのObjectDetector.mlmodelファイルの情報)と比較。
上記の記述のまま実行するとリアルタイムで検知し続ける。
ちなみに、いずれかのモデルと一致するものが含まれていた際に結果が返ってくるのはsetupVision()
メソッド内の実装から察せられる通りdrawVisionRequestResults(_:)
。
トラッキング対象を検知したときに描画するレイヤーを差し替える
検知したモデルの名前を画面に表示するためには、CALayerではなく、SKNodeを描画する。
不要なプロパティ削除
VisionObjectRecognitionViewControllerのdetectionOverlayプロパティは削除。前の手順の方で不要なメソッドを削除し切っていたらdrawVisionRequestResults(_:)
でのみエラーが発生するが、このタイミングでいったん放置。
SpriteKitで描画する準備
SKNodeを描画するための準備として、ViewControllerのsetupAVCapture()
メソッドを以下のように修正。
func setupAVCapture() {
let overlayScene = SKScene()
overlayScene.scaleMode = .aspectFill
previewView.presentScene(overlayScene)
previewView.delegate = self
previewView.session.delegate = self
}
新しい描画処理を実装
使用するプロパティを定義。
private var currentAnchor: ARAnchor?
private var anchorLabels = [UUID: String]()
そして、映像の中に登録済みのモデルが含まれていた場合に実行されるdrawVisionRequestResults(_:)
にて、ラベルの描画先となるARAnchorを設定する。
func drawVisionRequestResults(_ results: [Any]) {
for observation in results where observation is VNRecognizedObjectObservation {
guard let objectObservation = results.first as? VNRecognizedObjectObservation else {
continue
}
let hitTestResults = previewView.hitTest(
previewView.center, types: [.featurePoint, .estimatedHorizontalPlane])
if let result = hitTestResults.first {
let anchor = ARAnchor(transform: result.worldTransform)
if let currentAnchor = self.currentAnchor {
previewView.session.remove(anchor: currentAnchor)
}
previewView.session.add(anchor: anchor)
currentAnchor = anchor
let topLabelObservation = objectObservation.labels[0]
anchorLabels[anchor.identifier] = topLabelObservation.identifier
return
}
}
}
今回は画面内に1個、とりあえず画面の中心へ表示する想定。
描画するデリゲートメソッドを実装する
前述でpreviewView.sessionにARAnchorを追加するとview(_:nodeFor:)
が実行されるので、描画したい文字列をSKNodeインスタンスで返す。
override func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? {
guard let labelText = anchorLabels[anchor.identifier] else {
fatalError("missing expected associated label for anchor")
}
let label = SKLabelNode(fontNamed: "Chalkduster")
label.text = labelText
label.fontColor = SKColor.black
label.horizontalAlignmentMode = .center
label.verticalAlignmentMode = .center
label.zPosition = 1
label.fontSize = 10
return label
}
zPositionは親Node(previewView)より手前に表示してね、という設定。
フォントの色やサイズなどはお好みで。
できあがったものと感想
キャプチャは検知時の画像。
iPhoneXですが、動かしてみるとARでリアルタイム検知はやっぱりカクつきます。。
通常のカメラ映像(サンプルに手を加えない状態)だとぬるぬる動いてたので、AR表示の負荷が非常に高いだけでTensolFlowで通常のカメラ映像だと同じような結果になるかも・・・?
検知頻度はゆるやかにしたほうがよさげですが、無論実装コストはCore ML+Visionが圧勝だと思います。
ネイティブアプリ上での機械学習モデルの利用、本当にとっつきやすくなったなぁと今更ながら関心する機会となりました。
参考
タップしたら検知したオブジェクトの名前をAR表示してくれる公式サンプル(こちらを加工したほうが楽だったのではないだろうか)
Using Vision in Real Time with ARKit
SKLabelNode - SpriteKit | Apple Developer
いろんなモデルをてっとり早く試したい!って場合はここに並んでるサンプルアプリから引っこ抜くと良さそう
Vision | Apple Developer
自分で検知したいモデルを作りたい!って方はこちらの記事がとても分かりやすいです
[iOS 11] Core MLで焼き鳥を機械学習させてみた
さいごに
Core ML+Visionでオブジェクトトラッキングするまでの辺りも自分で作ってみるつもりだったんですが、ちゃんと調べてみたらすでにAppleさんがたいへんよくできたものを準備してくれたいたので路線変更しました。。
個人的にはAndroidよりか何かと開発者に不親切なイメージなんですが、案外見るべきところ見落としてるだけかしらと少し反省。
みなさまも是非是非、使えるものはフル活用して快適にアプリケーション開発をやっていきましょう!