この記事は、BrainPad Advent Calendar 2018 1日目の記事です。
今回は、深層学習ライブラリTensorFlowのJavaScript版であるTensorFlow.jsを、深層学習以外の面白そうなタスクに利用できないか、ということで「Webカメラによる脈波推定」をしてみようと思います。
- コードはこちら(CodePen)。
Webカメラによる脈波推定
Webカメラによる脈波推定のなんたるかは、オリジナルの上田さんのHPか、Qiitaの記事を参考にしていただければと思いますが、要約すると
- 普通のwebカメラで人物を撮影すると、脈拍に合わせてRGB値が周期的に変化していることがわかる。逆に、その周期を捉えると、心拍数を推定することができる
というものです。↓の図を見ていただければ、なんとなくなにをやっているかはわかると思います。
右のグラフは、RGBのうちのGの値の時間変化(にノイズを消すようなフィルタをかけたもの)をあらわしています。そこまできれいではありませんが、Gの値が周期的に変化しているのがわかると思います。この周期を計算してやれば、心拍数が求められる、という寸法です。
脈波推定のロジックについてはQiitaの記事を読んでいただければよいので、本記事ではTensorFlow.jsの使い方を中心に解説します。
TensorFlow.js とは
TensorFlow.js は、TensorFlowのJavaScript版とも言えるライブラリで、
- JavaScriptで深層学習モデルを記述できる(訓練および推論)
- WebGLによる高速な行列演算に対応
- KerasやTensorFlowのモデルを読み込んで利用することができる(外部コマンドによってTensorFlow.js用に変換できる)
といった特徴があります。JavaScriptなので、ブラウザ上で実行することができます。ブラウザ上で実行できるメリットとして
- インストール不要
- 特別なドライバが不要(CUDAがなくてもGPUの恩恵に預かることができる)
- インタラクティブなデモの構築が容易
- HTML5の機能を使って、様々なセンサ(スマホの加速度センサやカメラなど)を利用できる
といったメリットがあります。
ただし、今の所のデメリットとして
- 本格的にモデルを訓練するには大量のデータが必要なので、ブラウザ上では厳しい
- すべてのTensorFlowのオペレーターが実装されているわけではない(でもわりとできる)
- JavaScriptに慣れていないとコーディングが辛い
- 今の所、注意しないと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側のメモリが開放されるようになります。
上記の例だと、計算の途中も含めて x
、x.square()
、ax2
、bx
、ax.add(bx)
、y
の6つのテンソルが生成されますが、戻り値のy
以外については自動的に開放してくれます。
テンソル同士の演算
先程の例でちらっとお見せした通り、テンソル同士の演算にはmul
やadd
を使います。複雑な数式となると見づらいですが、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の値の変化をあらわしています(縦横比がおかしくなってますね...)。
やってみて気がついたのは、
- 瞬きがめっちゃとれる
- 輪郭が強く反応しているので、顔が(心拍に合わせて??)微妙に揺れているんじゃないかという気がする
- 環境によっては、顔以外も結構周期的に揺らいでいる(これは電気の周波数とカメラのフレームレートの関係にもよっているように見える)
まとめ
TensorFlow.jsを使って、前々から気になっていた脈波推定をやってみました。本当はピーク検出もして心拍数出したかったし、脈波推定以外にもいろいろ遊びたかったのですが、いったんここで示させてもらいます。
ではでは。