본문 바로가기

문돌이 존버/카카오 챗봇 스터디

Google Vision API OCR 및 AWS Translate API 예제 코드, feat. Opencv 전처리

반응형

오늘은 제가 프로젝트에 실제로 적용한 Google Vision API의 OCR Python 예제 코드와 AWS Translate API Python 예제 코드를 공유하려고 합니다. 당연히 구글이나 AWS 공식 문서에도 잘 나와있지만 커스터마이징한 버전이 필요할 것 같고, 저도 할 때마다 공식 문서를 보는 것이 귀찮아 제 블로그에 기록하기로 했습니다 ㅎㅎ

(주의!) 아래 코드를 돌리려면 사전에 GCP나 AWS 환경 세팅이 다 이루어져야 합니다. 관련 부분은 제가 작성한 다른 글을 먼저 봐주시기 바랍니다. 

먼저, 아래는 Google Vision API의 OCR Python 예제 코드입니다. 제가 임의로 바꾼 코드이니 공식 문서에 나온 예제 코드가 궁금하다면 이곳을 참고해주시기 바랍니다. 

이전 글에도 설명드렸듯이, 구글 API를 사용하려면 사용자 계정 인증을 먼저 활성화해줘야 합니다. 그래서 사용하기 전 터미널에 export GOOGLE_APPLICATION_CREDENTIALS="키파일저장경로/키파일명.json" 를 입력해줄 필요가 있었죠. 하지만 매번 이렇게 하기 귀찮으니 코드를 돌릴 때 환경 설정을 해주는 방식으로 os.environ['GOOGLE_APPLICATION_CREDENTIALS']="키파일저장경로/키파일명.json" 을 활용하기로 했습니다. 이렇게 하면 터미널에서 굳이 활성화를 하지 않아도 되는 것이죠.

아래 코드의 opencv 전처리 과정은 stackoverflow에 소개된 워낙 유명한 샘플 코드인 듯 하네요.

The MIT License (MIT)

Copyright (c) 2017 Dhanushka Dangampola

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-----------------------------------------------------------------------------------------------------------------
import os
from google.cloud import vision
import io
os.environ['GOOGLE_APPLICATION_CREDENTIALS']=r"키파일저장경로/키파일명.json"

photo = TBL_IMG_INFO.objects.last()
path = photo.image.path # 장고 모델 ImageField 사용 -> 이미지가 저장된 경로 출력

# opencv 전처리
large = cv2.imread(path)
rgb = cv2.pyrDown(large)
small = cv2.cvtColor(rgb, cv2.COLOR_BGR2GRAY) # grayscale 이미지
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) # 커널 모양 1
grad = cv2.morphologyEx(small, cv2.MORPH_GRADIENT, kernel) # dilation 이미지와 erosion 이미지 차이를 통해 노이즈 제거
_, bw = cv2.threshold(grad, 0.0, 255.0, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # binary 이미지
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 1)) # 커널 모양 2
connected = cv2.morphologyEx(bw, cv2.MORPH_CLOSE, kernel) # morphology close 기법

# RETR_CCOMP 대신 RETR_EXTERNAL 사용
contours, hierarchy = cv2.findContours(connected.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
mask = np.zeros(bw.shape, dtype=np.uint8)

# contour 필터링
for idx in range(len(contours)):
    x, y, w, h = cv2.boundingRect(contours[idx])
    mask[y:y+h, x:x+w] = 0
    # (255, 255, 255) = 흰색 = 픽셀 1로 표시
    cv2.drawContours(mask, contours, idx, (255, 255, 255), -1)
    # contour 부분에 위치한 0이 아닌, 즉 픽셀 1의 비율
    r = float(cv2.countNonZero(mask[y:y+h, x:x+w])) / (w * h)
    # contour 부분에 text가 포함되어 있다면 최소한 45%가 픽셀 1로 채워졌을 것이라고 가정
    if r > 0.45 and w > 8 and h > 8:
        cv2.rectangle(rgb, (x, y), (x+w-1, y+h-1), (0, 255, 0), 2)
cv2.imwrite('testocr_mod.jpg', rgb)

# 구글 Vision API 작동
client = vision.ImageAnnotatorClient()
with io.open('testocr_mod.jpg', 'rb') as image_file:
    content = image_file.read()
    
image = vision.Image(content=content)

response = client.text_detection(image=image)
texts = response.text_annotations

textlist = ''
for text in texts:
    textlist = textlist + text.description

# 아래는 텍스트가 아닌 텍스트 영역을 찾아낸 바운딩 박스 위치를 출력하는 부분으로 필자는 필요 없음
# vertices = (['({},{})'.format(vertex.x, vertex.y)
               # for vertex in text.bounding_poly.vertices])
# print('bounds: {}'.format(','.join(vertices)))

위 코드 세부사항을 살펴보기 전 저에게 정말 많은 도움을 준 옥수별님 블로그 글 감사합니다. 또한, 멈춤보단 천천히라도 블로그 글 역시 감사드립니다.

본격적으로 최대한 알기 쉽게 설명해보겠습니다. cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) 의 파라미터로 cv2.MORPH_ELLIPSE 가 사용되었습니다. 이는 타원형 모양으로 커널 매트릭스를 생성하고, 아래의 cv2.MORPH_RECT 는 직사각형으로 커널 메트릭스를 생성합니다. 아래 실제 실행 결과를 보면 더 이해가 쉬울 것입니다. 

cv2.MORPH_ELLIPSE: 

[[0 0 1 0 0]
 [1 1 1 1 1]
 [1 1 1 1 1]
 [1 1 1 1 1]
 [0 0 1 0 0]]

cv2.MORPH_RECT:

[[1 1 1 1 1]
 [1 1 1 1 1]
 [1 1 1 1 1]
 [1 1 1 1 1]
 [1 1 1 1 1]]

다음은 contour, 즉 윤곽선 전처리 코드를 설명하겠습니다. cv2.findContours(connected.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) 에서 cv2.RETR_EXTERNAL 은 이미지 객체의 가장 바깥쪽의 윤곽을 추출하는 것입니다. cv2.CHAIN_APPROX_NONE 은 contour를 구성하는 모든 점을 저장하는 방법입니다. 정리하자면 2번째 파라미터는 contour 추출 모드이고 2번째 리턴값인 hierarchy 에 영향을 주며, 3번째 파라미터는 contour 근사 방법입니다.

cv2.drawContours() 는 우리가 찾은 contour를 실제 그리는 함수로 5가지 파라미터를 받습니다. 가장 기본적인 형태는 다음과 같습니다. cv2.drawContours(image, contours, -1, (0, 255, 0), 3) 에서 contours는 cv2.findContours() 의 2번째 리턴값이며 리스트 형태입니다. 3번째 파라미터 -1 은 실제로 그릴 contour의 인덱스 파라미터로 리스트 형태라고 했으니 -1이면 모든 contour를 그린다는 것을 아시겠죠. 4번째 파라미터 (0, 255, 0) 은 contour 표시 색상을 의미하고, 5번째 파라미터 3 은 contour 선의 두께입니다.  

제 코드에선 cv2.drawContours() 함수의 첫 번째 파라미터가 image가 아닌 mask로 되어있는데, mask 변수를 보시면 binary 이미지의 shape을 따라 0으로 표시한 형태이죠. 그리고 boundingRect() 함수를 통해 구한 텍스트 영역을 둘러싼 박스 좌표를 활용하여 필터링 역할을 하고 있습니다. 결국 일부 조건(r > 0.45, w > 8, h > 8)을 만족하는 순간의 박스 좌표를 바탕으로 초록색 바운딩 박스를 그리는 것이죠. 

boundingRect() 함수를 살펴보면, contours 리스트의 모든 인덱스를 계산하여 이미지 객체에 외접하고 똑바로 세워진 직사각형의 좌상단 꼭짓점 좌표 (x, y) 와 가로 세로 폭을 출력합니다. 그래서 마지막에 rectangle() 함수에서 좌상단 꼭짓점 좌표와 우하단 꼭짓점 좌표가 파라미터로 들어간 것이죠. 

mask = np.zeros(bw.shape, dtype=np.uint8) 에서 mask의 구조에 대해 더 이해해봅시다.(제가 많이 헷갈렸거든요^^) 머릿속으로 그려지지 않는다면 역시 간단한 실험을 해봐야겠죠. bw는 3차원 구조이고, mask[y:y+h, x:x+w] = 0 은 column은 건드리지 말고 row와 list를 건드린다고 생각하면 됩니다. 아래 출력값을 보면 더욱 이해하기 쉬울 것입니다. 

bw = (5, 5, 1)
mask = np.ones(bw, dtype=np.uint8)
mask[0:2, 0:2] = 0
print(mask)
[[[0] # index가 0인 row
  [0] # index가 1인 row
  [1]
  [1]
  [1]]

# index가 1인 리스트
 [[0] # index가 0인 row
  [0] # index가 1인 row
  [1]
  [1]
  [1]]

 [[1]
  [1]
  [1]
  [1]
  [1]]

 [[1]
  [1]
  [1]
  [1]
  [1]]

 [[1]
  [1]
  [1]
  [1]
  [1]]]

다만 본 코드에서는 이미 mask=np.zeros() 를 선언한 상태라 모든 값이 0인 상황입니다. 제가 판단하기에 mask[y:y+h, x:x+w] = 0 을 굳이 넣은 이유는 바로 뒤에 나오는 cv2.drawContours() 에서 1이 추가될 것이기 때문에 명확한 비교를 위해 추가한 듯 합니다. 

마지막으로 언급해야 할 부분은 다음의 if r > 0.45 and w > 8 and h > 8: 조건문인데요. 0.45, 8, 8 처럼 특정 숫자(like 임계값)가 적혀 있는 것을 알 수 있습니다. 이는 사실 인식하는 이미지마다 달라야 하겠지만 저는 참고한 소스 코드 그대로 가져왔기 때문에 바꾸지 않았습니다. 여러 이미지를 실험해보면서 적절한 크기를 조정하거나 또는 최적의 임계값을 찾는 함수가 존재할 수도 있겠습니다.

최종 결과 이미지는 아래와 같고 잘 작동한 듯 하네요.

아래는 Google Vision API의 OCR 결과입니다. 

Texts:
GERICARE NDC 57896-911-36 ADULT LOW STRENGTH CHEWABLE ASPIRIN Pain Reliever (NSAID) Orange Flavored 
PACKAGE NOT CHILD RESISTANT 36 Tablets 81 mg each
Tamper Evident: Do not use if imprinted seal under cap is missing or broken.
Drug Facts Cama an
GERICARENDC57896-911-36ADULTLOWSTRENGTHCHEWABLEASPIRINPainReliever(NSAID)OrangeFlavoredPACKAGENOTCHILDRESISTANT36Tablets81mgeachTamperEvident:Donotuseifimprintedsealundercapismissingorbroken.DrugFactsCamaan

 


 

이제 AWS Translate API Python 예제 코드입니다. 마찬가지로 공식 문서에 나온 예제 코드가 궁금하신 분은 이곳을 방문해 원본을 참고해보세요. 

참고로 아래 SourceLanguageCode="auto" 부분은 번역해야 하는 언어가 무슨 언어인지 모를 경우 선택할 수 있는 사항으로, API가 자동으로 식별해주는 굉장히 강력한 기능이죠. 하지만 이 경우 AWS Comprehend API가 실행되어야 하기 때문에 해당 API에 대한 사용 권한이 우선시되어야 합니다. 해당 API에 대한 사용 권한만 있다면 따로 코드를 작성할 필요 없이 자동으로 식별할 것입니다. 

translate = boto3.client('translate')
result = translate.translate_text(Text=textlist,
                                SourceLanguageCode="auto", # 언어 자동 탐지
                                TargetLanguageCode="ko") # 한국어
translated = result["TranslatedText"]

번역 API는 구글의 것을 사용하지 않고 AWS를 사용해봤는데요, 다음에 한 번 구글 번역 API도 쓰고 비교해보겠습니다. 아마도 (당연하게도?) 구글 번역 API 품질이 훨씬 좋을 것이라고 예상됩니다.

728x90
반응형