[C++]opencv 알파벳 이미지 덤프파일 추출

글쓴이 Engineer Myoa 날짜

html/css/js로 static한 웹을 작업할 프로젝트가 있어서 알파벳 이미지를 구하고있었는데

대부분 이쁜 아이콘들은 더미식 ai파일로 모아져있더라..

 

이미지는 아래와같은데 이걸 일일히 포토샵으로 따기가 귀찮은 바람에

opencv활용겸 일을 키웠다.

1단계로 추출한 이미지 사이즈는 2831×2879(px)이다.
추출이라 할 것도 없고 그냥 인식률을 올리기위해 워터마크 부분을 제거했다.

Simple Digit Recognition OCR in OpenCV-Python

http://stackoverflow.com/questions/9413216/simple-digit-recognition-ocr-in-opencv-python

를 참고하여  (해당 소스는 python코드지만 c++와 토시하나 안틀리고 같으므로 함수 사용법만 알면 변환에 문제가 없다)

 

#pragma warning(disable: 4819)

#include "opencv.hpp"
using namespace cv;
using namespace std;

int main()
{
	Mat src = imread("H:\\opencv\\alphabet.png");
	if (src.empty()) {
		return -1;
	}
	Mat src2;
	src.copyTo(src2);

	Mat gray, blur, thresh;

	cvtColor(src, gray, COLOR_BGR2GRAY);
	GaussianBlur(gray, blur, Size(5, 5), 0);
	adaptiveThreshold(blur, thresh, 255, CV_ADAPTIVE_THRESH_MEAN_C, CV_THRESH_BINARY, 11, 2);
	// maxValue는 0~maxValue 사이의 이진화된 영상을 만들어준다.
	// blockSize는 이미지에서 이진화시 쪼갤(정확하게는 주변 픽셀 계산범위) 단위
	// 반드시 홀수여야 함 ( val mod 2 == 1)
	// C는 상수로 평균이나 가중치평균에서 감산하는데 음수가 가능.
	// 정확한 의미는 아직 모르겠음.


	imshow("test1", thresh);
	waitKey(0);
	return 0;

}

 

순서대로

gray = src를 그레이스케일로 변환

blur = gray에서 가우시안 블러를 통해 쓸모없는 외곽선을 흐뜨리는듯

thresh = 영상 이진화

스레숄드 함수에 대해서는 주석으로 기록해놨다.

 

 

위는 스레숄드 결과.

stackoverflow의 코드를 그대로 재현하다보니 발생하는 문제이겠으나

나의 경우는 이미지가 매우크기때문에 생각보다 영상 이진화가 세세하게 표현되어버렸다.

아마도 이상태로 contours를 찾아 내면 문제가 발생하지 않을까…

 

일단 이대로 contours를 찾아서 그려보기로 했다.

	//Mat contours, hierarchy;
	vector< vector<Point> > contours; // 외곽선 배열
	vector<Vec4i> hierarchy; // 외곽선간 계층구조

	findContours(thresh, contours, hierarchy, CV_RETR_LIST, CV_CHAIN_APPROX_SIMPLE);
	//findContours()


	cout << contours.size() << endl;

	for (int i = 0; i < contours.size(); i++) {
		if (contourArea(contours[i]) > 1000) { // 면적이 50이상일 경우
			Rect temp = boundingRect(contours[i]);
			if (temp.height > 28) {
				rectangle(gray, temp, Scalar(0, 0, 255), 2);
			}

		}


	}

Contours같은 경우는

스레숄드된 이진화 영상,  array of arrays, array 를 요구하기 때문에

Mat클래스를 대입하는것이아니라 vector의 vector와 vector<Vector4i>가 필요하다.

기본 클래스 공부하다가 갑자기 여기로 넘어오니 모를만하다.. 일단 참조쪽 코드는 파이썬이기 때문에 다이나믹 타입. 내가 삽질해야하므로..

 

무튼 외곽선 배열들과 외곽선간의 계층구조 배열을 이용하여 영역을 그린다.

외곽선 영역은 잡는데 이래선 추출할 수가 없다.

adaptiveThreshold(blur, thresh, 255, CV_ADAPTIVE_THRESH_MEAN_C, CV_THRESH_BINARY, 1001, 100);

일단 블록사이즈와 상수값의 변화에따라 어떻게 변화되는지 알아보자

그만 알아보도록 하자.

 

저래 되는 이유는 상수값이 너무커서 음영이 연하게 있던 부분을 전부 감산되버린 것.

감산범위 상수만 10으로 줄이고 다시 실행해보자.

맘에드는 결과다.

이걸가지고 contours를 찾아보자.

 

아아주 맘에 드는 영역결과이다. 다만 내부 영역도 잡히면 곤란하므로

contourarea의 범위를 넓혀보자.

그전에 contourarea값이 대충 얼마인질 알아야하니

이전 포스트에서 작성한 putText( or addText)를 활용하자.

대충보니 10000은 무조건 넘어야 할 것 같고, O나 Q같은 문자에서 더 영역이 크게 잡히지 않을까 싶다.

모니터가 4k가 아닌지라 이미지가 너무 커서 전체가 보이지않아

파일을 출력하여 뷰어로 확인한다.

 

예상대로 O와 Q는 영역이 17743으로 잡힘.

영역범위를 대충 20000정도로 하면된다.

하지만 간과한것은 이건 폭은 다르지만 높이가 고정됐기 때문에

높이를 기준으로 찾으면 된다는 사실.

 

따라서 코드를 수정한다.

딱봐도 세로가 200픽셀은 넘기때문에 적당히 250픽셀정도로 잡았다.

(실제로 세로는 약 400px)

완벽하게 영역을 추출했다.

이제 각 영역을 이미지로 추출하는 작업을 진행한다.

이미 추출된 contours와 원본 src를 대조하여 각 영역들을 추출해 저장한다.

ROI(Rect Of Interesting)를 통해 관심영역을 지정했었다.

따라서 이 관심영역을 지정해 이미지를 추출하면된다.

 

if (contourArea(contours[i]) > 20000) { // 면적이 20000이상일 경우
			//cout << contours[i] << endl;
			//cout << contourArea(contours[i]) << endl;
			//cout << "===============================" << endl;
			Rect temp = boundingRect(contours[i]);

			if (temp.height > 250) {
				rectangle(gray, temp, Scalar(0, 0, 255), 2);
				putText(gray, to_string(contourArea(contours[i])), Point(temp.x, temp.y - 20), FONT_HERSHEY_SIMPLEX, 1.0, Scalar(0, 0, 0));

				Mat outputImage = src(temp);
				imwrite("H:\\opencv\\" + to_string(i) + ".png", outputImage);

			}


		}

가장 간단하게 ROI를 설정해 추출하는 방법은 원본 객체에 파라미터로 rect를 넘겨주면 된다.

 

#pragma warning(disable: 4819)

#include "opencv.hpp"
using namespace cv;
using namespace std;

int main()
{
	Mat src = imread("H:\\opencv\\alphabet.png");
	if (src.empty()) {
		return -1;
	}
	Mat src2;
	src.copyTo(src2);

	Mat gray, blur, thresh;

	cvtColor(src, gray, COLOR_BGR2GRAY);
	GaussianBlur(gray, blur, Size(5, 5), 0);
	adaptiveThreshold(blur, thresh, 255, CV_ADAPTIVE_THRESH_MEAN_C, CV_THRESH_BINARY, 1001, 2);
	// maxValue는 0~maxValue 사이의 이진화된 영상을 만들어준다.
	// blockSize는 이미지에서 이진화시 쪼갤(정확하게는 주변 픽셀 계산범위) 단위
	// 반드시 홀수여야 함 ( val mod 2 == 1)
	// C는 상수로 평균이나 가중치평균에서 감산하는데 음수가 가능.
	// 정확한 의미는 아직 모르겠음.

	//threshold(blur, thresh, 200, 256, CV_THRESH_BINARY);

	//imshow("test1", thresh);
	//waitKey(0);

	// ================= rdy 4 contour processing ========================

	vector< vector<Point> > contours; // 외곽선 배열
	vector<Vec4i> hierarchy; // 외곽선간 계층구조

	findContours(thresh, contours, hierarchy, CV_RETR_LIST, CV_CHAIN_APPROX_SIMPLE);


	cout << contours.size() << endl;

	for (int i = 0; i < contours.size(); i++) {
		if (contourArea(contours[i]) > 20000) { // 면적이 20000이상일 경우
			//cout << contours[i] << endl;
			//cout << contourArea(contours[i]) << endl;
			//cout << "===============================" << endl;
			Rect temp = boundingRect(contours[i]);

			if (temp.height > 250) {
				if (temp.width == src.size().width && temp.height == src.size().height) {
					continue;
				}
				rectangle(gray, temp, Scalar(0, 0, 255), 2);
				putText(gray, to_string(contourArea(contours[i])), Point(temp.x, temp.y - 20), FONT_HERSHEY_SIMPLEX, 1.0, Scalar(0, 0, 0));
				putText(gray, to_string(i), Point(temp.x - 10, temp.y - 20), FONT_HERSHEY_SIMPLEX, 1.0, Scalar(0, 0, 0));
				Mat outputImage = src(temp);
				imwrite("H:\\opencv\\output\\" + to_string(i) + ".png", outputImage);

			}

		}

	}

	imwrite("H:\\opencv\\output\\contours.png", gray);
	imshow("test1", gray);
	waitKey(0);

	return 0;
}

 

중간결산겸 전체코드

중간에

if (temp.width == src.size().width && temp.height == src.size().height) {
continue;
}

를 빼먹었는데, 혹시라도 영역추출시 전체 영역을 추출하는것을 방지하기 위함이다.

 

정상적으로 추출이 되었는데 문제가 발생하였다.

contours의 순서가 사람의 입장에서 봤을 때 일정치 않다는 것.

아마 findContours의 파라미터중 상수값을 다르게 주면 해결되지 않을까 싶다.

 

는 안되는거 같아 내가 contours영역중 유요한 영역을 다시 재할당 시켜야 한다는것을 깨닫고..

	vector<Rect> priorityROI(contours.size()); // 동적할당

	int *tempIndex = new int;
	*tempIndex = -1;
	for (int i = 0; i < contours.size(); i++) {
		if (contourArea(contours[i]) > 20000) {
			Rect temp = boundingRect(contours[i]);
			if (temp.height >= 250) {
				if (temp.width == src.size().width && temp.height == src.size().height) {
					continue;
				}
				*tempIndex = *tempIndex +1;
				priorityROI[*tempIndex] = boundingRect(contours[i]);
			}
		}
	}

	//cout << contours.size() << endl;
	cout << priorityROI.size() << endl;
	cout << *tempIndex << endl;

	if (*tempIndex > -1) { // 하나도 찾지 못하였을 경우에는 resize를 하지않음.
		priorityROI.resize(*tempIndex); //
		cout << priorityROI.size() << endl;
	}
	else {
		priorityROI.resize(0);

	}

	free(tempIndex);

일단 의미있는 영역만 priorityROI라는 RECT vector 배열에 할당하기로 했다.

따라서 resize시 배열의 요소(RECT단위)는 26개가 되어야한다.

 

 

이제 이 배열을 가지고 Bubble Sort를 할 차례다. Sort의 우선순위는 y값, x값 순서다.

가만보아하니 배열자체는 1차원이지만 내부 객체가 vector의 값을 가지므로 (x, y)

y에 한번, x에 한번 소팅 해주었다.

#pragma warning(disable: 4819)

#include "opencv.hpp"
using namespace cv;
using namespace std;

int main()
{
	Mat src = imread("H:\\opencv\\alphabet.png");
	if (src.empty()) {
		return -1;
	}
	Mat src2;
	src.copyTo(src2);

	Mat gray, blur, thresh;

	cvtColor(src, gray, COLOR_BGR2GRAY);
	GaussianBlur(gray, blur, Size(5, 5), 0);
	adaptiveThreshold(blur, thresh, 255, CV_ADAPTIVE_THRESH_MEAN_C, CV_THRESH_BINARY, 1001, 2);
	// maxValue는 0~maxValue 사이의 이진화된 영상을 만들어준다.
	// blockSize는 이미지에서 이진화시 쪼갤(정확하게는 주변 픽셀 계산범위) 단위
	// 반드시 홀수여야 함 ( val mod 2 == 1)
	// C는 상수로 평균이나 가중치평균에서 감산하는데 음수가 가능.
	// 정확한 의미는 아직 모르겠음.

	//threshold(blur, thresh, 200, 256, CV_THRESH_BINARY);

	//imshow("test1", thresh);
	//waitKey(0);

	// ================= rdy 4 contour processing ========================

	vector< vector<Point> > contours; // 외곽선 배열
	vector<Vec4i> hierarchy; // 외곽선간 계층구조

	findContours(thresh, contours, hierarchy, CV_RETR_TREE, CV_CHAIN_APPROX_SIMPLE);

	vector<Rect> priorityROI(contours.size()); // 동적할당

	int *tempIndex = new int;
	*tempIndex = -1;
	for (int i = 0; i < contours.size(); i++) {
		if (contourArea(contours[i]) > 20000) {
			Rect temp = boundingRect(contours[i]);
			if (temp.height >= 250) {
				if (temp.width == src.size().width && temp.height == src.size().height) {
					continue;
				}
				*tempIndex = *tempIndex +1;
				priorityROI[*tempIndex] = boundingRect(contours[i]);
				cout << *tempIndex << " : "<< priorityROI[*tempIndex] << endl;
			}
		}
	}

	cout << "==============================" << endl;

	//cout << contours.size() << endl;
	cout << priorityROI.size() << endl;
	cout << *tempIndex << endl;

	if (*tempIndex > -1) { // 하나도 찾지 못하였을 경우에는 resize를 하지않음.
		priorityROI.resize(*tempIndex+1);
		cout << priorityROI.size() << endl;
	}else {
		priorityROI.resize(0);
	}

	free(tempIndex);

	for (int i = 0; i < priorityROI.size(); i++) {
		for (int j = 0; j < priorityROI.size() - 1; j++) { // 버블sort이므로 1 작게
			int vH = priorityROI[i].y - priorityROI[j].y;
			if (vH <= -100) {
				Rect temp = priorityROI[j];
				priorityROI[j] = priorityROI[i];
				priorityROI[i] = temp;
			}
		}
	}

	for (int i = 0; i < priorityROI.size(); i++) {
		for (int j = 0; j < priorityROI.size() - 1; j++) { // 버블sort이므로 1 작게
			int vH = priorityROI[i].y - priorityROI[j].y;

			if (abs(vH) <= 20 && priorityROI[i].x < priorityROI[j].x) {
				Rect temp = priorityROI[j];
				priorityROI[j] = priorityROI[i];
				priorityROI[i] = temp;
			}
		}
	}



	for (int i = 0; i < priorityROI.size(); i++) {
		cout << i << " : " << priorityROI[i] << endl;
		Mat outputImage = src(priorityROI[i]);
		imwrite("H:\\opencv\\output\\" + to_string(i) + ".png", outputImage);
		rectangle(gray, priorityROI[i], Scalar(0, 0, 255), 2);
		//putText(gray, to_string(contourArea(contours[i])), Point(priorityROI[i].x, priorityROI[i].y - 20), FONT_HERSHEY_SIMPLEX, 1.0, Scalar(0, 0, 0));
		putText(gray, "(" + to_string(priorityROI[i].x) + ", " + to_string(priorityROI[i].y) + ")", Point(priorityROI[i].x, priorityROI[i].y - 20), FONT_HERSHEY_SIMPLEX, 1.0, Scalar(0, 0, 0));
		putText(gray, to_string(i), Point(priorityROI[i].x - 5, priorityROI[i].y - 60), FONT_HERSHEY_SIMPLEX, 1.0, Scalar(0, 0, 255));

	}
	imwrite("H:\\opencv\\output\\contours.png", gray);

	return 0;
}

 

결과는 매우 흡족.

 

이로써 알파벳 이미지 통파일에서 경계영역을 통해 알파벳을 분리하는 방법을 알아보았다.

 


69개의 댓글

답글 남기기

Avatar placeholder

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다