47
31

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-12-01

この記事は、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を使って、前々から気になっていた脈波推定をやってみました。本当はピーク検出もして心拍数出したかったし、脈波推定以外にもいろいろ遊びたかったのですが、いったんここで示させてもらいます。

ではでは。

47
31
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
47
31