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

こちらは[iOS Advent Calendar](https://qiita.com/advent-calendar/2018/ios)の8日目の記事です。

# はじめに

1年以上前くらいにTensorFlow利用でオブジェクトトラッキング+AR表示検証用アプリをこしらえたことがあり、リアルタイムで頑張ろうとすると3,4秒に1回しか画面が更新されないレベルのカックカクで、0.5秒に1回画面のキャプチャ読み込みするくらいじゃないと重たくて使い物にならなかった記憶。

今iOSで標準準備された機械学習のライブラリを使うと、どれだけ楽に同じ要件の代物を実現できるのか？
サンプルのアプリにちょっと手を加えて、ちょっとした電脳ハック感を体験してみましょう。

# 作るもの
- カメラ映像を表示
- 映像に機械学習済みのトラッキング対象が含まれていたら、該当の名前を表示
    - 名前はAR表示

# 手順

## サンプルアプリのダウンロード

以下より。
[Recognizing Objects in Live Capture | Apple Developer](https://developer.apple.com/documentation/vision/recognizing_objects_in_live_capture)

## カメラ映像を差し替える

AR表示するにはモーションセンサによる奥行きなどの情報も併せて取得している映像が必要なので、Visionで検知する先のカメラ映像を差し替えましょう。

### AVFoundationを除去

これを、

```swift:ViewController.swift
import UIKit
import AVFoundation
import Vision
```

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

```swift:ViewController.swift
import UIKit
import SpriteKit
import ARKit
import Vision
```

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

これを、

```swift: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)
```

こうします。

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

<img width="740" alt="スクリーンショット 2018-12-07 8.38.01.png" src="https://qiita-image-store.s3.amazonaws.com/0/113982/48cd1538-f33e-057a-7b4a-63cd0bcd26fb.png">


### エラーをつぶす

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


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

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

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

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

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

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

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

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

### 新しい描画処理を実装

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

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

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

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

```swift: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 width="375" alt="IMG_0082.png" src="https://qiita-image-store.s3.amazonaws.com/0/113982/82cebb46-d943-f2b1-ca00-3af533d9a8e7.png">

キャプチャは検知時の画像。
iPhoneXですが、動かしてみるとARでリアルタイム検知はやっぱりカクつきます。。
通常のカメラ映像(サンプルに手を加えない状態)だとぬるぬる動いてたので、AR表示の負荷が非常に高いだけでTensolFlowで通常のカメラ映像だと同じような結果になるかも・・・？
検知頻度はゆるやかにしたほうがよさげですが、無論実装コストはCore ML+Visionが圧勝だと思います。

ネイティブアプリ上での機械学習モデルの利用、本当にとっつきやすくなったなぁと今更ながら関心する機会となりました。

# 参考

タップしたら検知したオブジェクトの名前をAR表示してくれる公式サンプル(こちらを加工したほうが楽だったのではないだろうか)
[Using Vision in Real Time with ARKit](
https://developer.apple.com/documentation/arkit/using_vision_in_real_time_with_arkit)

[SKLabelNode - SpriteKit | Apple Developer](https://developer.apple.com/documentation/spritekit/sklabelnode)

いろんなモデルをてっとり早く試したい！って場合はここに並んでるサンプルアプリから引っこ抜くと良さそう
[Vision | Apple Developer](https://developer.apple.com/documentation/vision#topics)

自分で検知したいモデルを作りたい！って方はこちらの記事がとても分かりやすいです
[[iOS 11] Core MLで焼き鳥を機械学習させてみた](https://dev.classmethod.jp/smartphone/iphone/ios-11-core-ml-2/)

# さいごに

Core ML+Visionでオブジェクトトラッキングするまでの辺りも自分で作ってみるつもりだったんですが、ちゃんと調べてみたらすでにAppleさんがたいへんよくできたものを準備してくれたいたので路線変更しました。。
個人的にはAndroidよりか何かと開発者に不親切なイメージなんですが、案外見るべきところ見落としてるだけかしらと少し反省。

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