#17 BodyPixを使って肉体を3Dアバターに置き替えるWebアプリケーションを作る
YATTA!駆動開発の17回、BodyPixを使って肉体をリアルタイムに3Dアバターに置き替えるWebアプリケーションを作る。
https://youtu.be/uXMZTTAdiqQ
基本情報
前回:#16 VRoidのTシャツ用テクスチャ作成支援ツールを作ってみる
次回:#18 wsl2でReactアプリの開発する
主な仕様ライブラリ
BodyPix
three.js
pixiv/three-vrm
BodyPix
BodyPixはTensorFlow.js によるリアルタイム人体セグメンテーションを行える機械学習モデルです。
BodyPix は画像内のピクセルを、1) 人と表すピクセルと 2) 背景を表すピクセルの 2 種類に分類できます。
出典:Google Developers Japan: BodyPix の概要: ブラウザと TensorFlow.js によるリアルタイム人セグメンテーション(2020/9/22)
導入
リポジトリ
tfjs-models/body-pix at master · tensorflow/tfjs-models
npm install @tensorflow-models/body-pix
npm install @tensorflow/tfjs-core@1.3.1
code:ts
const bodyPix = require('@tensorflow-models/body-pix');
カメラ映像から人間の体を消す
BodyPixによって人体が映っているピクセルが取得できます。
code:ts
const {
data: map,
allPoses: allPoses,
} = await this._NN.segmentPerson(src, {
segmentationThreshold: 0.2,
internalResolution: 'high',
});
srcには入力となる映像を指定します.
ImageData
HTMLImageElement
HTMLCanvasElement
HTMLVideoElement
認識結果の返り値
data:画像のピクセルが順に入っている一次元配列
1が人体部分、0が背景部分
左から右
上から下
allPoses
部位ごとの画像中の二次元座標
認識結果からマスク処理を行います。
背景部分はピクセルを更新し、人体部分は前フレームを参照する。
画像の欠損補完とかやってみたかったですが、これ以上の処理をリアルタイムにやるのは厳しそうです
canvasのImageData.dataはピクセル順にRGBAが並んでいます。
code:ts
for (let i = 0; i < map.length; i++) {
const current = [
imgDatai * 4,
imgDatai * 4 + 1,
imgDatai * 4 + 2,
imgDatai * 4 + 3
];
const old = [
cacheDatai * 4,
cacheDatai * 4 + 1,
cacheDatai * 4 + 2,
cacheDatai * 4 + 3
];
[
newImgDatai * 4,
newImgDatai * 4 + 1,
newImgDatai * 4 + 2,
newImgDatai * 4 + 3
] = !!mapi ? old : current;
}
実際は膨張処理とかもしている
結果をCanvasに表示させてみます
https://youtu.be/8yuR4Su9pT4
これだけでも結構面白いです。
ポーズを取得しVRMに適用する
BodyPixはPoseNetと共通している部分があり人体のポーズを取得することができます。
内包?
より具体的には部位ごとの画像中の二次元座標を取得できます。
肘や手の位置からVRMに適用するポーズを生成します。
ポーズの形式は以下の通り
VRMPose | @pixiv/three-vrm
ポーズ変換
とりあえず2次元的なFKでやっていますがIKの方が良いかもしれません。
Quaternion – three.js docs
code:ts
enum KeypointPartsID {
nose = 0,
leftEye = 1,
// ...
}
export interface Keypoint {
"position": {
"y": number,
"x": number
},
"part": string,
"score": number
}
// ....
export class PoseConverter {
public static generateVRMPose(keypoints: Keypoint[]): {
pose: VRMPose,
position?: {
x: number,
y: number,
z: number
}
} {
let vrmPose: VRMPose = {};
// left UpperArm
if (this._isDetect([
keypointsKeypointPartsID.leftShoulder,
keypointsKeypointPartsID.leftElbow,
])) {
vrmPoseVRMSchema.HumanoidBoneName.LeftUpperArm = {
rotation: this._pointsToRotation(
keypointsKeypointPartsID.leftShoulder.position,
keypointsKeypointPartsID.leftElbow.position,
{ x: 1, y: 0 }
)}
}
private static _pointsToRotation(
from: {
x: number,
y: number,
},
to: {
x: number,
y: number
},
reference: {
x: number,
y: number
}
): RawVector4 {
const rad = this._pointsToRadian(from, to);
const refRad = Math.atan2(reference.y, reference.x);
let rotation = new THREE.Quaternion();
rotation.setFromAxisAngle(this._axisZ, rad - refRad);
rotation.normalize();
const vec4: RawVector4 = rotation.x, rotation.y, rotation.z, rotation.w;
return vec4
}
奥行き取得
正面を向いた時の左右の目の間の距離から奥行き方向にどれくらい離れているかを推定しています。
当然横を向いているとうまく推定できません
アバターが奥行き方向で遠くにいってしまう
対策としては胴の長さにするとか
でも、胴は曲がるし難しそう
位置合わせ
腰の位置のスクリーン座標を求めます。
スクリーン座標系からカメラ座標系へ座標変換します。
カメラをワールド座標系の原点に配置しているのでワールド座標系への変換はしてません。
code:ts
public toWorldpos(u: number, v: number, z: number): Vector3 | undefined {
const cs = new Vector3(u * z, v * z, z);
const srcW = this._renderer?.domElement?.width;
const srcH = this._renderer?.domElement?.height;
const fov = Math.PI / 2;
if (!srcH)
return;
if (!srcW)
return;
const cx = srcW / 2;
const cy = srcH / 2;
const f = 1.0 / (2.0 * fov / 2.0) * srcW;
if (!this._camera)
return;
const K = new Matrix3();
K.set(
f, 0, cx,
0, f, cy,
0, 0, 1
);
K.getInverse(K);
const cc = cs.applyMatrix3(K);
const R = this._camera.matrixWorld;
const T = this._camera.position;
const cw = cc.applyMatrix4(R).add(T);
return cw
}
そのままVRMに適用すると足元で座標が指定されてしまうので対応する
VRMの腰の高さ分下げる
今回はこれ
人体から足元の座標を取得
そもそもカメラの範囲に映らないことが多かったので不採用
部屋の広さに依存
code:ts
// ...
position.y = position.y + offsetY;
// ...
public setPosition(position: {
x: number,
y: number,
z: number
}) {
this._vrm?.scene.position.set(position.x, position.y, position.z);
}
ローパスフィルタ
keypointの座標が結構荒ぶるのでとりあえずRCフィルタを適用する
code:ts
private _updateKeypoints(keypoints: Keypoint[]) {
const threshold = 0.5;
const a = 0.75;
if (!this._cacheheKeypoints) {
this._cacheheKeypoints = keypoints;
return;
}
keypoints.forEach((keypoint, index) => {
if (keypoint.score > threshold) {
this._cacheheKeypoints!index.score = keypoint.score;
this._cacheheKeypoints!index.position.x =
a * this._cacheheKeypoints!index.position.x
+ (1 - a) * keypoint.position.x;
this._cacheheKeypoints!index.position.y =
a * this._cacheheKeypoints!index.position.y
+ (1 - a) * keypoint.position.y;
}
})
}
結果
あとはVRMを表示させているcanvasを人体を非表示にしたcanvasの上に表示させて完成です。
https://youtu.be/uXMZTTAdiqQ
横を向いたりするのに対応できていないのが悔しいところです。
参考
Google Developers Japan: BodyPix の概要: ブラウザと TensorFlow.js によるリアルタイム人セグメンテーション
VRMPose | @pixiv/three-vrm
BodyPix と PoseNet の歩き方 - Qiita
threejsで座標変換行列の使い方を理解する - Qiita
3次元CGと座標系
https://dev.classmethod.jp/articles/convert-coords-screen-to-space/
次回は#18 wsl2でReactアプリの開発する