[iOS] [Swift] Watson Visual Recognition と Core ML でリアルタイム画像認識をやってみた

# はじめに

WatsonのVisual Recognition（以下、VR）という画像認識サービスと、iOSのCore MLを連携させて、iPhoneのカメラでリアルタイム画像認識をやってみました。

Core MLを利用するためには、一般的には、機械学習ライブラリで作ったモデルファイルをXcodeに取り込みますが、私にとっては「少しハードルが高いな…」と思っていました。
一方、Watson VRとCore MLを連携させると、それよりも簡単にCore MLを利用することができます。:thumbsup:

![Qiita.PNG](https://qiita-image-store.s3.amazonaws.com/0/122917/b1fcb4ca-4f5f-6eac-8f6a-f841949bd7e9.png)

リアルタイムに画像認識する場合は、通信のボトルネックがあるWeb API型のサービスでは対応できませんので、オフラインで画像認識できるCore MLが威力を発揮します。

デモ動画はこちら↓
[YouTube](https://youtu.be/us6gz4OCPWQ)

なお、"score"は確信度で、1.0 = 100%です。

開発環境は、Xcode 9.4 - Swift 4.1です。

## おことわり

- 本稿に記載しているサービスについての情報は公式なものではありません。
- また、本稿の情報は執筆時点のものであり、最新の情報と異なっている可能性があります。
    - 本稿の内容をご活用される際は、必ずご自身で最新の公式情報をご確認ください。
- 本稿の内容はあくまで私個人の見解であり、所属企業における立場、戦略、意見を代表するものではありません。

# アーキテクチャーと開発手順をザックリ解説

![coreml_pattern.png](https://qiita-image-store.s3.amazonaws.com/0/122917/475907a2-a752-af16-671c-ea68623a0b0f.png)
[IBM Developer](https://developer.ibm.com/jp/patterns/deploy-a-core-ml-model-with-watson-visual-recognition/)より転載

**上図の①**
以下の作業を、IBM Cloud上のWatson StudioというWebサービスで実施します。

- トレーニング用の画像を準備して、Watson VRにインポートして分類用のタグを付けます。
- Watson VRモデルをトレーニングして、分類結果をテストします。
    - つまり、どんな画像をどのように分類（認識）するのかは、ここで作ったモデル次第というわけです。
- 作業はすべてブラウザ上のGUIで実施できるので、分かりやすく簡単です。
- iOSアプリ側で利用するための"Model ID"が発行されるのでコピっておきます。※"Classifier ID"と呼ぶ場合もあります。

**図の②と③**
以下の作業をXcodeで行います。

- iOSアプリに[Watson Swift SDK](https://github.com/watson-developer-cloud/swift-sdk)を組み込みます。
- SDKのパラメータに"Model ID"をセットすることで、SDKがモデルファイルをダウンロードしてアプリ内に取り込んでくれます。※これはオンライン処理です。
- アプリを実行すると、Core MLは、モデルファイルを利用して画像をオフラインで解析します。

**上図の④（と②）**

- ユーザーからのフィードバックなどを元に精度を上げたモデルを、アプリをアップデートすることなく、再デプロイすることが出来ます。

Watson Studioでの手順をさらに詳しく知りたい方はこちら↓をご参照ください。

- [IBM Developer - Watson Visual Recognition と Core ML を利用して、画像をオフラインで分類する](https://developer.ibm.com/jp/patterns/deploy-a-core-ml-model-with-watson-visual-recognition/)
- [【演習】IBM Watson Services for Core ML Tutorial](https://qiita.com/ymunemasa/items/cfef3cb27d04ad45d492)

# ソースコード

- 事前作業
    - [Watson Swift SDK](https://github.com/watson-developer-cloud/swift-sdk)をCarthageまたはCocoaPodsで組み込みます。詳細はリンク先を参照してください。
    - カメラを使うので、info.plistに"Privacy - Camera Usage Description"を設定しておきます。
    - モデルのアップデートの際に表示するインジケーターとして、[SwiftSpinner](https://github.com/icanzilb/SwiftSpinner)というOSSを使っています。
        - リンクのGitHubリポジトリからSwiftSpinner.swiftファイルを取得して、プロジェクトに追加しておきます。
- "Single View App"のテンプレートにてデフォルトで作成される、ViewController.swiftに以下のコードを書きました。StoryBoardはデフォルトのままです。

```swift:ViewController.swift
import UIKit
import AVKit
import CoreML
import Vision
import ImageIO
import VisualRecognitionV3

class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate {
    
    // MARK: - Propertys
    
    let identifierLabel: UILabel = {
        let label = UILabel()
        label.backgroundColor = .white
        label.textAlignment = .center
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    let apiKey = "WatsonVRのAPIKeyをコピー"
    let classifierId = "ModelIDをコピー"
    let version = "2017-12-07"
    var visualRecognition: VisualRecognition!
    var localModels: [String]?
    
    // 全フレームを画像認識する必要はないので、適当に間引くためのカウンター
    var frameCounter: Int = 0
    
    // MARK: - UIViewController LifeCycle
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.visualRecognition = VisualRecognition(apiKey: apiKey, version: version)
        
        setupCaptureSession()
        setupIdentifierConfidenceLabel()
    }
    
    override func viewDidAppear(_ animated: Bool) {
        // Watson VRと連携してモデルをアップデートする
        localModels = try? visualRecognition.listLocalModels()
        if let models = localModels, models.contains(self.classifierId) {
            let alert = UIAlertController(title: "",
                                          message: "学習モデルをアップデートしますか？",
                                          preferredStyle: UIAlertControllerStyle.alert)
            alert.addAction(UIAlertAction(title: "いいえ", style: .cancel, handler: nil))
            alert.addAction(UIAlertAction(title: "はい", style: .default, handler: { (action) in
                self.invokeModelUpdate()
            }))
            self.present(alert, animated: true, completion: nil)
        } else {
            self.invokeModelUpdate()
        }
    }
    
    /// カメラのキャプチャーを設定する
    func setupCaptureSession() {
        let captureSession = AVCaptureSession()
        captureSession.sessionPreset = .photo
        
        guard let captureDevice = AVCaptureDevice.default(for: .video) else { return }
        guard let input = try? AVCaptureDeviceInput(device: captureDevice) else { return }
        captureSession.addInput(input)
        
        captureSession.startRunning()
        
        let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
        view.layer.addSublayer(previewLayer)
        previewLayer.frame = view.frame
        
        let dataOutput = AVCaptureVideoDataOutput()
        dataOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "videoQueue"))
        captureSession.addOutput(dataOutput)
    }
    
    /// 結果表示用のラベルを配置する
    func setupIdentifierConfidenceLabel() {
        view.addSubview(identifierLabel)
        identifierLabel.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -32).isActive = true
        identifierLabel.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
        identifierLabel.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
        identifierLabel.heightAnchor.constraint(equalToConstant: 50).isActive = true
    }
    
    // MARK: - AVCaptureVideoDataOutputSampleBufferDelegate
    
    /// キャプチャー出力時のDelegate
    func captureOutput(_ output: AVCaptureOutput,
                       didOutput sampleBuffer: CMSampleBuffer,
                       from connection: AVCaptureConnection) {
        if localModels == nil { return }
        
        // フレームを間引く
        frameCounter += 1
        if frameCounter % 5 != 0 { return }
        
        DispatchQueue.main.sync {
            //バッファーをUIImageに変換
            guard let image = imageFromSampleBuffer(sampleBuffer: sampleBuffer) else {
                return
            }
            // 画像認識処理
            let failure = { (error: Error) in
                print(error)
                DispatchQueue.main.async {
                    self.identifierLabel.text = "Could not classify image"
                }
            }
            visualRecognition.classifyWithLocalModel(image: image,
                                                     classifierIDs: [classifierId],
                                                     threshold: 0.0,
                                                     failure: failure)
            { classifiedImages in
                if classifiedImages.images.count > 0,
                    classifiedImages.images[0].classifiers.count > 0,
                    classifiedImages.images[0].classifiers[0].classes.count > 0,
                    let score = classifiedImages.images[0].classifiers[0].classes[0].score {
                    DispatchQueue.main.async {
                        let className = classifiedImages.images[0].classifiers[0].classes[0].className
                        self.identifierLabel.text = "\(className): score = \(score)"
                    }
                }
            }
        }
    }
    
    // MARK: - Custom Methods
    
    /// Watson VR SDKにより、機械学習モデルをアップデートする
    func invokeModelUpdate() {
        let failure = { (error: Error) in
            print(error)
            DispatchQueue.main.async {
                self.identifierLabel.text = "Could not update model"
                SwiftSpinner.hide()
            }
        }
        let success = {
            DispatchQueue.main.async {
                SwiftSpinner.hide()
                self.localModels = try? self.visualRecognition.listLocalModels()
            }
        }
        SwiftSpinner.show("Compiling model...")
        visualRecognition.updateLocalModel(classifierID: classifierId, failure: failure, success: success)
    }
    
    /// キャプチャーのバッファーからUIImageを取り出す
    func imageFromSampleBuffer(sampleBuffer: CMSampleBuffer) -> UIImage? {
        guard let imageBuffer: CVPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
            print("CMSampleBufferGetImageBuffer get error!")
            return nil
        }
        let ciImage: CIImage = CIImage(cvPixelBuffer: imageBuffer)
        let context: CIContext = CIContext.init(options: nil)
        guard let cgImage: CGImage = context.createCGImage(ciImage, from: ciImage.extent) else {
            print("createCGImage error!")
            return nil
        }
        let uiImage: UIImage = UIImage.init(cgImage: cgImage)
        return uiImage
    }
}
```

エラー系の動作確認はあまり実施していませんので、上のコードをご活用される際は、自己責任でお願いします！

なお2018年9月30日現在、[Watson Swift SDK](https://github.com/watson-developer-cloud/swift-sdk)はXcode 10 - Swift 4.2に対応しています。

# 所感

- Watson VRと連携させると、Core MLの利用開始手順がとても簡単
- Watson VRと連携させると、Core MLの機械学習モデルのアップデートが簡単

問題は、これを何にどう使えば有益なモノが作れるのか、そのアイデアですね…:rolling_eyes:

# 参考にさせていただいた記事

- [LBTA - CoreML: Real Time Camera Object Detection with Machine Learning](https://www.letsbuildthatapp.com/course_video?id=1592)
- [Qiita - 無音カメラ on Swift4](https://qiita.com/nasutaro211/items/9267ba651db90ecf977e)