定番の顔メッシュ貼り替えをWeb+facemeshで実装してみた

顔メッシュ貼り替えといえばWWDC2017でAppleが発表した[Face Tracking](https://developer.apple.com/videos/play/tech-talks/601/)が有名ですが、今年の3月にGoogleが公開した[facemesh](https://developers-jp.googleblog.com/2020/04/mediapipe-tensorflowjs.html)でも似たようなことができます。
WWDC2017から結構時間が経ったことで顔メッシュ貼り替えは枯れたネタではありますが、今回は[Three.js](https://threejs.org/)とfacemeshを組み合わせて顔メッシュ貼り替えを試しました。

昨夜作り始めて割とさくっとできました。
実装ではcanvasの画像から顔メッシュを自動生成しまして、カメラに写った人物の顔の座標に顔メッシュを適用しています。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">誰得ではありますがイラスト屋さんの顔になれます。<a href="https://t.co/BNmTQ1wa5O">https://t.co/BNmTQ1wa5O</a><a href="https://twitter.com/hashtag/facemesh?src=hash&amp;ref_src=twsrc%5Etfw">#facemesh</a> <a href="https://t.co/wyTayzUfO7">pic.twitter.com/wyTayzUfO7</a></p>&mdash; o2 (@mb_otsu) <a href="https://twitter.com/mb_otsu/status/1267297579978481664?ref_src=twsrc%5Etfw">June 1, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# Three.jsでの顔メッシュ生成

実装のコア部分となるThree.jsの顔メッシュですが、facemeshの推論の戻り値と、[TRIANGULATION](https://github.com/mbotsu/facemask/blob/master/triangulation.js)の頂点を混ぜ合わせると顔のメッシュは作れます。


```javascript
// facemeshの戻り値
var keypoints = prediction.scaledMesh;

var texture = new THREE.CanvasTexture(canvas);
texture.flipY = false;

mesh.geometry.vertices = new Array(keypoints.length);
for (let i = 0; i < keypoints.length; i++) {
    const [x,y,z] = keypoints[i];
    mesh.geometry.vertices[i] = new THREE.Vector3(x,y,z);
}

mesh.geometry.faces = new Array(TRIANGULATION.length / 3);
for (let i = 0; i < TRIANGULATION.length / 3; i++) {

    let id0 = TRIANGULATION[i*3+0];
    let id1 = TRIANGULATION[i*3+1];
    let id2 = TRIANGULATION[i*3+2];
    mesh.geometry.faces[i] = new THREE.Face3(id0,id1,id2);

    // uvはcanvasのサイズに合わせて正規化
    let uv = [
    new THREE.Vector2(keypoints[id0][0] / videoWidth, keypoints[id0][1] / videoHeight),
    new THREE.Vector2(keypoints[id1][0] / videoWidth, keypoints[id1][1] / videoHeight),
    new THREE.Vector2(keypoints[id2][0] / videoWidth, keypoints[id2][1] / videoHeight),
    ];
    mesh.geometry.faceVertexUvs[0][i] = uv;
}
mesh.material = new THREE.MeshBasicMaterial({ map: texture , 
                                                side: THREE.DoubleSide });
```

## 顔の部分を推論

![face.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35973/6fb3f114-92de-16da-c9f7-614622b321d3.png)
画像のようにAIが推論した顔メッシュの座標を取得し、UV座標を抽出します。

## オンラインデモ

https://mbotsu.github.io/facemask/

## コード

https://github.com/mbotsu/facemask

## 顔メッシュ生成をBufferGeometryに書き直す

冒頭のThree.jsのGeometryの結果をBufferGeometryに変換して読み解きます。
`bufgeom = new THREE.BufferGeometry().fromGeometry(geom);`
変換結果を見ると、facesとverticesをまとめてpositionに書くことが解りました。
何故そうなるのか調べたところ、[こちら](https://qiita.com/masato_makino/items/2ad5b1eb01a118d0a155)を読むと、positionはWebGLでの頂点形式とのこと。
また、Geometryは、処理時にBufferGeometryに置き換えてから実行しているそうです。
facesとverticesを分けて書くのはユーザーフレンドリーなGeometryの仕様と覚えておくと良さそうです。

```javascript

let pos = new Float32Array(
  Array.prototype.concat.apply([],TRIANGULATION.map(index => keypoints[index])));

let uv = new Float32Array(
  Array.prototype.concat.apply([],
    TRIANGULATION.map(index => 
      [keypoints[index][0] / modelWidth, keypoints[index][1] / modelHeight])));

mesh.geometry.setAttribute('position', new THREE.BufferAttribute(pos, 3));
mesh.geometry.setAttribute('uv', new THREE.BufferAttribute(uv, 2));

mesh.geometry.attributes.position.needsUpdate = true;
```

## その他

よろしければこちらも合わせてご覧ください。
[TensorFlow.jsのfacemeshで顔向き推定を試してみた その２](https://qiita.com/otmb/items/74b39160f519a1dd2f73)

## References

- [ピクセル座標で3Dオブジェクトを再配置](https://qiita.com/watabo_shi/items/0811d03390c18e46be86)
- [PromiseベースでImage().onload](https://qiita.com/sin_tanaka/items/b17a099d2a6a5e9a94b7)
- [Three.js BufferGeometry vs Geometry](https://qiita.com/masato_makino/items/2ad5b1eb01a118d0a155)
- [facemesh@github](https://github.com/tensorflow/tfjs-models/tree/master/facemesh)
- [Real-time Facial Surface Geometry from Monocular Video on Mobile GPUs](https://arxiv.org/abs/1907.06724)
- [Face Tracking with ARKit](https://developer.apple.com/videos/play/tech-talks/601/)