프로그래밍/Opencv

[Opencv] C++ Mnist 데이터셋 읽어오기 (CPU 엔디언, magic number 설명)

하루에 한번 방문하기 2021. 3. 21. 19:36

 

Mnist 데이터셋 출력 GIF

 


 

Mnist 데이터 파일 형식

Mnist 손글씨 데이터셋 공식 데이터 베이스 :

yann.lecun.com/exdb/mnist/index.html

 

MNIST handwritten digit database, Yann LeCun, Corinna Cortes and Chris Burges

 

yann.lecun.com

위 문서를 내리다 보면,

파일 형식 부분이 나옵니다.

 

내용을 표로 정리하면

훈련 레이블 파일 32bit(=4byte) + 32bit(=4byte) + 8bit(=1byte) * 이미지 수
안에 들어있는 데이터 매직 넘버 이미지 수 레이블(정답 데이터)
훈련 세트 파일 32bit(=4byte) +32bit(=4byte) +32bit(=4byte) +32bit(=4byte) + 8bit(=1byte) *
행*열*이미지 수
들어있는 데이터 매직 넘버 이미지 수 이미지 픽셀 행 이미지 픽셀 열 픽셀

이렇게 됩니다.

 

문서의 내용은 이렇습니다.

훈련 세트 레이블 파일 (train-labels-idx1-ubyte) :

[오프셋] [유형] [값] [설명]
0000 32 비트 정수 0x00000801 (2049) 매직 넘버 (MSB 우선)
0004 32 비트 정수 60000 항목 수
0008 unsigned byte ?? 레이블
0009 unsigned byte ?? 레이블
........
xxxx unsigned byte ?? 레이블

레이블 값은 0-9입니다.

훈련 세트 이미지 파일 (train-images-idx3-ubyte) :

[오프셋] [종류] [값] [설명]
0000 32 비트 정수 0x00000803 (2051) 매직 넘버
0004 32 비트 정수 60000 이미지 수
0008 32 비트 정수 28 행 수
0012 32 비트 정수 28 열 수
0016 unsigned byte ?? 픽셀
0017 unsigned byte ?? 픽셀
........
xxxx unsigned byte ?? 픽셀

픽셀은 행 단위로 구성됩니다. 픽셀 값은 0 ~ 255입니다. 0은 배경 (흰색), 255는 전경 (검정)을 의미합니다.

 


 

파일에서 매직 넘버 데이터 파싱(parsing)

먼저 매직 넘버를 읽겠습니다.

 

마찬가지로 내용을 표로 정리하면 이렇습니다.

총합 4byte(32bit) 첫번째 byte(=8bit) 두번째 byte(=8bit) 세번째 byte(=8bit) 네번째 byte(=8bit)
매직 넘버 0 (공백) 0 데이터 유형(16진수) 차원 수

 

문서의 내용은 이렇습니다.

IDX 파일 형식

IDX 파일 형식은 다양한 숫자 유형의 벡터 및 다차원 행렬에 대한 간단한 형식입니다.

기본 형식은

차원 0의 매직 넘버 크기
차원 1의 크기
차원 2의 크기
.....
차원 N 데이터의 크기

매직 넘버는 정수 (MSB 먼저)입니다. 처음 2 바이트는 항상 0입니다.

세 번째 바이트는 데이터 유형을 코드합니다.
0x08 : 부호 없는 바이트
0x09 : 부호 있는 바이트
0x0B : short (2 바이트)
0x0C : int (4 바이트)
0x0D : float (4 바이트)
0x0E : double (8 바이트)

4 번째 바이트는 벡터 / 행렬의 차원 수를 코딩합니다. 벡터는 1, 행렬은 2입니다.

각 차원의 크기는 4 바이트 정수입니다. (대부분의 비 Intel 프로세서에서와 ​​같이 MSB 우선, 하이 엔디안).

데이터는 C 배열처럼 저장됩니다. 즉, 마지막 차원의 인덱스가 가장 빠르게 변경됩니다.

 


엔디안이란?

MNIST 데이터베이스의 파일 형식

....
파일의 모든 정수는 대부분의 비 Intel 프로세서에서 사용하는 MSB 우선 (하이 엔디안) 형식으로 저장됩니다. 

Intel 프로세서 및 기타 로우 엔디안 시스템 사용자는 헤더의 바이트를 뒤집어야 합니다.

저는 Intel CPU를 쓰고 있습니다.

출처 :   ko.wikipedia.org/wiki/엔디언

빅 엔디언 CPU는 32bit integer(정수)를 받았을 때

 8bit(=1byte) 씩 앞에서부터 메모리에 저장하는데,

Intel 제조사 같은리틀 엔디언 방식 CPU는 뒤에서부터 메모리에 저장합니다.

(각 방식의 장단점은 위키피디아,
자신의 CPU 엔디안 확인법 등은 www.joinc.co.kr/w/Site/Network_Programing/Documents/endian 에 잘 나와있습니다.)

 

매직 넘버 등 32비트 integer 형을 읽을 때 byte 단위로 거꾸로 재배치해야 합니다.

 


 

리틀 엔디안 CPU에서 32bit 정수형을 읽기 위해 재배치

ReverseInt 함수 결과

int main(){
	//시프트 연산 테스트
	std::cout << "10진수 2144444444 = 2진수로 "<<std::bitset<32>(2144444444) << std::endl;
	std::cout << "10진수 2144444444를 ReverseInt 2진수로 " <<std::bitset<32>(op.ReverseInt(2144444444)) << std::endl;
	return 0;
}

//1바이트(8비트)씩 거꾸로 배열한 int를 반환
int OpencvPractice::ReverseInt(int i)
{
	unsigned char ch1, ch2, ch3, ch4;
	ch1 = i & 255;
	ch2 = (i >> 8) & 255; //i를 8비트만큼 시프트연산으로 버리고, 8비트만큼 AND연산으로 읽음
	ch3 = (i >> 16) & 255;
	ch4 = (i >> 24) & 255;
	return((int)ch1 << 24) + ((int)ch2 << 16) + ((int)ch3 << 8) + ch4;
}

32bit integer인 i를 2,144,444,444라고 가정해보고 실제로 연산해보면
 8bit(=1byte)씩 거꾸로 배열되었다는 것을 알 수 있습니다.

 


 

이미지 수, 이미지 행 열 정보, 픽셀 데이터 파싱

이미지 수와 행 열 정보도 매직 넘버를 ReverseInt로 파싱 했듯, 똑같이 ReverseInt로 파싱 합니다.

픽셀 데이터는 unsigned byte (=1byte)이기 때문에 unsigned char (=1byte)로 파싱 합니다.


전체 코드

필요한 헤더 파일 :

#include "opencv2/opencv.hpp"
#include <vector>
#include <iostream>
#include <fstream>

메인 함수

int main() {
	OpencvTutorial op;

	std::cout << "Hello OpenCV" << CV_VERSION << std::endl;

	//read MNIST iamge into OpenCV Mat vector
	std::vector<cv::Mat> trainingVec;
	std::vector<uchar> labelVec;
	op.MnistTrainingDataRead("Resources/train-images.idx3-ubyte", trainingVec, 10);
	op.MnistLabelDataRead("Resources/train-labels.idx1-ubyte", labelVec, 10);
	op.MatPrint(trainingVec, labelVec);
    
	return 0;
}

Mnist 데이터 읽어오기, 출력 함수들

//인자로 받은 vector에 Mnist 훈련 데이터를 파싱해 Matrix으로 저장
void OpencvPractice::MnistTrainingDataRead(std::string filePath, std::vector<cv::Mat>& vec, int readDataNum)
{
	std::ifstream file(filePath, std::ios::binary);
	if (file.is_open())
	{
		int magic_number = 0;
		int number_of_images = 0;
		int n_rows = 0;
		int n_cols = 0;

		//ifstream::read(str, count)로 count만큼 읽어 str에 저장
		//char은 1바이트, int는 4바이트이므로 int 1개당 char 4개의 정보만큼 가져옴
		file.read((char*)&magic_number, sizeof(magic_number));
		magic_number = ReverseInt(magic_number);
		file.read((char*)&number_of_images, sizeof(number_of_images));
		number_of_images = ReverseInt(number_of_images);
		file.read((char*)&n_rows, sizeof(n_rows));
		n_rows = ReverseInt(n_rows);
		file.read((char*)&n_cols, sizeof(n_cols));
		n_cols = ReverseInt(n_cols);
		
		if (readDataNum > number_of_images || readDataNum <= 0)
			readDataNum = number_of_images;

		for (int i = 0; i < readDataNum; ++i)
		{
			cv::Mat tp = cv::Mat::zeros(n_rows, n_cols, ConvertCVGrayImageType(magic_number));
			for (int r = 0; r < n_rows; ++r)
			{
				for (int c = 0; c < n_cols; ++c)
				{
					//magicnumber에서 얻은 타입 정보가 unsigned byte 일 경우
					if (ConvertCVGrayImageType(magic_number) == CV_8UC1) {
						unsigned char temp = 0;
						file.read((char*)&temp, sizeof(temp));
						tp.at<uchar>(r, c) = (int)temp;
					}
				}
			}
			vec.push_back(tp);
		}
	}
}

void OpencvPractice::MnistLabelDataRead(std::string filePath, std::vector<uint8_t>& vec, int readDataNum)
{
	std::ifstream file(filePath, std::ios::binary);
	if (file.is_open())
	{
		int magic_number = 0;
		int number_of_images = 0;

		//ifstream::read(str, count)로 count만큼 읽어 str에 저장
		//char은 1바이트, int는 4바이트이므로 int 1개당 char 4개의 정보만큼 가져옴
		file.read((char*)&magic_number, sizeof(magic_number));
		magic_number = ReverseInt(magic_number);
		file.read((char*)&number_of_images, sizeof(number_of_images));
		number_of_images = ReverseInt(number_of_images);
		if (readDataNum > number_of_images || readDataNum <= 0)
			readDataNum = number_of_images;

		for (int i = 0; i < readDataNum; ++i)
		{
			//magicnumber에서 얻은 타입 정보가 unsigned byte 일 경우
			if (ConvertCVGrayImageType(magic_number) == CV_8UC1) {
				uint8_t temp = 0;
				file.read((char*)&temp, sizeof(temp));
				vec.push_back(temp);
			}
		}
	}
}

int OpencvPractice::ConvertCVGrayImageType(int magicNumber)
{
	magicNumber = (magicNumber >> 8) & 255; //3번째 바이트(픽셀 타입)만 가져오기
	//리틀 엔디안 CPU에서 magicNumber = ((char*)&magicNumber)[1];와 같음
	//빅 엔디안 CPU에서 magicNumber = ((char*)&magicNumber)[2];와 같음

	switch (magicNumber) {
	case 0x08: return CV_8UC1;//unsigned byte, 흑백 채널 단일
	case 0x09: return CV_8SC1;//signed byte, 흑백 채널 단일
	case 0x0B: return CV_16SC1;//short(2 바이트), 흑백 채널 단일
	case 0x0C: return CV_32SC1;//int(4 바이트), 흑백 채널 단일
	case 0x0D: return CV_32FC1;//float(4 바이트), 흑백 채널 단일
	case 0x0E: return CV_64FC1;//double(8 바이트), 흑백 채널 단일
	default: return CV_8UC1;
	}
}

void OpencvPractice::MatPrint(std::vector<cv::Mat>& trainingVec, std::vector<cv::uint8_t>& labelVec)
{
	std::cout << "읽어온 훈련 데이터 수 : " << trainingVec.size() << std::endl;
	std::cout << "읽어온 정답 데이터 수 : " << labelVec.size() << std::endl;

	cv::namedWindow("Window", cv::WINDOW_AUTOSIZE);

	for (int i = 0; i < labelVec.size() && i < trainingVec.size(); i++) {
		imshow("Window", trainingVec[i]);
		std::cout << i << "번째 이미지 정답 : " << (int)labelVec[i] <<std::endl;
		//아무 키나 누르면 다음
		if (cv::waitKey(0) != -1)
			continue;
	}
}

출처 :

Mnist 데이터셋 도움됐던 설명글 (ubyte 파일 읽기)

givemesource.tistory.com/1?category=681642

 

Mnist with OpenCV (1)

Mnist 시작하기 (1)  C++과 OpenCV를 활용하여 Mnist를 하려고 하니, 자료 찾기가 너무 힘들어서 공유하고자 만들었다. 우선, Mnist의 데이터 셋은 Yann LeCun 사이트에 있다. 해당 사이트에서 트레이닝할

givemesource.tistory.com

blog.naver.com/acwboy/220584307823

 

파이썬으로 mnist 읽기 (python + mnist)

딥러닝에 관심을 가지다보니 대부분 이미지처리에 관한 얘기들이 많더군요딥러닝에 대해 정보를 찾던중에 ...

blog.naver.com

2진수 출력

blog.naver.com/herbbread/220817372685

 

c++ cout 2진수, 8진수, 16진수 출력

<< c++ cout 2, 8, 16 진수 출력하기 >> //< ↓ #include ↓ cout <<...

blog.naver.com

프로세서에 따라 다른 비트 수

blog.naver.com/eslectures/80143511938

 

고정된 길이의 정수 데이터 형

임베디드 프로그래밍 환경에서는 C 언어의 표준 데이터 형 대신 uint8_t, int16_t 등의 사용자 정의 데이...

blog.naver.com


손글씨 숫자 인식할 때 

스마트폰과 로컬 호스트끼리 연결해서 해보고 싶네요.

통신할 때도 엔디안 나오던데.. 아무튼 서버 쪽 공부해야 할 듯