高速に顔検出ができるJavaScriptライブラリ「pico.js」を使って、WEBカメラに映った自分の顔にリアルタイムでモザイク風のマスクをかぶせてみます。
続きものの3回目です。
前回(2)と前々回(1)と重複する部分は、今回説明していません。
JavaScriptのみでブラウザ上で顔検出ができる「pico.js」はこちらから。
GitHubからダウンロード解凍して、HTMLファイルを置くフォルダに「js」というサブフォルダを作って、その下に「pico.js」と「examples」フォルダにある「camvas.js」をコピーします。
HTML側の記述です。
まず、<head></head>の間にスクリプトタグを置きます。
<script src="js/camvas.js"></script> <script src="js/pico.js"></script>
変換すみのカメラ映像を表示するCanvasタグと、マスクする画像を読み込むimgタグを置きます。
<div> <canvas id="canvas" width="640" height="480" ></canvas> </div> <div style="display:none;"> <img id="mask" src="images/mask.jpg"> </div>
処理はjs/pico_webcam_sample.jsファイルに記述します。
bodyの最後の方で以下のように読み込みます。
<script src="js/pico_webcam_sample.js"></script>
HTML側はこんな感じです。
マスクで使う画像(mask.jpg)の画像はこのようなものを準備しました。
モザイク風・・です。
あと、ポイントがひとつ。
WEBカメラを使うのに、<video>タグではなく、<canvas>タグだということです。
WEBカメラの映像は、camvas.jsの中で動的に定義され<video>タグに流し、それに加工して、canvasに描きだすということをやっているからです。
次は、pico_webcam_sample.jsの内容です。
おおまかな流れは、前々回(1)と同じです。
事前準備として以下をやります。
- 顔検出カスケードダウンロード
- 検出対象の画像の準備
そして、以下の処理をリアルタイムで繰り返します。
- パラメータ設定
- 顔検出の実行
- 検出した顔の範囲にマスク画像を表示する
顔検出カスケードダウンロードとは、pico.jsの顔検出に必要な作成済決定木をダウンロードする工程です。
var facefinder_classify_region = function(r, c, s, pixels, ldim) {return -1.0;}; var update_memory = pico.instantiate_detection_memory(5); var cascadeurl = 'https://raw.githubusercontent.com/nenadmarkus/pico/c2e81f9d23cc11d1a612fd21e4f9de0921a5d0d9/rnt/cascades/facefinder'; fetch(cascadeurl).then(function(response) { response.arrayBuffer().then(function(buffer) { var bytes = new Int8Array(buffer); facefinder_classify_region = pico.unpack_cascade(bytes); console.log('* cascade loaded'); }) })
ここは、あれこれ考えず、この通りにする以外の手はありません。
次に、検出対象の画像の準備として、Canvasとマスク画像をロードします。
var ctx = document.getElementById('canvas').getContext('2d'); //var img = document.getElementById('image'); var msk_img = document.getElementById('mask'); //img.onload = () => ctx.drawImage(img, 0, 0); var nrow = 480 var ncol = 640
幅と高さは、HTMLで定義するcanvasのWidthとHeightのサイズにあわせます。
これで準備ができたので、WEBカメラの画像にあわせてリアルタイムで繰り返すコールバック関数内で以下の3つの処理を定義します。
- パラメータ設定
- 顔検出の実行
- 検出した顔の範囲にマスク画像を表示する
リアルタイムで繰り返すコールバック関数は、「processfnc」としました。
このコールバック関数名は「camvas.js」を使うときに引数として渡します。
var mycamvas = new camvas(ctx, processfunc);
これだけで、WEBカメラから映像を読み込んで、processfncに定義した顔検出処理を行い、結果をcanvasに 出力する処理を「camvas」がやってくれます。
さてprocessfncの内容です。
最初に「processfnc」全体のソースを示します。
var processfunc = function(video, dt) { ctx.drawImage(video, 0, 0); var rgba = ctx.getImageData(0, 0, ncol, nrow).data; image = { "pixels": rgba_to_grayscale(rgba, nrow, ncol), "nrows": nrow, "ncols": ncol, "ldim": ncol } params = { "shiftfactor": 0.1, "minsize": 100, "maxsize": 1000, "scalefactor": 1.1 } dets = pico.run_cascade(image, facefinder_classify_region, params); dets = update_memory(dets); dets = pico.cluster_detections(dets, 0.2); qthresh = 50.0 for(i=0; i<dets.length; ++i){ if(dets[i][3]>qthresh) { ctx.beginPath(); ctx.drawImage(msk_img, dets[i][1]-(dets[i][2]/2), dets[i][0]-(dets[i][2]/2), dets[i][2], dets[i][2]) } } }
ここで、3つのステップに対応する部分は以下の通りです。
パラメータ設定
ctx.drawImage(video, 0, 0); var rgba = ctx.getImageData(0, 0, ncol, nrow).data; image = { "pixels": rgba_to_grayscale(rgba, nrow, ncol), "nrows": nrow, "ncols": ncol, "ldim": ncol } params = { "shiftfactor": 0.1, "minsize": 100, "maxsize": 1000, "scalefactor": 1.1 }
顔検出の実行
dets = pico.run_cascade(image, facefinder_classify_region, params); dets = update_memory(dets); dets = pico.cluster_detections(dets, 0.2);
検出した顔の範囲にマスク画像を表示する
qthresh = 50.0 for(i=0; i<dets.length; ++i){ if(dets[i][3]>qthresh) { ctx.beginPath(); ctx.drawImage(msk_img, dets[i][1]-(dets[i][2]/2), dets[i][0]-(dets[i][2]/2), dets[i][2], dets[i][2]) } }
静止画の処理との違いは、入力が「video」になっていることと、パラメータ「qthresh = 50.0」の値が、静止画の約10倍くらいになっていることです。
この値が小さいと感度があがりすぎて、チラチラした見づらい画面になるためです。
50.0くらいがちょうどいい感じみたいです。
静止画の処理のポイントは、前々回(1)に書いています。
動かしてみました。
おーー。顔がモザイクされたっぽくなってる。
顔を横にずらしてみます。
ちゃんと追随してきます。
スペックの高くないノートPCでも、動きが非常にスムースです。
いいですね。
確かに顔を横にむけたり、カメラの端で顔が一部隠れたりすると、顔が検出されずにマスクがはずれたりしますけど、この実行速度を考えたら、検出精度もたいしたもんだと思いますけどね。
今回のJavaScriptソースだけ全体を掲載しておきます。
var facefinder_classify_region = function(r, c, s, pixels, ldim) {return -1.0;}; var update_memory = pico.instantiate_detection_memory(5); var cascadeurl = 'https://raw.githubusercontent.com/nenadmarkus/pico/c2e81f9d23cc11d1a612fd21e4f9de0921a5d0d9/rnt/cascades/facefinder'; fetch(cascadeurl).then(function(response) { response.arrayBuffer().then(function(buffer) { var bytes = new Int8Array(buffer); facefinder_classify_region = pico.unpack_cascade(bytes); console.log('* cascade loaded'); }) }) var ctx = document.getElementById('canvas').getContext('2d'); //var img = document.getElementById('image'); var msk_img = document.getElementById('mask'); //img.onload = () => ctx.drawImage(img, 0, 0); var nrow = 480 var ncol = 640 function rgba_to_grayscale(rgba, nrows, ncols) { var gray = new Uint8Array(nrows*ncols); for(var r=0; r<nrows; ++r) for(var c=0; c<ncols; ++c) gray[r*ncols + c] = (2*rgba[r*4*ncols+4*c+0]+7*rgba[r*4*ncols+4*c+1]+1*rgba[r*4*ncols+4*c+2])/10; return gray; } var processfunc = function(video, dt) { ctx.drawImage(video, 0, 0); var rgba = ctx.getImageData(0, 0, ncol, nrow).data; image = { "pixels": rgba_to_grayscale(rgba, nrow, ncol), "nrows": nrow, "ncols": ncol, "ldim": ncol } params = { "shiftfactor": 0.1, "minsize": 100, "maxsize": 1000, "scalefactor": 1.1 } dets = pico.run_cascade(image, facefinder_classify_region, params); dets = update_memory(dets); dets = pico.cluster_detections(dets, 0.2); qthresh = 50.0 for(i=0; i<dets.length; ++i){ if(dets[i][3]>qthresh) { ctx.beginPath(); ctx.drawImage(msk_img, dets[i][1]-(dets[i][2]/2), dets[i][0]-(dets[i][2]/2), dets[i][2], dets[i][2]) } } } var mycamvas = new camvas(ctx, processfunc);
ではでは。