Toshusai blog

知識の保管庫

【JavaScript】MNISTデータベース(バイナリファイル)を扱う

はじめに

JavaScriptでバイナリデータを扱いたい。 MNISTデータベースの教師用画像をJavaScriptcanvasを用いて表示する。 Qiitaから移行。

MNISTデータセットの入手

よく手書き文字認識に使われるデータベース http://yann.lecun.com/exdb/mnist/ ここからダウンロードできる。 今回は教師画像データの"train-images-idx3-ubyte.gz"をダウンロードして使う。 Windowsコマンドプロンプトgzip -d train-images-idx3-ubyte.gz で展開するとtrain-images-idx3-ubyteが出てくる。

上記のサイトに中身のフォーマットが書いてある

[offset] [type] [value] [description]
0000 32 bit integer 0x00000803(2051) magic number
0004 32 bit integer 60000 number of images
0008 32 bit integer 28 number of rows
0012 32 bit integer 28 number of columns
0016 unsigned byte ?? pixel
0017 unsigned byte ?? pixel
........
xxxx unsigned byte ?? pixel
Pixels are organized row-wise. Pixel values are 0 to 255. 0 means background (white), 255 means foreground (black).

HTMLを用意する

ファイルを読み込むためにinputを用意しておく。 今回はcanvasを使うのであらかじめhtmlにcanvasも用意しておく。 bodyは最低限でこんな感じ。

<body>
    <input type="file" id="imgfile">
    <br/>
    <canvas id="canvas"></canvas>
    <script type="text/javascript" src="main.js"></script>
</body>

バイナリデータを扱いやすく変換する

バイナリファイルを読み込むときに使うのがDataViewオブジェクト

new DataView(buffer [, byteOffset [, byteLength]]) DataView.prototype.getUint8() ビューの開始位置から指定されたバイト単位のオフセットで符号無し8ビット整数値(unsigned byte) を取得します。。 DataView.prototype.getInt32() ビューの開始位置からの指定されたバイト単位のオフセットで符号あり32ビット整数値(long)を取得します。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/DataView

画像の枚数、高さ、幅をフォーマットに従って取得してみる。

var input = document.getElementById("imgfile");           //input要素を取得
input.addEventListener("change", function(event){         //input要素に変更があったら発火
    var file = event.target.files;                        //FileListオブジェクトを取得
    var binaryReader = new FileReader();                  //FileReaderオブジェクトを生成
    binaryReader.readAsArrayBuffer(file[0]);              //ファイルをArrayBufferオブジェクトとして格納
    
    binaryReader.onload = function(){                     //読み込みが成功したら発火
        var dataView = new DataView(binaryReader.result); //DataViewオブジェクトを生成
        var number_of_images = dataView.getInt32(4);      //画像の枚数を取得
        var row = dataView.getInt32(8);                   //画像の行数(高さ)を取得
        var column = dataView.getInt32(12);               //画像の列数(幅)を取得
        console.log(number_of_images, row, column);       //確認用に出力
    }
}

無事60000 28 28と出力された。 それでは画像1枚1枚のデータを取得していきたい。 1枚の画像は28x28の784個のunsigned byte(0-255)で構成されるので、 要素が784個の1次元配列を60000個の、[60000, 784]の2次元配列で6万の画像を扱う。 DataViewからその配列に変換する関数を作成した。

var mnistToImageArray = function(dataView, number_of_images, row, column){
    var images = [];
    var offset = 0;
    for(var i = 0; i < number_of_images; i++){
        images.push([]);                                        //画像データを入れる1次元配列を挿入
        for(var j = 0; j < row; j++){
            for(var k = 0; k < column; k++){
                images[i].push(dataView.getUint8(16 + offset)); //Uint8を入れていく
                offset++;                                       //offsetを次へ
            }
        }
    }
    return images;
}

画像をcanvasに描画する

まずはcanvas、contextを取得してImageDataオブジェクトを作る。 ImageDataとは

ImageData インターフェイスは、 要素の領域の基礎をなすピクセルデータを表します。ImageData() コンストラクターや、canvas に関連付けられた CanvasRenderingContext2D オブジェクトの createImageData() メソッドおよび getImageData() メソッドによって生成されます。ImageData は putImageData() メソッドの第 1 引数として利用可能です。

ImageData.data 読取専用 RGBA の順で 0 から 255 の間の整数 (両端の値を含む) を並べたデータを持つ 1 次元配列を表す Uint8ClampedArray です。

https://developer.mozilla.org/ja/docs/Web/API/ImageData

canvasを28x28に変えてからImageDataオブジェクトを作る。

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
canvas.width = column;
canvas.height = row;
var dstData = ctx.createImageData(canvas.width, canvas.height);

作ったImageDataを先ほど1次元配列にした画像データを入れる関数を作る。 1次元配列なので2重ループでのインデックスに注意。 MNISTのデータは0を黒、255を白にしているので注意。

var dataArrayToImageData = function(dataArray, imageData){  //dataArray->独自に変換した画像データ
    for(var i = 0; i < imageData.height; i++){
        for(var j = 0; j < imageData.width; j++){
            var idx = i + j * imageData.width;               //1次元配列の適したインデックスを指すように変数を作る
            var idx2 = idx * 4;                              //ImageData.dataはRGBAの情報があるのでインデックスを4倍する
            imageData.data[idx2] = 255 - dataArray[idx];     //白黒反転させる
            imageData.data[idx2 + 1] = 255 - dataArray[idx]; //RGBに同じ値を入れる
            imageData.data[idx2 + 2] = 255 - dataArray[idx];
            imageData.data[idx2 + 3] = 255;                  //不透明度は最大
        }
    }
}

ここまできたら今までのを全部まとめて、ctx.putImageData()でImageDataをcanvasに入れるだけ。 最終的なソースがこれ。

var mnistToImageArray = function(dataView, number_of_images, row, column){
    var images = [];
    var offset = 0;
    for(var i = 0; i < number_of_images; i++){
        images.push([]);                                        //画像データを入れる1次元配列を挿入
        for(var j = 0; j < row; j++){
            for(var k = 0; k < column; k++){
                images[i].push(dataView.getUint8(16 + offset)); //Uint8を入れていく
                offset++;                                       //offsetを次へ
            }
        }
    }
    return images;
}

var dataArrayToImageData = function(dataArray, imageData){  //dataArray->独自に変換した画像データ
    for(var i = 0; i < imageData.height; i++){
        for(var j = 0; j < imageData.width; j++){
            var idx = i + j * imageData.width;               //1次元配列の適したインデックスを指すように変数を作る
            var idx2 = idx * 4;                              //ImageData.dataはRGBAの情報があるのでインデックスを4倍する
            imageData.data[idx2] = 255 - dataArray[idx];     //白黒反転させる
            imageData.data[idx2 + 1] = 255 - dataArray[idx]; //RGBに同じ値を入れる
            imageData.data[idx2 + 2] = 255 - dataArray[idx];
            imageData.data[idx2 + 3] = 255;                  //不透明度は最大
        }
    }
}

var input = document.getElementById("imgfile");           //input要素を取得
input.addEventListener("change", function(event){         //input要素に変更があったら発火
    var file = event.target.files;                        //FileListオブジェクトを取得
    var binaryReader = new FileReader();                  //FileReaderオブジェクトを生成
    binaryReader.readAsArrayBuffer(file[0]);              //ファイルをArrayBufferオブジェクトとして格納
    
    binaryReader.onload = function(){                     //読み込みが成功したら発火
        var dataView = new DataView(binaryReader.result); //DataViewオブジェクトを生成
        var number_of_images = dataView.getInt32(4);      //画像の枚数を取得
        var row = dataView.getInt32(8);                   //画像の行数(高さ)を取得
        var column = dataView.getInt32(12);               //画像の列数(幅)を取得
      
        var canvas = document.getElementById("canvas");
        var ctx = canvas.getContext("2d");
        canvas.width = column;
        canvas.height = row;
        var dstData = ctx.createImageData(canvas.width, canvas.height);
        
        var images = mnistToImageArray(dataView, number_of_images, row, column);
        dataArrayToImageData(images[0], dstData);
        ctx.putImageData(dstData, 0, 0);
    }
}, false);

最後に

Deep Learningを勉強していてMNISTデータベースを使った手書き文字認識というのが最初にでてきたので、やってみようと思った。 Deep Learningの主流はpythonで、pythonでモジュールを使って楽にMINSTデータベースを扱えるらしいが、なんとなくJavaScriptで書いてみることにした。。