파이썬 Tesseract를 이용한 OCR 프로그램 만들기

    목차
반응형

클립보드 번역기를 만든 주된 이유는 한글패치가 되지 않은 게임을 하고 싶을 때 게임 내 텍스트를 실시간으로 번역하여 플레이하고 싶었기 때문이다. 그래서 함께 이용한 것이 Capture2Text라는 프로그램인데, 이런 OCR 프로그램을 나의 클립보드 번역기에 내장하고 싶었다.

 

생각한 과정은 아래와 같다.

- Tesseract OCR을 이용하기 위해서 불러올 이미지 파일이 필요하다.

→ 게임 스크린샷을 불러와야한다.

→ 스크린샷 중에서도 텍스트가 적힌 위치를 지정해야한다.

 

그래서 처음으로 찾게 된 방법은 어떤 방법으로든 화면 스크린샷을 찍고 cv2의 ROI 기능을 이용하여 원하는 부분을 사용자가 지정하게 하는 것이었다. 하지만 Capture2Text 프로그램처럼 실시간이 되지 않고, cv2가 띄우는 스크린샷도 전체화면이 되지 않는 문제가 있었다.

 

그래서 방법을 바꿔 단축키가 입력된 순간의 마우스 위치와 마우스 왼쪽 클릭을 감지했을 때의 마우스 위치를 대각선으로 갖는 직사각형의 스크린샷을 찍으면 되겠다는 생각을 했다.

 

1. 단축키 만들기

더보기
from tkinter import *
import keyboard

root = Tk()

def crop_image():
    # 코딩할 위치
    pass

keyboard.add_hotkey('ctrl+q', crop_image)
mainloop()

pip install keyboard 를 통해 keyboard 라이브러리를 설치하고

keyboard.add_hotkey로 Ctrl+q 버튼으로 이미지를 따오도록 해보았다.

 

tkinter를 이용하여 ocr된 텍스트를 띄울것이기 때문에 tkinter를 일단 불러왔다.

사실 tkinter에도 root.bind()라는 함수를 통해 단축키 생성이 가능하다.

하지만 프로그램 특징상, 프로그램이 비활성화된 상태에서도 ocr이 작동해야하는데 bind는 그걸 해결해주지 못했다.

그래서 가져온 것이 keyboard 라이브러리였다.

 

또다른 문제점은 keyboard로 add_hotkey를 한 뒤 root.mainloop()를 했으나 단축키를 한 번 사용하고 나면 프로그램이 꺼지는 것이었다.

그렇다고 root.mainloop()를 hotkey 앞에 넣으면 단축키가 씹히는 문제가 생겼다.

그래서 찾은 해결책이 root.mainloop()가 아닌 바로 mainloop()를 넣는 것이었다.

이유는 잘 모르겠다.

 

2. 마우스 클릭 감지 후 직사각형의 스크린샷 찍기

더보기
import mouse
import pyautogui

def crop_image():
    pos1 = pyautogui.position() # 단축키 입력 시 마우스 위치
    if mouse.is_pressed("left") :
        pos2 = pyautogui.position() # 마우스 왼버튼 클릭 시 마우스 위치
        x1, y1 = pos1.x, pos1.y
        x2, y2 = pos2.x, pos2.y
        absx = abs(x2-x1)
        absy = abs(y2-y1)
        
        crop_img_path = 'textimage.png'
        pyautogui.screenshot(crop_img_path, region=(min(x1,x2),min(y1,y2),absx,absy))

pip install mouse, pip install pyautogui를 하고

mouse.is_pressed("left")로 마우스 왼버튼 클릭 감지 시나리오를 넣었다.

 

그리고 pyautogui로 마우스 위치 정보와 스크린샷을 동시에 해결했다.

 

여기까지 했을 때 거의 다 됐다라고 생각했다. 하지만 조금 부족한 그 부분 때문에 애를 엄청 먹었다.

부족한 것이란, Capture2Text처럼 OCR 할 부분을 사용자가 알 수 있어야 하지만, 위 코드만으로는 OCR이 되는 위치가 어디인지 사용자가 알 수가 없다.

그래서 다시 cv2의 ROI를 이용해야하나 고민을 엄청했고, 그냥 내가 그런 기능을 만들자는 생각으로 바꿨다.

 

 

3. tkinter의 canvas를 이용하여 실시간 화면에 사각형 띄우기 (투명 canvas)

더보기
import time

def crop_image() :
    # 투명한 캔버스 띄우기
    toplevel = Toplevel(root)

    canvas = Canvas(toplevel, highlightthickness=0, bd=0, relief='ridge', bg='white')
    canvas.master.wm_attributes('-transparentcolor', 'white')
    canvas.pack(expand=True, fill="both")

    # 화면 반짝거림 줄이기
    toplevel.update()
    
    # 전체화면, 맨위, 창 상단바 없애기
    toplevel.attributes('-fullscreen', True)
    toplevel.wm_attributes('-topmost', True)
    toplevel.focus_force()
    toplevel.overrideredirect(True)
    toplevel.update()

    # Crop할 부분 사각형 처리 및 좌표 가져오기 (마우스 왼쪽 버튼 클릭)
    pos1 = pyautogui.position()
    canvas.create_rectangle(0,0,0,0, tags="rect")    
    while True:
        curpos = pyautogui.position()
        canvas.delete("rect")
        canvas.create_rectangle(pos1.x, pos1.y, curpos.x, curpos.y, outline='red', tags="rect", width=2)
        canvas.update()
        time.sleep(0.01)
        if mouse.is_pressed("left") :
            pos2 = pyautogui.position()

            # 위에서 얻은 좌표로 이미지 Crop하기
            x1, y1 = pos1.x, pos1.y
            x2, y2 = pos2.x, pos2.y
            absx = abs(x2-x1)
            absy = abs(y2-y1)
            crop_img_path = 'textimage.png'
            pyautogui.screenshot(crop_img_path, region=(min(x1,x2)+1,min(y1,y2)+1,absx-2,absy-2))    
            
            toplevel.destroy()
            toplevel.update()
            break

tkinter의 canvas를 이용하여 사각형을 그릴 수 있고, canvas 배경을 투명으로 하여 cv2의 ROI처럼 (영상이나 게임의) 정지된 화면에서 Crop하는 것이 아닌, 실시간으로 Crop할 수 있게 되었다.

canvas = Canvas(toplevel, highlightthickness=0, bd=0, relief='ridge', bg='white') 에서

highlightthickness=0, bd=0, relief='ridge' 를 통해 canvas의 테두리 선을 없앴으며 배경을 흰색으로 지정하여

canvas.master.wm_attributes('-transparentcolor''white') 코드를 통해 흰색=투명으로 인식하여 하여 투명한 canvas를 만들어냈다.

 

그리고 canvas가 들어갈 toplevel을 항상 맨 위, 전체화면, 상단바 없애기를 통해 사용자가 toplevel이라는 새 창이 열려도 이전과 다른 게 없도록 해주었다.

toplevel.overrideredirect(True) : 사용자가 창을 멋대로 닫거나 이동하는 걸 막기 위해 상단바가 사라지는 기능이라고 한다. 창을 맘대로 끌 순 없지만 깔끔한 창을 만들기에는 좋은 것 같다.

 

중간에 toplevel.update()가 3번 정도 들어가 있는데, 안 그러면 화면이 번쩍거린다던가 하는 문제가 발생한다.

 

 

4. Tesseract로 OCR 기능 추가하기

더보기
import pytesseract
from tkinter import *
from PIL import Image

pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'

root = Tk()
output_box = Text(root)
output_box.pack(fill='both', expand=True)

# ocr_lang = 'kor'
# ocr_lang = 'eng'
ocr_lang = 'jpn+jpn_vert'
def text_out():
    img= 'textimage.png'
    img_gray=cv2.imread(img,cv2.IMREAD_GRAYSCALE)
    # cv2.imshow('gray', img_gray)
    img_gray = cv2.threshold(img_gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]
    # cv2.imshow('gray', img_gray)
    # cv2.waitKey(0)
    # cv2.destroyAllWindows()
    
    text_ocr = pytesseract.image_to_string(img_gray, lang=ocr_lang)[:-1].strip()
    text_output = ''
    if ocr_lang == 'jpn+jpn_vert' :
        for t in text_ocr :
            if t != ' ' and t != '\n':
                text_output += t
    else :
        text_output = text_ocr.replace('\n',' ')
    
    output_box.delete("1.0",END)
    output_box.insert(END, text_output)

해야할 것이 좀 많다.

pytesseract와 PIL을 설치해야하고,

tesseract는 https://github.com/UB-Mannheim/tesseract/wiki 에서 본인 pc에 맞는 버전을 다운 받아 설치해줘야한다.

주의할 점은 설치할 때 본인이 ocr할 언어를 함께 설치해야 하며 위 코드의

pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'

에 tesseract가 설치된 올바른 경로를 입력해줘야 한다는 것이다.

 

문자 인식률을 높이기 위해 

img_gray = cv2.imread(img,cv2.IMREAD_GRAYSCALE)

img_gray = cv2.threshold(img_gray, 0255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]

를 추가했다.

 

http://www.pandora.tv/view/kuru2se/37565297#33530970_new

영상의 한 장면이다. 

빨갛게 된 부분이 OCR을 할 영역이다.

img_gray = cv2.imread(img,cv2.IMREAD_GRAYSCALE)을 한 결과는 아래와 같다.

원본에서 이미 회색이라 잘 티가 나지 않지만, 회색 scale로 이미지를 조정해준다.

 

img_gray = cv2.threshold(img_gray, 0255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]을 한 결과는 아래와 같다.

역치를 이용하여 텍스트만 남게 된다. 자세한 건 cv2 문서를 읽는 게 나을 것이다. 여러 옵션들이 있는 것 같지만 나도 자세하겐 모른다.

 

그리고 ocr 결과는 이렇다.

띄어쓰기를 없애는 코드로 짜놨는데 안 그러면 가끔씩 OCR 문제로 사이 사이에 띄어쓰기가 엄청 되어 있는 걸 발견할 수 있다.

그래서 띄어쓰기가 없는 일본어는 다 없앴고, 영어, 한글 등 다른 언어는 줄바꿈만 없앴다.

나중에 배포용 프로그램을 만들 때는 줄바꿈 없애기 옵션을 추가할 생각이다.

 

* Tesseract 단점

일본어 : 세로쓰기를 위해 jpn에 jpn_vert를 추가해야 하기 때문에 jpn만 썼을 때보다 약간 느리다. 큰 차이는 없겠지만. 이건 옵션으로 세로/가로/자동 선택할 수 있게 해야할 것 같다.

한국어 : 이따금씩 인식 자체를 못한다. 그리고 일본어 ocr에는 영어가 기본적으로 들어가있다고 tesseract에 설명되어있으나 한국어는 아니라 그런지 일본어와 영어가 섞인 글은 잘 인식하지만 한국어와 영어가 섞인 글은 인식률이 엄청 좋지 않다. 할거라면 eng+kor로 둘 다 인식하게 하거나 아예 영어 인식을 포기하는 쪽으로 가닥을 잡아야 하는데 어느 쪽이 더 좋은지 모르겠다.

 

5. 마무리

더보기
x1 = None

def crop_image() :
    global x1, y1, x2, y2
    ...
    text_out()

def crop_image_again():
    if x1 is None :
        print("Error")
    else :
        absx = abs(x2-x1)
        absy = abs(y2-y1)
        crop_img_path = 'textimage.png'
        pyautogui.screenshot(crop_img_path, region=(min(x1,x2)+1,min(y1,y2)+1,absx-2,absy-2))
        text_out()
        
keyboard.add_hotkey('ctrl+r', crop_image_again)

OCR 함수를 짰으니 기존에 짰던 함수에 넣어줬다.

게임 대부분이 같은 위치에 대사가 계속해서 나온다. 그렇기 때문에 같은 장소를 다시 ocr하도록 함수를 짰다. 기존의 좌표를 가져와서 해야하는 점을 주의해서 코드를 짰다.

 

 

이렇게 하면 일단 완성이다!

남은 과제는 여러 옵션들과 함께 클립보드 번역기에 집어 넣는 것이다....

힘들다! 라기 보단 귀찮다!!

그리고 넣었다.

https://boksup.tistory.com/20

 

* 전체 코드

더보기
import pyautogui
import pytesseract
import os
from tkinter import *
import cv2
import time
import mouse
import keyboard
from PIL import Image

pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'
root = Tk()
output_box = Text(root)
output_box.pack(fill='both', expand=True)

x1 = None

# 저장/불러오기 경로 : 실행파일과 같은 폴더
def resource_path(relative_path):
    try:
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")
    return os.path.join(base_path, relative_path)

# 텍스트 위치의 이미지 crop
def crop_image() :
    global x1, y1, x2, y2

    # 투명한 캔버스 띄우기
    toplevel = Toplevel(root)

    canvas = Canvas(toplevel, highlightthickness=0, bd=0, relief='ridge', bg='white')
    canvas.master.wm_attributes('-transparentcolor', 'white')
    canvas.pack(expand=True, fill="both")

    # 화면 반짝거림 줄이기
    toplevel.update()

    # 전체화면, 맨위, 창 상단바 없애기
    toplevel.attributes('-fullscreen', True)
    toplevel.wm_attributes('-topmost', True)
    toplevel.focus_force()
    toplevel.overrideredirect(True)
    toplevel.update()

    # Crop할 부분 사각형 처리 및 좌표 가져오기 (마우스 왼쪽 버튼 클릭)
    pos1 = pyautogui.position()
    canvas.create_rectangle(0,0,0,0, tags="rect")    
    while True:
        curpos = pyautogui.position()
        canvas.delete("rect")
        canvas.create_rectangle(pos1.x, pos1.y, curpos.x, curpos.y, outline='red', tags="rect", width=2)
        canvas.update()
        time.sleep(0.01)
        if mouse.is_pressed("left") :
            pos2 = pyautogui.position()

            # 위에서 얻은 좌표로 이미지 Crop하기
            x1, y1 = pos1.x, pos1.y
            x2, y2 = pos2.x, pos2.y
            absx = abs(x2-x1)
            absy = abs(y2-y1)
            crop_img_path = resource_path('textimage.png')
            pyautogui.screenshot(crop_img_path, region=(min(x1,x2)+1,min(y1,y2)+1,absx-2,absy-2))    
            
            toplevel.destroy()
            toplevel.update()
            break
    text_out()

# 같은 위치 또 crop
def crop_image_again():
    if x1 is None :
        print("Error")
    else :
        absx = abs(x2-x1)
        absy = abs(y2-y1)
        crop_img_path = resource_path('textimage.png')
        pyautogui.screenshot(crop_img_path, region=(min(x1,x2)+1,min(y1,y2)+1,absx-2,absy-2))
        text_out()

# 위에서 얻은 이미지로 OCR 후 프로그램에 출력
# ocr_lang = 'kor'
ocr_lang = 'eng'
# ocr_lang = 'jpn+jpn_vert'
def text_out():
    img= resource_path('textimage.png')
    img_gray = cv2.imread(img,cv2.IMREAD_GRAYSCALE)
    # cv2.imshow('gray', img_gray)
    img_gray = cv2.threshold(img_gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]
    # cv2.imshow('gray', img_gray)
    # cv2.waitKey(0)
    # cv2.destroyAllWindows()
    
    text_ocr = pytesseract.image_to_string(img_gray, lang=ocr_lang)[:-1].strip()
    text_output = ''
    if ocr_lang == 'jpn+jpn_vert' :
        for t in text_ocr :
            if t != ' ' and t != '\n':
                text_output += t
    else :
        text_output = text_ocr.replace('\n',' ')
    
    output_box.delete("1.0",END)
    output_box.insert(END, text_output)


keyboard.add_hotkey('ctrl+q', crop_image)
keyboard.add_hotkey('ctrl+r', crop_image_again)
mainloop()
728x90
반응형