#17 BodyPixを使って肉体を3Dアバターに置き替えるWebアプリケーションを作る https://youtu.be/uXMZTTAdiqQ
基本情報
主な仕様ライブラリ
BodyPix は画像内のピクセルを、1) 人と表すピクセルと 2) 背景を表すピクセルの 2 種類に分類できます。
導入
リポジトリ
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 = [
];
const old = [
];
[
] = !!mapi ? old : current; }
結果をCanvasに表示させてみます
https://youtu.be/8yuR4Su9pT4
これだけでも結構面白いです。
内包?
より具体的には部位ごとの画像中の二次元座標を取得できます。
肘や手の位置からVRMに適用するポーズを生成します。
ポーズの形式は以下の通り
ポーズ変換
とりあえず2次元的なFKでやっていますがIKの方が良いかもしれません。 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([
])) {
rotation: this._pointsToRotation(
{ 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();
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
横を向いたりするのに対応できていないのが悔しいところです。
参考