p5.jsとml5.jsでWebカメラに写った顔にグリッチエフェクトをかける
- 在宅勤務でSnap Cameraが流行っている
- Snap CameraみたいにWebカメラの映像にエフェクトをかけるの作ってみたい
- エフェクトかけるならglitchエフェクトかけてみたい
- 顔だけglitchしたい
という謎な思いつきをしてしまったのでやってみた。
気軽に試したいのでjsでなんとかすることにして、glitchエフェクトをかけるのは普段使い慣れてるp5.jsを使うことにする。p5.jsでglitchしてるコードを探していたら
https://codepen.io/tksiiii/pen/xdQgJX
こんなかっこいいのを見つけたので、このコードを参考にしつつWebカメラのソースに適用する。 顔認識についてはml5.jsを使ってみる。
ml5.js
ml5js·Friendly Machine Learning For The Web
ml5.jsはgoogleが作ってるTensorFlow.jsのwrapperで、細かいことは知らなくてもやりたいことベースで機械学習の技術を使うことができるようなjsライブラリになってる。今回はFaceAPIを使い、顔を認識して顔エリアの長方形座標や大きさを取得する。
FaceApi - ml5 - A friendly machine learning library for the web.
使い方は上記リンクのQuickstart
の通りだけど、
const faceapi = ml5.faceApi(detectionOptions, modelLoaded);
このコードのようにFaceAPIを初期化するときに、第2引数にコールバックを指定する。コールバックは予め機械学習で作成されたモデルの読み込みが完了したタイミングで呼び出される。読み込みは結構時間がかかる。コールバック内では
faceapi.detect(myImage, (err, results) => { });
このようにdetect
すると、顔の識別が完了したタイミングでコールバックが呼び出され、results
に顔のエリアの座標や大きさ、顔の各パーツの頂点座標が入ってくる。FaceAPIは複数の顔を認識できるので、results
は配列のデータになっていて一つの配列要素が一つの顔のデータに対応する。
for(let i = 0; i < results.length; i++){ const box = results[i].alignedRect._box; const x = box._x; const y = box._y; const width = box._width; const height = box._height; }
こんな感じで顔エリアを矩形で表現するための座標、幅、高さを取得できる。 そのほかにも
for(let i = 0; i < results.length; i++){ const mouth = results[i].parts.mouth; for(let j = 0; j < mouth; j++){ const x = mouth[j]._x const y = mouth[j]._y } }
とすることで、口の各頂点座標が取得できたりする。この頂点を線で結ぶことで口の形になる。両眉毛、両目、鼻の頂点座標も同様に取得できる。
p5.jsとml5.jsでwebcamの映像から顔を認識するコードについては
p5.js Web Editor | FaceApi_Video_Landmarks
これが参考になると思う。ポイントは
video = createCapture(VIDEO);
で作ったvideoを、FaceAPIの初期化関数の第一引数にいれるところ。
faceapi = ml5.faceApi(video, detection_options, modelReady)
モデル読み込み完了時に呼ばれるコールバックの中身も見ておくと
function modelReady() { faceapi.detect(gotResults) } function gotResults(err, result) { // いろいろやる faceapi.detect(gotResults) }
顔を識別したタイミングでコールバックが走り、処理が終わったら再度顔を識別する、というように繰り返し識別処理が走る。p5.jsのdraw関数だと毎フレーム処理が走ってしまうので、もしdraw関数内でdetect
した場合、顔識別が終わる前に次の顔識別が走ったりしてしまうと思う。draw関数は使わずに再帰でやるほうがマシンにやさしそう。*1
GLSL
今回は顔のエリアだけにエフェクトをかけたいので、上記のdetect
で取得できる座標と大きさの範囲を加工する。しかし素直にp5.jsで実装してみたら重すぎてカメラの映像が固まってしまった。カメラ解像度分のピクセル一つ一つに対して色情報を変更していく処理を1秒間に数十回繰り返すので、直列に処理していくCPUでは厳しいのだろう。
こういうときはそれぞれのピクセル(テクセル)に対してGPUを使って並列に処理をしていけるシェーダーを作ると良いらしい。
シェーダーについては知識0だったけど、p5.jsを使ってシェーダーを学んでいける下のサイトがとてもわかり易かった。
Introduction to p5.js shaders - p5.js shaders
シェーダーはGLSLという言語で書いていく。 最初に貼ったcodepenのGlitchエフェクトでは4種類のエフェクトが使われている。4つのエフェクトを一つのフラグメントシェーダーで再現するのは中々骨が折れるので、4つのフラグメントシェーダーを使ってそれぞれのシェーダーで一つのエフェクトをかけていって重ねがけすることにした。
複数のシェーダーを重ねがけする方法については、p5.jsのshader exampleにあるMulti-Pass Blurのコードを参考にした。
p5jsShaderExamples/sketch.js at gh-pages · aferriss/p5jsShaderExamples
コードの要点だけを書くとこんな感じ。
function preload(){ // load the shaders, we will use the same vertex shader and frag shaders for both passes blurH = loadShader('base.vert', 'blur.frag'); blurV = loadShader('base.vert', 'blur.frag'); } function setup() { //初期化処理 // initialize the createGraphics layers pass1 = createGraphics(windowWidth, windowHeight, WEBGL); pass2 = createGraphics(windowWidth, windowHeight, WEBGL); // noStrokeとかする } function draw() { // カメラ映像をテクスチャとして最初のシェーダーに渡す blurH.setUniform('tex0', cam); // we need to make sure that we draw the rect inside of pass1 pass1.rect(0,0,width, height); // set the shader for our second pass pass2.shader(blurV); // エフェクトをかけたpass1のテクスチャを次のシェーダーに渡す blurV.setUniform('tex0', pass1); // again, make sure we have some geometry to draw on in our 2nd pass pass2.rect(0,0,width, height); // draw the second pass to the screen image(pass2, 0,0, width, height); }
エフェクトをかけた結果をそのまま画面に出力せず、一旦createGraphics
で作成したcanvas上に出力する。このcanvasはメモリに載ってるだけなので画面には出てこない。*2 最後のエフェクト処理が終わった後にimage
で最終結果を画面に表示している。
最終的にできたコードは下記のglitch.comにあげておいた。*3 埋め込み右下のView App
ボタンをクリックするとデモが確認できるはず。モデル読み込みが遅くて起動に30秒程度かかるので注意。*4 現状SafariとIEでは動かないようだ。
課題としては、シェーダー使ってるのにまだ重い。シェーダープログラミング力がまだまだ足りないと思った。createGraphics
が何個もあるから重いのか?いい感じのコードを思いついた人いたら教えて下さい。