결론부터 말하면 정말 멍청하고도 단순한 실수로 애꿎은 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/
이 글의 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,을 각각 읽어 숫자로 변환할 수가 없기 때문에 이 경계 테두리를 지우는 과정이 필요하다.
답 주위의 원 또는 네모는 안밖의 경계가 두개씩 생기는 점을 이용했다.
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모델이 문제없이 작동했다.
숫자 이미지 모델 이용해 예측해보기
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)
이 부분은 주석이 친절해서 생략..
장하다 내새끼,,, 똑똑해,,,
'졸업프로젝트 🎓 > OpenCV 🌉' 카테고리의 다른 글
[ OpenCV ] 가로 직선, 세로 직선 지우기 -> 문제 별로 크롭 && 문제 영역 확보해서 자르기 (0) | 2021.02.10 |
---|---|
[ OpenCV ] colab에서 이미지 읽어오기, 보여주기, 저장하기 // 공유드라이브 마운트 (3) | 2021.01.22 |
[ OpenCV ] 이미지 영역 추출하기, 영역 그리기 (0) | 2021.01.12 |
[ OpevCV ] 이미지 흑백 변환, 저장하기 (0) | 2021.01.11 |
[OpenCV] Colab 사용 이미지 가져오기 (0) | 2021.01.11 |