12
2016-Mar
[OpenCV] 문자 인식
작성자: Blonix
IP ADRESS: *.148.87.98 조회 수: 1466
뭔가 사업 아이디어가 번뜩여서 찾아보게 됨.
사실 아래 강좌만큼 지저분한 환경의 문자인식은 아니지만, 굉장히 좋은 자료를 찾아서 퍼오지 않을 수 없었다.
아래 링크에 찾아가면 이 외에도 각종 OpenCV 강좌가 들어 있다. 전부 볼 만 하다.
일단 출처 :: http://martinblog.tistory.com/859
예제파일
http://fogeaters.cafe24.com/xe/board11/13777
문자인식 강좌 01. 문자 인식 개론
K. Martin 2009.12.30 15:09안녕하세요 마틴입니다.
문자인식이라는 광범위한 주제의 강좌를 본격적으로 진행해 보고자 합니다.
사실, 문자인식 분야를 제법 오~래 공부해온 저 나름의 자존심 문제도 있고 해서,
어려운 논문들을 참조해가며 체계적이고 또한 학문적인 강좌를 진행하고 싶은 욕심이 컸지만,
금새 바닥을 보일 실력... 거짓말은 하지 말자는 생각에,
이야기 하기 쉽고 많은 사람들이 공감할 수 있는 내용들을 이곳에 정리해 가는 것이
원래 강좌를 시작한 취지에도 맞고 훨씬 바람직하다는 생각이 들었습니다.
역시 그러한 과정에서 저 스스로도 많이 배우고 또 깨달을 수 있는 계기가 될 수 있을 것도 같구요.
두산중공업의 출입문을 지나는 차량의 번호판을 인식하는 일을 시작으로,
삼성테크윈의 불법주정차 차량 번호판 인식, 신선대부두의 컨테이너 식별자 인식 등
주로 실세계 영상의 번호 인식에 관련된 일들을 해왔고,
또한 그런 이유로 본 강좌의 최종 목표가 그쪽 방향이 될 것 같은데요,
아무쪼록 글을 읽으시는 분들에게 도움이 되는 강좌가 되었으면 좋겠습니다.
문자 인식에 앞서...
문자를 인식한다라고 하면 구체화 해두어야 할 몇 가지가 떠오르는데,
단연코 그 첫 번 째 는 어떤 문자를 인식 할 것인가일 것이다.
여기서 '어떤'의 의미 또한 광범위 한데,
그것은 한글이냐, 영어냐, 숫자냐의 종류를 나타내는 '어떤'이 될 수도 있고,
인쇄된 문자를 인식할 것인지 손으로 쓴 글씨를 인식 할 것인지를 구분하는 '어떤'이 될 수도 있고,
길 가다가 찍은 사진에서 문자를 인식 할 것인지, 스캐너로 입력받은 문서를 인식 할 것인지의 대상을 구분하는 '어떤'이 될 수도 있겠다.
이 '어떤'을 고민하는 이유는,
문자를 인식하기 위해서 먼저 무엇이 문자인가?에 대해서 컴퓨터가 알아 듣게끔 프로그램을 개발해야 하기 때문이고,
그말인 즉슨, 우리 개발자들이 눈으로 문자를 보고 그것이 문자라는 것을 판단하기 까지,
어떤 특징들을 가지고 그것이 문자라고 인지하게 되는지를 한 번도 생각해 볼 겨를이 없었기 때문에
뭔가 깊게 생각해볼 여지가 있다는 얘기가 된다.
특징이라고 하는 것은 생김새와 크기가 될 수도 있고, 그것의 색상이 될 수도 있을 텐데,
예를들어, 우리가 사과를 보고 그것이 사과인줄 알게 되는 것은
그것이 가지는 둥그스름한 고유의 생김새와 주먹보다 조금 큼직한 크기,
붉은색 혹은 녹색계열의 색상, 그리고 특유의 꼭지 등을 보기 때문인 것 처럼,
문자를 인식하는 것 또한 여러가지 사물들 중에 사과를 인지해 내듯
문자만이 가진 고유한 특징을 결정하는 것이 우선시 되어야 한다.
무엇이 문자인가?
앞서 '어떤'의 예에 대해 잠시 언급한 바 있는데,
한글과 영어, 또는 한자나 일본어, 숫자... 이런 구분을 설명하기 이전에 과연 영상에서 무엇이 문자인가?에 대해서 구분해 낼 수 있는 방법에 대해 생각 할 필요가 있다. 그리고 그것은 문자가 가진, 다른 어떤것들과 구분되는 특징을 찾아야 한다는 말이 된다.
문자가 가진 특징들 중 먼저 문자라고 하는 것은 '획'으로 이루어져 있다는 점에 주목 할 필요가 있다.
[명사] 1. 글씨나 그림에서, 붓 따위로 한 번 그은 줄이나 점.
2. {수량을 나타내는 말 뒤에 쓰여}글씨나 그림에서, 붓 따위를 한 번 그은 줄이나 점을 세는 단위.
3. 역수(易數)의 괘를 나타내는 산가지에서 가로 그은 표시. 양(陽)을 나타내는 ‘-’과 음(陰)을 나타내는 ‘--’를 이른다.
'획'은 가늘고 긴 선이고, 문자는 적게는 하나, 많게는 10개 이상의 획을 가지기도 한다.
'획'은 영상에서는 윤곽선이라고 이야기 할 수 있는데,
이것이 글자와 함께 있는 다른 어떤 것들의 윤곽선 보다는 조밀하고 숫자가 많기 때문에
문자와 다른 어떤 것들을 구분될 수 있는 특징이 된다.
또한 '문자'가 '문자열'이 되면 획의 수는 증가 되고, 이 특징은 다른 어떤 것들과 더 확실하게 구분 될 수 있다.
어떤 문자를 인식 할 것인가?
'획'이라는 특징을 가지고 문자를 다른 어떤 것들과 구분하다보면,
'획'만으로는 구분이 힘든 경우가 생기기 마련이다.
나무 기둥의 거친 줄기도, 울창하게 우거진 5월의 푸른 나무잎들도,
안경을 끼고 하품을 하고 있는 내 주름 많은 얼굴도, 체크무늬 옷을 입고 있는 강아지도,
심지어는 아스팔트의 조각 하나하나에 비친 햇빛도
많은 윤곽선들 때문에 문자로 구분될 수 있는 여지가 있다.
그런 이유로, '획' 이상의 특징들을 마치 저그가 앞마당 멀티를 펼치듯 추가하기 위하여,
어떤 문자를 인식할 것인지 범위를 좁혀갈 필요가 있다.
하얀 종이위에 볼펜으로 쓴 글씨, 프린트된 문서들은 하얀것은 종이요 까만것은 글자구나!라는 특징을,
자동차 번호판의 문자들은 녹색 배경에 흰색 글자, 노란색 배경에 어두운 색의 글자, 흰색 배경에 검은 글자 등의 특징을,
길가다 찍은 경고표지판의 문자는 보색으로 눈에 잘 띄고 강렬한 인상을 준다는 특징을 추가할 수가 있겠다.
영어나 숫자는 세로 획이 많고, 한글이나 한자는 가로획도 존재하지만 획의 분포가 다른 어떤 것들이 비해 조밀하고,
이들 모두는 대게 가로 길이 보다는 세로 길이가 길다.
붓글씨는 획이 굵고 가변적이며, 인쇄체는 획의 굵기가 일정하다는 것들도 특징으로 추가될 수 있겠다.
이처럼 인식하고자 하는 문자가 어떤 것인가에 따라 해당 문자가 가지는 특징들을 추가하여,
무엇이 문자인가?의 문제를 풀고 문자를 추출해 낼 수 있을 것이다.
문자 인식하기
문자 하나하나를 추출해내고 나면 이들 문자가 무슨 문자인지를 알아낼 수 있도록 해야한다.
앞서 문자를 추출하는 데에 문자 고유의 특징들이 필요했듯,
문자를 인식하는 데에도 각각의 문자가 가지는 특징들을 묻고 답하는 과정이 필요하다.
마치 옛날 화장실에서 귀신이 같은 휴지임에도 빨간휴지 줄까 파란휴지 줄까 물어보는것처럼.
일곱장의 패를 들고 그림이 비슷한 것이 나오면 딱! 때려서 가져 오는 고스톱 같이
0~9까지의 이미지를 들고 추출된 문자와 생김새를 비교해가며 가장 비슷한 것을 답으로 내어 놓을 수도 있겠고,
삼각형은 직선이 세 점에서 만나 이룬 도형이고, 사각형은 네 점에서 만나는 도형이고, 원은 꺾이는 곳이 없다는 식으로
'가'는 꺾이는 부분이 한군데이고 직선의 가운데서 가지가 튀어 나와 있고, '갈'은 가로획이 네 개이고 어쩌고 저쩌고..
'1'은 획이 하나면서 직선이고 '2'는 획이 하나이지만 구부러져 있다는 식으로 각각을 구분할 수 있겠다.
이렇게 각각을 구분할 특징들은 개발자가 직접 정해주기도 하고,
특징들의 조합은 신경망의 학습 과정을 통해 우리는 알지 못하는 소숫점 형태로 계산하여 저장할 수도 있다.
이렇게 말로 주저리 주저리 읊어 나가는 것을 컴퓨터가 할 수 있도록 하는 과정이,
인식 알고리즘의 개발 과정이 된다.
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
문자인식 강좌 02. 문자영역 추출기법 - 1
K. Martin 2010.03.02 08:11모폴로지 연산을 이용하여 문자 영역을 추출하는 방법이다.
모폴로지란 영상을 형태학적 관점에서 보고 접근하는 방법으로,
연산 결과를 눈으로 볼 수 있어서 이해가 쉽다는 장점이 있으며,
대표적인 모폴로지 연산의 예로는 침식(erosion)연산과 팽창(dilation)연산이 있다.
가. 침식(erosion)연산
침식연산은 연산의 이름에서 보듯 깍아나간다는 뜻으로,
정해진 영역(window) 내에서 가장 작은 값을 픽셀 중심부의 값으로 바꾸는 최소값(min)필터의 역할을 한다.
이진영상에서는 객체(object)영역을 흰색으로 볼 때 이들 영역이 줄고 검은색 영역이 늘어나며,
그레이영상(또는 컬러영상)에서는 밝은 영역이 줄고 어두운 영역이 늘어나는 쪽으로 연산 결과가 변한다.
/* erodes input image (applies minimum filter) one or more times.
If element pointer is NULL, 3x3 rectangular element is used */
CVAPI(void) cvErode( const CvArr* src, CvArr* dst,
IplConvKernel* element CV_DEFAULT(NULL),
int iterations CV_DEFAULT(1) );
사용 예 1)
cvErode(src, dst); // 위 주석에서 보는 바와 같이 기본 필터크기는 3x3, 반복 횟수는 1회임
사용 예 2)
IplConvKernel *element;
element = cvCreateStructuringElementEx (9, 9, 4, 4, CV_SHAPE_RECT, NULL); // 필터의 크기를 9x9로 설정
cvErode( src, dst, element );
IplImage* FilterMin(IplImage *src,int nX, int nY)
{
int nWidth = src->width;
int nHeight = src->height;
IplImage* tmp;
tmp = cvCreateImage( cvGetSize( src ), 8, 1 );
cvSetZero(tmp);
int nMin;
for(int j = 0; j < nHeight; j++ )
for(int i = 0; i < nWidth; i++ )
{
nMin = (uchar)src->imageData[j * widthStep + i];
for(int n = j - nY; n < j + nY; n++ )
for(int m = i - nX; m < i + nX; m++ )
{
if( n > -1 && n < nHeight && m > -1 && m < nWidth )
{
if( nMin > (uchar)src->imageData[n * widthStep + m] )
nMin = (uchar)src->imageData[n * widthStep + m];
}
}
tmp->imageData[j * widthStep + i] = (uchar)nMin;
}
return tmp;
}
나. 팽창(dilation)연산
팽창연산은 침식연산과 반대로 영역을 넓혀가는 연산이며 최대값(max)필터의 역할을 한다.
/* dilates input image (applies maximum filter) one or more times.
If element pointer is NULL, 3x3 rectangular element is used */
CVAPI(void) cvDilate( const CvArr* src, CvArr* dst,
IplConvKernel* element CV_DEFAULT(NULL),
int iterations CV_DEFAULT(1) );
사용 예 1)
cvDilate(src, dst); // 위 주석에서 보는 바와 같이 기본 필터크기는 3x3, 반복 횟수는 1회임
사용 예 2)
IplConvKernel *element;
element = cvCreateStructuringElementEx (5, 5, 3, 3, CV_SHAPE_RECT, NULL); // 필터의 크기를 5x5로 설정
cvDilate( src, dst, element, 2 ); // 5x5 크기의 필터로 두 번의 팽창연산 수행
IplImage* FilterMax(IplImage *src,int nX, int nY)
{
int nWidth = src->width;
int nHeight = src->height;
IplImage* tmp;
tmp = cvCreateImage( cvGetSize( src ), 8, 1 );
cvSetZero(tmp);
int nMax;
for(int j = 0; j < nHeight; j++ )
for(int i = 0; i < nWidth; i++ )
{
nMax = (uchar)src->imageData[j * widthStep + i];
for(int n = j - nY; n < j + nY; n++ )
for(int m = i - nX; m < i + nX; m++ )
{
if( n > -1 && n < nHeight && m > -1 && m < nWidth )
{
if( nMax < (uchar)src->imageData[n * widthStep + m] )
nMax = (uchar)src->imageData[n * widthStep + m];
}
}
tmp->imageData[j * widthStep + i] = (uchar)nMax;
}
return tmp;
}
아래의 그림은 세 가지 종류의 영상에 대해 침식 및 팽창 연산을 수행한 결과이다.
반복횟수에 따른 결과 비교
그림에서 보듯 침식연산은 그 필터의 크기 및 사용 횟수에 따라 작은 덩어리의 객체들은 사라지게 할 수도 있고,
반대로 팽창연산은 객체 내부에 있는 작은 구멍(hole)들을 사라지게 할 수 있다.
이처럼 침식, 팽창연산은 잡영을 제거하는데 주로 사용된다.
또한 이들 침식, 팽창 연산을 적절히 섞어 사용하면 객체의 크기는 크게 변화가 없으면서 원하는 잡영을 제거할 수 있는데,
작은 객체들을 사라지게 할 것인지, 작은 구멍들을 사라지게 할 것인지 그 목적에 따라
침식, 팽창 연산을 사용하는 순서가 달라지게 된다.
다. 열기(opening)연산
침식연산을 먼저 수행하고 팽창연산을 수행하여 잡영들을 제거할 수 있다.
사용 예 1)
IplConvKernel *element;
element = cvCreateStructuringElementEx (11, 11, 6, 6, CV_SHAPE_RECT, NULL); // 필터의 크기를 11x11로 설정
cvErode(src, dst, element, 1);
cvDilate(dst, dst, element, 1);
사용 예 2)
cvMorphologyEx(src, dst, NULL, element, CV_MOP_OPEN, 1);
라. 닫기(closing)연산
팽창연산을 먼저 수행하고 침식연산을 수행하여 구멍들을 제거할 수 있다.
사용 예 1)
IplConvKernel *element;
element = cvCreateStructuringElementEx (11, 11, 6, 6, CV_SHAPE_RECT, NULL); // 필터의 크기를 11x11로 설정
cvDilate(src, dst, element, 1);
cvErode(dst, dst, element, 1);
사용 예 2)
cvMorphologyEx(src, dst, NULL, element, CV_MOP_CLOSE, 1);
이들 두 연산은, 수행 후 각각의 객체가 원래의 크기를 유지하므로,
침식 또는 팽창연산을 반복적으로 사용하는 것 처럼 반복적으로 사용한다고 해서 결과가 계속적으로 변하지 않으며,
변화의 정도를 조절하기 위해서는 필터링 하는 영역(window)의 크기를 변화시킬 필요가 있다.
또한, 닫기연산은 작은 크기의 구멍을 채우는데는 효과적이나
커다란 크기의 구멍을 채우기 위해 무작정 영역의 크기를 키우는 것은
객체끼리 붙어버린다던가 수행시간이 오래걸린다던가 하는 원인이 되므로,
이런 경우에는 채움연산을 사용하는 것이 좋다.
마. Top-Hat (Black-Hat) 연산
이와 같이 열림연산(또는 닫힘연산)을 수행한 후
원본 영상과 열림연산(또는 닫힘연산)의 결과의 차이(Top-Hat 또는 Black-Hat Morphology)를 구하면
영상의 텍스쳐 정보(또는 윤곽선 정보)를 얻을 수 있다.
다시말해, 위 결과와 같이 필터의 크기를 적절하게(문자 획의 두께정도) 조절하면
열림연산(또는 닫힘연산)의 결과는 문자과 같은 텍스쳐(윤곽선) 정보가 제거된
마치 배경영상(실제로 이를 배경영상이라 부르기도 한다)과 같은 형태로 남게 되며
이를 원본 영상과 비교하면 문자의 획과 같은 텍스쳐 정보만 남길 수가 있다.
사용 예 1)
cvMorphologyEx(src, dst, NULL, element, CV_MOP_OPEN, 1);
cvSub( src, dst, dst ); // 원본 영상과 열림연산 결과의 차를 구함
사용 예 2)
cvMorphologyEx(src, dst, NULL, element, CV_MOP_TOPHAT, 1);

이렇게 생성된 텍스쳐정보들을 레이블링 한 후
문자의 특성을 가지는 레이블들만 남기고 제거하면
실제 추출하고자하는 문자들만을 남길 수 있다.

이렇게 고해상도 이미지에서는 추출된 문자이미지들을 인식을 위해 바로 사용할 수 있지만,
저해상도 이미지에서는 위 두 번재 예에서와 같이 생성된 레이블링 결과에서 문자영역과 잡영영역을 판단하기 어려우므로,
문자 배열, 문자의 크기 등을 염두에 두고 클러스터링과정을 통해 문자들을 하나하나의 덩어리로 구분한 뒤
텍스쳐들을 뭉치는 작업을 통해 문자 영역을 먼저 추출한 다음,
카메라를 이용하여 줌을 당긴다던가 하여 문자를 추출해 내기도 한다.
본 강의에서는 복잡한 클러스터링 방법 대신 번호판이 가로로 쓰여진 문자이므로 가로로 긴 형태의 윈도우를 사용하여
Top-Hat(또는 Black-Hat)연산 및 팽창연산을 통해 간단하게 문자영역을 추출한다.
이때, 윈도우의 너비를 정하기 위해 앞서 모폴로지 사용 시 글자의 너비를 알고 있으면 유리하듯이
각 글자의 간격을 어느정도 알고 있어야 한다는 제약사항이 따르지만,
앞서 말한바와 같이 비교적 쉽게 문자영역을 추출할 수 있는 좋은 방법이다.
이후 이전 강좌에서 소개된 적 있는 레이블링 기법을 이용하여,
각 덩어리들을 레이블링 한 뒤,
비율, 크기등의 요소들을 고려하여 문자 영역에 가까운(가로 길이가 긴) 것들만을 남겨주면 문자 후보영역을 추출할 수 있다.

향후 다른 형태의 문자 영역 추출에서도 공통적으로 거론 되겠지만,
실제로 이러한 문자의 특징을 찾는 일이 문자 영역을 검증하는 일 보다 먼저 일어나다보니,
문자영역은 최대한 살리고 비문자영역은 줄이려고 범위를 조절하다 보면,
문자만이 가진 또다른 형태의 특징들을 발견해야 할 때가 많다.
이를테면 번호판의 글자를 추출하기 위해 주변의 색상정보를 이용 한다거나,
문자 수가 한정된 인식 대상을 고려하여 획의 개수를 센다거나
대개의 문자들이 배경색과 보색관계에 있으므로 채도 또는 밝기의 차이가 급변하는 통계학적 정보를 이용하는 일이 그것이다.
문자 영역의 검증단계에서 또한 이러한 문자들의 특징을 조금더 한정하여,
최종적인 문자영역을 남길 필요가 있는데,
주로 이진화 후 밝기 값의 밀도를 계산하여 검증의 자료로 사용한다.
자,
이렇게 간단하지만 결코 간단하지만은 않은 여러 과정을 통해 문자영역을 추출하였다.
앞서 이야기한 바와 같이 본 강좌의 예는,
문자의 크기 또는 세로의 굵기가 예측이 가능한 경우 이들 문자의 추출에는 좋은 성능을 보이지만,
영상 내부에 추출하고자 하는 문자의 크기가 다양한 경우는 조금 더 생각해 보아야 할 문제이다.
또한 저해상도 이미지에서 문자 영역을 추출하는 것과 문자를 추출하는 것은 엄연히 다른 문제로,
각각의 문자들을 추출하기 위해서는 설명한 방법들을 통해 문자들이 있는 위치를 추정한 후,
다시 그 영역 내부에서 문자를 추출하기 위한 여러가지 방법들을 고민해야 한다.
영상 입력 -> 이진화 -> 모폴로지 연산 -> 레이블링 -> character segmentation -> 패턴정합법