Visionなしで、Core ML単体で使う

Appleのサンプルを含め、Web上で見つかる記事のほとんどがCore MLのモデルを利用する際、Visionを併用した実装となっています。純正フレームワークとはいえ、不要な依存は少ないに越したことはありません。**Visionを使わず、Core MLだけで実装**するとしたらどんな感じになるんだろう、Visionはどのあたりを楽にしてくれているのか？ということに興味があり、やってみました。

比較として、以下の記事に書いたVisionを用いた実装と併記してみます。使用モデルはInception v3です。

https://qiita.com/shu223/items/e200e379e34629018dca

## モデル読み込み

```swift:Visionあり
private let model = try! VNCoreMLModel(for: Inceptionv3().model)
```

```swift:Visionなし
private let model = Inceptionv3()
```

Visionありの場合は`.mlmodel`から自動生成されたクラスから`MLModel`オブジェクトを取り出し、`VNCoreMLModel`を生成しましたが、Core ML単体のときは自動生成クラスをそのまま使います。（そのクラスに自動で生やされたメソッドを使いたいので）

## 画像入力

```swift:Visionあり
let handler = VNImageRequestHandler(cgImage: cgImage)
```

```swift:Visionなし
let input = Inceptionv3Input(image: pixelBuffer)
```

Visionありの場合は、リクエストハンドラを生成する際に入力画像を渡します。引数に渡せる入力画像の型は`CVPixelBuffer`, `CGImage`, `CIImage`, `URL`, `Data`等。

Visionなしの場合は、自動生成された入力型を生成します。初期化時に引数に渡せる入力画像の型は`CVPixelBuffer`のみ。

これだけ見ると大して違いはなさそうですが、実は、Visionを使わない場合には**自分でモデルの入力に合わせてリサイズを行う必要**があります。

たとえば、Inceptionv3.mlmodelから自動生成された入力型`Inceptionv3Input`には次のようなコメントがあります。

```swift:Inceptionv3.swift
/// Input image to be classified as color (kCVPixelFormatType_32BGRA) image buffer, 299 pixels wide by 299 pixels high
```

つまり、UIImageなりCGImageなりの手元にある画像データを、299x299にリサイズし、`CVPixelBuffer`に格納する必要があります。（このあたりからVisionのありがたみがわかってきます）

このあたりの面倒さを助けてくれるヘルパークラスの集合として、

- https://github.com/hollance/CoreMLHelpers

というものを見つけました。

こんな感じで`UIImage`のリサイズ 〜 `CVPixelBuffer`への変換を簡単に書けるようになります。

```swift
guard let pixelBuffer = image.pixelBuffer(width: 299, height: 299) else {fatalError()}
```

ちなみにこのヘルパーライブラリのリサイズ機能、いまのところは単純にスケールされてしまうので、アスペクトを維持したい場合には多少自分でコードを書く必要があります。

一方Visionの`VNCoreMLRequest`を使う場合は`imageCropAndScaleOption`という便利オプションがあります。

### `BGR` や `RGB` といった `color_layout` の処理

モデルによってはRGBではなくBGR画像を入力として想定しているものもあります。

そういった色チャンネルの並び順はCore MLモデルフォーマットでは `color_layout` として指定できるようになっています。

https://apple.github.io/coremltools/source/coremltools.converters.mil.input_types.html#imagetype

Vision の `VNImageRequestHandler` を通してCore MLモデルに画像を渡す場合は、この `color_layout` を踏まえてよしなにやってくれます [^1]。

[^1]: ただし、画像が `CVPixelBuffer` のように[ピクセルフォーマットの情報](https://developer.apple.com/documentation/corevideo/enum_(unnamed)-jw68)を持つ場合。UIImageおよびCGImageは、[アルファチャンネルの位置やエンディアンの情報は保持できる](https://developer.apple.com/documentation/coregraphics/cgbitmapinfo)ものの、BGRかRGBかの情報は型自体には保持していないので、明示的に入れ替えたりする必要がありそう。要確認。

## 推論処理の実行

```swift:Visionあり
try! handler.perform([request])
```

```swift:Visionなし
let output = try! model.prediction(input: input)
```

こんな感じで、これだけ見るとあまり変わらなそうですが、Visionを使う場合は、

- `VNCoreMLRequest`には`preferBackgroundProcessing`でバックグラウンド処理を指定することもできる
- `perform()`は複数の`VNCoreMLRequest`を引数に取ることができる

等々、並列処理を楽にさせてくれる機能があります。

## 結果の取得

```swift:Visionあり
guard let results = request.results as? [VNClassificationObservation] else {return}

results.forEach({ (result) in
    print("\(result.identifier) \(result.confidence * 100)")
})
```

```swift:Visionなし
print(output.classLabel)      // 1位候補のラベル
print(output.classLabelProbs) // 各クラスのラベルと確率
```

Visionなしの方のプロパティは、モデルから自動生成される出力型`Inceptionv3Output`に生えているものなので、モデル依存です。

さらに、`classLabelProbs`の方は、結果がソートされていません。


## Visionを使うメリット

というわけで、Core MLモデルを使った推論を行う際に、Core ML単体での実装と、Visionを併用する場合の実装の比較を行ってみました。

ここから見えてきたVisionを使うことのメリットとしては、

- モデルに（比較的）依存しない処理が書ける
- 入力画像のサイズやチャンネル数やチャンネル並び順（BGR等）を気にしなくてよい
- 並列処理とかが簡単にできる

といったあたりでしょうか。

## 追記

`MLModel`オブジェクトを利用すれば、**Visionを使わない場合でもモデルに依存せず処理をかけます**。というわけで上述のいくつかの部分は修正が必要なので、時間ができたときにアップデートします。