Webカメラで脈波推定: TensorFlow.js 版

この記事は、BrainPad Advent Calendar 2018 1日目の記事です。
今回は、深層学習ライブラリTensorFlowのJavaScript版であるTensorFlow.jsを、深層学習**以外**の面白そうなタスクに利用できないか、ということで「Webカメラによる脈波推定」をしてみようと思います。

- コードは[こちら(CodePen)](https://codepen.io/ohtaman/pen/yEyBMj)。

## Webカメラによる脈波推定

Webカメラによる脈波推定のなんたるかは、オリジナルの[上田さんのHP](http://www.neo-tech-lab.jp/MyProfile.htm)か、[Qiitaの記事](https://qiita.com/taichi-iki/items/0b9165deab187bb2d3f9)を参考にしていただければと思いますが、要約すると

- 普通のwebカメラで人物を撮影すると、脈拍に合わせてRGB値が周期的に変化していることがわかる。逆に、その周期を捉えると、心拍数を推定することができる

というものです。↓の図を見ていただければ、なんとなくなにをやっているかはわかると思います。
右のグラフは、RGBのうちのGの値の時間変化（にノイズを消すようなフィルタをかけたもの）をあらわしています。そこまできれいではありませんが、Gの値が周期的に変化しているのがわかると思います。この周期を計算してやれば、心拍数が求められる、という寸法です。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/24770/bbdc14cf-c5a0-05d2-17a4-3898112a580e.png)

脈波推定のロジックについては[Qiitaの記事](https://qiita.com/taichi-iki/items/0b9165deab187bb2d3f9)を読んでいただければよいので、本記事ではTensorFlow.jsの使い方を中心に解説します。

## TensorFlow.js とは

TensorFlow.js は、TensorFlowのJavaScript版とも言えるライブラリで、

- JavaScriptで深層学習モデルを記述できる（訓練および推論）
- WebGLによる高速な行列演算に対応
- KerasやTensorFlowのモデルを読み込んで利用することができる（外部コマンドによってTensorFlow.js用に変換できる）

といった特徴があります。JavaScriptなので、ブラウザ上で実行することができます。ブラウザ上で実行できるメリットとして

1. インストール不要
2. 特別なドライバが不要（CUDAがなくてもGPUの恩恵に預かることができる）
3. インタラクティブなデモの構築が容易
4. HTML5の機能を使って、様々なセンサ（スマホの加速度センサやカメラなど）を利用できる

といったメリットがあります。
ただし、今の所のデメリットとして

1. 本格的にモデルを訓練するには大量のデータが必要なので、ブラウザ上では厳しい
2. すべてのTensorFlowのオペレーターが実装されているわけではない（でもわりとできる）
3. JavaScriptに慣れていないとコーディングが辛い
4. 今の所、注意しないとGPU側のメモリリークがおきやすい

という点が挙げられるかと思います。1.の制約があるので、ベースとなるモデルはPythonで普通に実装と訓練を行い、ブラウザ上では推論に特化するか、訓練するとしても最低限のファインチューニングに留める、というのが一般的な使いかたになるかなと思います。

## TensorFlow.js の基本

### 準備

インストール不要と書きましたが、TensorFlow.jsを使うのはとても簡単です。HTMLに

```html
<script src="https://cdnjs.cloudflare.com/ajax/libs/tensorflow/0.12.7/tf.min.js"></script>
```

という1行を入れるだけです。(すでに`0.13.x`も出ていますが、今回は 0.12.7 を使っています)
[CodePen](https://codepen.io/)を使う場合は`Settings`から以下のように設定します。

<img src="https://qiita-image-store.s3.amazonaws.com/0/24770/3a23039f-3563-eb32-23c2-020873f9ebb0.png" width="360px">

JavaScriptに慣れていない方は、CodePenのようなサービスを使って、その中のコンソールでいろいろ試してみるのが一番楽だと思います。

### テンソルとメモリ

TensorFlow.jsでは、TensorFlowと同じくテンソル(多次元配列)に対する演算が主な役割です。TensorFlow.jsのテンソルは、CPU上のメモリではなく、GPU上のメモリに確保されます。そのため、JavaScriptの配列をそのまま計算対象とするのではなく、必ずテンソルを生成しなければいけません。

テンソルはいくつかの方法で生成することができます。例えばJavaScriptの配列から生成するには `tf.tensor` 関数を使って

```javascript
// 2x3 Tensor
const shape = [2, 3]; // 2 rows, 3 columns
const a = tf.tensor([1.0, 2.0, 3.0, 10.0, 20.0, 30.0], shape);
a.print(); // print Tensor values
// Output: [[1 , 2 , 3 ],
//          [10, 20, 30]]
```
([公式チュートリアル](https://js.tensorflow.org/tutorials/core-concepts.html)より)

とします。また、`video`タグや`canvas`タグに表示されている画像からテンソルを生成するには、

```javascript
const img = tf.fromPixels(video)
```
などとします。`img`には要素型が`uint8`の3次元（height, width, channels）テンソルが入ります。

注意しないといけないのは、**テンソルはGPUのメモリ上に確保さえているため、ガベージコレクションの対象とならない**点です。そのため、**一度生成したテンソルは明示的に開放する必要があります**。具体的には、`tf.dispose`関数を使います。

```javascript
tf.dispose(a)
// Or, a.dispose()
```

ただ、テンソルはちょっとした計算をするたびに生成されるので、すべてのテンソルを`dispose`によって管理するのはほぼ不可能です。そこで、不要なテンソルを一括で開放してくれる機能が準備されています。

```javascript
tf.tidy(() => {
    const x = tf.scalar(input);

    const ax2 = a.mul(x.square());
    const bx = b.mul(x);
    const y = ax2.add(bx).add(c);

    return y;
});
```

具体的には、`tf.tidy`という関数の中に、具体的な演算処理を表す関数を書きます。すると、関数の戻り値となるテンソル以外は、自動的にGPU側のメモリが開放されるようになります。

上記の例だと、計算の途中も含めて `x`、`x.square()`、`ax2`、`bx`、`ax.add(bx)`、`y` の6つのテンソルが生成されますが、戻り値の`y`以外については自動的に開放してくれます。

### テンソル同士の演算

先程の例でちらっとお見せした通り、テンソル同士の演算には`mul`や`add`を使います。複雑な数式となると見づらいですが、JavaScriptには演算子のオーバーロード機能がないので、しようがないでしょう。

### 深層学習での使い方

TensorFlowを生で使うこともできればKerasから使うこともできるように、TensorFlow.jsもプリミティブな書き方も抽象的な書き方もサポートしています。Keras的な書き方については[こちら](https://docs.google.com/presentation/d/e/2PACX-1vQfv2VjMln4MM6SMvqQfYwYZVAEHx0XgeRqKCdk89BwlmjW91gI4TH5lqMUVGFubyKq69QfH8_nnug1/pub?start=false&loop=false&delayms=3000&slide=id.g3b8157672e_0_522)を見ていただければ、感覚はつかめると思います。

## 脈波推定

では、TensorFlow.jsの基本がわかったところで、具体的な脈波推定処理に移っていこうと思います。
まずは、Webカメラからのデータ取得です。

Webカメラを使うには、通常通り`GetUserMedia`を使って、得られた`stream`を対応する`video`タグの`srcObject`に代入します。

```javascript
function startCapture() {
  navigator.getUserMedia(
    {video: true},
    (stream) => {
      store.video.srcObject = stream
      store.startButton.disabled = true
      store.stopButton.disabled = false
      store.stream = stream
    },
    (err) => {
      console.log(err)
      store.startButton.disabled = false
      store.stopButton.disabled = true
      store.video.srcObject = null
    }
  )
}
```

次に、人間の顔の中心部の明るさを抽出する `getBrightness` 関数を100msec毎に実行します。

```javascript
function getBrightness() {
  return tf.tidy(() => {
    let img =  tf.fromPixels(store.video).cast('float32')
    let xCenter = store.video.width/2
    let yCenter = store.video.height/2
    if (store.face !== null) {
      xCenter = parseInt(store.face.x + store.face.width/2)
      yCenter = parseInt(store.face.y + store.face.height/2)
    }
    let cropSize = [60, 60]
    let channel = 1
    return img.slice(
      [yCenter - cropSize[0]/2, xCenter - cropSize[1]/2, channel],
      [cropSize[0], cropSize[1], 1]
    ).flatten().mean().dataSync()[0]
  })
}
```

コードが雑ですが、store.face には、`face-api.js`というライブラリを使った顔認識の結果が入っています。
顔の中心の60x60の領域について、RGBのGの値の平均をとっています。(`dataSync`関数をつかって、テンソルではなく実際の値(CPUのメモリ上に確保されている)を返すようにしてあります。戻り値がテンソルではないので、`tf.tidy`は、ここで生成されたテンソルのGPUメモリをすべて開放してくれます)

過去8回分の計算した結果は、`store.brightness`にためておきます。この値をそのまま時系列グラフとして表示しても、顔を動かさなければ、周期的な振動を見ることができますが、ここでは、上田さんに合わせて、簡易的なローパスフィルタをかけることで、結果を見やすくします。具体的には

```javascript
return 2*sum(array.slice(-6, -2)) - sum(array.slice(-8)) // array には直近の brightness が入っている
```

という演算をします。少し分かりづらいですが、[Qiitaの記事](https://qiita.com/taichi-iki/items/0b9165deab187bb2d3f9)にある**矩形波相関フィルタ**と呼ばれるものです。

あとはこれを `Chart.js` でいい感じに表示してやれば、冒頭のキャプチャのように、心拍が表示されるようになります。（ただし、そんなに精度が高くないので、顔を動かさないでじっとしていなければならず、あまり実用的ではありません。工夫はできるのかもしれませんが。。。）

### face-api.js による顔認識

さて、しれっと`store.face`に顔認識の結果が入っていると説明しましたが、これには[face-api.js](https://github.com/justadudewhohacks/face-api.js)というライブラリを利用しています。

```javascript
async function detectFace() {
  let faces = await faceapi.tinyFaceDetector(store.video)
  if (faces.length > 0) {
    store.face = faces[0].forSize(
      store.video.width,
      store.video.height
    ).box
  }
}
```

このライブラリは、裏でTensorFlow.jsを利用しており、深層学習による高精度な顔認識が可能です。また、利用目的に応じて、精度の高いモデルではなく、よりコンパクトで高速なモデルを利用することもできます。使い方もシンプルで、必要なモデルをロードして、上記のコードのように認識対象のソースを指定するだけです。

```javascript
async function loadModels ()  {
  await faceapi.loadTinyFaceDetectorModel(FACE_DETECT_MODEL_URL)
  await faceapi.loadFaceLandmarkTinyModel(FACE_LANDMARK_MODEL_URL)
}
```

## 畳み込み化

さて、TensorFlow.jsを使っているため、高速な演算が可能です。
例えば、顔の中心だけを使って1つの値を出すのではなく、60x60の畳み込みをかけることで、全画素の周辺で、Gの値がどのように変化しているかを見ることもできます。実際にやってみたのが[こちら](https://codepen.io/ohtaman/pen/vQQMoM)です。

右側の白黒の画像が各点ごとのGの値の変化をあらわしています（縦横比がおかしくなってますね...）。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/24770/ff92674c-305d-2194-6cae-d7c74a9c69d7.png)

やってみて気がついたのは、

- 瞬きがめっちゃとれる
- 輪郭が強く反応しているので、顔が（心拍に合わせて??）微妙に揺れているんじゃないかという気がする
- 環境によっては、顔以外も結構周期的に揺らいでいる（これは電気の周波数とカメラのフレームレートの関係にもよっているように見える）

## まとめ

TensorFlow.jsを使って、前々から気になっていた脈波推定をやってみました。本当はピーク検出もして心拍数出したかったし、脈波推定以外にもいろいろ遊びたかったのですが、いったんここで示させてもらいます。

ではでは。