iOSで動く姿勢推定モデルを色々と試す

iOSのCore MLモデルは、Appleが公式で色々と配布していますが、[6種類のうちなんと5種類が物体認識](https://qiita.com/shu223/items/53f0dd423a9d5271bc2a)（imagenetの1000クラス分類）。しかし、GitHub等をみると、多種多様なモデルが公開されてきています。

今回は**姿勢推定（Pose Estimation）のCore MLモデル**で公開されているものをいくつかピックアップし、それらがどれぐらい使えるのか、試してみました。

## PoseNet-CoreML

まずはこちら。

https://github.com/infocom-tpo/PoseNet-CoreML

### サンプルをそのまま試す

こちらはサンプルが同梱されていて、すぐにビルドして試せました。

結果はこんな感じです。

<img src="https://qiita-image-store.s3.amazonaws.com/0/3180/e73d007f-47d7-c644-c742-92607bdc776f.jpeg" width="400">

（実際にiPhone XS上で実行してスクリーンショットを撮った結果を正方形にクロップして載せています）

後ろの観客の姿勢推定はできているようですが、肝心の前方のプレイヤーの姿勢が取れていません。。

他にも画像が同梱されていたので、変えてみました。

<img src="https://qiita-image-store.s3.amazonaws.com/0/3180/9f302aab-f981-fe2d-debc-ebf1d908ee9c.jpeg" width="400">

あれ・・・片足や腕など、諸々検出できておらず、正直精度はイマイチです。


### モデルを入れ替えてみる

プロジェクトを見てみると、mlmodelが複数入っていました。

<img width="243" alt="Screen Shot 2019-02-05 at 16.47.12.png" src="https://qiita-image-store.s3.amazonaws.com/0/3180/df030e6c-e41c-ab35-0732-ab96347e5c73.png">

ソースコードを見てみると、使っているのは`posenet513_v1_075.mlmodel`のようです。

```swift
let model = posenet513_v1_075()
```

"_075"というのは何か精度よりも軽量化あるいは高速化に振ったような感じを受ける命名なので、他のモデルを試してみます。実際、モデルサイズは他のものより小さいようでした。

<img width="565" alt="Screen Shot 2019-02-05 at 16.57.36.png" src="https://qiita-image-store.s3.amazonaws.com/0/3180/00cd1949-aa25-78f6-936d-0e04e263ece1.png">

`posenet513.mlmodel`というモデルを見ると、

<img width="589" alt="Screen Shot 2019-02-05 at 16.57.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/3180/6f67d895-1d83-8090-30d2-611499489672.png">


**入力形式は同じなので、そのまま実行できそう**です。こちらに入れ替えて試してみましょう。

```swift
let model = posenet513()
```

<img src="https://qiita-image-store.s3.amazonaws.com/0/3180/d81d5773-d4f2-c2c0-186a-8cbd95c235b6.jpeg" width="400">

なんと！プレイヤーの姿勢もばっちりとれるようになりました。

重量挙げの方も見てみましょう。

<img src="https://qiita-image-store.s3.amazonaws.com/0/3180/f06b45bd-5a93-c466-92e7-d36826d4abbb.jpeg" width="400">

こちらは腕や足はしっかりとれるようになったものの、胴がとれなくなりました。まだまだ満足の行く精度とはいえないようです。

### 実装

入力としては画像をCVPixelBuffer型で受け取ります。

```swift
func runCoreML(_ img: CVPixelBuffer) -> [Pose]{        
    let result = try? model.prediction(image__0: img)
```

ここは普通ですが、出力側の扱いがかなり従来の物体認識等のモデルと違っていました。

```swift
let tensors = result?.featureNames.reduce(into: [String: Tensor]()) {
    $0[$1] = getTensor(
        result?.featureValue(for: $1)?.multiArrayValue)
}
let sum = tensors!["heatmap__0"]!.reduce(0, +) / (17 * 33 * 33)
print(sum)

let poses = posenet.decodeMultiplePoses(
    scores: tensors!["heatmap__0"]!,
    offsets: tensors!["offset_2__0"]!,
    displacementsFwd: tensors!["displacement_fwd_2__0"]!,
    displacementsBwd: tensors!["displacement_bwd_2__0"]!,
    outputStride: 16, maxPoseDetections: 15,
    scoreThreshold: 0.5,nmsRadius: 20)
```

最終的に`Pose`という構造体を結果として生成しているのですが、

```swift
struct Pose {
    var keypoints: [Keypoint]
    var score: Float
}
```

上に載せたコードの通り、Core MLモデルからの出力をアプリ側でゴニョゴニョとした上でこの`Pose`構造体にしています。これらの意味は理解できてないので使いこなすにはもっとモデル自体への理解を深める必要がありそうです。精度もこのへんの扱いが影響してそうです。

・・・とここまで書いてREADMEを見て気付いたのですが、どうやら作者の方は日本人で、御本人が書かれた記事を見つけてしまいました。

- [PoseNetをマルチプラットフォームで実装してみた - Qiita](https://qiita.com/otmb/items/561a62d3413295cc744e)

後ほど拝読します。


## PoseEstimationForMobile

https://github.com/edvardHua/PoseEstimationForMobile

こちらはAndroidしかデモが同梱されておらず、iOSのデモは別の方が別リポジトリで公開しているようです。

https://github.com/tucan9389/PoseEstimation-CoreML

こちらのリポジトリにはmlmodelが入っていません。「PoseEstimationForMobile」リポジトリのreleaseフォルダに生成済みのmlmodelファイルが入っているので、ここからサンプル配下のフォルダにコピーしてきます。サンプルプロジェクトには

- model_hourglass.mlmodel
- model_cpm.mlmodel

の２つのモデルへの参照が追加されていますが、コードを見ると`model_hourglass`の方はテストでしか使われていないので、いったんプロジェクトから削除してしまってもサンプルは動作します。

model_cpm.mlmodelの入力・出力形式は次のようになっていました。

<img width="422" alt="Screen Shot 2019-02-08 at 12.18.24.png" src="https://qiita-image-store.s3.amazonaws.com/0/3180/2dd8b03e-8f3d-9449-189a-9c922336638b.png">



サンプルを動かしてみると、こちらはリアルタイムカメラ式で、比較のため「PoseNet-CoreML」のサンプルに入っていたテスト用画像をモニタに移して姿勢推定させてみました。

<img src="https://qiita-image-store.s3.amazonaws.com/0/3180/fbb7987f-dc21-0440-dc02-c6dd484a3c56.jpeg" width="400">

<img src="https://qiita-image-store.s3.amazonaws.com/0/3180/e8505056-8dd3-73ee-9ec7-cd9cb9955abd.jpeg" width="400">

かなり良さそうです。

せっかくなのでリアルタイムで動いている様子も録りました。まわりに人がいないので対象がGoogle画像検索結果ですが。。

![Untitled.2019-02-08 12_53_36.gif](https://qiita-image-store.s3.amazonaws.com/0/3180/0f581947-844e-33ee-b481-7f78ad3c5bd8.gif)


### 実装

入力は普通にカメラから得たCVPixelBufferを渡しています。

重要なのは出力のMultiArrayオブジェクトをどう処理するかですが、

```swift:JointViewController.swift
if let observations = request.results as? [VNCoreMLFeatureValueObservation],
    let heatmap = observations.first?.featureValue.multiArrayValue {
    
    // convert heatmap to [keypoint]
    let n_kpoints = heatmap.convertHeatmapToBodyPoint()
    
    DispatchQueue.main.sync {
        // draw line
        self.jointView.bodyPoints = n_kpoints
        
        // show key points description
        self.showKeypointsDescription(with: n_kpoints)
        
        // end of measure
        self.👨‍🔧.🎬🤚()
    }
}
```

こんな感じになっていて、MultiArrayオブジェクトから取得した諸々をゴニョゴニョして人間の姿勢の各点を表す`BodyPoint`の配列を返しているようです。`MultiArray`から`BodyPoint`の配列を得る処理`convertHeatmapToBodyPoint`は`MLMultiArray+PoseEstimation.swift`にあります。