1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

0. はじめに

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

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

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

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

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

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

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

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

画像は全部借り物です。
きつね(仮)さんお借りしてます

2. 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がつながらない国用のオプション

機械学習の用語を取り除いて、数値を書き換えると何が起こるのかをベースに説明しました。
詳しくはこちら

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
});

ぶっちゃけ、デフォルトの設定値でよければ、以下のコードで大丈夫
設定値が気になる方はこちら

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の回答をそのままいれてます。

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のサンプルがよくなくてですね!!!

ちゃんと読まないとだめなんですよ>このあたり

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が提供してくれてる関数

ホントは、

  • bodyPix.toMask
  • bodyPix.toColoredPartMask
  • bodyPix.drawMask
  • bodyPix.drawPixelatedMask

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

bodyPix.drawBokehEffect

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

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

bodyPix.blurBodyPart

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

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

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

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

これの応用ですね!

オリジナルフィルタをためには…(上級者編)

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

ビデオを扱う心構え

toMaskのコードをみてもらうとわかるんですが、すごいループ回るんですよね。

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一本でいきましょう。
segmentPersonsegmentPersonPartsがあれば実現できます。

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

bodyPix.blurBodyPart

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

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

オリジナル実用例

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

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の技術的記事については、以下の記事が参考になります。

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

若干、公開されているカスケード分度器の問題もあるかなとは思いましたが...
利用したカスケード分度器は公式の分度器です。

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が限界ぐらいでした。
1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?