【JavaScript】MNISTデータベース(バイナリファイル)を扱う
はじめに
JavaScriptでバイナリデータを扱いたい。 MNISTデータベースの教師用画像をJavaScriptのcanvasを用いて表示する。 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.data 読取専用 RGBA の順で 0 から 255 の間の整数 (両端の値を含む) を並べたデータを持つ 1 次元配列を表す Uint8ClampedArray です。
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で書いてみることにした。。