졸업프로젝트 🎓/OpenCV 🌉

[ OpenCV + MNIST ] 손글씨로 쓴 숫자 인식하기 !

컴공생 C 2021. 2. 17. 20:15
반응형

결론부터 말하면 정말 멍청하고도 단순한 실수로 애꿎은 CNN 을 괴롭히고 있었다 🥲 (언니가 미안해...)

지금 내가 하고 있는 작업은 프론트에서 받아온 원본 사진을 처리해서 문제 영역 자르기-> 자른 영역에서 네모 친 답영역 추출 -> 손글씨로 쓴 답 ocr 하기

이중에서 답영역 추출 + ocr 하기를 시도해 보았다.

 일단 사용할 이미지는 이것이다.

MNIST 모델 
#MNIST 모델 
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt

(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()

# 각종 파라메터의 영향을 보기 위해 랜덤값 고정
tf.random.set_seed(1234)

# Normalizing data
x_train, x_test = x_train / 255.0, x_test / 255.0

# (60000, 28, 28) => (60000, 28, 28, 1)로 reshape
x_train = x_train.reshape(-1, 28, 28, 1)
x_test = x_test.reshape(-1, 28, 28, 1)

# One-hot 인코딩
y_train = tf.keras.utils.to_categorical(y_train, 10)
y_test = tf.keras.utils.to_categorical(y_test, 10)

model = tf.keras.Sequential([
    tf.keras.layers.Conv2D(kernel_size=(3,3), filters=64, input_shape=(28,28,1), padding='same', activation='relu'),
    tf.keras.layers.Conv2D(kernel_size=(3,3), filters=64, padding='same', activation='relu'),
    tf.keras.layers.MaxPool2D(pool_size=(2,2)),

    tf.keras.layers.Conv2D(kernel_size=(3,3), filters=128, padding='same', activation='relu'),
    tf.keras.layers.Conv2D(kernel_size=(3,3), filters=256, padding='valid', activation='relu'),
    tf.keras.layers.MaxPool2D(pool_size=(2,2)),

    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(units=512, activation='relu'),
    tf.keras.layers.Dropout(0.5),
    tf.keras.layers.Dense(units=256, activation='relu'),
    tf.keras.layers.Dropout(0.5),
    tf.keras.layers.Dense(units=10, activation='softmax')
])

model.compile(loss='categorical_crossentropy', optimizer=tf.optimizers.Adam(lr=0.001), metrics=['accuracy'])
model.summary()

model.fit(x_train, y_train, batch_size=100, epochs=10, validation_data=(x_test, y_test))

result = model.evaluate(x_test, y_test)
print("최종 예측 성공률(%): ", result[1]*100)

출처: laboputer.github.io/machine-learning/2020/03/12/mnist995/

 

MNIST 숫자 분류, 기본 모델부터 정확도 99.5% 까지

텐서플로우(Tensorflow 2.0) 기반으로 여러가지 예측모델을 구현하면서 MNIST 숫자 이미지 분류를 정답률 99.5% 까지 달성하기까지의 과정을 포스팅합니다. 기본적인 로지스틱 회귀(Logistic regression)부

laboputer.github.io

이 글의 4번 부분을 사용했다. 일단은 돌아가는지 확인하기 위해 구글링한 코드를 사용했고, 추후에 좀더 보완할 예정 :)

바깥 테두리 지우고 숫자영역 찾아내기

일단 이미지를 처리해주었다.

import cv2
import matplotlib.pyplot as plt
%matplotlib inline

#이미지 읽어오기
img = cv2.imread("/content/drive/MyDrive/GRADING_Study/kh/hand/ex/788.png")
plt.figure(figsize=(15,12))
print("img")

#이미지 흑백처리
img_gray=cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 

#이미지 블러
img_blur = cv2.GaussianBlur(img_gray, (5, 5), 0)

#이미지 내의 경계 찾기
ret, img_th = cv2.threshold(img_blur, 127, 255, cv2.THRESH_BINARY_INV)
contours, hierachy= cv2.findContours(img_th.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)

#경계를 직사각형으로 찾기
rects = [cv2.boundingRect(each) for each in contours]

#왼쪽에 있는 경계 순서대로 정렬
rects=sorted(rects)
thickness=abs(rects[0][2]-rects[1][2])*2

#가장 밖에 있는 경계선 찾기
contour_sizes = [(cv2.contourArea(contour), contour) for contour in contours]
biggest_contour = max(contour_sizes, key=lambda x: x[0])[1]

#찾은 경계선 흰색으로 칠하기
cv2.drawContours(img_blur, biggest_contour,-1,(255,255,255),thickness)
cv2_imshow(img_blur)

#경계선 지우고 경계 다시 찾기 : 숫자만 찾기 위해서
ret, img_th = cv2.threshold(img_blur, 127, 255, cv2.THRESH_BINARY_INV)
contours, hierachy= cv2.findContours(img_th.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

#경계 직사각형 찾기
rects = [cv2.boundingRect(each) for each in contours]
#왼쪽부터 읽어와야 하므로 정렬 
rects=sorted(rects)

# 사각형 영역 추출 확인하기
# for rect in rects:
#     print(rect)
#     cv2.circle(img_blur, (rect[0],rect[1]),10,(0,0,255), -1)
#     cv2.circle(img_blur, (rect[0]+rect[2],rect[1]+rect[3]),10,(0,0,255), -1)
#     cv2.rectangle(img_blur,(rect[0],rect[1]),(rect[0]+rect[2],rect[1]+rect[3]),(0,255,0),3)
    
cv2_imshow(img_blur)
더보기

cv2.threshold(img, threshold_value, value, flag)

img: grayScale이고 threshold_value는 픽셀 문턱값이고 문턱값 이상이면 flag 에 따라 value로 바꿈. 

flacv2.THRESH_BINARY: threshold보다 크면 value로, 작으면 0으로 

cv2.THRESH_BINARY_INV: threshold보다 크면 0으로 작으면 value로 

cv2.THRESH_TRUNC: threshold보다 크면 value로 지정 작으면 기존의 값 그대로 사용 

cv2.THRESH_TOZERO: treshold_value보다 크면 픽셀 값 그대로 작으면 0으로 

cv2.THRESH_TOZERO_INV: threshold_value보다 크면 0으로 작으면 그대로

출처: https://hoony-gunputer.tistory.com/entry/opencv-python-이미지-Thresholding [후니의 컴퓨터]

 

가장 유용하게 사용한 함수는 아래 findContours함수이다.

더보기

_, countors, _ = cv2.findContours(thr, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

이미지에서 찾는 contours와 contours의 계층 구조를 반환해줍니다. 우리는 contours에만 관심이 있기 때문에 필요없는 것들은 '_'으로 받았습니다.

thr: threshold 해준 이미지 데이터를 첫번째 인자로 집어넣는다.

cv2.RETR_TREE: 두 번째 인자는 contour 추출 모드이며, 2번째 리턴값인 hiearchy의 값에 영향을 준다. 

cv2.RETR_EXTERNAL: 이미지 가장 바깥쪽의 contour만 추출

cv2.RETR_LIST: contour의 계층구조 상관관계를 고려하지 않고 contour을 추출

cv2.RETR_CCOMP: 이미지에서 모든 contour를 추출한 후, 2단계 contour 계층 구조로 구성함. 

1단계 계층에서는 외곽 경계 부분을, 2단계 계층에서는 구멍(hole)의 경계 부분을 나타내는 contour러 구성됨

cv2.RETR_TREE: 이미지에서 모든 contour을 추출하고 contour들간의 상관관계를 추출함

cv2.CHAIN_APPROX_SIMPLE: 세번째 인자는 contour 근사 방법이다.

cv2.CHAIN_APPROX_NONE: contour를 추출하고 Contour들간의 상관관계를 추출함 

cv2.CHAIN_APPROX_SIMPLE: contour의 수평, 수직, 대각선 방향의 점은 모두 버리고 끝점만 만겨둠 

CHAIN_APPROX_TC89_L1: Teh_Chin 연결 근사 알고리즘을 적용함 
출처: https://hoony-gunputer.tistory.com/entry/OpenCV-python-Contour [후니의 컴퓨터]

처음엔 cv2.RETR_TREE를 사용해서 모든 경계를 찾았다.

여기에서 사용한 217+ 동그라미이미지에 cv2.RETR_EXTERNAL을 적용하면 217+O를 하나의 덩어리로 인식해 버린다.

그러면 2,1,7,을 각각 읽어 숫자로 변환할 수가 없기 때문에 이 경계 테두리를 지우는 과정이 필요하다.

(좌) cv2.RETR_TREE로 찾은 경계 (우) cv2.RETR_EXTERNAL로 찾은 경계

답 주위의 원 또는 네모는 안밖의 경계가 두개씩 생기는 점을 이용했다. 

rects=[cv2.boundingRect(each) for each in contours] 를 통해 만들어진 경계들을 직사각형 형태로 만들었다.

rects 를 찍어보면 총 네개 의 값이 나온다.

이런식으로 나오는데 rects의 요소를 rect로 받아 살펴보면

rect[0] : 직사각형의 왼쪽 상단 점의 x 좌표

rect[1]: 직사각형의 왼쪽 상단 점의 y 좌표

rect[2]: 직사각형의 가로 길이

rect[3]: 직사각형의 세로 길이 이다.

#왼쪽에 있는 경계 순서대로 정렬
rects=sorted(rects)
thickness=abs(rects[0][2]-rects[1][2])*2

그래서 rects 를 정렬하면 rect[0]값을 기준으로 rect들이 정렬된다.

이를 이용해서 가장 바깥 테두리의 경계의 두께(thickness) 를 구한다. <= 얘를 이용해서 경계를 흰색으로 칠할 것이기 때문!

rects의 요소의 세로길이 차를 이용했다. 그리고 두께가 균일하지 않을 것을 고려해 차의 2배를 두께로 설정한다.

#가장 밖에 있는 경계선 찾기
contour_sizes = [(cv2.contourArea(contour), contour) for contour in contours]
biggest_contour = max(contour_sizes, key=lambda x: x[0])[1]

(contour들의 넓이, contour)로 contour_sizes의 배열을 구성한다. 딕셔너리 처럼 쓰는 방식

contour_sizes 중 contour 넓이(x[0])를 키값으로 사용해 넓이가 가장 큰 폐곡선(max(여기서 찾은 x)의 [1]) 을 이용한다.

#찾은 경계선 흰색으로 칠하기
cv2.drawContours(img_blur, biggest_contour,-1,(255,255,255),thickness)
cv2_imshow(img_blur)

img_blur 라는 이미지에 위에서 구한 가장 바깥 경계인 biggest_contour의 전체(-1을 이용) 를 흰색(255,255,255)으로, 그리고 두께는 위에서 구한 두께(thickness) 로 칠한다.

결과는 다음과 같다.

이제 이 이미지를 바탕으로 다시 숫자 박스들을 찾고, MNIST 모델로 숫자를 예측하면 된다.

MNIST train, test 데이터는 사이즈가 (-1,28,28,1)로 정해져있다.

위 이미지를 추출하면 사이즈가 제각각이다.

#경계선 지우고 경계 다시 찾기 : 숫자만 찾기 위해서
ret, img_th = cv2.threshold(img_blur, 127, 255, cv2.THRESH_BINARY_INV)
contours, hierachy= cv2.findContours(img_th.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

#경계 직사각형 찾기
rects = [cv2.boundingRect(each) for each in contours]
#왼쪽부터 읽어와야 하므로 정렬 
rects=sorted(rects)

주석처리한 부분을 해제하고 실행해보면

순서대로 영역을 찾아냈음을 확인

 

추출한 숫자영역 MNIST 돌리기 전처리
from google.colab.patches import cv2_imshow

#이전에 처리해놓은 이미지 사용
img_for_class = img_blur.copy()

#최종 이미지 파일용 배열
mnist_imgs=[]
margin_pixel = 15

#숫자 영역 추출 및 (28,28,1) reshape

for rect in rects:
	print(rect)
	#숫자영역 추출
    im=img_for_class[rect[1]-margin_pixel:rect[1]+rect[3]+margin_pixel,rect[0]-margin_pixel:rect[0]+rect[2]+margin_pixel]
    row, col = im.shape[:2]
    
    #정방형 비율을 맞춰주기 위해 변수 이용
    bordersize= max(row,col)
    diff=min(row,col)
    
    #이미지의 intensity의 평균을 구함
    bottom = im[row-2:row, 0:col]
    mean = cv2.mean(bottom)[0]

    # border추가해 정방형 비율로 보정
    border = cv2.copyMakeBorder(
    	im,
    	top=0,
    	bottom=0,
    	left=int((bordersize-diff)/2),
    	right=int((bordersize-diff)/2),
    	borderType=cv2.BORDER_CONSTANT,
    	value=[mean, mean, mean]
    )
    
    
    square=border
    cv2_imshow(square)
    
    #square 사이즈 (28,28)로 축소
    resized_img=cv2.resize(square,dsize=(28,28),interpolation=cv2.INTER_AREA)
    mnist_imgs.append(resized_img)
    cv2_imshow(resized_img)

 

이 부분이 빠져서 가장 애를 먹었던 부분이다 🤨

처음에 단순하게 숫자영역을 추출하면 비율이 제각각이다.

예를들어 3을 손글씨로 쓰게되면 대충 가로:세로의 비율이 55:100 정도라 할때 reshape로 비율을 무시하고 (28,28)로 맞춰버리니까

굉장히 찌그러진 3이 된다. 결국 MNIST CNN 모델에 돌려도 제대로 읽지 못했다.

하지만 비율을 미리 맞추고 reshape를 하니 CNN모델이 문제없이 작동했다.

차례로 각 숫자의 rect, border, resized_img

숫자 이미지 모델 이용해 예측해보기
for i in range(len(mnist_imgs)):

    img = mnist_imgs[i]
    cv2_imshow(img)
     # 이미지를 784개 흑백 픽셀로 사이즈 변환
    img=img.reshape(-1, 28, 28, 1)
   

    # 데이터를 모델에 적용할 수 있도록 가공
    input_data = ((np.array(img) / 255) - 1) * -1
    input_data

    # 클래스 예측 함수에 가공된 테스트 데이터 넣어 결과 도출
    res = np.argmax(model.predict(input_data), axis=-1)

    print(res)

이 부분은 주석이 친절해서 생략..

장하다 내새끼,,, 똑똑해,,,

 

반응형