SE_BOKUのまとめノート的ブログ

SE_BOKUが知ってること・勉強したこと・考えたことetc

JavaScriptで高速顔認識ができる「pico.js」の使い方(1)

f:id:arakan_no_boku:20210215234003p:plain 

目次

JavaScriptライブラリ「pico.js」を使う準備

ブラウザ上で高速に顔認識ができるJavaScriptライブラリを試してみます。

以下のサイトにあります。 

github.com

上記のリンク(GitHub)から「ZIP」でダウンロードし、適当なところに解凍します。

f:id:arakan_no_boku:20210215234542p:plain

中に。

  • pico.js
  • lploc.js

という2つのスクリプトファイルが含まれています。

今回は「pico.js」のみ使います。

lploc.jsは「瞳孔認識」をするライブラリです。

目の動きでお絵描きするとか面白いそうなんですけど、今回は使いません。

HTMLファイルを置くフォルダに「js」というサブフォルダを作って、その下に「pico.js」をコピーします。

pico.jsを使う

こんな感じに画面上に表示した画像に対して、ボタンを押したら顔認識して、結果を表示する感じにしてみました。

f:id:arakan_no_boku:20210216001127p:plain

 

pico.jsを使う:HTML側のポイント 

まず。

<head></head>部で、上記でコピーした「pico.js」を読み込みます。

<script src="js/pico.js"></script>

顔認識の対象とする画像をimgタグで読み込みます。 

<img id="image" width="512" height="341" src="images/people-512x341.jpg" />

顔認識結果画像の表示エリアは「canvas」タグで指定します。

<canvas id="canvas" width="512" height="341" ></canvas>

画像認識をスタートさせるボタンを配置します。

<input type="button" value="検出開始" onclick="fnc_callback(341, 512)">

今回は顔認識処理の本体は「s/pico_sample.js」に書くので、それをロードします。、

<script src="js/pico_sample.js"></script>

HTML側のポイントはこれだけです。

pico.jsを使う:JavaScript側処理の流れ

pico.jsで顔認識を行う手順は。

事前準備として以下をやっておき

  1. 顔認識カスケードダウンロード
  2. 認識対象の画像の準備

検出開始ボタンが押されたとき

  1. パラメータ設定
  2. 顔認識の実行
  3. 認識した顔の範囲を囲む(今回は円<arc>)

を実行する・・という感じです。

pico.jsを使う:準備1.顔認識カスケードダウンロード 

顔認識カスケードダウンロードで作成済の顔認識用の決定木をダウンロードします。 

pico.jsの顔認識は、450もの決定木を25段階に連結しているので「カスケード」(同じものがいくつも数珠つなぎに連結された構造や、連鎖的あるいは段階的に物事が生じる様子)と呼びます。

それを行う部分が以下です。

var facefinder_classify_region = function(r, c, s, pixels, ldim) {return -1.0;};
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');
	})
})

ここは、サンプルのまま使うしかないです。

pico.jsを使う:準備2. 顔認識対象の画像の準備

次に、認識対象の画像の準備で、Canvasに変換対象の画像をロードします。

var ctx = document.getElementById('canvas').getContext('2d');
var img = document.getElementById('image');
img.onload = () => ctx.drawImage(img, 0, 0);

事前準備は以上です。 

pico.jsを使う:認識処理_コールバック関数 パラメータ設定

ここからは「検出開始」ボタンを押された後(つまり、fnc_callback関数の中身)の処理になります。

pico.jsで顔認識するメソッドに渡すパラメータを準備します。

  • image
  • params

の2つです。

imageパラメータの「pixels」には、顔認識対象画像データの生のピクセル値(赤、緑、青+アルファ形式を取得して、グレースケール変換したものをセットします。

あとのパラメータは、とりあえずサンプル(デフォルト)のままでやるのが無難です。

ctx.drawImage(img, 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": 20,       
        "maxsize": 1000,     
        "scalefactor": 1.1 
    }

グレースケール変換しているのは「rgba_to_grayscale」メソッドです。

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;
}

画像データのWidthとHeightの指定が必要です。

pico.jsを使う:認識処理_コールバック関数:顔認識の実行 

顔認識の実行は以下のようにします。

2行でワンセットと考えたほうがいいです。

dets = pico.run_cascade(image, facefinder_classify_region, params);
dets = pico.cluster_detections(dets, 0.2);

上の1行だけだと、同じ顔を少しずつ違った座標で何回も認識してしまいます。 

それを整理して、一番有効なひとつに絞り込むのが2行目・・って感じです。

pico.jsを使う:認識処理_コールバック関数:認識した顔を囲む 

認識した顔の範囲は上記の「dets」にはいってます。

複数認識しても、全部はいってます。

なので、ループでまわして、認識した顔のまわりを線で囲います。

qthresh = 5.0
for(i=0; i<dets.length; ++i){
    if(dets[i][3]>qthresh)
    {
        ctx.beginPath();
        ctx.arc(dets[i][1], dets[i][0], dets[i][2]/2, 0, 2*Math.PI, false);
        ctx.lineWidth = 3;
        ctx.strokeStyle = 'red';
        ctx.stroke();
    }
}

beginPathから始まる一連の処理で「赤い円」を描いてます。 

このへんは、第二回目で詳しくやるので、今回は簡単に流します。

html5canvasの処理なので、興味があれば、こちらをどうぞ。

developer.mozilla.org

Webサーバーを立ち上げて実行  

WEBサーバーたちあげて「http://localhost」で実行します。

HTMLをダブルクリックで動かそうとしてもエラーになります。

Chromeだとこんな感じのメッセージ。

Uncaught DOMException: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The canvas has been tainted by cross-origin data

これは「 Chrome では情報漏洩対策ですべてのローカルファイルが別オリジンだとみなされる」という仕様によるものなので、回避不可能です。

横着しないで、ローカルでもWEBサーバーをちゃんと立てましょうということですね。

実行結果の例です 

こういう写真に対して「検出開始」ボタンを押すと。

f:id:arakan_no_boku:20210217225001p:plain

結果はこんな感じ。

f:id:arakan_no_boku:20210217225114p:plain

よしよし。

とりあえず、いけてます。

JavaScriptのソース全体 

HTMLは上記を参考にして、適当にレイアウトして作ってください。

顔写真は適当にネットからひろってきて、リサイズしてください。

横を向いた顔だと認識されないときがあったり、あまり、サイズの小さい顔はスルーされたりもしますので、何枚かためして、感触をつかむ必要はあるかもです。

var facefinder_classify_region = function(r, c, s, pixels, ldim) {return -1.0;};
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 nrow = 341;
//var ncol = 512;
var ctx = document.getElementById('canvas').getContext('2d');
var img = document.getElementById('image');
img.onload = () => ctx.drawImage(img, 0, 0);

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;
}

function fnc_callback(nrow, ncol) {
    ctx.drawImage(img, 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": 20,       
        "maxsize": 1000,     
        "scalefactor": 1.1 
    }

    dets = pico.run_cascade(image, facefinder_classify_region, params);
    dets = pico.cluster_detections(dets, 0.2);
    qthresh = 5.0
    for(i=0; i<dets.length; ++i){
        if(dets[i][3]>qthresh)
        {
            ctx.beginPath();
            ctx.arc(dets[i][1], dets[i][0], dets[i][2]/2, 0, 2*Math.PI, false);
            ctx.lineWidth = 3;
            ctx.strokeStyle = 'red';
            ctx.stroke();
        }
    }
}

ではでは。

参考:第二回目と第三回目のリンクです

arakan-pgm-ai.hatenablog.com

arakan-pgm-ai.hatenablog.com