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 現状SafariIEでは動かないようだ。

埋め込みが見れない場合はこちら

課題としては、シェーダー使ってるのにまだ重い。シェーダープログラミング力がまだまだ足りないと思った。createGraphicsが何個もあるから重いのか?いい感じのコードを思いついた人いたら教えて下さい。

*1:ただし再帰だけでやる場合は、顔を認識しない限り映像が止まる。

*2:offscreen renderingと呼ばれている

*3:glitch effectだけに

*4:あとたまにサイトが落ちている...