【Vision・Core ML・iOS・Swift】リアルタイム映像のオブジェクトを識別する

#この記事は何か？
Apple Developerのサンプルコード「[Recognizing Objects in Live Capture](https://developer.apple.com/documentation/vision/recognizing_objects_in_live_capture)」を独自に解説するものです。

###環境
macOS 10.15.7
Xcode 12.1
Swift5.3
iOS 14.2

#概要
Visionフレームワークを使用すると、ライブキャプチャしたオブジェクトを識別できます。
Core MLモデルを使ったVisionのリクエストは、キャプチャしたシーンで見つかったオブジェクトを識別した結果を`VNRecognizedObjectObservation`オブジェクトとして返します。

このサンプルアプリでは、以下の方法を紹介しています。
- カメラをライブキャプチャ用に設定する
- Core MLモデルをVisionに組み込む
- 結果を解析して、オブジェクトを分類する

￼

##ライブキャプチャを設定する
AVライブキャプチャの実装は、他のキャプチャアプリと似ていますが、Visionアルゴリズムで最適に動作するようにカメラを設定するには、いくつかの微妙な違いがあります。

###キャプチャに使用するカメラを設定する
AVFoundationフレームワークが出力するカメラ映像をメインビューコントローラに送信します。
まずは、`AVCaptureSession`を設定するために、セッションのインスタンスを作成します。

````Swift:
private let session = AVCaptureSession()
````

重要なのは、**アプリに最適な解像度**を選択することです。
特に要件がない場合でも、最高解像度を安易に選択しないでください。
解像度が低ければ、Visionはより効率的に結果を処理できます。
Xcodeのモデルパラメータを見て、「アプリが`640x480`ピクセル以下の解像度を必要とするか」を確認します。

以下のコードは、カメラの解像度を「モデルで使用する画像の解像度以上で、最も近い解像度」に設定します。

````Swift:デバイスとセッションの解像度を設定する
//「背面にある広角のビデオカメラ」のインスタンスを作成
let videoDevice = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: .back).devices.first
do {
    deviceInput = try AVCaptureDeviceInput(device: videoDevice!) // カメラからの入力
} catch {
    print("Could not create video device input: \(error)")
    return
}

session.beginConfiguration()        // セッションの設定を開始する
session.sessionPreset = .vga640x480 // モデル画像はより小さくする
````

スケーリング手続きは、Visionが行います。

以下のコードは、カメラをデバイスとして追加して、セッションにビデオ入力を追加します。

````Swift:セッションにカメラデバイスからの入力を追加する
guard session.canAddInput(deviceInput) else {
    print("Could not add video device input to the session")
    session.commitConfiguration()
    return
}
session.addInput(deviceInput)
````

以下のコードは、セッションにビデオ出力を追加し、ピクセルフォーマットを指定します。

````Swift:
if session.canAddOutput(videoDataOutput) {
    // ビデオ出力をセッションに追加できる場合
    session.addOutput(videoDataOutput)    // ビデオからの出力をセッションに追加する
    videoDataOutput.alwaysDiscardsLateVideoFrames = true
    videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)]
    videoDataOutput.setSampleBufferDelegate(self, queue: videoDataOutputQueue)
} else {
    // ビデオ出力をセッションに追加できない場合は、セッション設定を終わる
    print("Could not add video data output to the session")
    session.commitConfiguration()
    return
}
````

すべてのフレームを処理するが、一度に複数のVisionリクエストを保持しない
バッファキューが使用可能なメモリを超えると、カメラは動作を停止します。
バッファ管理を簡単にするために、キャプチャ出力では、前のリクエストが必要とする限り、Visionは呼び出しをブロックします。
その結果、AVFoundationは必要に応じてフレームをドロップすることがあります。
サンプルアプリでは、キューサイズを1にしています。
別のリクエストが利用可能になったときに、Visionリクエストが処理のためにすでにキューに入っている場合、余分なものを保持する代わりにそれをスキップします。

````Swift:
let captureConnection = videoDataOutput.connection(with: .video) // 全フレームを処理する
captureConnection?.isEnabled = true
do {
    try  videoDevice!.lockForConfiguration()
    let dimensions = CMVideoFormatDescriptionGetDimensions((videoDevice?.activeFormat.formatDescription)!)
    bufferSize.width = CGFloat(dimensions.width)
    bufferSize.height = CGFloat(dimensions.height)
    videoDevice!.unlockForConfiguration()
} catch {
    print(error)
}
````
以下のコードは、セッション設定を決定します。

````Swift:セッション設定を決定する
session.commitConfiguration()
````

以下のコードは、ビューコントローラにプレビューレイヤーを設定して、カメラがそのフレームをアプリのUIに送信できるようにします。

````Swift:
previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
rootLayer = previewView.layer
previewLayer.frame = rootLayer.bounds
rootLayer.addSublayer(previewLayer)
````

##デバイスの向きを指定する
カメラの向きは、デバイスの向きを使用して適切に入力する必要があります。
Visionアルゴリズムは方向に依存しないので、リクエストを行う際には、キャプチャデバイスの方向と相対的な方向を使用してください。

````Swift:デバイスの向きに基づいて、画像方向を取得する
let curDeviceOrientation = UIDevice.current.orientation // 実際のデバイスの向き
let exifOrientation: CGImagePropertyOrientation         // 画像方向のメタ情報

switch curDeviceOrientation {
case UIDeviceOrientation.portraitUpsideDown:  // デバイスが縦向き（ホームボタンが上）
    exifOrientation = .left
case UIDeviceOrientation.landscapeLeft:       // デバイスが水平（ホームボタンが右）
    exifOrientation = .upMirrored
case UIDeviceOrientation.landscapeRight:      // デバイスが水平（ホームボタンが左
    exifOrientation = .down
case UIDeviceOrientation.portrait:            // デバイスが縦向き（ホームボタンが下）
    exifOrientation = .up
default:
    exifOrientation = .up
}
````

##Core ML分類器を使って、ラベルを決定する
アプリに含めるCore MLモデルは、Visionのオブジェクト分類器に使用されるラベルを決定します。
このサンプルアプリのモデルは、Darknet YOLO（You Only Look Once）を使用してTuri Create 4.3.2でトレーニングされています。
Turi Createを使用して独自のモデルを生成する方法については、オブジェクト検出を参照してください。
Visionはこれらのモデルを解析し、オブザベーションを`VNRecognizedObjectObservation`オブジェクトとして返します。

以下のコードは、`VNCoreMLModel`を使用してモデルをロードします。

````Swift:モデルを作成する
let visionModel = try VNCoreMLModel(for: MLModel(contentsOf: modelURL))
````

以下のコードは、導入したモデルから`VNCoreMLRequest`を作成します。

````Swift:リクエストを作成する
let objectRecognition = VNCoreMLRequest(model: visionModel, completionHandler: { (request, error) in
    DispatchQueue.main.async(execute: {
        // UIの更新はメインキューで実行する
        if let results = request.results {
            self.drawVisionRequestResults(results)
        }
    })
})
````

完了ハンドラ自体はバックグラウンドキュー上で実行される可能性があります。
そのため、UIの更新をメインキュー上で実行して、即座に視覚的なフィードバックを提供します。
リクエストの完了ハンドラでは、`requests`プロパティを介して結果にアクセスします。

##オブザベーションを分析する
結果プロパティはオブザベーションの配列です。
配列の要素は、ラベルとバウンディング・ボックスのセットです。

以下のコードは、配列を反復して、これらのオブザベーションを解析します。

````Swift:
for observation in results where observation is VNRecognizedObjectObservation {
    guard let objectObservation = observation as? VNRecognizedObjectObservation else {
        continue
    }
    // 最も信頼度が高いラベルのみを選択する
    let topLabelObservation = objectObservation.labels[0]
    let objectBounds = VNImageRectForNormalizedRect(objectObservation.boundingBox, Int(bufferSize.width), Int(bufferSize.height))
    
    let shapeLayer = self.createRoundedRectLayerWithBounds(objectBounds)
    
    let textLayer = self.createTextSubLayerInBounds(objectBounds,
                                                    identifier: topLabelObservation.identifier,
                                                    confidence: topLabelObservation.confidence)
    shapeLayer.addSublayer(textLayer)
    detectionOverlay.addSublayer(shapeLayer)
}
````

`labels`配列は、信頼度の高いものから低いものへと順に、各分類識別子とその信頼度の値をリストアップします。
サンプルアプリでは、要素0にある最も信頼度の高い分類のみをメモし、この分類と信頼度をテキストのオーバーレイで表示します。

バウンディングボックスには、オブジェクトが観測された座標情報が含まれています。
サンプルでは、この座標情報を使用してオブジェクトの周囲にバウンディングボックスを描画します。

このサンプルは、上位の分類のみを返すことで分類を単純化し、配列は信頼度スコアの小さい順に並べられます。
信頼度スコアを分析して複数の分類を表示すれば、検出されたオブジェクトをより詳細に説明したり、競合する分類を表示したりすることもできます。

また、オブジェクトを識別した結果として得られる`VNRecognizedObjectObservation`を使用して、`VNTrackObjectRequest`のようなオブジェクトトラッカーを初期化することもできます。
オブジェクト・トラッキングの詳細については、[Tracking Multiple Objects or Rectangles in Video](https://developer.apple.com/documentation/vision/tracking_multiple_objects_or_rectangles_in_video)を参照してください。