이전 글을 통해 이미지의 픽셀에 접근하는 방법을 알아보았습니다.

 

글을 작성하면서 조금 아쉽게 느껴졌던 게 "픽셀에 접근하는 방법을 알겠지만 이를 어떻게 응용할 수 있는 것인가?" 에

 

대한 질문을 나에게 던지면 막상 떠오르는 것이 없었던 것이였습니다. 그래서 며칠을 생각한 결과, 입력받은 텍스트를

 

이미지 속 픽셀값에 변조시켜 이미지에 텍스트를 숨길 수 있는 방법에 대하여 소개하려 합니다.

 

 

 

먼저 암호화 시키는 기술에 대해 간단히 알아보도록 하겠습니다.

 

전달하려는 정보를 이미지나 MP3 파일 등에 암호화 하여 숨기는 기술을 스테가노그래피[Steganographt]이라고 합니다.

 

이 기술은 크게 삽입 기법수정 기법으로 나뉘어집니다.

 

 

삽입 기법은 이미지 파일 데이터를 변경하지 않고 추가 데이터를 파일 앞이나 뒤에 붙이는 방식입니다.

다시 말해,  jpg, png 등의 파일에서 데이터의 시작을 알리는 SOI(Start Of Image) 이전에 또는 끝을 알리는

EOI(End Of Image) 뒤에 전달하려는 정보를 추가하는 것입니다.

 

이러한 방법은 정보를 숨기려는 이미지에 가시적인 영향을 주진 않지만 이미지의 크기가 커지게 됩니다.

 

 

수정 기법은 이미지 파일에서 RGB 값의 최하위 비트(LSB)를 수정하여 추가 데이터를 입력하는 기법입니다.

 

이는 육안으로 알아차리기 힘들기 때문에 많이 이용되는 방식입니다.

 

다시 말해, 픽셀 값의 표현 가능한 수의 범위인 8비트(0~255)에서 특정 값이 255일 경우 최하위비트를 수정해 254 변조시켜도 우리 눈에는 255과 254가 거의 동일하게 보인다는 것입니다.

 

 

 

아래 코드는 스테가노그래피의 수정 기법을 이용한 방법입니다.

 

* 본 코드는 Opencv 3.1.0 Version에서 사용하였습니다.

 

< Image Encording >

#include <opencv2/opencv.hpp> 
#include <iostream>
#include <time.h>
#include <math.h>

using namespace cv;
using namespace std;

int main(int argc, char *argv[]) {

	// 이미지 입력
	/*
	if (argc < 2) {
		printf("이미지 경로를 입력하세요.\n");
		printf("파일 이름, 이미지 경로\n\n");
		exit(0);
	}
	else if (argc > 2) {
		printf("인자를 초과하여 작성하였습니다.\n");
		printf("파일 이름, 이미지 경로\n\n");
		exit(0);
	}
	*/
	
	Mat img = imread("C:\\test\\bridge.bmp");

	int max_text = img.cols*img.rows;  // 텍스트 작성 최대 범위 변수
	int t_length; // 입력 텍스트 길이
	int t_length_clone = 0;
	int inf_lo = 0; // 위치 정보 변수
	int data_text = 0;
	int data_start = 0;
	int x = 0, y = 0; // 좌표
	int cnt = 0;

	char str[10000];
	char e_bit_text[8] = { 0, };
	char e_bit_lo[8] = { 0, };
	char text_length[14] = { 0, };

	if (max_text < 50 * 50) {
		printf("50x50 크기 이상의 이미지를 입력하세요.\n");
		exit(0);
	}

	// 최대 텍스트 입력 수
	if (((max_text - 14) / 25) > 10000)
		max_text = 10000;
	else
		max_text = (max_text - 14) / 25;

	max_text = max_text - 1; // 널문자 제외
	do {
		printf("입력 가능 텍스트 수(띄어쓰기 포함) : %d \n\n", max_text);
		printf("------------------------------------------------------------------------\n");
		printf("TEXT : ");
		fgets(str, sizeof(str), stdin);
		t_length = strlen(str);
		printf("\n\nText length : %d\n", t_length - 1);
		printf("------------------------------------------------------------------------\n");

		if (t_length == 1) {
			printf("텍스트를 입력하지 않았습니다.\n\n");
			printf("다시 입력해주세요.\n\n\n\n");
		}

		else if (t_length  > max_text + 1) {
			printf("입력 가능 텍스트 수를 초과하였습니다.\n\n");
			printf("다시 입력해주세요.\n\n\n\n");
		}
	} while ((t_length == 1) || (t_length > max_text + 1));



	///////////////////////////////////////
	// 14bit 내, 위치 정보 저장  (범위  0~37,777) //
	//////////////////////////////////////

	t_length = t_length - 1; // 널문자 제외
	t_length_clone = t_length; // 복사

    // 입력 텍스트 길이 2진수 14비트로 저장
	for (int t = 0; t < 14; t++) {
		if (t_length_clone > 0) {
			text_length[13 - t] = t_length_clone % 2;
			t_length_clone = t_length_clone / 2;
		}
		else
			text_length[13 - t] = 0;
	}

	// 입력 텍스트 길이 데이터 좌표에 입력
	for (int t = 0; t < 14; t++) {
		if (img.at<Vec3b>(y, x)[0] % 2 == 1) {
			if (text_length[x] == 0)
				img.at<Vec3b>(y, x)[0] = img.at<Vec3b>(y, x)[0] - 1;
		}
		else if (img.at<Vec3b>(y, x)[0] % 2 == 0) {
			if (text_length[x] == 1)
				img.at<Vec3b>(y, x)[0] = img.at<Vec3b>(y, x)[0] + 1;
		}

		x++;
		if (x == img.cols) {
			x = 0;
			y++;
		}
	}
	srand(time(NULL)); // 랜덤값

	while (cnt < t_length)
	{
		// 첫 번째 정보(텍스트 정보가 Y축에 어느 X축 위치인지에 대한 정보
		inf_lo = rand();
		while (inf_lo > 10)
			inf_lo = inf_lo / 10;
		//inf_lo = inf_lo % 10;
		inf_lo = 9;
		//printf("난수 %d \n", inf_lo);

		// 위치 정보와 텍스트 데이터 위치 사이 좌표 거리
		data_start = inf_lo;

		// 문자 1개 변수에 대입
		data_text = str[cnt];

		// 위치 정보, 문자 8비트로 표현
		for (int t = 0; t < 8; t++)
		{
			// 위치
			if (inf_lo > 0) {
				e_bit_lo[7 - t] = inf_lo % 2;
				inf_lo = inf_lo / 2;
			}
			else
				e_bit_lo[7 - t] = 0;

			// 문자
			if (data_text > 0) {
				e_bit_text[7 - t] = data_text % 2;
				data_text = data_text / 2;
			}
			else
				e_bit_text[7 - t] = 0;
		}

		// 위치 정보 입력 // (y,0)을 기준으로 8픽셀 맨 하위 비트에 입력
		for (int q = 0; q < 8; q++) {

			if (img.at<Vec3b>(y, x)[0] % 2 == 1) {
				if (e_bit_lo[q] == 0)
					img.at<Vec3b>(y, x)[0] = img.at<Vec3b>(y, x)[0] - 1;
			}
			else if (img.at<Vec3b>(y, x)[0] % 2 == 0) {
				if (e_bit_lo[q] == 1)
					img.at<Vec3b>(y, x)[0] = img.at<Vec3b>(y, x)[0] + 1;
			}

			x++;
			if (x == img.cols) {
				x = 0;
				y++;
			}
		}


		x += data_start;
		if (x >= img.cols) {
			x = x - img.cols;
			y++;
		}

		// 데이터 정보 입력 // (y,8+위치정보)를 기준으로 8픽셀 맨 하위 비트에 입력
		for (int q = 0; q < 8; q++) {

			if (img.at<Vec3b>(y, x)[0] % 2 == 1) {
				if (e_bit_text[q] == 0)
					img.at<Vec3b>(y, x)[0] = img.at<Vec3b>(y, x)[0] - 1;
			}
			else if (img.at<Vec3b>(y, x)[0] % 2 == 0) {
				if (e_bit_text[q] == 1)
					img.at<Vec3b>(y, x)[0] = img.at<Vec3b>(y, x)[0] + 1;
			}

			x++;
			if (x == img.cols) {
				x = 0;
				y++;
				if (y == img.rows)
					printf("텍스트 입력 가능 범위 초과\n");
			}
		}
		cnt++;
	}

	imwrite("Encording_image.bmp", img);

	printf("텍스트 인코딩 완료!!!\n\n");
}

 

입력 텍스트

 

원본 이미지

 

수정 기법이 적용된 이미지

 

< Image Decording>

#include <opencv2/opencv.hpp> 
#include <iostream>
#include <math.h>

using namespace cv;
using namespace std;

int main() {

	// 데이터 입력
	Mat img = imread("Encording_image.bmp");

	if (img.empty() == 1) {
		printf("이미지를 로드하지 못했습니다.\n\n");
		exit(0);
	}

	int text_length[14] = { 0, };
	int lo_inf[8] = { 0, };
	int text_inf[8] = { 0, };
	int length = 0;
	int cnt = 0;
	int x = 0, y = 0;
	int text = 0;
	int lo = 0;
	int data_start = 0;
	char str[10000] = { 0, };


	// 텍스트 길이 정보 비트 추출
	for (int t = 0; t < 14; t++) {
		if (img.at<Vec3b>(0, t)[0] % 2 == 1)
			text_length[t] = 1;
		else if (img.at<Vec3b>(0, t)[0] % 2 == 0)
			text_length[t] = 0;
	}

	// 텍스트 길이 값
	for (int t = 0; t < 14; t++)
		length += text_length[t] * pow(2, 13 - t);

	// 텍스트 길이가 포함된 14비트 길이
	x = 14;

	while (cnt < length)
	{
		// 위치 정보 비트 추출
		for (int t = 0; t < 8; t++) {
			if (img.at<Vec3b>(y, x)[0] % 2 == 1)
				lo_inf[t] = 1;
			else if (img.at<Vec3b>(y, x)[0] % 2 == 0)
				lo_inf[t] = 0;

			x++;
			if (x == img.cols) {
				x = 0;
				y++;
			}
		}

		// 위치 정보 값
		for (int t = 0; t < 8; t++)
			lo += lo_inf[t] * pow(2, 7 - t);


		x += lo;
		if (x >= img.cols) {
			x = x - img.cols;
			y++;
		}

		// 텍스트 정보 비트 추출
		for (int t = 0; t < 8; t++) {
			if (img.at<Vec3b>(y, x)[0] % 2 == 1)
				text_inf[t] = 1;
			else if (img.at<Vec3b>(y, x)[0] % 2 == 0)
				text_inf[t] = 0;

			x++;
			if (x == img.cols) {
				x = 0;
				y++;
				if (y == img.rows)
					printf("디코딩 오류..\n");
			}
		}

		//텍스트 정보 값
		for (int t = 0; t < 8; t++)
			text += text_inf[t] * pow(2, 7 - t);

		// 정보 저장
		str[cnt] = text;

		//초기화
		lo = 0;
		text = 0;

		cnt++;
	}
	printf("받은 텍스트 : ");
	for (int i = 0; i < length; i++)
		printf("%c", str[i]);
	printf("\n\n");
}

 

Image Decording으로 추출한 텍스트

 

 

코드를 살펴보면, Image Encording을 수행할 때 가장 먼저 텍스트를 입력받고 텍스트의 길이와 각각의 글자들을

바이너리로 표현한다. 이후 아래 그림과 같이 바이너리 값을 원본 이미지의 픽셀에 입력시켜 변조합니다.

 

아래 그림은 이미지의 픽셀단위로 표현한 것이며, 위 결과의 입력 텍스트인 "I am Groot!!!"에서 일부인 글자 I와

 

띄어쓰기가 입력된 바이너리 값을 나타 것입니다. 아래 그림을 참고하면 이해 하는 데 도움이 될 것입니다.

 

 

 

파란색은 입력 받은 텍스트의 길이 정보(14비트) 

 

노란색은 조금의 보안성을 높일 수 있도록 텍스트 데이터를 입력시킬 픽셀의 위치 정보(8비트)

* 위치 정보는 랜덤값을 호출하여 나타냄

 

초록색은 텍스트 데이터의 정보(8비트)

 

 

이미지 픽셀에 입력된 정보의 바이너리 값

 

 

 

아래 구글 드라이브 올라간 파일은 위 코드의 exe파일입니다

 

위 코드를 작성하여 확인하는 것 보다 실행파일을 받아 하는 게 더 빠르게 확인이 가능할 거 같아 올렸습니다.

 

cmd창에서 실행 파일 경로로 들어가 [Image Encording.exe] [입력시킬 bmp 이미지] 순으로 작성하여 실행해 

 

텍스트를 입력한다. 그러면 새로운 Encording_image.bmp 파일이 생성됩니다.

 

그리고 다시 cmd창에서 Image Decording.exe을 실행하면 같은 폴더에 있는 Encording_image.bmp를 입력받아

 

받은 텍스트를 나타냅니다.

 

https://drive.google.com/file/d/1DG-47-y3NrUEZinmrq8rYmONPyN7JQ86/view?usp=sharing

 

Hiding Text.zip

 

drive.google.com

 

 

< 정리글 >

   

   오늘은 이미지의 픽셀에 접근해 입력받은 텍스트를 암호화 시키는 코드를 작성해보았습니다.  약간의 고찰이 있었고,

 

바로 JPG 파일 형식의 이미지를 불러와 픽셀을 변조시킨 후 다시 저장을하면 픽셀 값이 내가 변조한 값과 다르게

나타난다는 것입니다. 확실히 파악하진 못했지만 Opencv에서 사진을 저장할 때 JPG 형식이 압축 기법이 들어간 것이기

 

때문이라고 짐작하고 있습니다.

 

참고로 위 방법을 사용하실거면 압축되지 않은 bmp 형식의 이미지를 이용해야 해야 하며, 중간에 오류가 발생하는

 

부분을 해결하지 못해 이미지의 크기는 50x50 이상인 경우에만 동작하도록 제한을 걸어놓았습니다.

 

추후에 디버깅을 하여 수정된 코드를 올리도록 하겠습니다.

 

 

 

 

< 참고 사이트 >

https://gnu-cse.tistory.com/36

 

이미지 파일 안에 file, text 숨기기 - 스테가노 그래피

사진, 음악, 동영상 등의 일반적인 파일안에 텍스트나 파일을 숨기는 암호화 기법을 steganography라고 한다. 이미지에 파일을 숨기는 경우 어떤 테크닉이 있는지 간단히 알아보자. 이미지 파일에 메세지를 숨기는..

gnu-cse.tistory.com

https://blue-shadow.tistory.com/21

 

스테가노그래피 (Steganography)

참고 링크 : - [보안뉴스] [보안용어 A to Z] 스테가노그래피(Steganography) (2015-08-26 18:01) - [위키백과] 스테가노그래피 스테가노그래피 데이터 은폐 기술 중 하나이며, 데이터를 다른 데이터에 삽입하는..

blue-shadow.tistory.com

 

 

 

오늘은 이미지 속 픽셀(pixel)에 접근하는 방법에 대해 알아보겠습니다.

 

그 전에 픽셀이 무엇인지에 대해서 살펴보도록 하지요.

 

픽셀이란 디지털 영상에서 더이상 쪼갤 수 없는 최소 단위를 뜻하며 픽셀 또는 화소로 얘기합니다.

 

이미지의 가로/세로 사이즈를 줄인다는 말에서 사이즈가 바로 픽셀을 말하는 거죠.

 

아래 사진을 예로 보시게 되시면 640x480 크기의 이미지가 있으며. 픽셀의 수는 640x480 = 307,200 개가 되네요.

 

640x480 이미지

 

 

 

우리가 흔하게 보는 이미지는 대게 RGB인 3개의 채널의 혼합으로 이루어진 이미지 입니다.

 

여기서 RGB는 색상인 Red, Green, Blue를 뜻합니다.

 

이미지속 한 픽셀마다 RGB의 3가지 값이 혼합되어있습니다.

 

이 내용은 아래의 앵무새 그림을 보고 이해하시면 됩니다.

 

 

RGB 벤다이어그램

 

RGB로 이루어진 이미지 

 

 

 

 

그렇다면 오늘의 주제인 이미지 픽셀에 접근하는 방법을 코드를 통해 알아보도록 하겠습니다.

 

* 본 코드는 Opencv 3.1.0 Version에서 사용하였습니다.

#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main() {

	Mat img = imread("D:\\학교\\티스토리\\color\\mountain.jpg");

	Mat only_r = img.clone();
	Mat only_g = img.clone();
	Mat only_b = img.clone();

	for (int y = 0; y < img.rows; y++) {
		for (int x = 0; x < img.cols; x++) {	
			// B Channel => img.at<Vec3b>(y,x)[0]
			// G Channel => img.at<Vec3b>(y,x)[1]
			// R Channel => img.at<Vec3b>(y,x)[2]

			// Only Red 
			only_r.at<Vec3b>(y, x)[0] = 0; // zero B
			only_r.at<Vec3b>(y, x)[1] = 0; // zero G

			// Only Green
			only_g.at<Vec3b>(y, x)[0] = 0; // zero B
			only_g.at<Vec3b>(y, x)[2] = 0; // zero R

			// Only Blue
			only_b.at<Vec3b>(y, x)[1] = 0; // zero G
			only_b.at<Vec3b>(y, x)[2] = 0; // zero R
		}
	}

	imshow("원본 이미지", img);
	imshow("Only Red", only_r);
	imshow("Only Green", only_g);
	imshow("Only Blue", only_b);

	waitKey(0);
}

 

위 코드는 한 이미지를 불러와 RGB로 혼합되어 이루어진 이미지를 각각의 채널만을 남기고 나머지 채널은

0 값을 준 것입니다. 이렇게 코드를 돌려보면 위에 앵무새 그림과 동일하게 나오게 됩니다.

 

추가적으로 코드 내 Vec3b는 3 Byte로 된 벡터값을 의미합니다.

 

코드의 결과는 아래와 같습니다.

 

 

원본 이미지

 

Only Red
Only Green
Only Blue

 

 

 

< 정리글 >

 

오늘은 이미지 속 픽셀의 RGB 채널에 접근하는 방법을 알아보았습니다.

 

이 흐름을 이용하면 어떠한 영역에 접근하여 색상을 바꾸어 주거나 이미지 전체의 밝기를 조절할 수 있죠.

 

한가지 확실히 하고 넘어가야 되는건 위 코드의 결과는 RGB에서 1가지의 채널을 제외한 나머지 값을 0으로 만들어 나온

 

결과이며, 만약 각각의 채널을 따로 분리하여 1개 채널로 이미지를 보게 되면 회색 계열로 보이게 됩니다. 

 

 

 

 

우리가 이미지 속 특정 영역만을 추출해야되는 경우가 있습니다.

 

 

차량 번호판 검출를 예로 들겠습니다.

 

 

이미지 전체 탐색

 

이미지 속 특정 영역 탐색

 

첫 번째 그림과 같이 영상 속에서 차량 번호판을 검출하기 위해선

여러 알고리즘을 이용하여 이미지 전체를 탐색해야 됩니다. 

 

 

만약 아파트 입구에 설치된 주차 차단바의 상황라 가정하고 차량이 카메라 영역 속 특정 위치에 멈춰야 한다면

 

이미지 전체를 탐색하지 않고 두 번째 그림과 같이 이미지 속 특정 영역을 탐색해여 차량 번호판을 검출할 수 있으며,

처리 속도는 전체를 탐색하는 것 보다 월등히 차이가 나게 되죠.

 

 

 

 

바로 코드를 통해 Range() 함수를 사용해보겠습니다.

 

* 본 코드는 Opencv 3.1.0 Version에서 사용하였습니다.

#include <opencv2/opencv.hpp>  

using namespace cv;
using namespace std;

int main(){
	
	Mat img = imread("D:\\티스토리\\range\\벚꽃.jpg"); // 1000x1000 이미지
	Mat rect_img;

         // 영역 좌표
	int y1 = 300;
	int y2 = 600;
	int x1 = 250;
	int x2 = 750;

	rect_img = img(Range(y1, y2), Range(x1, x2));

	imshow("원본 이미지", img);
	imshow("결과 이미지", rect_img);

	waitKey(0);
}

 

원본 이미지

 

이미지 영역 분할 과정

 

 

결과 이미지

 

 

 

< 정리글 >

 

오늘은 Range() 함수에 대해서 알아보았습니다.

 

이 함수만을 이해하고 사용하시는 데 큰 어려움은 없을거라 생각합니다.

 

영상처리를 이용한 프로젝트를 하시게 되면 상황에 맞는 사용 이유와 응용 방법에 대해

잘 이해하실 수 있을거라 생각합니다

 

 

 

* 겨울이 다 지나고 벚꽃의 계절이 찾아온거 같네요.   

 

  코로나가 잠잠해지고 있는 것처럼 느껴지지만 이럴 시기일 수록 더욱 조심해야되지 않을까 싶습니다.

 

  모두가 힘든 시기를 잘 견뎌내어 이 상황을 이겨내길 바랍니다.

 

  (위에 벚꽃 사진은 2년전 군항제에 가서 찍은 사진입니다.)

 

 

오늘은 Opencv를 이용하여 폴더 내 jpg, bmp 형식의 모든 이미지를 읽어 처리하는 방법을 알려드리려 합니다.

 

조금이라도 Opencv 사용해보신 분이라면 한 가지의 이미지를 읽어들이는 방법에 대해 아실겁니다.

 

바로 imread() 함수를 이용해서죠.

 

#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;


int main()
{
	Mat img = imread("D:\\test\\building.jpg");
	Mat gray;

	// RGB -> Gray
	cvtColor(img, gray, CV_RGB2GRAY);

	imshow("Original", img);
	imshow("Grayscale", gray);

	waitKey(0);
}

 

한 개의 이미지를 읽어 Grayscale 처리를 한 결과

 

 

 

그렇다면 한 가지가 아닌 여러 개의 이미지를 불러오기 위해선 어떻게 해야 될까요?

 

물론 모든 이미지의 경로를 직접 작성해주셔도 되지만 이 방법이 좋은 방법이라고는 생각들지 않죠...

 

그래서 폴더 내 모든 이미지를 찾을 수 있는 glob() 함수에 대한 간단한 소개와 사용 예시를 알려드리려 합니다.

 

glob() 함수는 폴더 내 파일 목록을 가져오는 재귀 함수입니다.

 

아래 코드는 glob() 함수를 이용하여 폴더 내 jpg 확장자 이미지를 모두 읽은 후 Grayscale 적용해 

 

이미지를 저장하는 코드입니다.

 

* 본 코드는 Opencv 3.1.0 Version에서 사용하였습니다.

#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;


int main()
{
	Mat img;
	Mat gray;
	Mat p_img;
	Mat p_gray;

	String path("D:\\test\\*.jpg"); // *.jpg
	  	 		        // jpg 확장자 파일만 읽음


	vector<String> str; 

	// 이미지 저장을 위한 변수
	int index = 0;
	char buf[256];

	glob(path, str, false); // 파일 목록을 가져오는 glob 함수
                                // glob(찾을 파일 경로, 찾은 파일 경로, recusive(true or false)
                                // true : 폴더 내 하위 폴더 속 까지 파일을 찾음
                                // false : 폴더 내 파일을 찾음

	cout << "로드 개수 : " << str.size() << endl;
	if (str.size() == 0)
		cout << "이미지가 존재하지 않습니다.\n" << endl;

	for (int cnt = 0; cnt < str.size(); cnt++) 
	{
		p_img = imread(str[cnt]);
		cvtColor(p_img, p_gray, CV_RGB2GRAY);

		// Grayscale 처리된 이미지 저장
		sprintf(buf, "D:\\test2\\%d.jpg", index);
		imwrite(buf, p_gray); // imwrite(저장할 경로 및 파일 이름, Mat 형식)
		index++; // 파일 이름의 인덱스 증가

		printf("%d 번째 이미지 전처리를 완료. \n", cnt+1);
	}
	cout << "\n" << endl;
}

콘솔 창 결과

 

폴더 내 jpg 파일

 

처리된 이미지

 

 

 

 

< 정리글 >

 

오늘은 폴더 내 파일 목록을 가져오는 glob() 함수를 사용해보았습니다.

 

영상이 아닌 수 많은 사진들을 처리하는 경우는 드물다고 생각합니다. 

 

저 같은 경우엔 인공지능 모델에서 데이터셋을 구축하기 위해 이미지를 수집하다보면 파일 이름의 형식이 제 각각이라

 

이를 정리하거나 또는 데이터셋 자체를 전처리를 시키는데 사용했습니다.

 

누군가에게 도움이 되었으면 합니다. 

 

 

오늘의 주제에 대해서 설명하기 전에 딥러닝와 영상처리의 차이를 간단히 설명하고 넘어가려 합니다.

 

 

 

위 그림에는 바나나, 수박, 사과, 복숭아가 존재합니다.

 

이중 사과를 인식하기 위해선 어떻게 해야될까요?

 

가장 먼저 생각이 드는건 4가지 과일 중 "사과만이 가지고 있는 특징이 무엇일까?" 라는 겁니다.

 

예를 들어 사과의 색은 빨간색이고 사람의 주먹 정도의 크기이고 윗 부분은 파여있고

 

파여있는 부분에 꽁다리가 붙어있으며 사과 표면을 보면 노란색의 점들이 있는 등의 특징을 말할 수 있죠.

 

이러한 특징들을 이용하여 우리는 이것이 사과라는 것을 맞출 수 있습니다.

 

 

 

영상처리와 딥러닝에선 객체의 특징을 찾는 과정이 조금 다릅니다.

 

영상처리는 이러한 특징들을 찾기 위해 직접 코드로 구현해주어야 합니다.

 

예를 들어 사과를 인식하기 위해 "사과 이미지의 픽셀의 RGB 값의 범위는 어느정도로 범위로 줄 것인가?" 등이 있죠.

 

지금은 다른 것은 생각하지 말고 사과의 크기에 대해서만 생각해봅시다.

 

대부분의 사과는 동그랗기 때문에 일정 크기의 원의 범위를 이용하여 사과를 찾을 수 있습니다.

 

하지만 이 방법은 사과의 크기를 고정해야만 찾을 수 있죠.

 

다시말해 사과의 크기가 달라짐에 따라 위 방법을 이용하여 사과를 찾을 수 없게 됩니다.

 

 

 

딥러닝은 이러한 특징 찾기 위해 직접 코드로 구현하지 않아도 됩니다.

 

대체적으로 딥러닝을 사용하는 과정엔 훈련과 검출 및 인식으로 나뉘어져 있습니다.

 

가장 먼저 인식하고자 하는 이미지를 수집하고 이를 CNN(Convolution Nueral Network)에 입력시켜

 

Weight를 추출합니다. (딥러닝에 대한 자세한 설명은 추후에 글을 작성해 설명하겠습니다.)

 

최종적으로 나온 Weight가 바로 수집한 여러 이미지들 속 사과의 특징이 찾은것입니다.

 

다시말해 인식하고자 하는 객체의 이미지셋을 구축하여 CNN에 입력시키면 컴퓨터가 

 

사과의 특징을 찾아주게 됩니다.

 

 

 

위 내용을 통해 이론적으론 영상처리와 딥러닝의 차이를 간단하게 이해하셨을겁니다.

 

더 확실한 이해를 위해 영상처리와 딥러닝에 대해 각각의 방법으로 공통의 객체(숫자)를 인식해보도록 하겠습니다.

 

오늘은 영상처리를 이용한 객체 인식 방법에 대해 설명하고 딥러닝을 이용한 객체 인식 방법은

 

추후에 정리해서 글을 올리도록 하겠습니다.

 

 

 

 

 

 

 

이제 오늘의 주제인 히스토그램을 이용한 숫자 인식을 진행해보도록 하겠습니다.

 

그 전에 먼저 히스토그램의 개념을 알아야합니다.

 

히스토그램은 표로 되어있는 도수 분포를 막대선 그래프로 나타낸 것입니다.

 

 

           입력 영상           
히스토그램

( 히스토그램의 y축은 개수, x축은 숫자를 의미합니다.)

 

 

위 그림의 입력영상 표에 적힌 숫자의 갯수를 나타낸 것이 바로 히스토그램입니다.

 

다시 말하면 입력 영상의 픽셀 값의 분포를 나타낸 것이죠.

 

영상의 히스토그램을 쉽게 그릴 수 있도록 Opencv 라이브러리에서 함수를 제공하고 있으니 

찾아보시면 됩니다.

 

 

 

 

 

 

 

 

 

이제 본격적으로 숫자를 인식해봅시다.

 

전체적인 과정은 입력 이미지 -> GrayScale -> Binarization -> Historgram -> Histrogram comparison 입니다.

 

 

 

이미지는 대게 3개의 채널(3바이트)인 RGB로 표현됩니다.

 

GrayScale이란 한 이미지의 RGB인 3개의 채널의 각 픽셀값을 합쳐 3으로 나눠준 값이며,

 

8비트로 표현되는 회색 이미지입니다.

 

그렇다면 "우리가 이미지의 RGB 채널에 해당되는 픽셀에 접근해서 처리를 해야 되는거냐?" 라고 생각하실 수 있습니다.

 

네 맞습니다.. 하지만 Intel에서 개발한 영상처리에 특화된 라이브러리인 Opencv를 사용하면 쉽게 처리할 수 있습니다.

 

Opencv에선 RGB를 Gray로 변환해주는 함수가 존재하여서 함수와 매개변수를 조정하여 쉽게 변환할 수 있습니다.

 

#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main()
{
	Mat img;
	Mat gray;

	//이미지 입력
	img = imread("C:\\숫자 인식\\7.jpg");

	// Grayscale
	cvtColor(img, gray, CV_RGB2GRAY);

	imshow("Original_img", img);
	imshow("Grayscale", gray);

	waitKey(0);
}

입력 이미지
출력 이미지

 

 

다음은 이진화(Binarization)입니다.

 

이진화는 영상의 픽셀을 0과 255인 흑과 백으로 표현하는 겁니다.

 

이 또한 Opencv에서 함수를 제공하고 있습니다.

 

이진화 함수의 매개변수에서 이진화를 시키기 위해 임계값을 설정해줘야 합니다.

#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main()
{
	Mat img;
	Mat gray;
	Mat bi;

	//이미지 입력
	img = imread("C:\\숫자 인식\\7.jpg");

	// Grayscale
	cvtColor(img, gray, CV_RGB2GRAY);

	//Binarization
	threshold(gray, bi, 180, 255, THRESH_BINARY);

	imshow("Binarization", bi);

	waitKey(0);
}

 

이진화

 

 

 

 

다음은 이미지 속 숫자의 테두리 밖을 제거시켜줍니다.

 

테두리를 제거할 때 Y축 방향으로 (0,0) -> (0, img.cols)까지의 픽셀 값이 흰색인 개수를 파악합니다.

 

이를 (img.rows, 0) -> (img.rows, img.cols) 까지 반복해서 진행하며,

 

X축 방향일 때도 같게 진행합니다.

 

이렇게 진행하며 흰색 픽셀을 찾았을 경우의 우상단, 우하단, 좌상단, 좌하단의 좌표를 찾고 이미지를 자릅니다.

 

이는 추후에 히스토그램을 이용해 숫자 인식을 할 때 인식률을 향상시키기 위해 

 

어떠한 사진이든 동일한 위치에 두기 위함입니다.

 

#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main() {

	Mat img;
	Mat gray;
	Mat bi;
	Mat cut_img;

	int zero_cnt = 0;
	int x1 = 0, x2 = 0;
	int y1 = 0, y2 = 0;

	//이미지 입력
	img = imread("C:\\숫자 인식\\7.jpg");

	// Grayscale
	cvtColor(img, gray, CV_RGB2GRAY);

	//Binarization
	threshold(gray, bi, 180, 255, THRESH_BINARY);

	// 왼쪽 Y축
	for (int x = 0; x < bi.cols; x++)
	{
		for (int y = 0; y < bi.rows; y++)
		{
			if (bi.at<uchar>(y, x) == 255)
				zero_cnt++;
		}

		if (zero_cnt > 10) {
			x1 = x;
			zero_cnt = 0;
			break;
		}
		zero_cnt = 0;
	}

	//오른쪽 Y축
	for (int x = 0; x < bi.cols; x++) {
		for (int y = 0; y < bi.rows; y++) {
			if (bi.at<uchar>(y, (bi.cols - 1) - x) == 255)
				zero_cnt++;
		}

		if (zero_cnt > 0) {
			x2 = (bi.cols - 1) - x;
			zero_cnt = 0;
			break;
		}
		zero_cnt = 0;
	}

	// 위 X축
	for (int y = 0; y < bi.rows; y++) {
		for (int x = 0; x < bi.cols; x++) {
			if (bi.at<uchar>(y, x) == 255)
				zero_cnt++;
		}

		if (zero_cnt > 0) {
			y1 = y;
			zero_cnt = 0;
			break;
		}
		zero_cnt = 0;
	}

	// 아래 X축
	for (int y = 0; y < bi.rows; y++) {
		for (int x = 0; x < bi.cols; x++) {
			if (bi.at<uchar>((bi.rows - 1) - y, x) == 255)
				zero_cnt++;
		}
		if (zero_cnt > 0) {
			y2 = (bi.rows - 1) - y;
			zero_cnt = 0;
			break;
		}
		zero_cnt = 0;
	}

	cut_img = bi(Range(y1, y2), Range(x1, x2));
	resize(cut_img, cut_img, Size(200, 200), 0, 0, INTER_LINEAR);

	imshow("테두리 제거", cut_img);

	waitKey(0);
}

 

테두리 제거

 

 

 

 

다음은 제거된 테두리 이미지를 이용하여 히스토그램을 그려보겠습니다.

 

히스토그램은 Opencv에서 제공하는 함수를 사용하지 않고 위의 테두리를 제거하는 것과 비슷하게

 

X축 또는 Y축을 기준으로 이미지 속 흰색 픽셀의 수를 카운팅하여 선을 그려주는 line() 함수를 이용해 그려주었습니다.

 

#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main() {

	Mat img;
	Mat gray;
	Mat bi;
	Mat cut_img;
	Mat y_hist;
	Mat x_hist;
	Mat xy_hist;

	// 히스토그램 변수
	y_hist = Mat::zeros(200, 200, CV_8UC1);
	x_hist = Mat::zeros(200, 200, CV_8UC1);
	xy_hist = Mat::zeros(200, 400, CV_8UC1);
	
	// 이미지 속 x축 y축 라인의 픽셀 값 정보가 담길 변수
	int x_result[200] = { 0, };
	int y_result[200] = { 0, };

	int zero_cnt = 0;
	int x1 = 0, x2 = 0;
	int y1 = 0, y2 = 0;

	// 이미지 입력
	img = imread("C:\\숫자 인식\\7.jpg");

	//Grayscale
	cvtColor(img, gray, CV_RGB2GRAY);

	//Binarization
	threshold(gray, bi, 180, 255, THRESH_BINARY);
	bi = ~bi;



	// 오른쪽 Y축
	for (int x = 0; x < bi.cols; x++){
		for (int y = 0; y < bi.rows; y++){
			if (bi.at<uchar>(y, x) == 255)
				zero_cnt++;
		}

		if (zero_cnt > 0) {
			x1 = x;
			zero_cnt = 0;
			break;
		}
		zero_cnt = 0;
	}

	//왼쪽 Y축
	for (int x = 0; x < bi.cols; x++) {
		for (int y = 0; y < bi.rows; y++) {
			if (bi.at<uchar>(y, (bi.cols - 1) - x) == 255)
				zero_cnt++;
		}

		if (zero_cnt > 0) {
			x2 = (bi.cols - 1) - x;
			zero_cnt = 0;
			break;
		}
		zero_cnt = 0;
	}

	// 위 X축
	for (int y = 0; y < bi.rows; y++) {
		for (int x = 0; x < bi.cols; x++) {
			if (bi.at<uchar>(y, x) == 255)
				zero_cnt++;
		}

		if (zero_cnt > 0) {
			y1 = y;
			zero_cnt = 0;
			break;
		}
		zero_cnt = 0;
	}

	// 아래 X축
	for (int y = 0; y < bi.rows; y++) {
		for (int x = 0; x < bi.cols; x++) {
			if (bi.at<uchar>((bi.rows - 1) - y, x) == 255)
				zero_cnt++;
		}
		if (zero_cnt > 0) {
			y2 = (bi.rows - 1) - y;
			zero_cnt = 0;
			break;
		}
		zero_cnt = 0;
	}
	cut_img = bi(Range(y1, y2), Range(x1, x2));
	resize(cut_img, cut_img, Size(200, 200), 0, 0, INTER_LINEAR);



	//////////////
	// 히스토그램//
	/////////////

	// 세로 축 히스토그램
	for (int x = 0; x < cut_img.rows; x++){
		zero_cnt = 0;
		for (int y = 0; y < cut_img.cols; y++){
			if (cut_img.at<uchar>(y, x) == 255)
				zero_cnt++;
		}
		y_result[x] = zero_cnt;
	}
	for (int x = 0; x < y_hist.cols; x++)
		line(y_hist, Point(x, y_hist.rows - y_result[x]), Point(x, y_hist.rows), Scalar(255, 255, 255), 0);
	


	// 가로 축 히스토그램
	for (int y = 0; y < cut_img.rows; y++){
		zero_cnt = 0;
		for (int x = 0; x < cut_img.cols; x++){
			if (cut_img.at<uchar>(y, x) == 255)
				zero_cnt++;
		}
		x_result[y] = zero_cnt;
	}
	for (int y = 0; y < x_hist.rows; y++)
		line(x_hist, Point(0, y), Point(x_result[y], y), Scalar(255, 255, 255), 0);
	


	// 세로, 가로 축의 통합 히스토그램
	for (int a = 0; a < xy_hist.cols / 2; a++){
		line(xy_hist, Point(a, xy_hist.rows - y_result[a]), Point(a, xy_hist.rows), Scalar(255, 255, 255), 0);
		line(xy_hist, Point(a + 200, xy_hist.rows - x_result[a]), Point(a + 200, xy_hist.rows), Scalar(255, 255, 255), 0);
	}

	imshow("입력 이미지", cut_img);
	imshow("X축 히스토그램", x_hist);
	imshow("Y축 히스토그램", y_hist);
	imshow("통합 히스토그램", xy_hist);

	waitKey(0);
}

 

X, Y축의 히스토그램

 

톻합 히스토그램

 

 

 

이제 거의 다 왔습니다.

 

다음은 히스토그램을 이용해 숫자인식을 하기 위해서 입력데이터와 비교를 할 수 있도록

 

숫자 0~9까지의 표본 데이터가 필요합니다.

 

표본 데이터를 이용하여 위에 진행한대로 통합 히스토그램을 그립니다.

 

#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main() {

	Mat img[10];
	Mat cut_img[10];
	Mat y_hist[10];
	Mat x_hist[10];
	Mat xy_hist[10];
	for (int a = 0; a < 10; a++) {
		y_hist[a] = Mat::zeros(200, 200, CV_8UC1);
		x_hist[a] = Mat::zeros(200, 200, CV_8UC1);
		xy_hist[a] = Mat::zeros(200, 400, CV_8UC1);
	}

	string img_path;

	int zero_cnt = 0;
	int x1 = 0, x2 = 0;
	int y1 = 0, y2 = 0;
	int x_result[10][200] = { 0, };
	int y_result[10][200] = { 0, };

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

		// 변수 초기화
		zero_cnt = 0;
		x1 = 0, x2 = 0;
		y1 = 0, y2 = 0;

		img_path = format("C:\\숫자 인식\\표본 데이터\\%d.jpg", i);

		img[i] = imread(img_path, IMREAD_GRAYSCALE);

		threshold(img[i], img[i], 127, 255, THRESH_BINARY);
		img[i] = ~img[i];


		// 오른쪽 Y축
		for (int x = 0; x < img[i].cols; x++) {
			for (int y = 0; y < img[i].rows; y++) {
				if (img[i].at<uchar>(y, x) == 255)
					zero_cnt++;
			}
			if (zero_cnt > 0) {
				x1 = x;
				zero_cnt = 0;
				break;
			}
			zero_cnt = 0;
		}

		//왼쪽 Y축
		for (int x = 0; x < img[i].cols; x++) {
			for (int y = 0; y < img[i].rows; y++) {
				if (img[i].at<uchar>(y, (img[i].cols - 1) - x) == 255)
					zero_cnt++;
			}
			if (zero_cnt > 0) {
				x2 = (img[i].cols - 1) - x;
				zero_cnt = 0;
				break;
			}
			zero_cnt = 0;
		}

		// 위 X축
		for (int y = 0; y < img[i].rows; y++) {
			for (int x = 0; x < img[i].cols; x++) {
				if (img[i].at<uchar>(y, x) == 255)
					zero_cnt++;
			}
			if (zero_cnt > 0) {
				y1 = y;
				zero_cnt = 0;
				break;
			}
			zero_cnt = 0;
		}

		// 아래 X축
		for (int y = 0; y < img[i].rows; y++) {
			for (int x = 0; x < img[i].cols; x++) {
				if (img[i].at<uchar>((img[i].rows - 1) - y, x) == 255)
					zero_cnt++;
			}
			if (zero_cnt > 0) {
				y2 = (img[i].rows - 1) - y;
				zero_cnt = 0;
				break;
			}
			zero_cnt = 0;
		}

		cut_img[i] = img[i](Range(y1, y2), Range(x1, x2));
		resize(cut_img[i], cut_img[i], Size(200, 200), 0, 0, INTER_LINEAR);



		/////////////
		// 히스토그램//
		/////////////

		// 세로 축 히스토그램
		for (int x = 0; x < cut_img[i].rows; x++) {
			zero_cnt = 0;
			for (int y = 0; y < cut_img[i].cols; y++) {
				if (cut_img[i].at<uchar>(y, x) == 255)
					zero_cnt++;
			}
			y_result[i][x] = zero_cnt;
		}
		for (int x = 0; x < y_hist[i].cols; x++)
			line(y_hist[i], Point(x, y_hist[i].rows - y_result[i][x]), Point(x, y_hist[i].rows), Scalar(255, 255, 255), 0);

		// 가로 축 히스토그램
		for (int y = 0; y < cut_img[i].rows; y++) {
			zero_cnt = 0;
			for (int x = 0; x < cut_img[i].cols; x++) {
				if (cut_img[i].at<uchar>(y, x) == 255)
					zero_cnt++;
			}
			x_result[i][y] = zero_cnt;
		}
		for (int y = 0; y < x_hist[i].rows; y++)
			line(x_hist[i], Point(0, y), Point(x_result[i][y], y), Scalar(255, 255, 255), 0);

		// 세로, 가로 축의 total 히스토그램
		for (int a = 0; a < xy_hist[i].cols / 2; a++) {
			line(xy_hist[i], Point(a, xy_hist[i].rows - y_result[i][a]), Point(a, xy_hist[i].rows), Scalar(255, 255, 255), 0);
			line(xy_hist[i], Point(a + 200, xy_hist[i].rows - x_result[i][a]), Point(a + 200, xy_hist[i].rows), Scalar(255, 255, 255), 0);
		}

		String name1;
		name1 = format("%d", i);
		String name2;
		name2 = format("%d의 세로 히스토그램", i);
		String name3;
		name3 = format("%d의 가로 히스토그램", i);
		String name4;
		name4 = format("%d 세로,가로 통합 히스토그램", i);

		imshow(name1, cut_img[i]);
		//imshow(name2, y_hist[i]);
		//imshow(name3, x_hist[i]);
		imshow(name4, xy_hist[i]);

		//printf("%d 이미지의 흰색 픽셀 개수 %d\n", i, y_result);
	}
	waitKey(0);
}

 

표본 데이터

 

0~3까지의 통합 히스토그램

 

4~6까지의 통합 히스토그램

 

7~9까지의 히스토그램

 

 

 

마지막으로 비교하고싶은 숫자를 입력하여 통합 히스토그램을 추출하여

 

표본 숫자의 통합 히스토리와 가장 적게 차이나는 숫자를 찾도록 한다.

 

 

#include <opencv2/opencv.hpp>

#define PLUS 0

using namespace cv;
using namespace std;

int main() {

	// 표본 이미지
	Mat img[10];
	Mat cut_img[10];

	// 테스트 이미지
	Mat cp_img[10];
	Mat cp_cut_img[10];

	Mat y_hist[10];  // 세로 축
	Mat x_hist[10]; // 가로 축
	Mat xy_hist[10]; // 세로+가로
	Mat test_img_hist[10]; // 테스트 이미지 
	Mat dif_hist[10][10]; // 표본과 테스트의 차이

	// 다중 이미지를 위한 경로
	string img_path;
	string test_img_path;

	// 최종 결과
	int result[10][10] = { 0, };
	int clone_result[10][10] = { 0, };

	int zero_cnt = 0; // 흰색 픽셀 카운트 수
	int x1 = 0, x2 = 0;
	int y1 = 0, y2 = 0;
	int test_x[200] = { 0, }; // 카운트 수 결과
	int test_y[200] = { 0, }; // 카운트 수 결과

	int x_result[10][200] = { 0, };
	int y_result[10][200] = { 0, };

	int result_cnt = 0;
	int result_num = 0;

	// 0의 값을 가지고 있는 Mat 선언
	for (int a = 0; a < 10; a++) {
		y_hist[a] = Mat::zeros(200, 200, CV_8UC1);
		x_hist[a] = Mat::zeros(200, 200, CV_8UC1);
		xy_hist[a] = Mat::zeros(200, 400, CV_8UC1);
		test_img_hist[a] = Mat::zeros(200, 400, CV_8UC1);

		for (int b = 0; b < 10; b++)
			dif_hist[a][b] = Mat::zeros(200, 400, CV_8UC1);
	}

	// 표본 이미지
	for (int i = 0; i < 10; i++){

		// 변수 초기화
		zero_cnt = 0;
		x1 = 0, x2 = 0;
		y1 = 0, y2 = 0;

		// 이미지 포맷 변경
		img_path = format("C:\\숫자 인식\\표본 데이터\\%d.jpg", i);

		//  이미지 읽고 그레이 스케일 적용
		img[i] = imread(img_path, IMREAD_GRAYSCALE);

		// 이진화
		threshold(img[i], img[i], 127, 255, THRESH_BINARY);
		img[i] = ~img[i]; // 이미지 반전



		//////////////////////////////////////
		// 숫자 테두리를 따기 위한 잉여 배경을 제거//
		// 흰색 픽셀을 만날 경우를 판단                 //
		/////////////////////////////////////

		// 왼쪽 Y축
		for (int x = 0; x < img[i].cols; x++){
			for (int y = 0; y < img[i].rows; y++)
			{
				if (img[i].at<uchar>(y, x) == 255)
					zero_cnt++;
			}
			if (zero_cnt > 0) {
				x1 = x;
				zero_cnt = 0;
				break;
			}
			zero_cnt = 0;
		}

		//오 세로
		for (int x = 0; x < img[i].cols; x++) {
			for (int y = 0; y < img[i].rows; y++) {
				if (img[i].at<uchar>(y, (img[i].cols - 1) - x) == 255)
					zero_cnt++;
			}
			if (zero_cnt > 0) {
				x2 = (img[i].cols - 1) - x;
				zero_cnt = 0;
				break;
			}
			zero_cnt = 0;
		}

		// 위 가로
		for (int y = 0; y < img[i].rows; y++) {
			for (int x = 0; x < img[i].cols; x++) {
				if (img[i].at<uchar>(y, x) == 255)
					zero_cnt++;
			}
			if (zero_cnt > 0) {
				y1 = y;
				zero_cnt = 0;
				break;
			}
			zero_cnt = 0;
		}

		// 아래 가로
		for (int y = 0; y < img[i].rows; y++) {
			for (int x = 0; x < img[i].cols; x++) {
				if (img[i].at<uchar>((img[i].rows - 1) - y, x) == 255)
					zero_cnt++;
			}
			if (zero_cnt > 0) {
				y2 = (img[i].rows - 1) - y;
				zero_cnt = 0;
				break;
			}
			zero_cnt = 0;
		}

		// 숫자 테두리 영역 이미지 추출
		cut_img[i] = img[i](Range(y1 - PLUS, y2 + PLUS), Range(x1 - PLUS, x2 + PLUS));

		// 추출한 이미지 리사이징
		resize(cut_img[i], cut_img[i], Size(200, 200), 0, 0, INTER_LINEAR);




		////////////////////
		// 히스토그램 그리기//
		///////////////////

		// 세로 축 히스토그램
		for (int x = 0; x < cut_img[i].rows; x++){
			zero_cnt = 0;
			for (int y = 0; y < cut_img[i].cols; y++){
				if (cut_img[i].at<uchar>(y, x) == 255)
					zero_cnt++;
			}
			y_result[i][x] = zero_cnt;
		}
		for (int x = 0; x < y_hist[i].cols; x++)
			line(y_hist[i], Point(x, y_hist[i].rows - y_result[i][x]), Point(x, y_hist[i].rows), Scalar(255, 255, 255), 0);
		


		// 가로 축 히스토그램
		for (int y = 0; y < cut_img[i].rows; y++){
			zero_cnt = 0;
			for (int x = 0; x < cut_img[i].cols; x++){
				if (cut_img[i].at<uchar>(y, x) == 255)
					zero_cnt++;
			}
			x_result[i][y] = zero_cnt;
		}
		for (int y = 0; y < x_hist[i].rows; y++)
			line(x_hist[i], Point(0, y), Point(x_result[i][y], y), Scalar(255, 255, 255), 0);	

		// 세로, 가로 축의 통합 히스토그램
		for (int a = 0; a < xy_hist[i].cols / 2; a++){
			line(xy_hist[i], Point(a, xy_hist[i].rows - y_result[i][a]), Point(a, xy_hist[i].rows), Scalar(255, 255, 255), 0);
			line(xy_hist[i], Point(a + 200, xy_hist[i].rows - x_result[i][a]), Point(a + 200, xy_hist[i].rows), Scalar(255, 255, 255), 0);
		}
		//printf("%d 이미지의 흰색 픽셀 개수 %d\n", i, y_result);

		String name1;
		name1 = format("%d", i);
		String name2;
		name2 = format("%d의 세로 히스토그램", i);
		String name3;
		name3 = format("%d의 가로 히스토그램", i);
		String name4;
		name4 = format("%d 세로,가로 통합 히스토그램", i);

		//imshow(name1, cut_img[i]);
		//imshow(name2, y_hist[i]);
		//imshow(name3, x_hist[i]);
		//imshow(name4, xy_hist[i]);
	}



	// 테스트 이미지
	for (int i = 0; i < 10; i++){

		// 변수 초기화
		zero_cnt = 0;
		x1 = 0, x2 = 0;
		y1 = 0, y2 = 0;

		// 이미지 포맷 변경
		test_img_path = format("C:\\숫자 인식\\입력 데이터\\%d.jpg", i);

		//  이미지 읽고 그레이 스케일 적용
		cp_img[i] = imread(test_img_path, IMREAD_GRAYSCALE);

		// 이진화
		threshold(cp_img[i], cp_img[i], 127, 255, THRESH_BINARY);
		cp_img[i] = ~cp_img[i]; // 이미지 반전


		//////////////////////////////////////
		// 숫자 테두리를 따기 위한 잉여 배경을 제거//
		// 흰색 픽셀을 만날 경우를 판단                 //
		/////////////////////////////////////

		// 왼쪽 Y축
		for (int x = 0; x < cp_img[i].cols; x++)
		{
			for (int y = 0; y < cp_img[i].rows; y++)
			{
				if (cp_img[i].at<uchar>(y, x) == 255)
					zero_cnt++;
			}
			if (zero_cnt > 0) {
				x1 = x;
				zero_cnt = 0;
				break;
			}
			zero_cnt = 0;
		}

		//오른쪽 Y축
		for (int x = 0; x < cp_img[i].cols; x++) {
			for (int y = 0; y < cp_img[i].rows; y++) {
				if (cp_img[i].at<uchar>(y, (cp_img[i].cols - 1) - x) == 255)
					zero_cnt++;
			}
			if (zero_cnt > 0) {
				x2 = (cp_img[i].cols - 1) - x;
				zero_cnt = 0;
				break;
			}
			zero_cnt = 0;
		}

		// 위 X축
		for (int y = 0; y < cp_img[i].rows; y++) {
			for (int x = 0; x < cp_img[i].cols; x++) {
				if (cp_img[i].at<uchar>(y, x) == 255)
					zero_cnt++;
			}
			if (zero_cnt > 0) {
				y1 = y;
				zero_cnt = 0;
				break;
			}
			zero_cnt = 0;
		}

		// 아래 X축
		for (int y = 0; y < cp_img[i].rows; y++) {
			for (int x = 0; x < cp_img[i].cols; x++) {
				if (cp_img[i].at<uchar>((cp_img[i].rows - 1) - y, x) == 255)
					zero_cnt++;
			}
			if (zero_cnt > 0) {
				y2 = (cp_img[i].rows - 1) - y;
				zero_cnt = 0;
				break;
			}
			zero_cnt = 0;
		}
		cp_cut_img[i] = cp_img[i](Range(y1 - PLUS, y2 + PLUS), Range(x1 - PLUS, x2 + PLUS));
		resize(cp_cut_img[i], cp_cut_img[i], Size(200, 200), 0, 0, INTER_LINEAR);


		////////////////////
		// 히스토그램 그리기//
		///////////////////

		// 세로 축 히스토그램
		for (int x = 0; x < cp_cut_img[i].rows; x++){
			zero_cnt = 0;
			for (int y = 0; y < cp_cut_img[i].cols; y++){
				if (cp_cut_img[i].at<uchar>(y, x) == 255)
					zero_cnt++;
			}
			test_y[x] = zero_cnt;
		}

		// 가로 축 히스토그램
		for (int y = 0; y < cp_cut_img[i].rows; y++){
			zero_cnt = 0;
			for (int x = 0; x < cp_cut_img[i].cols; x++){
				if (cp_cut_img[i].at<uchar>(y, x) == 255)
					zero_cnt++;
			}
			test_x[y] = zero_cnt;
		}

		// 세로, 가로 축의 total 히스토그램
		for (int a = 0; a < test_img_hist[i].cols / 2; a++){
			line(test_img_hist[i], Point(a, test_img_hist[i].rows - test_y[a]), Point(a, test_img_hist[i].rows), Scalar(255, 255, 255), 0);
			line(test_img_hist[i], Point(a + 200, test_img_hist[i].rows - test_x[a]), Point(a + 200, test_img_hist[i].rows), Scalar(255, 255, 255), 0);
		}



		String name5;
		name5 = format("%d", i);
		String name6;
		name6 = format("%d의 세로 히스토그램", i);
		String name7;
		name7 = format("%d의 가로 히스토그램", i);
		String name8;
		name8 = format("%d 세로,가로 통합 히스토그램", i);

		//imshow(name5, cp_cut_img[i]);
		//imshow(name6, y_hist[i]);
		//imshow(name7, x_hist[i]);
		//imshow(name8, test_img_hist[i]);
	}


	/////////////////////////////////////
	// 표본과 테스트 이미지 히스토그램의 차이 //
	/////////////////////////////////////

	// 이미지 수 (표뵨 10개 / 테스트 10개)
	for (int i = 0; i < 10; i++){
		for (int j = 0; j < 10; j++){
			// 이미지 전체
			for (int y = 0; y < dif_hist[i][j].rows; y++)
			{
				for (int x = 0; x < dif_hist[i][j].cols; x++){
					if (xy_hist[i].at<uchar>(y, x) != test_img_hist[j].at<uchar>(y, x))
						result_cnt++;
				}
			}
			result[i][j] = result_cnt;
			clone_result[i][j] = result_cnt;

			// 비교 값 초기화
			result_cnt = 0;
		}
	}


	// 가장 비슷한 숫자 찾기
	for (int a = 0; a < 10; a++){
		for (int b = 0; b < 9; b++)
			if (clone_result[a][b] < clone_result[a][b + 1])
				clone_result[a][b + 1] = clone_result[a][b];

		for (int detect_num = 0; detect_num < 10; detect_num++)
			if (result[a][detect_num] - clone_result[a][9] == 0)
				result_num = detect_num;

		printf("(%d 번째 테스트)\n", a);
		printf("---------------------------------------------------\n");
		printf("숫자    :");
		for (int b = 0; b < 10; b++) {
			printf("   %d", b);
			//printf("%d\t", result[a][b]);
		}
		printf("\n");
		printf("---------------------------------------------------\n");
		printf("정답    :   ");
		for (int i = 0; i<result_num; i++)
			printf("    ");
		printf("%d\n\n\n", result_num);
	}
	waitKey(0);
}

 

입력 데이터

 

 

0~4까지의 히스토그램 비교 결과

 

5~9까지의 히스토그램 비교 결과

 

 

위 결과는 0.jpg부터 9.jpg가 차례대로 입력되어 나온 결과이다.

 

위의 결과를 보면 숫자가 잘 인식되는 것을 확인할 수 있다.

 

 

 

 

추가적으로 테스트를 진행해보면 위 방식의 히스토그램의 비교를 통한 숫자 인식은

 

인식이 되지 않지 경우가 더 많이 존재한다.

 

이러한 경우는 이미지 속 숫자가 조금 회전되어 있거나 글꼴이 다를 경우이다.

 

아래 그림은 숫자가 회전되어있는 이미지를 입력시킨 경우이다.

 

입력 데이터

 

 

 

0~4까지의 히스토그램 비교 결과

 

5~9까지의 히스토그램 비교 결과

 

위 결과에서 0의 숫자를 맞춘 경우는 10개중 3개입니다.

 

그리고 정답은 맞춘 3개는 회전이 조금 된 이미지이며, 나머지는 회전이 많이 된 이미지입니다.

 

* 파란 네모 : 정답

  빨간 네모 : 오답

 

 

 

 

 

 

<정리글>

 

오늘은 히스토그램을 이용하여 숫자를 인식해보았습니다.

 

대체적으로 숫자를 잘 인식하는 것처럼 보이지만, 이미지의 회전, 거리, 글꼴 등이 조금이라도 달라지면

 

오인식이 되는 문제가 발생하였습니다. 

 

오인식을 하지 않도록 하기 위해선 모든 상황에서의 각 숫자의 특징을 찾아야 하는 데, 이것은 쉬운일이 아닙니다...

 

만약 딥러닝을 이용하여 숫자를 인식하려 한다면 여러 이미지와의 공통된 특징점을 알아서 잡아주기 때문에

 

영상처리에서 어려웠던 일을 딥러닝 기술을 이용하면 쉬운일이 될 수 있습니다.

 

추후에 딥러닝을 이용한 숫자 인식을 진행하여 나온 결과를 통해 영상처리와 딥러닝을 비교해보도록 하겠습니다.

 

긴 글 읽어주셔서 감사합니다.

 

 

 

* 위 코드에서 반복되는 부분이 매우 많습니다. 추후에 정리하여 다시 올리도록 하겠습니다.

 

+ Recent posts