본문 바로가기

Machine Learning/영상처리

이미지 OCR

이번에는 이미지에 있는 숫자를 인식해보겠다. 다음 그림은 컴퓨터에서 숫자를 그린 것이다. 여러 개의 글자가 적혀 있으므로 일단 문자가 어디 적혀 있는지 인식시켜야 한다


OpenCV로 텍스트영역 확인하기

 

import sys
import numpy as np
import cv2
# 이미지 읽어 들이기 --- (※ 1)
im = cv2.imread('numbers.PNG')
# 그레이스케일로 변환하고 블러를 걸고 이진화하기 --- (※2)
gray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (5, 5), 0)
thresh = cv2.adaptiveThreshold(blur, 255, 1, 1, 11, 2)
# 윤곽 추출하기 --- (※3)
contours = cv2.findContours(
  thresh, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)[1]
# 추출한 윤곽을 반복 처리하기 --- (※4)
for cnt in contours:
  x, y, w, h = cv2.boundingRect(cnt) # --- (※5)
  if h < 20: continue # 너무 작으면 건너뛰기
  red = (0, 0, 255)
  cv2.rectangle(im, (x, y), (x+w, y+h), red, 2)
cv2.imwrite('numbers-cnt.PNG', im)

 

※1에서 이미지를 읽어들이고 ※2에서 블러를 적용한 후 검정색과 흰색으로 이진화한다. 여기서 블러는 이미지에서 화고값이 급격하게 변하는 부분들을 감소시킨다 -이미지가 흐려진다. cv2.adaptiveThreshold() 에서 첫번째 매개변수는 원본 이미지, 두번째 매개변수는 임계값, 세번째 배개변수는 임계값 이상일 경우 바꿀 최대값(보통 흰색인 255로 지정)을 지한다. 네번째 매개변수로 THRESH_BINARY를 사용하면 픽셀값이 임계값보다 클 경우 최대값으로 하고 픽셀값이 임계값보다 작은 경우 1(검은색)으로 이진화를 합니다.

v2.adaptiveThreshold()

※3에서는 cv2.findContours()메서드를 사용해 윤곽을 추출한다.(몰라 암튼 성능이 매우 좋은 함수라고 생각하자)

 

윤곽선을 찾아 텍스트영역을 확인함

결과는 훌룡하였다.

 


문자 인식 데이터 만들기

 

각 문자 영역을 추출했으므로 각 글자를 머신러닝으로 인식시켜 보겠다. mnist의 손글씨 숫자 데이터를 Keras(+TensorFlow)로 학습시키겠다. 일단 MNIST의 손글씨 숫자 데이터를 학습하고, 이를 가중치 데이터로 저장한다, 이어서 이후에 이것을 사용해 문자 인식을 시켜보겠다.

 

from keras.datasets import mnist
from keras.models import Sequential
from keras.layers.core import Dense, Dropout, Activation, Flatten
from keras.layers import Convolution2D, MaxPooling2D
from keras.optimizers import SGD, Adam, RMSprop
from keras.utils import np_utils
image_w = 28
image_h = 28
nb_classes = 10
def main():
  # MNIST 데이터 읽어 들이기
  (X_train, y_train), (X_test, y_test) = mnist.load_data()
  # 데이터 정규화
  X_train = X_train.reshape(X_train.shape[0], image_w * image_h).astype('float32')
  X_test  = X_test.reshape(X_test.shape[0], image_w * image_h).astype('float32')
  X_train /= 255
  X_test  /= 255
  y_train = np_utils.to_categorical(y_train, 10)
  y_test  = np_utils.to_categorical(y_test, 10)

mnist데이터를 읽어들이고 train,test로 데이터를 나눈후 스케일링 및 정답 레이블을 만든다.

  # 모델 구축
  model = build_model()
  model.fit(X_train, y_train,
    batch_size=128, nb_epoch=20, verbose=1,
    validation_data=(X_test, y_test))
  # 모델 저장
  model.save_weights('mnist.hdf5')
  # 모델 평가
  score = model.evaluate(X_test, y_test, verbose=0)
  print('score=', score)
def build_model():
  # MLP 모델 구축
  model = Sequential()
  model = Sequential()
  model.add(Dense(512, input_shape=(784,)))
  model.add(Activation('relu'))
  model.add(Dropout(0.2))
  model.add(Dense(512))
  model.add(Activation('relu'))
  model.add(Dropout(0.2))
  model.add(Dense(10))
  model.add(Activation('softmax'))
  model.compile(loss='categorical_crossentropy',
    optimizer=RMSprop(),
    metrics=['accuracy'])
  return model
if __name__ == '__main__':
  main()

 

여기서 추가로 알게 된점이 있는데 Dense층과 Convolution2D의 차이이다 본 내용은https://tykimos.github.io/2017/01/27/CNN_Layer_Talk/ 에서 자세하게 설명되어있다.(Keras에 관련하여 정말 유용한정보) 하지만 어째서 출력즉 특징맵을 1이상으로 하는지는 의문임

 

score = [0.12294695631083141, 0.98209999997]

MNIST 데이터의 정답률은 98%이다.

 


100개의 숫자를 MNIST 데이터로 인식해보기

이어서 100개의 숫자가 적혀있는 이미지를 문자 인식을 해보자, 깨끗하게 정된된 폰트로 그린 숫자는 괜찮게 나올듯 싶다. 

 

import sys
import numpy as np
import cv2
import ocr_mnist
# MNIST 학습 데이터 읽어 들이기 --- (※1)
mnist = ocr_mnist.build_model()
mnist.load_weights('mnist.hdf5')

# 이미지 읽어 들이기 --- (※2)
im = cv2.imread('numbers100.PNG')

# 윤곽 추출하기 --- (※3)
gray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY) # 그레이스케일로 변환하기
blur = cv2.GaussianBlur(gray, (5, 5), 0) # 블러
thresh = cv2.adaptiveThreshold(blur, 255, 1, 1, 11, 2) # 2진화
cv2.imwrite("numbers100-th.PNG", thresh)
contours = cv2.findContours(thresh, 
    cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[1]

 

※1 에서는 MNIST 손글씨 학습데이터를 읽어들인다.

 

※3 에서 이전에 했듯이 윤곽을 추출한다

 

# 추출한 좌표 정렬하기 --- (※4)
rects = []
im_w = im.shape[1]
for i, cnt in enumerate(contours):
    x, y, w, h = cv2.boundingRect(cnt)
    if w < 10 or h < 10: continue # 너무 작으면 생략하기
    if w > im_w / 5: continue # 너무 크면 생략하기
    y2 = round(y / 10) * 10 # Y좌표 맞추기
    index = y2 * im_w  + x
    rects.append((index, x, y, w, h))
rects = sorted(rects, key=lambda x:x[0]) # 정렬하기
# 해당 영역의 이미지 데이터 추출하기 --- (※5)
X = []
for i, r in enumerate(rects):
    index, x, y, w, h = r
    num = gray[y:y+h, x:x+w] # 부분 이미지 추출하기
    num = 255 - num # 반전하기
    # 정사각형 내부에 그림 옮기기
    ww = round((w if w > h else h) * 1.85) 
    spc = np.zeros((ww, ww))
    wy = (ww-h)//2
    wx = (ww-w)//2
    spc[wy:wy+h, wx:wx+w] = num
    num = cv2.resize(spc, (28, 28)) # MNIST 크기 맞추기
    # cv2.imwrite(str(i)+"-num.PNG", num) # 자른 문자 저장하기
    # 데이터 정규화
    num = num.reshape(28*28)
    num = num.astype("float32") / 255
    X.append(num)

 

※4에서는 숫자를 31415...처럼 순서대로 볼 수있게 좌표를 기반으로 정렬한다.

 

※5에서 자른 문자 이미지를 하나하나 MNIST 데이터처럼 28X28 픽셀로 리사이즈한다. 드런데 윤곽으로 자른 부분을 기반으로 28x28로 곧바로 리사이즈 하면 숫자형태에 변형이 온다 따라서 영역의 너비와 높이를 확인하고 긴 부분을 기반으로 사각형을 만든 뒤에 내부에 문자를 넣고 리사이즈하는 방법을 사용한다.

 

# 예측하기 --- (※6)
s = "31415926535897932384" + \
    "62643383279502884197" + \
    "16939937510582097494" + \
    "45923078164062862089" + \
    "98628034825342117067"
answer = list(s) 
ok = 0
nlist = mnist.predict(np.array(X))
for i, n in enumerate(nlist):
    ans = n.argmax()
    if ans == int(answer[i]):
        ok += 1
    else:
        print("[ng]", i, "번째", ans, "!=", answer[i], np.int32(n*100))
print("정답률:", ok / len(nlist))

 

 ※6에서는 잘라낸 이미지를 정답 숫자와 조합한다, 정답률을 표시한다.

 

Using TensorFlow backend.
[ng] 11번째 5 != 8 [1  0 14  6  0 38  1  1 28  3]
...
...

[ng] 88번째 5 != 8 [1  0 14  6  0 38  1  1 28  3]

 정답률 ; 0.94

정답률로 94%로 좋지 못한 상태인데 이는 다양한 폰트의 숫자를 학습하게 되면 해결된다