ml5.js の FaceApi で遊んでみる

# ml5.js

機械学習のJavascriptライブラリの1つである TensorFlow.js をなんか良い感じにラップして、
使いやすくしてくれているものです。
機械学習とかよく分かんないけど、なんかちょっとやってみたいという私のような人向けにうってつけです。

こちらのサイトが ml5.js でできる色々なサンプルを載せてくれていて分かりやすいです。
https://ml5js.github.io/ml5-examples/public/

# FaceApi

サンプルの中に、顔を認識してくれるFaceApiというのがあります。
下記を見ると分かる通り、顔の枠、眉毛、目、鼻、口の位置が認識されるようです。
> FaceApi_Video_Landmarks
> https://editor.p5js.org/ml5/sketches/FaceApi_Video_Landmarks

これはなにか遊べそうですね。

# 遊んでみよう

まずは、出来たものがこちらになります。
https://editor.p5js.org/kinoleaf/sketches/FAjy-36AV

先ほど見たサンプルのコードを元にして、クリスマスっぽく（？）ちょっと機能を追加してみました。

## 左右反転
写っている映像が左右逆になってて気持ち悪かったので反転させました。
`gotResults()` 内で `image(video, 0, 0, width, height);` としていた箇所を下記のように書き換えると、映像が左右反転してくれます。

```javascript
  const flippedVideo = ml5.flipImage(video);
  image(flippedVideo, 0, 0, width, height);
```
当初はcanvasを反転させていたのですが、頭がこんがらがりました。。
※ `ml5.flipImage` を使うと、faceApiで取得したX座標が逆になるので、`width - x` などに置き換えます。


## 帽子、ひげ、マスクを顔の位置に合わせて表示してみる

`faceapi.detect(gotResults)` により、顔が認識されると `gotResults()` が呼ばれます。
認識された情報が `result` に入っているので、その情報を `drawPic()` に渡して画像を表示します。

```javascript
function gotResults(err, result) {
  if (err) {
    console.log(err)
    return
  }

  detections = result;

  background(255);
  const flippedVideo = ml5.flipImage(video);
  image(flippedVideo, 0, 0, width, height);

  if (detections) {
    if (detections.length > 0) {
      // drawBox(detections)
      // drawLandmarks(detections)
      drawPic(detections)
    }
  }

  faceapi.detect(gotResults)
}
```

`drawPic()` の中では、認識された顔の情報を元に、
表示する画像の幅、高さ、X座標、Y座標を計算して、描画します。

```javascript
    // 帽子の表示
    if (document.getElementById('boushi').checked) {
      let boushiW = boxWidth * 1.3;
      let boushiH = (boxWidth / boushi.width) * boushi.height;
      let boushiX = x - 20;
      let boushiY = y - boushiH + 20;

      ctx.drawImage(boushi, boushiX, boushiY, boushiW, boushiH);
    }
```
※計算式は何度も試しながらめっちゃ調整しました。


## 顔の傾きに合わせて画像を回転させてみる

顔の傾きは、鼻のポイント2つを使って、角度を計算してみました。

```javascript
// 顔の傾き
rad = getRadian(
  width - nose[0]._x, nose[0]._y,
  width - nose[3]._x, nose[3]._y
);

// 傾き計算
function getRadian(x1, y1, x2, y2) {
  let rad = Math.atan2(y2 - y1, x2 - x1);
  return rad - (Math.PI / 2);
}
```

中心点(cx, cy)を軸に画像のみ回転させる関数を用意して、`ctx.drawImage()` を置き換えます。
帽子とひげは、boxの中心を軸にしてますが、マスクはズレてしまうので、鼻の0ポイント目を中心にしてます。

```javascript
function drawRotateImage(img, x, y, w, h) {
  ctx.save();

  ctx.setTransform(
    Math.cos(rad),
    Math.sin(rad),
    -Math.sin(rad),
    Math.cos(rad),
    cx - cx * Math.cos(rad) + cy * Math.sin(rad),
    cy - cx * Math.sin(rad) - cy * Math.cos(rad)
  );

  ctx.drawImage(img, x, y, w, h);
  ctx.restore();
}
```

## Retina 対応

出来たー！と思ったらmacでうまく表示されず、、Retinaのせいでした。。
canvasのサイズを window.devicePixelRatio 倍に拡大しつつ、cssで表示サイズを元のサイズに設定します。

```javascript
  let elm = createCanvas(width, height);

  // Retina対応
  if (window.devicePixelRatio > 1) {
    elm.canvas.width = width * window.devicePixelRatio;
    elm.canvas.height = height * window.devicePixelRatio;
    elm.canvas.style.width = width + 'px';
    elm.canvas.style.height = height + 'px';
  }
```

# まとめ

なんかそれっぽい感じになりましたね。

ひさびさに画像などを扱うものをやったのですが、位置調整にやたら時間掛かったので、だいぶ忘れてるなーというのと、
やっぱり素材の準備が大事だなと。

---
[:christmas_tree: FORK Advent Calendar 2020](/advent-calendar/2020/fork)
:arrow_left: 20日目 [MediaDevicesとWeb Audio API Vue.jsとThree.js で 音声の波形表示](/shogun-fork/items/20e2a8467c63abe80624) @shogun-fork