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一本でいきましょう。
segmentPerson
はsegmentPersonParts
があれば実現できます。
サンプルはすぐそこにある。
だいぶ読みました。
これ結構参考になる実装ですのでみてみるといいと思います。
バーチャル背景の実装とかは、上記の公式の関数の中身をよめば、実装できると思います。
オリジナル実用例
おそらくは思いつくとは思いますが、顔の右向き左向きと顔の矩形座標を取るロジックです。
※上記のゆっくり顔の描画位置の設定に使ってます。
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が限界ぐらいでした。