【機械学習】body-pixをちょっとわかって遊ぶ！

# 0. はじめに
最近ポエム的な内容しかかいてないので、技術記事をあげようと思ったので、それっぽいのを…
最近流行りですよね。機械学習…

ちょっと3年ぐらい遅いかもしれませんが、研究してる暇なかったので、研究してみました。

まぁ、完全に理解した～何もわからないの中間ぐらいかと思います。
（題名はちょっとわかるとか言ってますけど…）
で、**これはbody-pixの中身を解説したものではなくて、body-pixの利用を目的とした記事**です。

中身を理解したい場合には、機械学習の数学の本読むことをおすすめします。

# 1. body-pixでこんなもんできた。

どんなことできるかわからないだろうから、さっそくキャッチーな結果から！！

https://twitter.com/djsaxia/status/1395537159797370884

どうです？夢ひろがりんぐしません？

**フォローいいねRT自由にしてもらっていい**です。
**次の記事のモチベーションにつながります**。

画像は全部借り物です。
[きつね（仮）](https://twitter.com/yukkurisozai)さんお借りしてます

# 2. body-pixってなんぞ？

https://github.com/tensorflow/tfjs-models/tree/master/body-pix

簡単にいうと、TensorFlowで、人体をパーツごとに検知するライブラリです。
これに**画像を通すと、よしなに人の体のパーツごとに分解してくれます**。

コードレベルの詳しい話は、あとで…

# 3.body-pixを使ってみよう

## 3-1. とりま、ライブラリのインポート
```
<!-- Load TensorFlow.js -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.2"></script>
<!-- Load BodyPix -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/body-pix@2.0"></script>
```
たぶん、`https://unpkg.com/`にもあるとは思うので、どっち使うかはお好みで。
公式は上記の書き方だったので、そのまま拝借です。

## 3-2. body-pixのロードをしましょう
```
const net = await bodyPix.load();
```

超簡単！
ちなみに、この関数の定義はこうです。

```
export async function load(config: ModelConfig = MOBILENET_V1_CONFIG): Promise<BodyPix>
```

Promiseなので、awaitできない場合には、`then`でつなぐこともできます。
デフォルトの設定は`MOBILENET_V1_CONFIG`で、こやつなんやのってことですが…

```
const MOBILENET_V1_CONFIG = {
  architecture: 'MobileNetV1',
  outputStride: 16,
  quantBytes: 4,
  multiplier: 0.75,
} as ModelConfig;
```

公式が用意してくれてる、モバイル用の設定ですね。
実はコールドリーディングしてると、モバイル以外の設定もコメントで用意してくれてます。

```
// ```
// const RESNET_CONFIG = {
//   architecture: 'ResNet50',
//   outputStride: 32,
//   quantBytes: 4,
// } as ModelConfig;
// ```
```

わりとざっくりした、項目ごとの内容

|項目|設定値|簡易説明|
|----|-----------|-------|
|architecture|MobileNetV1 or ResNet50|利用する分にはモバイル用 or 汎用程度の認識でいい|
|outputStride|8, 16, 32|数が大きくなると精度がよくなる。ただし、ResNet50は32以外サポートしてないので、実質32固定 |
|quantBytes|1, 2, 4|モデルの量子化の値：数が大きいと精度が良くなるが速度が落ちるといった数値|
|multiplier|1.01, 1.0, 0.75, 0.50|MobileNetのみで有効な値 数が大きいと精度が良くなるが速度が落ちるといった数値|
|modelUrl|（日本では不要）|モデルのURLを直入れするオプション/GCPがつながらない国用のオプション|

機械学習の用語を取り除いて、数値を書き換えると何が起こるのかをベースに説明しました。
詳しくは[こちら](https://github.com/tensorflow/tfjs-models/blob/master/body-pix/src/body_pix_model.ts#L36)

## 3-3. body-pixの醍醐味！人体を検知しよう！

使う関数はこれらのどれかになります。

人体だけ検出したい場合

- segmentPerson
- segmentMultiPerson

人体のパーツごとに検出したい場合

- segmentPersonParts
- segmentMultiPersonParts

Multiがついてるかついていないかの差は**複数人写ったときに個々人を判別したいかどうか？**です。
判別しなくていいのであれば、Multiなしの関数でも複数人引っかかります。
※ ひっかからないと勘違いしがち…
※ ひっかかって、別れて判断されるか、同じ1枚の中で判断されるかの差です。
※ ちなみにMultiは重い…当然だけど…

使い方はシンプル

```
const segmentation = await net.segmentPersonParts(image, {
  flipHorizontal: false,
  internalResolution: 'medium',
  segmentationThreshold: 0.7
});
```

ぶっちゃけ、デフォルトの設定値でよければ、以下のコードで大丈夫
設定値が気になる方は[こちら](https://github.com/tensorflow/tfjs-models/tree/master/body-pix#params-in-segmentperson)

```
const segmentation = await net.segmentPersonParts(image);
```

imageとしてますが、` ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement`これらが対応してます。
Videoタグそのままぶっこめるのはかなり楽ちん！！

これでよしなに、**人体を検知してくれます**

## 3-4. 検知結果を知ろう。
### segmentPerson
```
{
  width: 640,
  height: 480,
  data: Uint8Array(307200) [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, …],
  allPoses: [{"score": 0.4, "keypoints": […]}, …]
}
```
オブジェクトが返ってきます。
公式がちょっと不親切だったので補足すると、
width / height はインプットのサイズが返ってきます。
dataなんですが、【大事】**0：人が検知できなかった　1: 人が検知できた**です。
allPosesについては、おそらくは[PoseNet](https://github.com/tensorflow/tfjs-models/tree/master/posenet#returns-1)の回答をそのままいれてます。

### segmentPersonParts
```
{
  width: 680,
  height: 480,
  data: Int32Array(307200) [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 1, 0, 0, …],
  allPoses: [{"score": 0.4, "keypoints": […]}, …]
}
```
まぁパーツごとにわかれてるか人体だけなのかだけの違いなので、data以外の説明は割愛します。
で、公式のdataのサンプルがよくなくてですね！！！

ちゃんと読まないとだめなんですよ＞[このあたり](https://github.com/tensorflow/tfjs-models/tree/master/body-pix#the-body-parts)

`The PartSegmentation object contains a width, height, Pose and an Int32Array with a part id from 0-24 for the pixels that are part of a corresponding body part, and -1 otherwise.`

ここ！！！大事！

**体のパーツと判断されたら、0から24のIDが入っていて、それ以外の場合は-1が入ります。**

なんだこの上のサンプルは…って感じ。

### segmentMultiPerson / segmentMultiPersonParts

これらは、上記のオブジェクトが人ごとに配列として入ってる。

## 3-5. 検知結果の活用をしてみよう！

### [body-pixが提供してくれてる関数](https://github.com/tensorflow/tfjs-models/tree/master/body-pix#output-visualization-utility-functions)

ホントは、

- bodyPix.toMask
- bodyPix.toColoredPartMask
- bodyPix.drawMask
- bodyPix.drawPixelatedMask

とかあるのですが、実用アプリつくるってなるとうーんって感じだったので、一旦割愛しました。

#### [bodyPix.drawBokehEffect](https://github.com/tensorflow/tfjs-models/tree/master/body-pix#bodypixdrawbokeheffect)

簡単に、背景をボカしてくれます。
Inputは**segmentPerson**の結果です。※Partsのほうじゃないからね！！！

Partsの結果をいれても、反映されません。

#### [bodyPix.blurBodyPart](https://github.com/tensorflow/tfjs-models/tree/master/body-pix#bodypixblurbodypart)

この関数を、体の一部をボカしてくれます。
Inputは**segmentPersonParts**の結果です。

で、これ裏技があります。
**背景ぼかしたいけど、Partsごとの分解がアプリで必要なんだよねぇ**

はい、コードリーディングして気づいてしまいました。
ぼかすパーツに**-1**を指定しましょう。

**体のパーツと判断されたら、0から24のIDが入っていて、それ以外の場合は-1が入ります。**

これの応用ですね！

### オリジナルフィルタをためには…（上級者編）

まぁ、上で書いたような関数をつかってても、僕がつくったような、ゆっくりフェイスカメラはできんわけですよ。
現在は`bodyPix.toMask`を採用していますが、正直リリースまでには不採用にします。
理由は次のとおりです。

#### ビデオを扱う心構え

[toMask](https://github.com/tensorflow/tfjs-models/blob/71f57d306c6e3ac4962b9819d17dc7761de62cf8/body-pix/src/output_rendering_util.ts#L166)のコードをみてもらうとわかるんですが、すごいループ回るんですよね。

toMaskを2回つかったりすると、かなりの回数まわることになるというか…
で、これなにを回してるかって言うと、segmentPersonとかsegmentPersonPartsの結果のdataですね。

ピクセル数が多くなると、わかりますよね？
1280×720でも、921600回転するんです。

**何回も何回もまわしてたら、30FPSすら確保難しくなります。**
**Imageの処理で速度を気にしないのであれば、採用してもいいかと思います。**

簡単に試したいだけなら、上記の関数を使うといいとは思いますが、カスタムフィルタはそうもいきません。

```
// 名前はちょっと考える必要はあるけど、パースして、ある程度マスクつくったり、計算する関数
function parseSegmentationWith(seg) {
    for(const [index, d] of seg.data.entries()){
        // ここで、全てのマスクなどを生成してしまう。
        // また、座標系など必要な位置もここで実装してしまう。
    }
}
```

あと、もう自明ですが、両方使いたいからといって**segmentPersonとかsegmentPersonPartsを両方使ってはいけない**です。
Partsわけが必要なら、segmentPersonParts一本でいきましょう。
`segmentPerson`は`segmentPersonParts`があれば実現できます。

#### サンプルはすぐそこにある。

[bodyPix.blurBodyPart](https://github.com/tensorflow/tfjs-models/blob/71f57d306c6e3ac4962b9819d17dc7761de62cf8/body-pix/src/output_rendering_util.ts#L586)

だいぶ読みました。
これ結構参考になる実装ですのでみてみるといいと思います。

**バーチャル背景の実装とかは、上記の公式の関数の中身をよめば、実装できる**と思います。

#### オリジナル実用例

おそらくは思いつくとは思いますが、顔の右向き左向きと顔の矩形座標を取るロジックです。
※上記のゆっくり顔の描画位置の設定に使ってます。

Math.maxをつかわない理由は、ループの回数をさらに削るためですね。

```
// minX minY 顔の左上の矩形点
// maxX maxY 顔の右下の矩形点
// lcount > rcount となれば左を向いている。逆なら右向き
(partsSegmentation) => {
            let lcount = 0;
            let rcount = 0;
            let minX = partsSegmentation.width;
            let minY = partsSegmentation.height;
            let maxX = -1;
            let maxY = -1;
            for (const [i, d] of partsSegmentation.data.entries()) {
                if (d === 0) {
                    lcount++;
                    const x = i % partsSegmentation.width;
                    const y = i / partsSegmentation.width | 0;
                    minX = x > minX ? minX : x;
                    minY = y > minY ? minY : y;
                    maxX = x > maxX ? x : maxX;
                    maxY = y > maxY ? y : maxY;
                }
                if (d === 1) {
                    rcount++;
                    const x = i % partsSegmentation.width;
                    const y = i / partsSegmentation.width | 0;
                    minX = x > minX ? minX : x;
                    minY = y > minY ? minY : y;
                    maxX = x > maxX ? x : maxX;
                    maxY = y > maxY ? y : maxY;
                }
            }　　　　　　
       }
```

はい、こんな感じで顔の矩形とれたりします。
あとは、**Canvasの仕様を説明することになってしまう**ので、ここいらで終わりにしておきます。

# 4. サンプルは？

少しまってほしいのですが、作ります。
色々、ネタはあるので…

# 5. ホントは力尽きたでしょ？

あ、はいごめんなさい。
この文章量は流石に、疲れました。

あれ、これ公式の説明してるだけじゃんとか、結構葛藤ありました…。
上級者向けのカスタムフィルタなんて、OSSなんだから読めっていう結論半分あったりとか…ごめんなさい。

**気になった方は質問とかコメントもらえると嬉しいです。**

# 99.【コラム】vs OpneCV 
過去にOpenCVのカスケード分度器つかって、顔認識をやったことあります。
OpenCVの技術的記事については、以下の記事が参考になります。

https://qiita.com/FukuharaYohei/items/ec6dce7cc5ea21a51a82

OpenCVもbody-pixも両方JavaScriptで検証したのですが…
自分のやりたいことに対しては、完全にbody-pixに軍配が上がりました。

若干、公開されているカスケード分度器の問題もあるかなとは思いましたが...
利用したカスケード分度器は[公式の分度器](https://github.com/opencv/opencv/tree/master/data/haarcascades)です。

## OpenCVを使うPros/Cons

まぁ、結論Consが多かったため、body-pix使うことになったのですが…
若干OpenCV力が弱いだけ感もあるので、もし教えてくれるツワモノは教えて貰えればと思います。

**GPUプログラミングは未学習なもの**で…

### Pros
- 画像編集が容易（OpenCVになれていれば）
- OpenCVの関数が使える
    - JavaScriptで扱う場合、APIがかなり異なるので癖になれる必要あり

### Cons
- マスクしてると反応しない
    - おそらくは、鼻と目と耳とかで形状認識していて鼻が隠れるため
- OpenCVの形式で扱う必要がある
    - Mat形式ってやつ。
    - Mat形式からCanvasに書き込むので二度手間
- Runtime読み込みイベントが特殊？
    - `body onload=""`みたいな単純なものではなかったです。
- 案外実行が遅い
    - 自分の書き方だけではないと信じたいけど、そんなに早くないです。
    - 1280 x 720で30FPSが限界ぐらいでした。