【CoreML】Cycle GANをCore MLに変換してiOS上で動作させる

Cycle GANをCore MLに変換する方法を書きます。

基本的には次の記事と同じ内容ですが、2020/11/1現在、モデルの変換はtf-coremlではなくCore ML Toolsでできるので、その部分の差分を書いておこうと思います。

Cycle GANをモバイル用モデルに変換（CoreML)
https://qiita.com/john-rocky/items/3fbdb0892b639187c234

結論から言うと、各ライブラリのバージョンを次のようにすれば問題なく変換できます。

```
tensorflow==2.2.0
keras==2.2.4
coremltools==4.0
```

### 手順

Google Colaboratoryでの手順を書きます。

**1.TensorFlowのチュートリアルを開く**

こちらのページからチュートリアルを開きます
https://colab.research.google.com/github/tensorflow/docs/blob/master/site/en/tutorials/generative/cyclegan.ipynb

**2.各ライブラリのバージョンをCore ML Tools 4.0に合わせる**

Colabにもともと入っているtensorflowやkerasをアンインストールしてから、適切なバージョンのものをインストールし直します。また、Core ML Tools 4.0をインストールします。Google Colabのランタイムの再起動が必要になるかもしれません。

```Python:Notebook
!pip uninstall -y tensorflow
!pip uninstall -y keras
!pip install tensorflow==2.2.0
!pip install keras==2.2.4
!pip install -U coremltools==4.0
```

**3.チュートリアルにしたがって学習を実行する**

チュートリアルは上から下に実行していくだけでCycle GANの学習ができるようになっているので、そのまま実行していきます。

**4.モデルを保存し、Core MLに変換する**

TensorFlowのモデルを保存します。

```Python:Notebook
generator_g.save('./savedmodel')
```

Core MLに変換します。
引数はtf-coremlに比べるとだいぶシンプルになっています。

```Python:Notebook
import coremltools as ct

input_name = generator_g.inputs[0].name.split(':')[0]
print(input_name) #Check input_name.
keras_output_node_name = generator_g.outputs[0].name.split(':')[0]
graph_output_node_name = keras_output_node_name.split('/')[-1]

mlmodel = ct.converters.convert('./savedmodel/',
                       inputs=[ct.ImageType(bias=[-1,-1,-1], scale=2/255,shape=(1, 256, 256, 3,))],
                       output_names=[graph_output_node_name],
                 )
```

変換したCore MLモデルを保存します。

```Python:Notebook
mlmodel.save('./cyclegan.mlmodel')
```

**5.Core MLモデルをXcodeのプロジェクトに読み込んで表示する**

Xcodeでプロジェクトを作成します。

カメラ画像をCycle GANで変換するアプリを作ります。

Main.storyboardにUIImageViewとUIButtonが１つだけあるシンプルな画面を作り、ViewControllerに接続します。UIImageは参照として、UIButtonはアクションとして接続します。

<img width="500px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/6fdb72b3-0060-4285-c088-84ef45d6e861.png">

今回はVision Frameworkに推論させますが、推論結果はMulti Array型で返ってきます。これをUIImageに変換する必要がありますが、[Core ML Helper](https://github.com/hollance/CoreMLHelpers)を使うと簡単です。そのために、CoreMLHelperからソースコードをコピーしておきます。

具体的には
`https://github.com/hollance/CoreMLHelpers/tree/master/CoreMLHelpers` 以下にあるソースコードを全部自分のプロジェクトにコピーしています。

<img width="300px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/f9dadf28-798b-018e-47f8-d69001ae57d7.png">

Core ML Helperの使い方はこちらが参考になります。

MultiArrayからImageへの変換　CoreMLHelper
https://qiita.com/john-rocky/items/bfa631aba3e7aabc2a66

ViewControllerに処理を書いていきます。
カメラ画像を取り込んで、Vision Frameworkに渡し、これをUIImageに変換して、UIImageViewに貼り付けています。

今回は、カメラ画像をVision Frameworkに渡して推論結果のUIImageを取り出すところまでを、`CycleGanConverter` というクラスに切り出しました。

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

class ViewController: UIViewController, UIImagePickerControllerDelegate & UINavigationControllerDelegate {
    @IBOutlet weak var imageView: UIImageView!
    // Cycle GANのコンバータ
    let converter = CycleGanConverter()

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    @IBAction func tappedCamera(_ sender: Any) {
        // カメラボタンが押されたらカメラを起動します
        if UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.camera) {

            let picker = UIImagePickerController()
            picker.modalPresentationStyle = UIModalPresentationStyle.fullScreen
            picker.delegate = self
            picker.sourceType = UIImagePickerController.SourceType.camera

            self.present(picker, animated: true, completion: nil)
        }
    }

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        defer {
            picker.dismiss(animated: true, completion: nil)
        }
        guard let image = info[UIImagePickerController.InfoKey(rawValue: UIImagePickerController.InfoKey.originalImage.rawValue)] as? UIImage else {return}
        // 変数imageにカメラで撮った画像が入ってきます
        // Cycle GANに渡す画像サイズに変換しています(CoreML HelperのExtensionを使用）
        let resized = image.resized(to: CGSize(width: 256, height: 256))
　　　　　// cgImage型で渡す必要があるので変換
        guard let cgImage = resized.cgImage else {return}
        // コンバーターに渡す
        converter.convert(image: cgImage) {
            images in
            // コンバーターは変換に成功したらimages配列に結果を返す
            guard images.count > 0 else {return}
            let image = images.first!
            DispatchQueue.main.async {
                // UIImageViewに貼り付ける
                self.imageView.image = image
            }
        }
    }
}

class CycleGanConverter {

    func convert(image: CGImage, completion: @escaping (_ images: [UIImage]) -> Void) {
        // Vision Frameworkを設定する
        let handler = VNImageRequestHandler(cgImage: image,options: [:])

        let model = try! VNCoreMLModel(for: cyclegan().model)
        let request = VNCoreMLRequest(model: model, completionHandler: {
            [weak self] (request: VNRequest, error: Error?) in
            // 推論結果が変数requestに返ってくるのでperseImageメソッドで変換する
            guard let images = self?.parseImage(request: request) else {return}
            completion(images)
        })

        try? handler.perform([request])
    }
    
    func parseImage(request: VNRequest) -> [UIImage] {
        guard let results = request.results as? [VNCoreMLFeatureValueObservation] else {return []}
        // results配列のそれぞれの要素のfeatureValue.multiArrayValueに変換後の画像が入ってくるのでこれをUIImageに変換する
        return results.compactMap {
            $0.featureValue.multiArrayValue?
                .image(min: -1, max: 1, channel: nil, axes: (3,2,1))? // Core ML Helperのメソッドで、UIImageへの変換をしてくれる
                .rotated(by: 90) // 画面表示用に回転をかける
                .withHorizontallyFlippedOrientation() // 画面表示用に反転をかける
        }
    }
}
```

### 仕上がり
<元の画像>
<img width="300px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/ea6e4f3e-e6c4-1f04-8837-537c65a019bc.png">

<変換後>
<img width="300px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/fc594ee8-f224-c532-f1af-96f5032bf3b5.png">

今回は時間の都合からトレーニング数がチュートリアルの指定よりも大幅に減らしているので、そこそこの変換結果になっています。

チュートリアルどおりにトレーニングすればもっと良い変換結果になるはずです。

### 最後に

NoteではiOS開発について定期的に発信していますので、フォローしていただけますと幸いです。
https://note.com/tokyoyoshida

Twitterでも発信しています。
https://twitter.com/jugemjugemjugem