BlazePoseでBodyPixによる人物抽出処理を置き換えよう

N.Mです。[以前の記事](https://qiita.com/HexagramNM/items/b967dfd3733ecee1a084)で、バーチャル背景用に人物映像のみを抽出するのにBodyPixを使用しておりました。しかし、古くなったのかBodyPixがDeprecatedとなっておりました。今回、BodyPixを後継のBlazePoseに代替するのに必要であったこと、代替したことによる効果を記事にしました。（[以前の記事](https://qiita.com/HexagramNM/items/b967dfd3733ecee1a084)の続きになります。まだご覧になっていない方は、先に前の記事をご覧ください。）

# 事の発端
[バーチャル背景](https://hexagramnm.coresv.com/NM_MicDisplay_Web/index.html)を作成し、実際に使用しているうちに、人物映像部分の更新が遅くなることに気が付きました。その際にBodyPixについて調べていたら、BodyPixがDeprecatedになっていることに気付きました。2022年8月ごろのことです。

今後使用できなくなる可能性も考えられたのと、新しいバージョンの学習モデルにして処理速度など改善されているかもしれないと考え、更新を行うことにしました。ちなみに、BlazePoseには2つのバージョンがあるそうですが、[ドキュメント](https://github.com/tensorflow/tfjs-models/tree/master/pose-detection)を見る限りで画像のセグメント情報を出力できるMediaPipe版のBlazePoseを使用しております。

# この記事で触れる内容
- **ビデオ映像からBlazePoseで背景をマスクする手段**
以前BodyPixで行っていた、人物のみを抽出する処理を、BlazePoseで行うにはどう変更すれば良いかを取り上げております。

- **BlazePoseで代替したことによる効果**
BlazePoseに代替したことで、処理速度が改善されるなどの効果があったので紹介します。

# この記事で触れない内容
- **BlazePoseのその他の使用方法**
BlazePoseはpose-detectionのパッケージにあるように、元々は人物映像からその人物のポーズを推定する学習モデルです。しかし、今回の目的ではポーズ推定処理は不要であるため、ポーズ推定の話は取り上げません。

- **背景マスクした画像をテクスチャとしてWebGL側に送る処理**
こちらは[以前の記事](https://qiita.com/HexagramNM/items/b967dfd3733ecee1a084)で取り上げているので割愛します。

# サンプルコード
[GitHubのリポジトリ](https://github.com/HexagramNM/BlazePoseWebGL_sample)に、人物のみのビデオ映像を奥行き方向に傾けて表示するサンプルを公開しました。（前のBodyPixのサンプルリポジトリから修正したものになります。）こちらのサンプルを参照していただきながら、この記事をご覧ください。大きな変更部分はblazePosePart.jsとindex.htmlになるので、忙しい方はそれらをご覧ください。（main.jsにも変更はありますが、関数名変更程度でやっていることは同じです。）BlazePoseの解析とWebGLの描画、それぞれにかかった1フレームあたりの平均時間も表示するようにしております。

# ビデオ映像からBlazePoseで背景をマスクする手段
BodyPixからBlazePoseに切り替えるにあたって、以下の変更が必要になります。

- index.htmlで読み込むスクリプトの変更
- BlazePoseの学習モデル初期化処理の変更
- BlazePoseの結果からセグメンテーション情報（背景か人物かの情報）を取り出す処理の追加

## index.htmlで読み込むスクリプトの変更
BlazePoseのドキュメントにもありますが、index.htmlのヘッダ部分でBlazePoseを使用するためのスクリプトをロードする必要があります。以下のようにタグを追加してください。BodyPixの時も似たような形でロードするので、BodyPixから切り替える場合はロードするスクリプトを変更することになります。ちなみに、MediaPipe版でないBlazePoseを使用する場合は、ここの時点でロードするスクリプトが違うようです。

```html:
<!-- Require the peer dependencies of pose-detection. -->
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/pose"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-core"></script>

<!-- You must explicitly require a TF.js backend if you're not using the TF.js union bundle. -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-backend-webgl"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/pose-detection"></script>

<!-- 独自のスクリプトはこの後に記述する。 -->

```

## BlazePoseの学習モデル初期化処理の変更
BodyPixの時も学習モデルの初期化処理がありましたが、BlazePoseも似たような初期化処理があります。メソッド名など異なるので、その部分を変更する必要があります。`detectorConfig`で指定する設定について詳細が気になる方は[ここのドキュメント](https://github.com/tensorflow/tfjs-models/tree/master/pose-detection/src/blazepose_mediapipe#usage)をご覧ください。

```javascript:blazePosePart.js

/*中略*/

var blazePoseNet = null;

/*中略*/

async function BlazePosePart_init(videoStream) {

    /*中略*/

    //runtime, enableSegmentation, solutionPathについてはこの通りに指定。
    //modelTypeについては、最も軽量な"lite", 精度重視で最も大きい"heavy", 中間の"full"があるため、その中から選択。
    //自分の場合はノートPCで動かすことを前提としているため、軽量な"lite"を選択しました。

    const detectorConfig = {
        runtime: "mediapipe",
        enableSegmentation: true,
        modelType:"lite",
        solutionPath: "https://cdn.jsdelivr.net/npm/@mediapipe/pose"
    };
    blazePoseNet = await poseDetection.createDetector(poseDetection.SupportedModels.BlazePose, detectorConfig);

    /*中略*/

}

/*中略*/

```

モデル初期化後、以下のようにcanvasなどの画像要素を学習モデルに入力することで、セグメンテーション情報などを含む結果を得ることができます。

```javascript:

//intermediateCanvasはカメラからの画像を表示したキャンバス
//estimatePosesは非同期処理であるため、結果を得るにはawaitをする必要がある。
var processedSegmentResult = await blazePoseNet.estimatePoses(intermediateCanvas);

```

## BlazePoseの結果からセグメンテーション情報（背景か人物かの情報）を取り出す処理の追加
`blazePoseNet.estimatePoses`メソッドで得られる結果は、ドキュメントを見る限りだと、検出された人物の数だけ要素をもつリストとなっているようです。そのため、以下のことに注意する必要があります。

- 注意点1: 人物が検出されない場合は要素数0のリストとして結果が出力されること
- 注意点2: セグメンテーション情報を画像データ（マスク画像）として出力するメソッドを使用する必要があり、このメソッドが非同期処理であること
- 注意点3: マスク画像はRGBA画像となっており、アルファ成分に人物のピクセルである確率として256段階の数値が入っていること。（BodyPixの時は人物か背景可の2値であった。）

```javascript:blazePosePart.js

/*中略*/

//i_ctxInputImageにはカメラ画像を映したキャンバスの画像データを、
//i_processedSegmentResultはblazePoseNet.estimatePosesメソッドで得られる結果を入力する。
async function BlazePosePart_drawTextureCanvas(i_ctxInputImage, i_processedSegmentResult) {
    var inputBytes = i_ctxInputImage.data;

    var outputPixIdx = 0;
    var resultColor = [0.0, 0.0, 0.0, 0.0]
    var resultColorUint32 = 0;

    if (i_processedSegmentResult.length > 0) { //注意点1
        var maskImage = await i_processedSegmentResult[0].segmentation.mask.toImageData(); //注意点2

        //ここで背景を除去した画像をテクスチャ用のキャンバスに作成。
        //mapTextureYToCanvasなどが何かについては、以前のQiita記事をご覧ください。
        for (var y = 0; y < virtualBackTextureSize; y++) {
            var yInputIdx = mapTextureYToCanvas[y] * intermediateCanvasSize.width;
            for (var x = 0; x < virtualBackTextureSize; x++) {
                var inputPixIdx = yInputIdx + mapTextureXToCanvas[x];
                var byteBaseInputIdx = 4 * inputPixIdx;
                var byteBaseOutputIdx = 4 * outputPixIdx;

                if (maskImage.data[inputPixIdx * 4 + 3] == 0) {
                    resultColorUint32 = 0x00;
                }
                else {
                    for (var colorIdx = 0; colorIdx < 3; colorIdx++) {
                        resultColor[colorIdx] = inputBytes[byteBaseInputIdx + colorIdx]
                    }
                    resultColor[3] = maskImage.data[inputPixIdx * 4 + 3]; //注意点3
                    resultColorUint32 = (resultColor[0] | (resultColor[1] << 8) | (resultColor[2] << 16) | (resultColor[3] << 24));
                }
                virtualBackOutputImageData[outputPixIdx] = resultColorUint32;
                outputPixIdx++;
            }
        }
    }
    else {
        for (var y = 0; y < virtualBackTextureSize; y++) {
            for (var x = 0; x < virtualBackTextureSize; x++) {
                virtualBackOutputImageData[outputPixIdx] = 0x00;
                outputPixIdx++;
            }
        }
    }
    virtualBackOutputImage.data.set(virtualBackOutputImageBuf8);
    virtualBackTextureCanvasCtx.putImageData(virtualBackOutputImage, 0, 0);
}

/*中略*/

```

## その他
今まで、WebGL側の処理もBlazePose(BodyPix)側の処理も`setTimeout`関数で繰り返しのタイマー処理をしておりましたが、この場合BlazePoseの処理を行っている間、WebGL側の更新処理が行われず、結果として全体の映像がカクつく状態になってしまいました。
WebGL側の画面更新処理を`requestAnimationFrame`関数に投げることで、BlazePoseの処理をしている間も画面更新ができ、映像はスムーズに動く状態にできました。


# BlazePoseで代替したことによる効果
BlazePoseに代替したことで、以下の2つの効果がありました。
- 処理速度の改善
- 抽出された人物映像の境界のぼかし

## 処理速度の改善
以下のスペックのノートPC環境で試したところ、BlazePose側の処理で1フレーム約45msecの処理時間がかかっておりました。BodyPixで処理した際は1フレーム約50msecかかっていたため、わずかではありますが処理速度が改善されました。そしてなにより、この処理速度がBodyPixの時よりも安定しており、[実装したバーチャル背景](https://hexagramnm.coresv.com/NM_MicDisplay_Web/index.html)では、人物映像がカクつくことが非常に少なくなりました。

CPU: Intel Core i7-6567U @ 3.30GHz
RAM: 16.0GB
GPU: Intel Iris Graphics 550

![processingTime_blazePose.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152791/5dc8f7e9-6fce-4cb7-78be-862dfcc5360c.png)

## 抽出された人物映像の境界のぼかし
BodyPixでは、画像のピクセルが人物のものか、背景のものかという2値的なセグメンテーションでしたが、BlazePoseでは人物のピクセルである確率という形で256段階で値が出るため、マスクの境界がぼかされる結果となります。BodyPixのときは境界をぼかすためにシェーダーなどで別に処理しておりましたが、その処理が不要になるため、負荷軽減につながりました。

# まとめ
BodyPixでも人物映像の抽出はできましたが、後継のMediaPipe版BlazePoseは負荷も軽く、同等以上のことができるようでした。BodyPixがDeprecatedとなった今、BodyPixを使用しているならば、BlazePoseに切り替えることで、さらなる恩恵が受けられると思いました。