12
10

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.

iOSAdvent Calendar 2018

Day 8

つい試したくなる? iOSアプリでオブジェクトトラッキング結果をリアルタイムでAR表示

Posted at

こちらは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を除去

これを、

ViewController.swift
import UIKit
import AVFoundation
import Vision

こうしましょう。継承先も同じく。

ViewController.swift
import UIKit
import SpriteKit
import ARKit
import Vision

AVFoundationから呼び出していたもろもろを除去

これを、

ViewController.swift
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)

こうします。

ViewController.swift

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の関連付けをし直します。
そうすると、以下のようになります。

スクリーンショット 2018-12-07 8.38.01.png

エラーをつぶす

setupAVCapture() 周辺にて怒られ始めると思うので、以下のようにARSKViewのセットアップもろもろの処理に差し替える。

ViewController.swift
    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へ以下を追記して、のちほど継承先で中身を実装。

ViewController.swift
    // MARK: - ARSKViewDelegate, ARSessionDelegate
    
    func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? {
        return nil
    }
    
    func session(_ session: ARSession, didUpdate frame: ARFrame) {
    }

オブジェクトトラッキングを実行するデリゲートメソッドを実装

まず、使用するプロパティを定義。

VisionObjectRecognitionViewController.swift
    private var currentBuffer: CVPixelBuffer?
    private var requests = [VNRequest]()

映像が更新されるたびに呼ばれるsession(_:didUpdate:)のメソッドで、以下のように実装。

VisionObjectRecognitionViewController.swift
    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()メソッドを以下のように修正。

ViewController.swift
   func setupAVCapture() {
        let overlayScene = SKScene()
        overlayScene.scaleMode = .aspectFill
        previewView.presentScene(overlayScene)
        previewView.delegate = self
        previewView.session.delegate = self
    }

新しい描画処理を実装

使用するプロパティを定義。

VisionObjectRecognitionViewController.swift
    private var currentAnchor: ARAnchor?
    private var anchorLabels = [UUID: String]()

そして、映像の中に登録済みのモデルが含まれていた場合に実行されるdrawVisionRequestResults(_:)にて、ラベルの描画先となるARAnchorを設定する。

VisionObjectRecognitionViewController.swift
    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インスタンスで返す。

VisionObjectRecognitionViewController.swift
    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)より手前に表示してね、という設定。
フォントの色やサイズなどはお好みで。

できあがったものと感想

IMG_0082.png

キャプチャは検知時の画像。
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よりか何かと開発者に不親切なイメージなんですが、案外見るべきところ見落としてるだけかしらと少し反省。

みなさまも是非是非、使えるものはフル活用して快適にアプリケーション開発をやっていきましょう!

12
10
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
12
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?