Kerasで作成したモデルを変換してiOSに組み込み

# やること
[前回記事(【Tensorflow・VGG16】転移学習による画像分類)](https://qiita.com/hiraku00/items/66a3606af3b2eed57778)で作成したモデルを使ってiPhone上で写真を撮影し、それを推定してみる。

# 手順概要
1. Kerasで作成したモデル(.h5)を変換
2. Xcodeでプロジェクトを作成
3. 画面のパーツ作成 & コードの関連付け
4. カメラへのアクセス許可 & カメラを起動するコードを追加
5. ボタン(写真撮影)押下時の挙動
6. モデルを読み込んで推定

# 動作環境
- macOS Catalina 10.15 beta
- Python 3.6.8
- keras 2.2.4
- coremltools 2.1.0
- Xcode 10.2.1

# 1. Kerasで作成したモデル(.h5)を変換
- [前回記事](https://qiita.com/hiraku00/items/66a3606af3b2eed57778)で作成したモデル(vgg16_transfer.h5)を使い、iOSで利用可能なモデルに変換する

```python:モデルの変換
import coremltools

coreml_model = coremltools.converters.keras.convert(
    'vgg16_transfer.h5',
    input_names='image',
    image_input_names='image',
    output_names='Prediction',
    class_labels=['apple', 'tomato', 'strawberry'],
    )

coreml_model.save('./vgg16_transfer.mlmodel')
```

# 2. Xcodeでプロジェクトを作成
- Xcodeを起動し、「Create a new Xcode project」を選択
<img width="494" alt="1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67194/361ef47a-8827-0955-0f2b-8341cb8808ba.png">
- 「Single View App」を選択
<img width="730" alt="2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67194/b642517d-7efe-4a8b-c7bd-dd3db99192c9.png">
- 'Product Name'は任意の名称を付ける。Languageは「Swift」、チェックボックス(Use XX, include XX)は全て外し、Nextボタンを押下する

# 3. 画面のパーツ作成 & コードの関連付け
- Xcode上で下記3つの画面のパーツを追加する
 - Image View(写真を表示する領域)
 - Text View(推定結果を表示する領域)
 - Button(カメラを起動するボタン)

- こんな感じのレイアウトにしておく
<img width="553" alt="3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67194/b327fe7a-0a54-bcb4-dd33-d03f5801d98c.png">


- 各パーツの関連付けを実施する
- Image ViewとText Viewは「class ViewController」の下に追加(パーツを選択し、controlキーを押下しながら追加)
- Buttonは「override func viewDidLoad()」の下に追加
- Name(とConnection)はそれぞれ下記で設定
 - Image View : imageDisplay(Connection : Outlet)
 - Text View : predictionDisplay(Connection : Outlet)
 - Button : takePhoto(Connection : Action)

```swift:各パーツの関連付け
    # Image View
    @IBOutlet weak var imageDisplay: UIImageView!
    # Text View
    @IBOutlet weak var predictionDisplay: UITextView!
    # Button
    @IBAction func takePhoto(_ sender: Any) {
    }
```

# 4. カメラへのアクセス許可 & カメラを起動するコードを追加
- info.plistに「Privacy - Camera Usage Description」を追加し、カメラへのアクセスを許可する
<img width="842" alt="４.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67194/4beff109-742e-82f8-e020-e117170714c6.png">

- 下記2つの「delegate」(イベントを検知・処理)を追加する。(カメラを表示する画面、写真を保存した時に元の画面に戻る操作を行うことが可能)
 - UIImagePickerControllerDelegate　
 - UINavigationControllerDelegate　
- カメラの撮影画面を表示するために「imagePicker」という変数を追加
- アプリが起動した直後に「imagePicker」を初期化するコードを「viewDidLoad()」内に追加


```swift:カメラを起動(before)
class ViewController: UIViewController {

    @IBOutlet weak var imageDisplay: UIImageView!
    @IBOutlet weak var predictionDisplay: UITextView!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }
    
    # ~~~ 省略 ~~~
}
```

```swift:カメラを起動(after)
class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {

    @IBOutlet weak var imageDisplay: UIImageView!
    @IBOutlet weak var predictionDisplay: UITextView!
    var imagePicker: UIImagePickerController!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        # 初期化
        imagePicker = UIImagePickerController()
        # delegate:イベントを受け渡しする変数
        imagePicker.delegate = self
        # sourceType : カメラからデータを撮るか、アルバムから読込か -> 今回はカメラ
        imagePicker.sourceType = .camera
    }

    # ~~~ 省略 ~~~
}
```

# 5. ボタン(写真撮影)押下時の挙動
- 4.で作成しておいたimagePickerを表示するため、present関数を「takePhoto」で呼び出す

```swift:before
    @IBAction func takePhoto(_ sender: Any) {
    }
```
```swift:after
    @IBAction func takePhoto(_ sender: Any) {
        present(imagePicker, animated: true, completion: nil)
    }
```

- 写真を撮り終わった後の処理を記載(imagePickerController)
- imageDisplayのimage属性を更新する
- infoに撮影した画像が入っているため、それを取り出してimageDisplayのimage属性にセットする
- infoのUIImagePickerController.InfoKeyに各種属性が入っているため、「originalImage」というプロパティを設定する('as? UIImage'でタイプを指定する)
- 画像を設定したら、先程のImagePickerを閉じる(dismiss)

```swift:after
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        imageDisplay.image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage
        imagePicker.dismiss(animated: true, completion: nil)
    }
```

# 6. モデルを読み込んで推定
- mlmodelを読み込みして、そのモデルに画像ファイルを渡して推定する
- 1.で変換したモデル(vgg16_transfer.mlmodel)をXcodeのプロジェクトにドラッグ・アンド・ドロップする

<img width="730" alt="7.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67194/f0d1f30f-a7af-3144-1076-0682caac3b0a.png">

<img width="323" alt="8.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67194/7a64d2ba-a3a9-51ae-7425-48bd0758188a.png">

- 下記ライブラリを追加する
 - CoreML : 機械学習のライブラリ
 - Vision : 画像ファイルを扱うライブラリ

```swift:ライブラリの追加
import CoreML
import Vision
```

- 推定処理を加える
- imagePickerControllerの後ろに推定処理(imagePrediction)を加える
- 引数に画像ファイルを指定(画面上に表示した内容と同じ(imageDisplay.image))

```swift:before
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        imageDisplay.image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage
        imagePicker.dismiss(animated: true, completion: nil)
    }
```

```swift:after
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        imageDisplay.image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage
        imagePicker.dismiss(animated: true, completion: nil)
        imagePrediction(image: (info[UIImagePickerController.InfoKey.originalImage] as? UIImage)!)
    }
```

- 推定処理(imagePrediction)の関数を作成
- モデルの読み込み

```swift:モデルの読み込み
    func imagePrediction(image: UIImage) {
        guard let model = try? VNCoreMLModel(for: vgg16_transfer().model) else {
            fatalError("Model not found")
        }
```

- VNCoreMLRequestのインスタンスを生成
- 推定結果をresultsに格納する(推定結果は「VNClassificationObservation」という変数に入って返ってくる)
- results.firstにスコアの高いデータが格納されている
- predictionDiaplayに推定結果を表示する
 - firstResult.confidence : 確率(少数で入っているので、100倍して％表示)
 - firstResult.identifier : ラベル(推定結果)

```swift:モデルによる推定処理
        let request = VNCoreMLRequest(model: model) {
            [weak self] request, error in
            
            guard let results = request.results as? [VNClassificationObservation],
                let firstResult = results.first else {
                    fatalError("No results found")
            }
            
            DispatchQueue.main.async {
                self?.predictionDisplay.text = "Accuracy: = \(Int(firstResult.confidence * 100))% \n\nラベル: \((firstResult.identifier))"
            }
        }
```

- requestに対する処理を記載
- ciImageというデータ型に変換できないと推定処理が出来ないため、取得出来ない場合はエラー
- Visionフレームワークで画像を使うためのimageHandlerという変数を宣言し、VNImageRequestHandlerを生成
- imageHandlerにrequestを実行させる

```swift
        guard let ciImage = CIImage(image: image) else {
            fatalError("Can't convert image.")
        }
        
        let imageHandler = VNImageRequestHandler(ciImage: ciImage)
        
        DispatchQueue.global(qos: .userInteractive).async {
            do {
                try imageHandler.perform([request])
            } catch {
                print("Error")
            }
        }
```

- コードの全体は以下の通り

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

class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {

    @IBOutlet weak var imageDisplay: UIImageView!
    @IBOutlet weak var predictionDisplay: UITextView!
    var imagePicker: UIImagePickerController!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        imagePicker = UIImagePickerController()
        imagePicker.delegate = self
        imagePicker.sourceType = .camera
    }
    @IBAction func takePhoto(_ sender: Any) {
        present(imagePicker, animated: true, completion: nil)
    }
    
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        imageDisplay.image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage
        imagePicker.dismiss(animated: true, completion: nil)
        imagePrediction(image: (info[UIImagePickerController.InfoKey.originalImage] as? UIImage)!)
    }
    
    func imagePrediction(image: UIImage) {
        guard let model = try? VNCoreMLModel(for: vgg16_transfer().model) else {
            fatalError("Model not found")
        }
        
        let request = VNCoreMLRequest(model: model) {
            [weak self] request, error in
            
            guard let results = request.results as? [VNClassificationObservation],
                let firstResult = results.first else {
                    fatalError("No results found")
            }
            
            DispatchQueue.main.async {
                self?.predictionDisplay.text = "Accuracy: = \(Int(firstResult.confidence * 100))% \n\nラベル: \((firstResult.identifier))"
            }
        }
        
        guard let ciImage = CIImage(image: image) else {
            fatalError("Can't convert image.")
        }
        
        let imageHandler = VNImageRequestHandler(ciImage: ciImage)
        
        DispatchQueue.global(qos: .userInteractive).async {
            do {
                try imageHandler.perform([request])
            } catch {
                print("Error")
            }
        }
    }
}
```

# ソースコード
https://github.com/hiraku00/ios_camera
('vgg16_transfer.h5'と'vgg16_transfer.mlmodel'は除外)

# 参考文献
- [Keras + CoreMLでリアルタイムでレシピを提案するiPhoneアプリを作ろう](https://qiita.com/Kosuke-Szk/items/3f95c8c5c107d70a6ca5)
- [Classifying Images with Vision and Core ML](https://developer.apple.com/documentation/vision/classifying_images_with_vision_and_core_ml)