「THE MNIST DATABASE of handwritten digits」ページからダウンロードし、Windowsなら7-Zipアプリなどで展開して現れたt10k-images.idx3-ubyte、t10k-labels.idx1-ubyte、train-images.idx3-ubyte、train-labels.idx1-ubyteファイルは、以降で説明するサンプルのHTMLファイルと同じフォルダなどに置いておきます。
以下では、MNISTの画像データをJavaScriptから扱えるようにするために、次のようなアプリを作成します。
- HTMLフォームの[ファイルを選択]ボタンをクリックする。
- [開く]ダイアログボックスで、t10k-images.idx3-ubyteかtrain-images.idx3-ubyteを選ぶ。
- ファイルに含まれるデータを読み取り、すべての画像データを含む配列を作成する。
- その配列を使って、画像を画面に表示する。
HTMLファイルでは、type=”file”を指定したinput要素を作成します。これは、画像データのファイルの読み取りに使用します。また画像表示に利用するdiv要素も作成します。
<input type="file" id="input-file">
<div id="image-container"></div>
以下はJavaScriptコードです。
const inputFile = document.getElementById('input-file');
inputFile.addEventListener('change', (e) => {
// 取得したファイル
const files = e.target.files;
console.log(files[0].name); // ファイル名
// ファイルの読み取りに成功したら、readPromise.then()を実行する
new Promise((resolve, reject) => {
const reader = new FileReader();
// ファイルの読み取りに成功したら解決する
reader.onload = (e) => {
resolve(e.target.result);
}
// MNIST画像ファイルをArrayBufferとして読み取る
reader.readAsArrayBuffer(files[0]);
}).then((buffer) => {
// ArrayBufferオブジェクトを受け取る
console.log(buffer); // ArrayBuffer(47040016)
// 扱いやすいようにDataViewオブジェクトにする
const dataView = new DataView(buffer);
// マジックナンバー
const magicNum = dataView.getInt32(0);
console.log(magicNum); // 2051
// 含まれる画像の数
const itemNum = dataView.getInt32(4);
console.log(itemNum); // 60000
//画像の高さ
const rows = dataView.getInt32(8);
console.log(rows); // 28
// 画像の幅
const columns = dataView.getInt32(12);
console.log(columns); // 28
// データを入れる配列
const dataArray = [];
let offset = 0;
// 含まれる画像数分だけ繰り返す
for (let i = 0; i < itemNum; i++) {
dataArray.push([]);
for (let j = 0; j < rows; j++) {
for (let k = 0; k < columns; k++) {
// dataArray配列に。dataViewからの画像データを追加
// 画像データは0016から始まるので、その分を足しておく
dataArray[i].push(dataView.getUint8(16 + offset));
offset++;
}
}
}
// dataArray配列には、60,000個の画像(28x28)データが含まれる
console.log(dataArray.length); // 60000
console.log(dataArray[0]); // 1つめのデータ
// dataArrayに集めた画像データの中から1つを実際に表示してみる
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = columns;
canvas.height = rows;
// 空のイメージデータ
imageData = context.createImageData(canvas.width, canvas.height);
// 表示したい画像の、dataArray配列でのインデックス番号
const dataIndex = 0;
// 空のimageDataに、dataArray配列内の適切なデータを割り当てる。
for (let x = 0; x < imageData.height; x++) {
for (let y = 0; y < imageData.width; y++) {
var index = x + (y * imageData.width);
let step4 = index * 4;
// RGBには同じ値を設定し、モノクロにする
// [R]
imageData.data[step4 + 0] = dataArray[dataIndex][index];
// [G]
imageData.data[step4 + 1] = dataArray[dataIndex][index];
// [B]
imageData.data[step4 + 2] = dataArray[dataIndex][index];
// [alpha]
// 透明度はなし
imageData.data[step4 + 3] = 255;;
}
}
// キャンバスにイメージデータを描画して、ページに表示する
context.putImageData(imageData, 0, 0);
document.getElementById('image-container').appendChild(canvas);
});
}, false);
たとえば訓練用画像データを含んだtrain-images.idx3-ubyteを開くと、下図のような結果が得られます。
1つめの出力はファイル名です。type属性を”file”にしたinput要素で、’change’イベントの発生を監視すると、ユーザーが能動的に開こうとしているファイルを特定することができます。ここではそのファイルをconst files = e.target.filesで取得し、ファイル名をfiles[0].nameで出力しています。
2つめのArrayBuffer(47040016)という出力は、ファイルをFileReaderのreadAsArrayBuffer()で読み取った結果の生データを指しています。47040016という値は、28 x 28 x 60,000 + 16(ヘッダ領域分)に一致します。ArrayBufferはそのままでは操作できないので、ここではDataViewオブジェクトを使って扱いやすくしています。
3つめの2051は、dataView.getInt32(0)によるマジックナンバーの出力です。同様に、60000はdataView.getInt32(4)の、2つの28はdataView.getInt32(8)とdataView.getInt32(12)で得た結果の出力です。いずれもtrain-images.idx3-ubyteを説明する次の表と一致しています。
コードではその後、配列dataArrayに画像データを集め、確認としてdataIndexで指定されたインデックスにあるdataArray内の画像データを、実際に描画しています。dataIndexを0にすると、下図の5に読める画像が表示されるので、dataArray内の最初の画像データは5であることが分かります。