JavaScriptのコンピュータビジョンライブラリ「jsfeat」を試す

jsfeatは純粋にJavaScriptのみで動作するコンピュータビジョンのライブラリです。

以下がプロジェクトのサイトです。いわゆる基本的な画像処理、線形代数、特徴点抽出などなど色々とサポートしており、期待のプロジェクトです。
http://inspirit.github.io/jsfeat/
https://github.com/inspirit/jsfeat

「このプロジェクトの目標は、最先端のコンピュータビジョンアルゴリズムを実装することで、JavaScript/HTML5の可能性を探ること」だそうです。コンピュータビジョンの研究者としてはここは目的と手段を逆に捉えるところだと思いますが、私は開発者志向なので、この姿勢は大変共感できるところです。

実際のところ触ってみて、思ったよりも精度よく、かつなかなか高速に動作し、十二分に最近のWebテクノロジの進展を感じたところです。
相当苦戦中ですが、ごく簡単な使い方をまとめておきます。

なお、本記事で作った実際に動作するサンプルは、以下からお試しください。

画像読み込み

Image → Canvas → ImageData の流れで、ピクセルデータを取得します。

1. Imageに読み込み

var image = new Image();
    
image.onload = function () {
    // 画像読み込み完了時の処理
};

image.src = "lenna.png";

2. Canvasに書き出し

HTMLにて、空の<canvas>要素を書いてある前提です。Image.onloadの中に書いていきます。

CanvasRenderingContext2D の扱いはリファレンスを参考に。
https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D

image.onload = function () {
    var canvas = $('#srcCanvas')[0];
    canvas.width = image.width;
    canvas.height = image.height;
    var context = canvas.getContext("2d");
    context.drawImage(image, 0, 0);

};

3. ImageDataを取得

var srcImageData = context.getImageData(0, 0, image.width, image.height);

回りくどいですが、これでピクセルデータが得られました。ImageDataオブジェクトの data の中を見れば、ピクセルデータが8ビット4チャネル(RGBA)の配列で並んでいるのが確認できます。

グレースケール化

ImageDataを入力とし、出力を jsfeat.matrix_t として、ここまでに取得したカラー画像をグレースケールに変換します。

var gray = new jsfeat.matrix_t(image.width, image.height, jsfeat.U8C1_t);
jsfeat.imgproc.grayscale(srcImageData.data, gray.data);

試している限りなので違うのかもしれませんが、このjsfeat、何事もグレースケールにしないと始まらないように見えます。
ガウシアンフィルタなどの処理は、カラー画像での実行がうまくできませんでした。本家のサンプルでもどれもグレースケールの処理になっており、そのような仕様なのかもしれません。

ガウシアンフィルタ処理

グレースケール化の後の処理のサンプルとして、ガウシアンフィルタをかけるコードを挙げておきます。
grayscale関数では、.dataを付けて画像を渡しましたが、gaussian_blur含めそのほかの関数では要らないようです。この辺の仕様はややこしい。

var gaussian = new jsfeat.matrix_t(width, height, jsfeat.U8C1_t);
jsfeat.imgproc.gaussian_blur(gray, gaussian, 7, 20);

Canvasへ出力

出力画像を見える形にするためにCanvasへ書き出したいところなのですが、入力同様ここも(おそらく)jsfeatはサポートしてくれていません。公式サンプルを見ると自分でごりごりピクセルデータを移し替えていたので、そうするしかないようです。

8ビット1チャネルのmatrix_t専用ですが、こんな変換関数を定義しておきます。

jsfeat.matrix_t.prototype.copyToImageDataU8C1 =
    function (dstImageData) {
        var width = dstImageData.width;
        var height = dstImageData.height;
        var dst = dstImageData.data;
        var src = this.data;
        var r, c, v, dstOffset;
        for (r = 0; r < height; r++) {
            for (c = 0; c < width; c++) {
                v = src[(r * width) + c];
                dstOffset = (r * width * 4) + (c * 4);
                dst[dstOffset + 0] = v; // R
                dst[dstOffset + 1] = v; // G
                dst[dstOffset + 2] = v; // B
                dst[dstOffset + 3] = 255; // A
            }
        }
    };

これを使うと、出力先CanvasのImageDataへピクセルデータを移し替えることができます。

var dstCanvas = $('#dstCanvas')[0];
dstCanvas.width = image.width;
dstCanvas.height = image.height;
var dstContext = dstCanvas.getContext("2d");
var dstImageData = dstContext.createImageData(image.width, image.height);
        
gaussian.copyToImageDataU8C1(dstImageData); // ここ
dstContext.putImageData(dstImageData, 0, 0);

ImageDataとmatrix_tの相互変換、知らないだけなら良いのですが、無いのなら何とか用意して頂きたいところですね。

サンプルコード一覧

jQueryを使っていますが、ここではたいして使っていません。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>jsfeat gaussian test</title>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
        <script type="text/javascript" src="jquery-ui/jquery-1.10.2.js"></script>
        <script type="text/javascript" src="jsfeat.js"></script>
        <script type="text/javascript">

jsfeat.matrix_t.prototype.copyToImageDataU8C1 =
    function (dstImageData) {
        var width = dstImageData.width;
        var height = dstImageData.height;
        var dst = dstImageData.data;
        var src = this.data;
        var r, c, v, dstOffset;
        for (r = 0; r < height; r++) {
            for (c = 0; c < width; c++) {
                v = src[(r * width) + c];
                dstOffset = (r * width * 4) + (c * 4);
                dst[dstOffset + 0] = v; // R
                dst[dstOffset + 1] = v; // G
                dst[dstOffset + 2] = v; // B
                dst[dstOffset + 3] = 255; // A
            }
        }
    };

$(document).ready(function () {
    var image = new Image();
    
    image.onload = function () {
        var width = image.width;
        var height = image.height;

        // src canvas
        var srcCanvas = $('#srcCanvas')[0];
        srcCanvas.width = width;
        srcCanvas.height = height;
        var srcContext = srcCanvas.getContext("2d");
        srcContext.drawImage(image, 0, 0);
        
        // dst canvas
        var dstCanvas = $('#dstCanvas')[0];
        dstCanvas.width = width;
        dstCanvas.height = height;
        var dstContext = dstCanvas.getContext("2d");
        
        // ImageData
        var srcImageData = srcContext.getImageData(0, 0, width, height);
        var dstImageData = dstContext.createImageData(width, height);
        
        // grayscale -> gaussian
        var gray = new jsfeat.matrix_t(width, height, jsfeat.U8C1_t);
        var gaussian = new jsfeat.matrix_t(width, height, jsfeat.U8C1_t);
        jsfeat.imgproc.grayscale(srcImageData.data, gray.data);
        jsfeat.imgproc.gaussian_blur(gray, gaussian, 7, 20);
        
        // render result image to canvas
        gaussian.copyToImageDataU8C1(dstImageData);
        dstContext.putImageData(dstImageData, 0, 0);
    };

    image.src = "lenna.png";
});
        </script>
    </head>
    <body>
        <canvas id="srcCanvas"></canvas>
        <canvas id="dstCanvas"></canvas>
    </body>
</html>

f:id:Schima:20140304233150p:plain

Cannyエッジ検出のサンプル

先日の記事にて、OpenCvSharp + ASP.NET + Ajax でCannyエッジ検出を実行するサンプルを公開していました。jsfeatを使えば、JavaScriptだけでそれが実現できてしまいます。Canny程度であれば充分高速です。

jQuery UIによるスライダでCannyの2つの閾値を変化させると、それが即時にCanvas画像に反映されるサンプルです。

記事の最初にも書きましたが、以下から実際に動作するものを閲覧できます。
http://notiz.flnet.org/jsfeat/canny.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>jsfeat canny test</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <link rel="stylesheet" href="jquery-ui/themes/base/jquery-ui.css">
    <script type="text/javascript" src="jquery-ui/jquery-1.10.2.js"></script>
    <script type="text/javascript" src="jquery-ui/ui/jquery-ui.js"></script>
    <script type="text/javascript" src="jsfeat.js"></script>
    <script type="text/javascript">

jsfeat.matrix_t.prototype.copyToImageDataU8C1 =
    function (dstImageData) {
        var width = dstImageData.width;
        var height = dstImageData.height;
        var dst = dstImageData.data;
        var src = this.data;
        var r, c, v, dstOffset;
        for (r = 0; r < height; r++) {
            for (c = 0; c < width; c++) {
                v = src[(r * width) + c];
                dstOffset = (r * width * 4) + (c * 4);
                dst[dstOffset + 0] = v; // R
                dst[dstOffset + 1] = v; // G
                dst[dstOffset + 2] = v; // B
                dst[dstOffset + 3] = 255; // A
            }
        }
    };

var image = new Image();
var initialTh1 = 50;
var initialTh2 = 200;

function update(th1, th2) {
    var width = image.width;
    var height = image.height;

    // creates source image data using canvas
    var canvas = $('#myCanvas')[0];
    canvas.width = width;
    canvas.height = height;
    var context = canvas.getContext("2d");
    context.drawImage(image, 0, 0);
    var imageData = context.getImageData(0, 0, width, height);
    
    // grayscale -> canny
    var gray = new jsfeat.matrix_t(width, height, jsfeat.U8C1_t);
    var canny = new jsfeat.matrix_t(width, height, jsfeat.U8C1_t);
    jsfeat.imgproc.grayscale(imageData.data, gray.data);
    jsfeat.imgproc.canny(gray, canny, th1, th2);
    
    // render result image to canvas
    canny.copyToImageDataU8C1(imageData);
    context.putImageData(imageData, 0, 0);
}

// onload
$(document).ready(function () {

    // setups sliders
    $("#slider1").slider({
        min: 0,
        max: 255,
        value: initialTh1,
        animate: "fast",
        slide: function (event, ui) {
            $("#sliderValue1").html(ui.value);
            update(ui.value, $("#slider2").slider("value"));
        },
    }).css({
        "width": "500px",
    });
    $("#slider2").slider({
        min: 0,
        max: 255,
        value: initialTh2,
        animate: "fast",
        slide: function (event, ui) {
            $("#sliderValue2").html(ui.value);
            update($("#slider1").slider("value"), ui.value);
        },
    }).css({
        "width": "500px",
    });
    $("#sliderValue1").html(initialTh1);
    $("#sliderValue2").html(initialTh2);

    // loading image
    image.onload = function () {
        update(initialTh1, initialTh2);
    };
    image.src = "lenna.png";
});
    </script>
</head>
<body>
    <div style="margin-top: 5px;">
        Threshold1: <span id="sliderValue1" style="font-weight: bold"></span>   
        <div id="slider1"></div>
    </div> 
    <div style="margin-top: 10px;">
        Threshold2: <span id="sliderValue2" style="font-weight: bold"></span>   
        <div id="slider2"></div>
    </div> 
    <canvas id="myCanvas" style="margin-top: 25px"></canvas>
</body>
</html>

f:id:Schima:20140304233159p:plain