Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

C++によるMNIST画像化と保存の方法

More than 3 years have passed since last update.

MNISTを画像化の環境

 MNISTとはなにか?についてはここでは詳しく述べませんが、0~9の数字の画像ファイルをバイナリーデーター化した機械学習用の訓練データファイルであることはご存知かと思います。機械学習を学ぶ上で初歩の段階で目にすることになるキーワードかもしれません。

 MNISTを画像化する方法はいくつかのアプローチがありますが、その一つをWindows環境とVisualStudioで実践します。使用した環境は以下の通りです。

  • Windows 10 (8.1以降)
  • Visual Studio 2017
  • C++
  • .NET Framework V4.5.2(バージョンは最新でなくてもOK)

 注意する点は、上記にある通り、.NET の環境が必要になります。Visual Studioには通常.NETが標準装備されていますから、あとはVSのコンポーネントとしてC++をダウンロードしておく必要があります。

 準備とその手順

 まずはMNISTをVisual Studioのプロジェクトのソースファイルのあるフォルダーに「data」という名前のフォルダーを作成します。名前は自由ですが、ここではdataということにします。

 そのフォルダーにMNSTからダウンロードしてきた以下のファイルを「7ZIP」で解凍したファイルを入れておきます。7zipはプログラマーでなくても必携ですので、必ずPCに入れておきたいソフトです。

  • train-images-idx3-ubyte.gz
  • train-labels-idx1-ubyte.gz

解凍すると、gzの部分が消え、新しく拡張子がつきます。ファイルが以下の名前であることを確認します。

  • train-images.idx3-ubyte
  • train-labels.idx1-ubyte

 この二つのファイルは一つの組になっており、imagesのほうは28×28のピクセルデーター、labelsのほうはピクセルデーターの数字名が入っています。imagesのピクセルを解読しながら、labelsからそのピクセルデーターの数字名を当てはめるという手法がとられています。

 また、データーは1,2,3,4・・・のように順番に並んでいるのではなく、8,3,7,5・・・のようにバラバラになった状態で入っています。

この二つのファイルが指定のフォルダーに入っていないと今回のプログラムは動作せず、終了します。

Vsiual Studio での編集

 Visual Studioが2017に最近アップグレードしましたが、それ以前のバージョンでも動作します。今回は2017を使用します。
 
C++で空のCLRプロジェクトを作成します。そこにcppファイルとヘッダーファイルを作成します。もちろん、cppファイル1枚にコードを書いても問題ありません。

 ヘッダーファイルの最初に以下の部分を書き込みますが、ここで名前空間System::Windows::Mediaを追加しても認識しないので、ソリューションエクスプローラーの参照を右クリック、追加でFramenetworkにある「PresentationCore」を追加しておきます。また、System.DrawingとSystemも合わせて参照に追加しておきます。

ヘッダーファイルに追加
#include <iostream>

using namespace System;
using namespace System::Drawing;
using namespace System::Windows::Media;
using namespace System::Windows::Media::Imaging;

 ではプログラムの内容を見ていきます。

 処理の流れは以下のようになります。

  • 画像を保存するフォルダーが存在するか確認
  • 上記フォルダーがなければ作成する
  • imagesファイルとlabelsを開く
  • imagesファイルの内容を解読
  • 同時にlabelsファイルを解読
  • 解読したデーターからbmpファイルを作成
  • bmpファイルを各数字のフォルダーに保存

画像を保存するフォルダーが存在するか確認

この処理ついてはいくつものやり方がありますから、開発者の趣味趣向によってコードも様々あるはずです。

フォルダーが存在するか確認と作成
    if ((_access("./image/", 0)) == -1) {
        _mkdir("./image/");
    }

    for (int i = 0; i < 10; i++) {

        stringstream ss;
        ss << "./image/" << i;
        string holder = ss.str();

        if ((_access(holder.c_str(), 0)) == -1) {
            _mkdir(holder.c_str());
        }
    }

関数_accessを使用するには<io.h>をインクルードします。フォルダーを確認し、無い場合は-1を返します。フォルダーを作成する_mkdirでは<direct.h>をインクルードします。

ここではimageというフォルダーとその中に0~9までの数字のフォルダーを作成します。

ファイルの解読と保存

BinaryReaderは.Netで使うバイナリーデータを読むための関数で、ReadInt32()は4バイト分のデーターを読み、同時にストリームの位置を4バイト進めるコマンドです。実は、train-images.idx3-ubyteというファイルの先頭から16バイト分はヘッダー部分で、ビッグエンディアンによって4バイトづつ4つの項目にファイル内容に関する情報が格納されています。Windowsマシンであればリトルエンディアンにする必要がありますので、変換する関数を後述します。ただ、画像を作成するだけならこの4つの項目は読み込まなくてもいいので、先頭から16バイト進んだ位置からデータを読み込んでも構いません。ここでは読み込みを0バイトから初めて、最初の16バイト分をコンソールで記述します。

以下にラベルファイル(train-labels.idx1-ubyte)のヘッダー部分読み込みコードを記述しました。ラベルファイルでは最初から8バイト部分がヘッダーです。4バイトづつ2回読み込みを繰り返すことで、ラベル用データー位置まで行きます。

ラベルのヘッダー解読
  System::String^ labelFile("./data/train-labels.idx1-ubyte");

    if (File::Exists(labelFile)) {
        labelReader = gcnew BinaryReader(File::Open(labelFile, FileMode::Open));
        Console::WriteLine("MNISTラベルファイルが見つかりました。");
        for (int i = 0; i < 2; i++) {
            labelValue[i] = labelReader->ReadInt32();
        }
        Console::WriteLine("マジック・ナンバー は " + reverseInt(labelValue[0]));
        Console::WriteLine("アイテム数は " + reverseInt(labelValue[1]) + "\n");

    }
    else { Console::WriteLine("MNISTラベルが見つかりませんでした。ファイルが存在するか確認してください。");
    system("pause");
    return 0;
    }

バイトオーダーの反転

上記のコードにreverseIntという関数があるのに注目してください。これはビッグエンディアンをリトルエンディアンに変換する関数です。これをしないと正しい数字が分かりません。変換するのはimagesファイルの最初の16バイト部分とlabelsファイルの最初の8バイト部分です。

reverceIntでバイトオーダーを反転
int reverseInt(int i)
{
    unsigned char c1, c2, c3, c4;

    c1 = i & 255;
    c2 = (i >> 8) & 255;
    c3 = (i >> 16) & 255;
    c4 = (i >> 24) & 255;

    return ((int)c1 << 24) + ((int)c2 << 16) + ((int)c3 << 8) + c4;
}


1バイトの読み込みを繰り返す

ようやく数字のデーター読み込みに入ります。数字は28×28の画像を、1ピクセルにつき1バイトでデーターが格納されています。1バイトは0-255で表示されます。labelReader->ReadByte()
によって、1バイト分データーを読み取っていきます。784ピクセル分を読み込んだら数字1文字分の画像が完成です。これを6万文字分繰り返すと完了です。テスト用コードでは20文字分を読み取るようにしてありますので、この部分を読みたい数字に変更してください。あとは画像ファイルを作成するだけです。

数字のピクセル読み込み
        for (int num = 0; num < numberOfImages; num++) {

            char numIndex = labelReader->ReadByte();
        //  Console::WriteLine(numIndex);

            for (int x = 0; x < 28; x++) {
                for (int y = 0; y < 28; y++) {
                    pixelV = imageReader->ReadSByte();
                    container[num].pixels[x][y] = pixelV;
                //  Console::Write(pixelV);
                }
            //  Console::Write("\n");
            }

            createBmpImage(container[num], num,numIndex);

        }

画像ファイルの作成

さっき読んだデーターを今度は画像ファイルに変換します。コードは.Netを使えば簡単です。データーはByte型で格納し、bmpImage->SetPixelで画像に書き込みます。

その下にあるFromArgb(255-colorCasted,255-colorCasted,255-colorCasted)の255-の部分を消すと、背景が黒、文字が白の反転文字になります。

保存ファイル名のbmppngjpgtiffexifgifにすることで画像の形式をかえることも可能です。

画像の作成
Bitmap^ createBmpImage(DigitImage image, int imageIndex,char imageLabel) {

    int width = 28;
    int height = 28;

    Bitmap^ bmpImage = gcnew  Bitmap(width, height);

    for (int x = 0; x < width; x++) {
        for (int y = 0; y < height; y++) {
            Byte colorDep = image.pixels[x][y];
            int colorCasted = colorDep;
            bmpImage->SetPixel(y, x, System::Drawing::Color::FromArgb(255-colorCasted,255-colorCasted,255-colorCasted));
        }
    }

    bmpImage->Save("./image/" +  imageLabel + "/index" + imageIndex + ".bmp");//  bmp,png,jpg,tiff,exif,gif
    return bmpImage;
}


終了すると下のようにプロジェクトフォルダーのimageフォルダーにそれぞれの数字が画像ファイル化されます。今回は20文字分だけ、解読し画像化しました。データーは6万文字あるので全てのファイルを解読するにはmain.cppのconst int numberOfImages = 20; を60000にしてください。

それでは Happy Hacking!

mnist.png

 MNIST_Readerのソースコード

projectHeader.h
#ifndef __PROJECTHEADER_H__
#define __PROJECTHEADER_H__

#include <iostream>

using namespace System;
using namespace System::Windows::Media;
using namespace System::Drawing;
using namespace System::Windows::Media::Imaging;

int reverseInt(int i)
{
    unsigned char c1, c2, c3, c4;

    c1 = i & 255;
    c2 = (i >> 8) & 255;
    c3 = (i >> 16) & 255;
    c4 = (i >> 24) & 255;

    return ((int)c1 << 24) + ((int)c2 << 16) + ((int)c3 << 8) + c4;
}

struct DigitImage {

    Byte pixels[28][28];

};


Bitmap^ createBmpImage(DigitImage image, int imageIndex,char imageLabel) {

    int width = 28;
    int height = 28;

    Bitmap^ bmpImage = gcnew  Bitmap(width, height);

    for (int x = 0; x < width; x++) {
        for (int y = 0; y < height; y++) {
            Byte colorDep = image.pixels[x][y];
            int colorCasted = colorDep;
            bmpImage->SetPixel(y, x, System::Drawing::Color::FromArgb(255-colorCasted,255-colorCasted,255-colorCasted));
        }
    }

    bmpImage->Save("./image/" +  imageLabel + "/index" + imageIndex + ".bmp");//  bmp,png,jpg,tiff,exif,gif
    return bmpImage;
}


#endif // 
main.cpp
#include "projectHeader.h"
#include <io.h>
#include <direct.h>
#include <sstream>

using namespace std;
using namespace System::IO;

int main()
{

    System::String^ imageFile("./data/train-images.idx3-ubyte");
    System::String^ labelFile("./data/train-labels.idx1-ubyte");

    int pixelValue[4];
    int labelValue[2];

    unsigned char pixelV;
    const int numberOfImages = 20;

    BinaryReader^ labelReader;


    if ((_access("./image/", 0)) == -1) {
        _mkdir("./image/");
    }

    for (int i = 0; i < 10; i++) {

        stringstream ss;
        ss << "./image/" << i;
        string holder = ss.str();

        if ((_access(holder.c_str(), 0)) == -1) {
            _mkdir(holder.c_str());
        }
    }

    if (File::Exists(labelFile)) {
        labelReader = gcnew BinaryReader(File::Open(labelFile, FileMode::Open));
        Console::WriteLine("MNISTラベルファイルがみつかりました。ファイルをロードします。");
        for (int i = 0; i < 2; i++) {
            labelValue[i] = labelReader->ReadInt32();
        }
        Console::WriteLine("マジックナンバーは、 " + reverseInt(labelValue[0]));
        Console::WriteLine("画像数は、 " + reverseInt(labelValue[1]) + "\n");

    }
    else { Console::WriteLine("MNISTラベルファイルが見つかりません。フォルダ内にファイルが存在するか確認してください。");
    return 0;
    }

    if (File::Exists(imageFile)) {
        Console::WriteLine("MNIST訓練ファイルが見つかりました。ロードします。");
        BinaryReader^ imageReader = gcnew BinaryReader(File::Open(imageFile, FileMode::Open));

        for (int i = 0; i < 4; i++) {
            pixelValue[i] = imageReader->ReadInt32();
        }

        Console::WriteLine("マジックナンバーは、 " + reverseInt( pixelValue[0]));
        Console::WriteLine("画像数は、" + reverseInt( pixelValue[1]));
        Console::WriteLine("画像の横ピクセル数は、" + reverseInt( pixelValue[2]));
        Console::WriteLine("画像の縦ピクセル数は、" + reverseInt( pixelValue[3]) + "\n");
        DigitImage container[numberOfImages];

        for (int num = 0; num < numberOfImages; num++) {

            char numIndex = labelReader->ReadByte();
        //  Console::WriteLine(numIndex);

            for (int x = 0; x < 28; x++) {
                for (int y = 0; y < 28; y++) {
                    pixelV = imageReader->ReadSByte();
                    container[num].pixels[x][y] = pixelV;
                //  Console::Write(pixelV);
                }
            //  Console::Write("\n");
            }

            createBmpImage(container[num], num,numIndex);

        }
    }
    else { Console::WriteLine("MNIST ファイルが存在しません。");
        system("pause");
    return 0;
    }

    system("pause");

    return 0;
}

takamon9
C/C++,ATL/MFC,C#(.NET),Python,OpenCv,VBAが得意。他にOpenSSL,ICU,SQL Serverなどを利用したWindowsアプリケーション作成などを嗜む。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away