글 작성자: 개발섭

안녕하세요. 머신러닝 마지막 기말고사로 간단한 프로젝트를 만들 기회가 있어서 저는 OpenCV를 통한 숫자 인식기를 만들어보았습니다. 

 

CNN의 경우 MNIST를 통해서 인식 시키는 Model은 여기저기 예제가 많기 때문에 굳이 여기서 명시하지 않고 참고 했던 사이트들을 나열하겠습니다.

 

일단 이번 기회에 중점적으로 설명드릴 부분은 OpenCV를 통해서 숫자를 인식 시키는 것입니다. 

 

1. OpenCV를 통해 숫자 인식시키기

일단 사진 속에서 "숫자"라는 것을 인식 시키는 것이 중요하다. 어떤 식으로 숫자를 인식 시킬 것인가라는 측면에서 이것저것 많이 시도 했다. 시도 자체는 많이 했던 것 같은데, 실제로 무슨 시도를 했었는지를 기록을 따로 하지 않아서 가장 최적의 결과가 나온 방식을 이야기해보겠습니다. (분명히 기억날줄 알았는데, 기억이 안나는건 뭘까...)

 

1. adaptiveThreshold를 통해서 실제로 검은 부분만 잘 픽셀화 시키기.

 

제가 가장 많이 헛갈렸고 가장 어려웠던 부분 중의 하나인 픽셀화 시키는 부분이 꽤나 어려움을 겪었습니다. 진짜 이 "픽셀화"과정이 제일 해맸던 파트중의 하나고 실제로 이 adaptiveThreshold 이후에는 이 사진을 직접적으로 눈으로 확인 할 수가 없기 때문에, matplotlib의 이미지를 넣어서 확인해보면 이런식으로 나오게 됩니다. 

 

matplotlib를 통해서 픽셀화된 부분을 시각적으로 볼수 있다.

실제로 이런 결과를 통해서 이 숫자가 정말로 재대로 픽셀화가 되었는지를 확인해보면서 강도 조절을 할 수 있게됩니다. 실제로 강도 조절을 재대로 하지 않으면 잡음이 너무 많이 생겨서 이상한 부분까지도 인식해버리는 상황이 생깁니다.

 

2. findContours 메소드를 통해 이 숫자의 사각형화. 

1의 경우는 픽셀화만 시키고 실제로 이 숫자가 어디 있는지를 확인하는 것은 불가능합니다. 010010255 이런식으로 판단은 할 수 있다는 것이죠. 하지만 그게 숫잔지 아니면 무엇인지를 인식하는 것은 findContours를 통해서 이 검은 무엇인가를 사각형 박스화 시켜 처리해버립니다. 

 

잘 되면 아주 잘 나온다

실제로 이 findContours를 하는 것은 저런 박스가 직접 그려지는 것은 아니고, 그 위치가 X,Y 값으로 저장되며, 그 인식된 무엇인가의 너비, 높이를 리스트로 저장합니다.

 

3. 인식 범위를 좁히는 과정을 거치자.

물론 잘 되면의 과정이 매우 까다롭긴한데요.. 검은 색이 있으면 대부분 인식을 해버리는 것이기때문에 실제로 너무 많은 점들이 인식되는 것도 문제입니다.

내눈에는 절대 안보이는 검은 점도 인식하는게 더 문제다.

물론 adaptiveThreshold를 통해서 저런 점을 없에는 것도 가능은 하겠지만.. 실제로 완벽하게 다 없에는 것은 사실상 불가능에 가깝죠. 그래서 전제 조건을 좀 잡아줄 필요가 생깁니다. 저런 점을 없애기 위해서요. 

사실상 숫자는 저런 사소한 점에 비해서는 엄청 큽니다. 실제로 저런 점들은 너비나 높이가 엄청 작기때문에 (기껏해봐야 1 이나 2정도? ) 넓이를 통해서 이런 필요없는 정보들을 처냅니다.

 

저는 개인적으로 혹시나 몰라 넓이가 1000인 아래인 점은 무조건 인식되지 않게끔 무조건 처리했습니다. 

 

4. 픽셀화된 부분의 데이터들을 가져오고 그것을 리사이즈화하기

 

픽셀화 된 부분의 데이터를 가져와서 딥러닝 모델의 Input에 들어갈 수 있게 그 모양을 맞춰줘야한다. 여기서 margin을 잡는것은 실제로 예상보다 숫자를 꽉차게 인식시켰을때보다 오히려 여백이 있는 상태에서 인식시키는 것이 더 인식률이 좋다.

실제로 MNIST 데이터들도 일정치의 여백이 있다.

특히 "1"의 경우 오히려 이 폭이 너무 좁아서 인식률이 정말 정말 낮은데, margin을 다른 숫자에 비해서 더 넓히는 방식을 통해서 인식률을 개선한 바가 있다. 

 

그리고 이 데이터를 모델에 넣기위해서 Resize를 시켜야한다. MNIST CNN의 예제의 경우 대부분 28*28로 처리하고 줄인 상태에서 reshape(1,28,28,1)로 바꿔서 처리를 하기도 한다. ㅋㅅㅋ;

 

5. 예측한 값을 사진에 넣고 완성~

 

모델을 불러오고, numpy의 argmax를 통해서 그 숫자가 무엇인지를 숫자화 시킨뒤에 이미지에 넣어주면 아까와 같은 사진이 나오게 됩니다.

그러면 완성!

 

물론 제 코드들이 완벽하지는 않습니다...

실제로 제 코드들은 최대한 모든 경우에 대해서 잘 되기를 기대하고 열심히 노력했으나, 실제로는 인식률이 오락가락해서 좀 안타까웠습니다. 

 

특히 얇은 펜으로 쓴 데이터들은 제가 직접 조정을 해줘야지만 인식이 잘 되는 Case가 있었기 때문에 유의미한 성과를 거뒀다고 생각해야할지... 정확하게는 모르겠네요. ㅠㅠ 

어쨌든 OpenCv를 통해서 이미지 처리와 실제 숫자 인식을 성공시켰고 실제로 숫자 예측을 하는 것을 제눈으로 직접 구현해보고 이것을 만들었다는 것에 의의를 두겠습니다!

 

마지막으로 실제 구동하는 동영상을 보여드리고 마무리 하겠습니다! 

 

 

 

 

 

 

참고 많이 했었던 사이트!

https://d2.naver.com/helloworld/8344782

실제 코드!


import cv2
import matplotlib.pyplot as plt
import numpy as np
from tensorflow.keras.models import load_model


img = cv2.imread("9.jpeg") # 이미지 읽어오기
img_gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) # 컬러 사진이기 때문에 흑백으로 변환해야함. 흑백으로 변환
# 그러면 사진의 연결되어 있는 것만 픽셀화함. numpy의 배열값으로 변환 해준다.
# img_checker = cv2.adaptiveThreshold(img_gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 175, 45)
img_checker = cv2.adaptiveThreshold(img_gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 191, 15)
plt.imshow(img_checker) # Numpy로 배열화된 이미지화해서 볼수 없기때문에 matplotlib로 변환해서봐야함
plt.show()
# 이미지에서 숫자가 있다면, 그 경계를 찾아주는 함수입니다.
_, contours, hierarchy = cv2.findContours(img_checker, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 그래서 경계선을 찾을것을 바탕으로 사각형화 시키는 것. 시작점의 X,y좌표와 너비와 높이를 배열에 저장.
rects = [cv2.boundingRect(contour) for contour in contours]
# 그 사각형 좌표들을 for문을 돌면서 체킹
for rect in rects:
    if rect[2]*rect[3] < 1000: # 너무 작은 점도 인식하기때문에, 그 작은 점의 넓이가 1000정도 되면 매우 작은 점은 인식하지 않고 넘길 수 있다.
        continue
    print(rect)
    # 사진 내에 숫자가 인식된 경우, 그 테두리를 초록색으로 그립니다.
    cv2.rectangle(img, (rect[0], rect[1]), (rect[0] + rect[2], rect[1] + rect[3]), (0, 255, 0), 3)
    if rect[2] < 50: # 전처리시 1 같은 너비가 좁은 숫자는 인식률이 좀 낮아서 너비를 넓히는 방향으로 진행.
        margin = 100
    else:
        margin = 30
    # x좌표-마진부터 x좌표+너비+마진 크기만큼 즉, 사각형의 배열의 일정 부분보다 좀더 가져올 수 있다.
    roi = img_checker[rect[1]-margin:rect[1]+rect[3]+margin, rect[0]-margin:rect[0]+rect[2]+margin]
    # 이미지 사이즈를 28*28사이즈로 줄임.
    try:
        roi = cv2.resize(roi, (28, 28),  cv2.INTER_AREA)
    except Exception as e:
        print(str(e))
    # 딥러닝 모델 불러오기
    model = load_model('model.h5')
    # roi 배열의 모든 값을 255.0으로 나눕니다
    roi = roi/255.0
    # 딥러닝 모델에 알맞게 input 데이터를 변형합니다.
    img_input = roi.reshape(1, 28, 28, 1)
    # 그리고 그 딥러닝 모델을 예측합니다.
    prediction = model.predict(img_input)
    # 그 예측값을 통해 argmax로 값을 직접 숫자화 합니다.
    num = np.argmax(prediction)
    print(num)
    location = (rect[0]+rect[2], rect[1] + 20)
    # 사진안에 그 이미지가 있는 위치에 예측되어진 숫자를 넣는다.
    cv2.putText(img, str(num), location, cv2.FONT_HERSHEY_COMPLEX, 1.5, (0, 0, 0), 2)

# 사진 보여주고 종료
cv2.imshow("Resulting Image", img)
cv2.imwrite("R10.jpg", img)
cv2.waitKey()