본문 바로가기
공부/python

[Pytesseract를 사용한 메이플스토리 길드 스코어 분석] tesseract를 사용해서 이미지에서 문자 인식하기

by 고기 2022. 3. 30.

이전 글에서 스크린샷 함수의 작동방식에 대해 알아보았다. 이번 글에서는 길드 컨텐츠 점수 읽어오는 함수 및 프로젝트의 핵심인 tesseract를 사용해서 이미지에서 문자를 인식하는 방법에 대해 알아본다.

 

먼저 코드를 설명하기 전에 함수의 전체적인 작동 과정은 스크린샷을 찍었던 전체 파일을 여러 조각으로 조각내서 저장하는 과정으로 되어 있다.

 

이렇게 스크린샷을 찍었던 사진을,

사진1

 

이렇게 사진 한 장당 17개의 조각으로 나눠준다. 

사진2

 

사진을 나눠주는 이유는 크게 두 가지다. 하나는 우선 tesseract를 사용해서 문자인식을 할 때, 굉장히 신뢰도가 낮기 때문에 할 수 있는 최대한의 전처리를 해줘야만 한다. 인식률을 최대한 높이기 위해서, 한 번 읽을 때 숫자 하나만 읽어오도록 사진을 17조각으로 나눴고, 검정색 배경으로 마스킹을 씌워주었다.

 

마스킹을 씌워준 이유는 인식률을 높이려고 별 짓을 다했는데 결과는 마스킹을 제외하면 오히려 안 하는것만 못하게 나왔다. 마지막에는 tesseract 훈련 데이터를 만들어야하나 진지하게 고민해봤는데 0을 제외하면 샘플 데이터가 많은 것도 아니라서 포기했다.

 

아무튼, 점수 읽어오기 함수에서 이런 식의 과정이 이루어지는데, 구체적인 동작 과정은 코드를 보면서 설명한다. 스크린샷 버튼과 마찬가지로 새로 버튼을 생성해서 버튼의 command 함수에 분석 함수를 연결시켰다.

library list

class UI:
    def __init__(self):   
        variable & window & button setting
        
        '''
        주간미션, 수로, 플래그 점수 읽기 버튼
        '''
        self.label_create = tk.Label(self.main_window, text = "[2] 사진 분석", font=font, bg="pink")
        self.label_create.place(x=410, y=50)
        btn_create = tk.Button(self.main_window, text="Check", font=font, bg="pink", overrelief="solid", command=self.call_Find_location, repeatdelay=1000)
        btn_create.place(x=410, y=90)        
        
    '''
    길드원 참여 현황 사진에서 주간미션 / 지하수로 / 플래그레이스 점수를 읽어옴
    '''
    def call_Find_location(self):
        self.main_window.destroy()

 

Find_location() 함수는 먼저 스크린샷이 저장된 위치 즉, 사진1에서 파일들이 존재하는지 확인 후, 있으면 리스트인 name에 담는다.

library list

class UI:
    def __init__(self):   
        variable & window & button setting          
    
    '''
    길드원 참여 현황 사진에서 주간미션 / 지하수로 / 플래그레이스 점수를 읽어옴
    '''
    def call_Find_location(self):
        self.progress.delete(1.0, END)
        
        '''
        스크린샷 찍은 사진 있으면 리스트에 담기
        '''
        for currentdir, dirs, files in os.walk('./'+self.date+'/save/'):
            for file in files :
                self.name.append(file)

 

15라인에서 3으로 나눈 name의 길이를 사용하는 이유는 스크린샷을 찍을때마다 주간미션, 지하 수로, 플래그 레이스에 대한 사진 3개가 한 번에 생성되기 때문이다. 24~26라인은 그 사진들을 하나씩 읽어오는 과정이고 28라인에서 x, y, w, h는 사진을 17부분으로 자를 때, 자를 사진의 크기를 설정한 것이다. 30~36라인에서 설정한 크기만큼 사진을 잘라서 저장한다.

library list

class UI:
    def __init__(self):   
        variable & window & button setting          
    
    '''
    길드원 참여 현황 사진에서 주간미션 / 지하수로 / 플래그레이스 점수를 읽어옴
    '''
    def call_Find_location(self):
        self.progress.delete(1.0, END)        
        스크린샷 찍은 사진 있으면 리스트에 담기
                
        for i in range(0, int(len(self.name)/3)):
            self.progress.insert(self.text_index, str(i+1) + 'th image Conversion...\n')
            self.text_index = self.text_index + 1.0
            self.main_window.update()
                        
            '''            
            스크린샷에서 점수 부분을 17부분으로 잘라서 저장
            '''
            mission_img  = cv2.imread('./'+self.date+'/save/mission_im'+str(i+11)+'.png')
            under_img = cv2.imread('./'+self.date+'/save/under_im'+str(i+11)+'.png')
            flag_img  = cv2.imread('./'+self.date+'/save/flag_im'+str(i+11)+'.png')
            
            _x, _y, _w, _h = 14, 9, 46, 24
            
            for j in range(0, 17):
                cv2.imwrite('./'+self.date+'/create_image/mission/mission_'+ (str(10+j+(17*i))) + '.png', mission_img[_y:_h, _x:_w])
                cv2.imwrite('./'+self.date+'/create_image/under/under_'+ (str(10+j+(17*i))) + '.png', under_img[_y:_h, _x:_w])
                cv2.imwrite('./'+self.date+'/create_image/flag/flag_'+ (str(10+j+(17*i))) + '.png', flag_img[_y:_h, _x:_w])
                _y = _y+24
                _h = _h+24
                self.main_window.update()

 

위에서 설명했던 것처럼, 21~41라인은 잘라낸 사진의 배경을 검정색으로 마스킹하는 과정이다. 

library list

class UI:
    def __init__(self):   
        variable & window & button setting          
    
    '''
    길드원 참여 현황 사진에서 주간미션 / 지하수로 / 플래그레이스 점수를 읽어옴
    '''
    def call_Find_location(self):
        self.progress.delete(1.0, END)        
        스크린샷 찍은 사진 있으면 리스트에 담기
           
        
        for i in range(0, int(len(self.name)/3)):            
            스크린샷에서 점수 부분을 17부분으로 잘라서 저장
                
            '''
            17개로 저장했던 사진 검정색 배경으로 마스킹
            '''            
            for j in range(0, 17): 
                m_img  = cv2.imread('./'+self.date+'/create_image/mission/mission_'+str(10+j+(17*i))+'.png', cv2.IMREAD_GRAYSCALE)
                m_lower=np.percentile(m_img, 5)
                m_upper=np.percentile(m_img,50)
                m_black = cv2.normalize(m_img, m_img, -m_lower, 255+m_upper, cv2.NORM_MINMAX)# 저장
                cv2.imwrite('./'+self.date+'/create_image/mission/mission_'+(str(10+j+(17*i)))+'.png', m_black)
                self.main_window.update()

                u_img = cv2.imread('./'+self.date+'/create_image/under/under_'+str(10+j+(17*i))+'.png', cv2.IMREAD_GRAYSCALE)
                u_lower=np.percentile(u_img, 5)
                u_upper=np.percentile(u_img,50)
                u_black = cv2.normalize(u_img, u_img, -u_lower, 255+u_upper, cv2.NORM_MINMAX)# 저장
                cv2.imwrite('./create_image/under/under_'+(str(10+j+(17*i)))+'.png', u_black)                  
                self.main_window.update()

                f_img  = cv2.imread('./'+self.date+'/create_image/flag/flag_'+str(10+j+(17*i))+'.png', cv2.IMREAD_GRAYSCALE)
                f_lower=np.percentile(f_img, 5)
                f_upper=np.percentile(f_img,50)
                f_black = cv2.normalize(f_img, f_img, -f_lower, 255+f_upper, cv2.NORM_MINMAX)# 저장
                cv2.imwrite('./'+self.date+'/create_image/flag/flag_'+(str(10+j+(17*i)))+'.png', f_black)
                self.main_window.update()
            
            cv2.waitKey(0)
            cv2.destroyAllWindows()

 

21~28라인은 변환된 사진을 문자로 변환하고, 변환한 것을 리스트에 저장하는 과정이다.

library list

class UI:
    def __init__(self):   
        variable & window & button setting          
    
    '''
    길드원 참여 현황 사진에서 주간미션 / 지하수로 / 플래그레이스 점수를 읽어옴
    '''
    def call_Find_location(self):
        self.progress.delete(1.0, END)        
        스크린샷 찍은 사진 있으면 리스트에 담기           
        
        for i in range(0, int(len(self.name)/3)):            
            스크린샷에서 점수 부분을 17부분으로 잘라서 저장
            17개로 저장했던 사진 검정색 배경으로 마스킹
            
            '''
            마스킹한 사진을 숫자 리스트로 저장함    
            '''
            for k in range(0, 17):
                self.save_m.append(str(pyt.image_to_string('./'+self.date+'/create_image/mission/mission_'+str(10+k+(17*i))+'.png', 
                                              lang = None, config='--psm 10 -c tessedit_char_whitelist=0123456789')))
                self.save_u.append(str(pyt.image_to_string('./'+self.date+'/create_image/under/under_'+str(10+k+(17*i))+'.png', 
                                              lang = None, config='--psm 10 -c tessedit_char_whitelist=0123456789')))
                self.save_f.append(str(pyt.image_to_string('./'+self.date+'/create_image/flag/flag_'+str(10+k+(17*i))+'.png', 
                                              lang = None, config='--psm 10 -c tessedit_char_whitelist=0123456789')))
                self.main_window.update()

            cv2.waitKey(0)
            cv2.destroyAllWindows()

    def function list

 

사실 이미지를 문자로 변환시키는 Find_location() 함수가 동작할 때, 중첩된 반복문을 너무 많이 써서 느려지는 것이 아닌가 싶어 많은 시도를 했었다. 그 결과 단순히 이미지를 문자로 변환하는 작업이 오래 걸리는 것 뿐이었다. 하긴 몇 백~몇 천만번 돌릴 것도 아니라서 느리다는 것을 체감하기도 어렵다. 그래서 그냥 내 나름의 가독성을 편하게 하려고 중첩 반복문을 여러번 사용했다.

 

아무튼 마지막으로 함수가 실행되고 종료됨을 출력할 수 있도록 출력문을 설정해준다. 아래 코드처럼 반복문마다 window를 update해줘야 프로그램이 멈춘 것처럼 보이지 않는다.

    '''
    길드 창에서 주간미션 / 지하수로 / 플래그레이스 점수를 읽어옴
    '''
    def call_Find_location(self):
        self.progress.delete(1.0, END)
        
        for currentdir, dirs, files in os.walk('./'+self.date+'/save/'):
            for file in files :
                self.name.append(file)
        
        for i in range(0, int(len(self.name)/3)):
            self.progress.insert(self.text_index, str(i+1) + 'th image Conversion...\n')
            self.text_index = self.text_index + 1.0
            self.main_window.update()
            
            
            '''            
            스크린샷에서 점수 부분을 17부분으로 잘라서 저장
            '''
            mission_img  = cv2.imread('./'+self.date+'/save/mission_im'+str(i+11)+'.png')
            under_img = cv2.imread('./'+self.date+'/save/under_im'+str(i+11)+'.png')
            flag_img  = cv2.imread('./'+self.date+'/save/flag_im'+str(i+11)+'.png')
            
            _x, _y, _w, _h = 14, 9, 46, 24
            
            for j in range(0, 17):
                cv2.imwrite('./'+self.date+'/create_image/mission/mission_'+ (str(10+j+(17*i))) + '.png', mission_img[_y:_h, _x:_w])
                cv2.imwrite('./'+self.date+'/create_image/under/under_'+ (str(10+j+(17*i))) + '.png', under_img[_y:_h, _x:_w])
                cv2.imwrite('./'+self.date+'/create_image/flag/flag_'+ (str(10+j+(17*i))) + '.png', flag_img[_y:_h, _x:_w])
                _y = _y+24
                _h = _h+24
                self.main_window.update()
                
                
            '''
            17개로 저장했던 사진 검정색 배경으로 마스킹
            '''            
            for j in range(0, 17): 
                m_img  = cv2.imread('./'+self.date+'/create_image/mission/mission_'+str(10+j+(17*i))+'.png', cv2.IMREAD_GRAYSCALE)
                m_lower=np.percentile(m_img, 5)
                m_upper=np.percentile(m_img,50)
                m_black = cv2.normalize(m_img, m_img, -m_lower, 255+m_upper, cv2.NORM_MINMAX)# 저장
                cv2.imwrite('./'+self.date+'/create_image/mission/mission_'+(str(10+j+(17*i)))+'.png', m_black)
                self.main_window.update()

                u_img = cv2.imread('./'+self.date+'/create_image/under/under_'+str(10+j+(17*i))+'.png', cv2.IMREAD_GRAYSCALE)
                u_lower=np.percentile(u_img, 5)
                u_upper=np.percentile(u_img,50)
                u_black = cv2.normalize(u_img, u_img, -u_lower, 255+u_upper, cv2.NORM_MINMAX)# 저장
                cv2.imwrite('./create_image/under/under_'+(str(10+j+(17*i)))+'.png', u_black)                  
                self.main_window.update()

                f_img  = cv2.imread('./'+self.date+'/create_image/flag/flag_'+str(10+j+(17*i))+'.png', cv2.IMREAD_GRAYSCALE)
                f_lower=np.percentile(f_img, 5)
                f_upper=np.percentile(f_img,50)
                f_black = cv2.normalize(f_img, f_img, -f_lower, 255+f_upper, cv2.NORM_MINMAX)# 저장
                cv2.imwrite('./'+self.date+'/create_image/flag/flag_'+(str(10+j+(17*i)))+'.png', f_black)
                self.main_window.update()
            
            '''
            마스킹한 사진을 숫자 리스트로 저장함    
            '''
            for k in range(0, 17):
                self.save_m.append(str(pyt.image_to_string('./'+self.date+'/create_image/mission/mission_'+str(10+k+(17*i))+'.png', 
                                              lang = None, config='--psm 10 -c tessedit_char_whitelist=0123456789')))
                self.save_u.append(str(pyt.image_to_string('./'+self.date+'/create_image/under/under_'+str(10+k+(17*i))+'.png', 
                                              lang = None, config='--psm 10 -c tessedit_char_whitelist=0123456789')))
                self.save_f.append(str(pyt.image_to_string('./'+self.date+'/create_image/flag/flag_'+str(10+k+(17*i))+'.png', 
                                              lang = None, config='--psm 10 -c tessedit_char_whitelist=0123456789')))
                self.main_window.update()

            cv2.waitKey(0)
            cv2.destroyAllWindows()  
        
        self.progress.insert(self.text_index, 'image Conversion fin...\n')
        self.text_index = self.text_index + 1.0
        self.main_window.update()

 

실행결과까지 같이 작성하려 했지만... 이 함수에 대한 결과만 따로 작성하자니 다른 글들에 대해서도 작성해줘야 할 것 같아서, 그냥 전체적인 실행과정 및 결과는 마지막으로 정리할 때 한번에 작성하는 것으로 한다. 

 

여기까지 이미지에서 문자를 인식하는 방법에 대해 알아보았다. 다음 글에서 이렇게 이미지를 문자로 변환한 것을 가지고 파일로 저장하는 방법에 대해 알아본다.

댓글