JavaScript
機械学習
TensorFlow

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

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

Webカメラによる脈波推定

Webカメラによる脈波推定のなんたるかは、オリジナルの上田さんのHPか、Qiitaの記事を参考にしていただければと思いますが、要約すると

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

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

image.png

脈波推定のロジックについてはQiitaの記事を読んでいただければよいので、本記事では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に

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

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

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

テンソルとメモリ

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

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

// 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]]

(公式チュートリアルより)

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

const img = tf.fromPixels(video)

などとします。imgには要素型がuint8の3次元(height, width, channels)テンソルが入ります。

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

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

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

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側のメモリが開放されるようになります。

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

テンソル同士の演算

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

深層学習での使い方

TensorFlowを生で使うこともできればKerasから使うこともできるように、TensorFlow.jsもプリミティブな書き方も抽象的な書き方もサポートしています。Keras的な書き方についてはこちらを見ていただければ、感覚はつかめると思います。

脈波推定

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

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

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毎に実行します。

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にためておきます。この値をそのまま時系列グラフとして表示しても、顔を動かさなければ、周期的な振動を見ることができますが、ここでは、上田さんに合わせて、簡易的なローパスフィルタをかけることで、結果を見やすくします。具体的には

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

という演算をします。少し分かりづらいですが、Qiitaの記事にある矩形波相関フィルタと呼ばれるものです。

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

face-api.js による顔認識

さて、しれっとstore.faceに顔認識の結果が入っていると説明しましたが、これにはface-api.jsというライブラリを利用しています。

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

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

畳み込み化

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

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

image.png

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

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

まとめ

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

ではでは。