face-landmarks-detectionを使って口の中心と開き具合を検出

# 概要
顔検出を使用して口の中心と開き具合を検出する

# 参考
https://github.com/tensorflow/tfjs-models/tree/master/face-landmarks-detection

# サンプルコード
https://github.com/zaburoh/mouth-center-detection.git

# 内容

## videoタグ
canvasに転写するためのvideoタグを作成する
```js
video = document.createElement('video');
video.id = 'video';
video.textContent = 'Video stream not available.';
video.setAttribute('playsinline', '');
video.setAttribute('muted', '');
video.display = 'none';
```
## videoタグの`srcObject`にカメラの映像を設定する
https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
- localhostであればhttpsじゃなくても大丈夫
- スマホから試したいときはローカル認証局などを使って回避
    - [参考](https://qiita.com/zabu/items/efe9c8aea00a449396f0)
- `facingMode: 'user'`でスマホの場合はインカメラを使用するようにする
- 今回はaudioを使わないのでfalseに設定する
```js
if(navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
// カメラとvideoタグの紐づけ
navigator.mediaDevices
  .getUserMedia({ video: { facingMode: 'user'}, audio: false })
  .then(function(stream) {
    video.srcObject = stream;
  })
  .catch((err) => {
    console.error(`An error occurred: ${err}`);
    alert('カメラの使用を許可してください。');
  });
} else {
    alert('ブラウザが対応していません。');
}
```

## videoタグの`srcObject`に設定した`stream`を`canvas`に描画
```js
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');

this.ctx.drawImage(
  this.video, 
  0, 0, this.video.videoWidth, this.video.videoHeight,
  0, 0, this.canvas.width, this.canvas.height
);
```

videoとcanvasの画像サイズに差異があるため記憶する
```js
scaleX = canvas.width / video.videoWidth;
scaleY = canvas.height / video.videoHeight;
```

## face-landmarks-detection
必要なライブラリをCDNから取得する場合は以下
```html
    <!-- Require the peer dependencies of face-landmarks-detection. -->
    <script src="https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh"></script>
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-core"></script>

    <!-- You must explicitly require a TF.js backend if you're not using the TF.js union bundle. -->
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-backend-webgl"></script>
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/face-landmarks-detection"></script>
````
### 検出器の作成

```js
// 検出オプション
estimationConfig = { flipHorizontal: false };

const model = faceLandmarksDetection.SupportedModels.MediaPipeFaceMesh;
// tfjs
// const detectorConfig = {
//   runtime: 'tfjs',
//   refineLandmarks: true,
// };

// mediapipe
const detectorConfig = {
  runtime: 'mediapipe',
  // CDN
  // solutionPath: 'https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh',
  // LOCAL
  solutionPath: 'node_modules/@mediapipe/face_mesh',
  // maxFaces: 1 // default value 1
}
const detector = await faceLandmarksDetection.createDetector(model, detectorConfig);
```

### 検出
[video.readyStateについて](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState)
`video.readyState<2`のときは顔検出を実行しないで待つ
```js
// videoの準備ができているか確認
if (video.readyState < 2) {
  await new Promise((resolve) => {
    video.onloadeddata = () => {
      resolve(video);
    };
  });
}

// 顔検出
faces = await detector.estimateFaces(video, estimationConfig);
```
### 口だけ検出
`estimateFaces()`の結果から口だけ使用する
`createDetector()`で指定した`maxFaces`プロパティによって検出する顔の数（faces）が変わる

```js
const face = faces[0];

// keypointsはカメラ画像のものであるためvideoとcanvasのスケール差を合わせる
const keypoints = face.keypoints.map((keypoint) => [keypoint.x*scaleX, keypoint.y*scaleY]);
const contours = faceLandmarksDetection.util
  .getKeypointIndexByContour(faceLandmarksDetection.SupportedModels.MediaPipeFaceMesh);

// ラベルごとにループする
for (const [label, contour] of Object.entries(contours)) {
  ctx.strokeStyle = '#E0E0E0';
  ctx.lineWidth = 1;

  const path = contour.map((index) => keypoints[index]);
  if (path.every(value => value != undefined)) {
    // lips以外にも取得できる要素はあるが、今回はlips（口）のみ使用        
    switch(label) {
      case 'lips':
        return this._getMouthCenter(path);
        break;
      default:
        break;
    }
  }
}
```

### 口の中心位置と開き具合
口の`path`は以下のようにkeypointがあるため、口の中心位置と開き具合を検出するために必要な以下のkeypointのみ使用した。
- 内側下唇の真ん中（path[25]）
- 内側上唇の真ん中（path[35]）

|場所|index|
|---|---|
|外側下唇|左から 0 ~ 9|
|外側上唇|左から 10 ~ 19|
|内側下唇|左から 20 ~ 29|
|内側上唇|左から 30 ~ 39|

```js
// 内側下唇の中心
const innerLowerLip = path[25]
// 内側上唇の中心
const innerUpperLip = path[35]

// 口の開き具合：唇中心位置のy座標の差分
const openMouthSize = innerLowerLip[1] - innerUpperLip[1];
// 口の中心位置のy座標
const y = innerLowerLip[1] - openMouthSize / 2;

return [innerLowerLip[0], y, openMouthSize < MOUTH_MIN_SIZE ? MOUTH_MIN_SIZE : openMouthSize];
```