인공지능캡스톤디자인 결과보고서

캡스톤디자인 번호	2021-06
캡스톤디자인 분야	PC 프로그램
캡스톤디자인 지도교수	노기섭 교수님
캡스톤디자인 명	(국문) 스캐너 클라우드
	(영문) Scanner Cloud
캡스톤디자인 책임자	팀 명		SCV
	학 과		인공지능소프트웨어
	성 명		김태호			학 번
	휴대폰					E-mail
캡스톤디자인 세부현황	번 호	세부역할			성 명		전 공		학 번
	1	소스코드 작성			김민식		인공지능 소프트웨어
	2	소스코드 작성			김태호		인공지능 소프트웨어
	3	소스코드 작성			오준석		인공지능 소프트웨어
캡스톤디자인 수행기간	2021년 3월 17일부터 2021년 6월 10일 까지(13주)
2021년도 캡스톤디자인을 성실히 수행하여 캡스톤디자인 결과보고서를 제출합니다. 2021 . 06 . 17 . 캡스톤디자인 책임자 김태호 서명 인공지능소프트웨어 전공PD 귀하

1. 요약서

※ 본 요약서는 캡스톤디자인 전체에 대한 요약서임. 캡스톤디자인 전체의 총괄적인 내용을 위주로 간략하게  1page 이내로 작성함

캡스톤디자인 번호	2021-06
캡스톤디자인 명	스캐너 클라우드
캡스톤디자인 책임자	김태호

○ 캡스톤디자인 목표

OCR을 활용하여 이미지 파일을 텍스트로 변환시키는 기능과 변환된 텍스트를 단어 구름으로 만들어 중요한 내용을 시각화하는 프로그램을 만들 것이다.

○ 캡스톤디자인 내용

OCR(광학 문자 인식)은 사람이 쓰거나 기계로 인쇄한 문자의 영상을 이미지 스캐너로 획득하여 기계가 읽을 수 있는 문자로 변환하는 것이다. 단어 구름은 메타데이터에서 얻어진 태그들을 분석하여 중요도나 인기도 등을 고려하여 시각적으로 늘어놓아 표시하는 것이다. OCR 기능을 통하여 얻어진 텍스트를 분석하고 단어의 출현 빈도수에 따라서 중요도를 판별하고 이미지로 표시해준다.

○ 캡스톤디자인 개발에 따른 기대성과

OCR을 사용할 때는 이미지를 텍스트로 변환하는 이유는 내용을 정리하거나 또는 다른 사용 목적이 있기 때문이다. 이것을 단어 출현 빈도수에 따라 중요도를 판별하여 단어 구름으로 만들면 내용정리에 도움이 될 것이다. 겉모양을 만들고 단어의 출현 빈도수에 따라 색 또는 크기로 표시해준다면 특히 학습목적에 큰 도움이 될 것이라고 생각한다. 손글씨도 인식할 수 있다면 이 부분 또한 필기부분에 도움이 된다고 생각한다.

캡스톤디자인 계획서

캡스톤디자인 명 : 스캐너 클라우드

캡스톤디자인 책임자 : 인공지능소프트웨어전공 / 김태호

1. 캡스톤디자인의 필요성

공부를 하다보면 어떤 내용이 중요한지 알기 힘들 때가 있다. 그 분야에 대해 처음 공부하거나 나와 있는 내용이 많을 때는 더욱 심화된다. 단어 출현 빈도수에 따라서 그 단어를 강조해주거나 연관된 이미지로 나타내어 준다면 학습하기에 더욱 수월하다. 따라서 OCR과 단어 구름을 활용하여 위와 같은 어려움을 해결해주는 프로그램을 만들어 보려고 한다.

2. 캡스톤디자인의 목표 및 내용

가. 캡스톤디자인 디자인 목표

요즘 OCR 스캐너 어플리케이션과 프로그램이 많이 있다. 대부분은 단순히 사진을 텍스트로만 바꿔주는 기능만 존재 한다. 학습을 목적으로 했을 때, 텍스트로 변환된 것을 다시 정리해야한다는 번거로움이 있는데 한 두 번이면 괜찮지만 많은 양을 해야 한다고 했을 때는 고단한 일이다. 이에 대한 단어 구름을 만들어주는 기능을 추가하면 어떤 내용이 중요한지 알기 쉽다. 이러한 기능이 추가된다면 학습시간을 줄여주고 더 효율적으로 만들어 줄 수 있다.

나. 캡스톤디자인 내용

	프로그램을 제작하여 화면캡쳐 및 이미지 불러오기 기능을 넣고 이를 활용하여 OCR을 사용할 수 있도록 만든다. Python을 사용하여 코드를 작성하고, OCR을 이용하여 이미지를 텍스트로 변환한 후 내용에 따라 이미지를 결정하고 텍스트에서 나온 단어 빈도수에 따라서 색 및 크기로 단어 구름을 만들어준다.

3. 캡스톤디자인의 추진전략 및 체계

1. Python을 사용하여 소스 코드를 작성한다.

2. OCR 기능을 소스 코드 작성하고 작동이 순조롭게 될 때 까지 수정 및 보완작업을 실행한다.

3. TensorFlow를 이용한 머신러닝으로 데이터를 정제한다.

4. CNN을 이용한 딥러닝으로 손 글씨 해석을 위한 이미지학습을 한다.

5. 단어구름 기능을 추가하고 작동이 순조롭게 될 때 까지 수정 및 보완작업을 실행한다.

6. 작품이 완성되면 담당 교수님께 보여주고 피드백을 받아서 추가적인 수정 및 보완 또는 문제점들을 적절하게 조치한다.

7. PPT에 일부코드 및 작동영상을 보여주어 제품의 완성도를 전달한다.

8. 작품이 완성되면 제품을 홍보한다. 좋은 아이디어로 채택되면 학습에 도움이 되도록 대중화시킨다.

4. 캡스톤디자인의 기대효과 및 활용방안

기존의 OCR은 이미지를 단순히 텍스트로 변환시켜주기만 한다. 문제가 되지는 않지만 활용하기에는 아쉬움이 따르는 부분이 있다. 그래서 우리는 변환된 텍스트를 활용하여 관련된 이미지를 비춰주고, 내용에 포함된 단어의 빈도수에 따라서 색 또는 글자 크기로 표기해주는 단어 구름으로 만들어 학습에 도움이 되도록 만들게 되었다. 제품이 완성되면 적당한 마케팅을 통해 이 제품에 대해 알게 될 것이고, 제품이 대중화되어 사용한다면 요즘 같은 인터넷강의가 활성화되는 시대에 도움이 되고 학습이 편리해 질 수 있을 것이라고 생각한다. 따라서 이 제품은 실용성이 있다고 판단 될 수 있도록 많은 시행착오를 겪어 완벽하게 제작할 필요성을 느끼고 있고, 그렇게 된다면 사용하는 사람이 계속 증가 할 것으로 보인다.

5. 작업분할 구조도(WBS, Work Breakdown Structure)

6. 참여인력

가. 인력 편성표

						캡스톤디자인 책임자
						( 김태호 )
	소스코드 작성						소스코드 작성						소스코드 작성
김민식						김태호						오준석
정보 분석 OCR 소스코드 작성 TensorFlow를 이용한 데이터 정제						진행 총괄 및 보고서 작성 OCR, 단어구름 소스코드 작성 CNN을 이용한 이미지 학습						정보 분석 단어구름 소스코드 작성 테스트 및 EXE 파일 제작

나. 참여 인력 현황

번호	분 야	학 번	성 명	참여율(%)
1	OCR 소스코드 작성 TensorFlow를 이용한 데이터 정제	15422002	김민식	33%
2	OCR, 단어구름 소스코드 작성 CNN을 이용한 이미지 학습	15422007	김태호	34%
3	단어구름 소스코드 작성 테스트 및 EXE 파일 제작	15422016	오준석	33%

다. 캡스톤디자인 책임자 및 참여인원의 상세 정보

1) 인적사항
	성 명		김민식
	학 번					생년월일
	주 소
	연 락 처		핸드폰				e-mail
	공학 인증		참여	O			미참여
2) 학적 사항
연도	학적변동 사항
2003	흥덕초등학교 입학
2009	대성중학교 입학
2012	봉명고등학교 입학
2015	청주대학교 입학
3) 현장실습, 인턴쉽, 어학연수, 교육연수 현황
연도(부터～까지)		연수 명			연수기관
4) 자격증 현황
자격증 명		발행기관			취득년도
1종 보통		충북지방경찰청			2015

1) 인적사항
	성 명		김태호
	학 번					생년월일
	주 소
	연 락 처		핸드폰				e-mail
	공학 인증		참여	O			미참여
2) 학적 사항
연도	학적변동 사항									비고
2003	경산초등학교 입학
2009	가경중학교 입학
2012	주성고등학교 입학
2015	청주대학교 입학
3) 현장실습, 인턴쉽, 어학연수, 교육연수 현황
연도(부터～까지)		연수 명			연수기관						비 고
4) 자격증 현황
자격증 명		발행기관			취득년도						비고

1) 인적사항
	성 명		오준석
	학 번					생년월일
	주 소
	연 락 처		핸드폰				e-mail
	공학 인증		참여				미참여
2) 학적 사항
연도	학적변동 사항									비고
2003	동주초등학교 전학
2009	금천중학교 입학
2012	주성고등학교 입학
2015	청주대학교 입학
3) 현장실습, 인턴쉽, 어학연수, 교육연수 현황
연도(부터～까지)		연수 명			연수기관						비 고
4) 자격증 현황
자격증 명		발행기관			취득년도						비고
1종 보통		충북지방경찰청			2015

7. 소요장비 및 재료

장비 및 재료 명	규격	수량	용 도	비 고
개인용 컴퓨터 (PC)		3	작업
웹 브라우저 (Chrome)		3	정보 및 자료 검색
한컴오피스 한글 2010		3	문서 작성
Microsoft Power Point		3	PPT 제작
Microsoft Power Excel		3	일정 작성 및 조율
VS CODE		3	코드 작성

8. 캡스톤디자인 추진일정 계획

	세부 추진 내용			추 진 일 정 (월)
				3	4	5	6
스캐너 클라우드	1		개발 환경 구축
	2	OCR	캡쳐 / 이미지 불러오기
	3		이미지 텍스트 변환
	4		TXT 파일로 저장
	5		OCR 구현 테스트 및 수정
	6	단어구름	단어구름 생성
	7		텍스트 불러오기
	8		단어구름 이미지 파일로 저장
	9		단어구름 구현 테스트 및 수정
	10	학습	TensorFlow를 이용한 데이터 정제
	11		CNN을 이용한 이미지 학습
	10	프로그램 제작	OCR, 단어 구름 연동 확인 및 수정
			OCR, 단어구름 텍스트 변환 테스트
	11		UI 디자인
	12		EXE 파일로 제작
	13		최종 수정 및 최종 발표

캡스톤디자인 결과보고서

목  차

1. Scanner Cloud ∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙	15
1.1. Scanner Cloud ∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙	15
1.2. 발생하는 오류 ∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙	25
2. 한글 음절 인식기 ∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙	26
2.1. 한글 음절 인식기 ∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙	26
3. 실패 원인 분석 ∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙	42
3.1. Tesseracct OCR의 문제점 ∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙	42
3.2. 위와 이어지는 학습 데이터 적용의 문제 ∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙	42
3.3. 속단하여 발생한 문제점 ∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙	42
4. 마무리 ∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙	43
4.1. 향후 추진 계획 ∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙	43
4.2. 정리 ∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙	43
5. 부록 ∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙	44
6.1. 사용자 매뉴얼 ∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙	44

1. Scanner Cloud

1.1 Scanner Cloud

Tesseract 설치

스캐너 클라우드 사용을 위해 https://github.com/UB-Mannheim/tesseract/wiki 에서

Tesseract 설치를 해줍니다

설치가 완료 된 후 [시스템 속성]에서 환경 변수를 등록해 줍니다.

소스 코드

gui.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164	import pytesseract import cv2, os,sys from PIL import Image import PyQt5 from PyQt5.QtGui import * from PyQt5.QtCore import * from PyQt5.QtWidgets import * from PyQt5 import uic from PyQt5 import QtCore,QtGui,QtWidgets import glob from wordcloud import WordCloud import numpy as np # Here we will get the path of the tessdata # For 64 bit installation of tesseract OCR language_path = 'C:\\Program Files\\Tesseract-OCR\\tessdata\\' language_path_list = glob.glob(language_path+"*.traineddata") dr = ‘kor+kor1+kor2+kor3+kor4+kor5+kor6+kor7+kor8+kor9+kor10+kor11+kor12 +kor13+kor14+kor15+kor16+kor17+kor18+kor19+kor20+kor21+kor22+kor23+ kor24+kor25+kor26+kor27+kor28+kor29+kor30+kor31+kor32+kor33+kor34+kor35+ kor36+kor37 language_names_list = [dr, 'kor+eng'] for path in language_path_list: base_name = os.path.basename(path) base_name = os.path.splitext(base_name)[0] language_names_list.append(base_name) font_list = [] font = 2 for font in range(110): font+=2 font_list.append(str(font)) class CSV_OCR(QtWidgets.QMainWindow): def __init__(self): QtWidgets.QMainWindow.__init__(self) self.ui = uic.loadUi('main.ui',self) self.image = None self.ui.pushButton.clicked.connect(self.open) self.ui.pushButton_2.clicked.connect(self.save) self.ui.pushButton_3.clicked.connect(self.wordcloud) self.rubberBand = QRubberBand(QRubberBand.Rectangle,self) self.ui.label_2.setMouseTracking(True) self.ui.label_2.installEventFilter(self) self.ui.label_2.setAlignment(PyQt5.QtCore.Qt.AlignTop) self.language = 'eng' self.comboBox.addItems(language_names_list) self.comboBox.currentIndexChanged['QString'].connect(self.update_now) self.comboBox.setCurrentIndex(language_names_list.index(self.language)) self.font_size = '20' self.text = '' self.comboBox_2.addItems(font_list) self.comboBox_2.currentIndexChanged['QString']. connect(self.update_font_size) self.comboBox_2.setCurrentIndex(font_list.index(self.font_size)) self.ui.textEdit.setFontPointSize(int(self.font_size)) self.setAcceptDrops(True) def update_now(self,value): self.language = value print('Language Selected as:',self.language) def update_font_size(self,value): self.font_size = value self.ui.textEdit.setFontPointSize(int(self.font_size)) self.ui.textEdit.setText(str(self.text)) def open(self): filename = QFileDialog.getOpenFileName(self,'Select File', '', '이미지 파 일 (*.png; *.jpeg; *.jpg)') self.image = cv2.imread(str(filename[0])) frame = cv2.cvtColor(self.image, cv2.COLOR_BGR2RGB) image= QImage(frame, frame.shape[1], frame.shape[0], frame.strides[0], QImage.format_RGB888) self.ui.label_2.setPixmap(QPixmap.fromImage(image)) def image_to_text(self,crop_cvimage): gray = cv2.cvtColor(crop_cvimage,cv2.COLOR_BGR2GRAY) gray = cv2.medianBlur(gray,1) crop = Image.fromarray(gray) text = pytesseract.image_to_string(crop,lang = self.language) print('Text:',text) return text def save(self): fname = QFileDialog.getSaveFileName(self,'','','텍스트 파일 (*.txt)') data = self.textEdit.toPlainText() with open(fname[0], 'w', encoding='UTF8') as f: f.write(data) def wordcloud(self): filename = QFileDialog.getOpenFileName(self, 'Select File', '', '텍스트 파일 (*.txt)') with open(filename[0], 'r', encoding='UTF8') as f: lists = f.read().splitlines() word_counts = {} for text in lists: for word in text.split(): specialChars = "\'\",.;:!#$%^&*()" for specialchar in specialChars: word = word.replace(specialchar, '') if word not in word_counts: word_counts[word] = 0 word_counts[word] += 1 wc = WordCloud( font_path = 'NanumGothicBold.ttf', background_color = 'white', width = 1000, height = 1000 ) wc_img = wc.generate_from_frequencies(word_counts) wc_img.to_file('wordcloud.jpg') def eventFilter(self,source,event): width = 0 height = 0 if (event.type() == QEvent.MouseButtonPress and source is self.ui.label_2): self.org = self.mapFromGlobal(event.globalPos()) self.left_top = event.pos() self.rubberBand.setGeometry(QRect(self.org,QSize())) self.rubberBand.show() elif (event.type() == QEvent.MouseMove and source is self.ui.label_2): if self.rubberBand.isVisible(): self.rubberBand.setGeometry(QRect(self.org,self. mapFromGlobal(event.globalPos())).normalized()) elif(event.type() == QEvent.MouseButtonRelease and source is self.ui.label_2): if self.rubberBand.isVisible(): self.rubberBand.hide() rect = self.rubberBand.geometry() self.x1 = self.left_top.x() self.y1 = self. left_top.y() width = rect.width() height = rect.height() self.x2 = self.x1+ width self.y2 = self.y1+ height if width >=10 and height >= 10 and self.image is not None: self.crop = self.image[self.y1:self.y2, self.x1:self.x2] cv2.imwrite('cropped.png',self.crop) self.text = self.image_to_text(self.crop) self.ui.textEdit.setText(str(self.text)) else: self.rubberBand.hide() else: return 0 return QWidget.eventFilter(self,source,event) app = QtWidgets.QApplication(sys.argv) mainWindow = CSV_OCR() mainWindow.show() sys.exit(app.exec_())

기본 language는 eng로 했고, Tesseract-OCR\tessdata 폴더에 있는 language에 따라 kor, kor+eng를 고를 수 있게 했다. Open()함수로 .png, .jpeg, .jpg 이미지 파일만 열 수 있게 하였고 image_to_text() 함수에 있는 pytesseract.image_to_string으로 불러온 이미지 파일을 텍스트로 변환하였다. save()함수로 .txt 텍스트 파일로 저장할 수 있고, wordcloud()함수로 .txt파일을 읽은 후 단어 : 빈도 수 형태의 딕셔너리로 나눈 후 빈도 수에 따라 wordcloud 이미지로 저장 할 수 있다.

wordcloud.py
1 2 3 4 5 6 7 8 9	// FILE = os.path.dirname(sys.executable) FONT_PATH = os.environ.get('FONT_PATH', os.path.join(FILE, 'NanumGothicBold.ttf')) STOPWORDS = set(map(str.strip, open(os.path.join(FILE, 'stopwords')).readlines())) //

gui.py를 exe로 빌드 후 wordcloud 라이브러리에 쓰이는 stopwords 파일과 글꼴 파일 ‘NanumGothicBold.ttf’를 exe와 같은 위치에 둔다.

UI

UI 설정

Qt designer 사용을 위해 (https://www.anaconda.com/products/individual#Downloads)에서

Anaconda를 우선 설치해 준다

Anaconda Prompt에서 designer 커맨드를 입력해 Qt Designer 실행

Qt Designer에서 UI설정 후 저장하면 .ui파일이 생성 된다

main.ui
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132	<?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> <class>MainWindow</class> <widget class="QMainWindow" name="MainWindow"> <property name="geometry"> <rect> <x>0</x> <y>0</y> <width>740</width> <height>600</height> </rect> </property> <property name="windowTitle"> <string>SCV OCR</string> </property> <widget class="QWidget" name="centralwidget"> <layout class="QGridLayout" name="gridLayout_3"> <item row="0" column="0"> <widget class="QScrollArea" name="scrollArea"> <property name="verticalScrollBarPolicy"> <enum>Qt::ScrollBarAlwaysOn</enum> </property> <property name="horizontalScrollBarPolicy"> <enum>Qt::ScrollBarAlwaysOn</enum> </property> <property name="widgetResizable"> <bool>true</bool> </property> <widget class="QWidget" name="scrollAreaWidgetContents"> <property name="geometry"> <rect> <x>0</x> <y>0</y> <width>703</width> <height>276</height> </rect> </property> <layout class="QGridLayout" name="gridLayout"> <item row="0" column="0"> <widget class="QLabel" name="label_2"> <property name="styleSheet"> <string notr="true">background-color: rgb(170, 255, 255);</string> </property> <property name="text"> <string>Photo</string> </property> <property name="alignment"> <set>Qt::AlignCenter</set> </property> </widget> </item> </layout> </widget> </widget> </item> </layout> </widget> <widget class="QStatusBar" name="statusbar"/> <widget class="QDockWidget" name="dockWidget"> <attribute name="dockWidgetArea"> <number>8</number> </attribute> <widget class="QWidget" name="dockWidgetContents_2"> <layout class="QGridLayout" name="gridLayout_2"> <item row="1" column="0"> <widget class="QTextEdit" name="textEdit"/> </item> <item row="0" column="0"> <layout class="QHBoxLayout" name="horizontalLayout"> <item> <widget class="QLabel" name="label_3"> <property name="text"> <string>Font Size:</string> </property> </widget> </item> <item> <widget class="QComboBox" name="comboBox_2"/> </item> <item> <spacer name="horizontalSpacer"> <property name="orientation"> <enum>Qt::Horizontal</enum> </property> <property name="sizeHint" stdset="0"> <size> <width>40</width> <height>20</height> </size> </property> </spacer> </item> <item> <widget class="QLabel" name="label"> <property name="text"> <string>Select Language:</string> </property> </widget> </item> <item> <widget class="QComboBox" name="comboBox"/> </item> <item> <widget class="QPushButton" name="pushButton"> <property name="text"> <string>Open Image</string> </property> </widget> </item> <item> <widget class="QPushButton" name="pushButton_2"> <property name="text"> <string>Save</string> </property> </widget> </item> <item> <widget class="QPushButton" name="pushButton_3"> <property name="text"> <string>Wordcloud</string> </property> </widget> </item> </layout> </item> </layout> </widget> </widget> </widget> <resources/> <connections/> </ui>

생성된 .ui파일을 에디터에서 열면 아래와 같이 나온다.

생성된 UI 파일을 exe 파일과 같은 위치에 둔다.

Pyinstaller 사용법

py 파일을 exe 파일로 변경

cmd를 py가 있는 곳으로 이동 후 다음과 같은 명령어를 사용한다.

pyinstllaer –w –F gui.py

-w는 콘솔창이 출력되지 않게하는 명령어, -F는 실행파일 하나만 생성되게 하는 명령어이다.

1.2 발생하는 오류

현재 확인된 오류를 나열하겠다.

- 폴더 이름과 이미지 파일 이름을 한글로 지정하면 작동하지 않는다.

- 파일 열기를 누르고 취소를 눌렀을 때 작동하지 않고 튕겨버린다.

- UI파일과 같은 위치에 있는 것이 아니라면 실행이 되지않는다.
(위의 문제는 stopwords 파일과 폰트도 포함한다.)

- Tesseract를 설치하지 않으면 실행되지 않는다.

- 환경변수를 설정해서 Path추가를 해줘야 작동한다.

아래의 두 가지는 Tesseract OCR 고유의 문제점이며, 위의 문제들은 프로그램으로 생성하면서 발생한 문제들이다. 향후 개선 방향에 추가하였으며 추후에 바꾼 기능들을 테스트하면서 수정할 예정이다.

2. 한글 음절 인식기

학습데이터

학습데이터는 크롤링하여 모은 이미지와 AI HUB에서 받은 이미지 약 40만장을 사용하였다.

AI HUB

손글씨

소스코드

dataset.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 4041 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98	# -*- coding: utf-8 -*- import tensorflow as tf from tensorflow.keras.preprocessing.image import load_img, img_to_array import numpy as np import os import argparse import random class DataGenerator(tf.keras.utils.Sequence): def __init__(self, data_path, opt, shuffle=True, train=True): self.path = data_path self.file_names = os.listdir(data_path) self.n_samples = len(self.file_names) #random.shuffle(self.file_names) self.shuffle=shuffle self.train=train self.batch_size = opt.batch_size self.num_class = len(opt.character) self.idx_to_char = list(opt.character) self.char_to_idx = {} for i, char in enumerate(self.idx_to_char): self.char_to_idx[char] = i self.on_epoch_end() print(f'{self.n_samples} images loaded') def __len__(self): return int(np.floor(self.n_samples / self.batch_size)) def on_epoch_end(self): self.indices = np.arange(self.n_samples) if self.shuffle: np.random.shuffle(self.indices) def __getitem__(self, index): indices = self.indices[index*self.batch_size:(index+1)*self.batch_size] files = [self.file_names[i] for i in indices]#range(index*self.batch_size, (index+1)*self.batch_size)] xs = [] ys = [] for file in files: x = load_img(os.path.join(self.path, file), target_size=(32,32)) x = img_to_array(x) x = x / 255. xs.append(x) if self.train == True: label = file[0] ys.append(self.char_to_idx[label]) return np.array(xs), np.array(ys) class DataGeneratorByPath(tf.keras.utils.Sequence): def __init__(self, file_paths, labels, opt, shuffle=True): self.file_paths = file_paths self.labels = labels self.batch_size = opt.batch_size self.shuffle = shuffle self.idx_to_char = list(opt.character)#['?'] + list(opt.character) self.char_to_idx = {} for i, char in enumerate(self.idx_to_char): self.char_to_idx[char] = i#+1 #self.char_to_idx[self.idx_to_char[0]] = 0 self.num_class = len(self.idx_to_char) self.n_samples = len(self.file_paths) self.on_epoch_end() print(f'{self.n_samples} images loaded') def __len__(self): return int(np.floor(self.n_samples / self.batch_size)) def on_epoch_end(self): self.indices = np.arange(self.n_samples) if self.shuffle: np.random.shuffle(self.indices) def __getitem__(self, index): indices = self.indices[index*self.batch_size:(index+1)*self.batch_size] files = [self.file_paths[i] for i in indices] labels = [self.labels[i] for i in indices] xs = [] ys = [] for file, label in zip(files, labels): x = load_img(file, target_size=(32,32)) x = img_to_array(x) x = x / 255. xs.append(x) ys.append(self.char_to_idx[label]) return np.array(xs), np.array(ys)

Data는 이미지의 경로와 라벨을 csv파일에 저장후, csv파일을 읽어서 Train/Vailid Dataset을 생성했다.

Dataset 생성 과정에서 이미지의 크기를 32px X 32px로 resize하고, pixel값이 [0, 1]의 값을 갖도록 normalization을 적용해주었다.

model.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35	# -*- coding: utf-8 -*- import tensorflow as tf def VGG_FeatureExtractor(num_class, input_shape=(32,32,3), output_channel=512): output_channel = [int(output_channel / 8), int(output_channel / 4), int(output_channel / 2), output_channel] # [64, 128, 256, 521] inputs = tf.keras.layers.Input(input_shape) # 32x32x3 outputs = tf.keras.layers.Conv2D(output_channel[0], 3, padding='same', activation='relu')(inputs) # 32x32x64 outputs = tf.keras.layers.MaxPooling2D(2, 2)(outputs) # 16x16x64 outputs = tf.keras.layers.Conv2D(output_channel[1], 3, padding='same', activation='relu')(outputs) # 16x16x128 outputs = tf.keras.layers.MaxPooling2D(2, 2)(outputs) # 8x8x128 outputs = tf.keras.layers.Conv2D(output_channel[2], 3, padding='same', activation='relu')(outputs) # 8x8x256 outputs = tf.keras.layers.Conv2D(output_channel[2], 3, padding='same', activation='relu')(outputs) # 8x8x256 outputs = tf.keras.layers.MaxPooling2D(2, 2)(outputs) # 4x4x256 outputs = tf.keras.layers.Conv2D(output_channel[3], 3, padding='same', activation='relu', use_bias=False)(outputs) # 4x4x512 outputs = tf.keras.layers.BatchNormalization()(outputs) outputs = tf.keras.layers.ReLU()(outputs) outputs = tf.keras.layers.Conv2D(output_channel[3], 3, padding='same', activation='relu', use_bias=False)(outputs) # 4x4x512 outputs = tf.keras.layers.BatchNormalization()(outputs) outputs = tf.keras.layers.ReLU()(outputs) outputs = tf.keras.layers.MaxPooling2D(2, 2)(outputs) # 2x2x512 outputs = tf.keras.layers.Flatten()(outputs) # 2048 outputs = tf.keras.layers.Dropout(0.3)(outputs) outputs = tf.keras.layers.Dense(num_class, activation='softmax')(outputs) model = tf.keras.models.Model(inputs=inputs, outputs=outputs) return model

train.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 4041 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115116 117 118 119 120121122123124125126127128129130131	# -*- coding: utf-8 -*- import tensorflow as tf from sklearn.model_selection import train_test_split from model import VGG_FeatureExtractor from dataset import DataGenerator, DataGeneratorByPath from utils import MyCallback import numpy as np import pandas as pd import argparse def train(opt, data_type=None): callbacks = MyCallback(opt) if data_type == 'All_data': # data from TextDataGenerator and AI Hub opt.num_class = len(opt.character) df = pd.read_csv(opt.data) xs = np.array(df['ids']) ys = np.array(df['labels']) # train/valid split and shuffle train_x, valid_x, train_y, valid_y = train_test_split(xs, ys, test_size=0.01) train_datagen = DataGeneratorByPath(train_x, train_y, opt) valid_datagen = DataGeneratorByPath(valid_x, valid_y, opt) else: # only data from TextDataGenerator opt.num_class = len(opt.character) train_datagen = DataGenerator(opt.train_data, opt) valid_datagen = DataGenerator(opt.valid_data, opt, shuffle=False) print(f'num class : {opt.num_class}') model = VGG_FeatureExtractor(opt.num_class) model.summary() start_epoch = 0 if opt.saved_model != '': model.load_weights(opt.saved_model) start_epoch = int(opt.saved_model.split('/')[-1].split('_')[0])+1 print(f'load weight of {start_epoch} epoch') model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss='sparse_categorical_crossentropy', metrics=['acc']) model.fit(train_datagen, initial_epoch=start_epoch, epochs=opt.num_epochs, validation_data=valid_datagen, callbacks=[callbacks]) if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--num_epochs', type=int, default=25) parser.add_argument('--batch_size', type=int, default=256) parser.add_argument('--save_path', type=str, default='./saved_model/') parser.add_argument('--saved_model', type=str, default='') #parser.add_argument('--character', type=str, default='가나다라마바사아자차카타파하') parser.add_argument('--character', type=str, default='가각간갇갈갉갊감갑값갓갔강갖갗같갚갛개객갠갤갬갭갯갰갱갸갹갼걀걋걍걔걘걜거걱건걷걸걺검겁것겄겅겆겉겊겋게겐겔겜겝겟겠겡겨격겪견겯결겸겹겻겼경곁계곈곌곕곗고곡곤곧골곪곬곯곰곱곳공곶과곽관괄괆괌괍괏광괘괜괠괩괬괭괴괵괸괼굄굅굇굉교굔굘굡굣구국군굳굴굵굶굻굼굽굿궁궂궈궉권궐궜궝궤궷귀귁귄귈귐귑귓규균귤그극근귿글긁금급긋긍긔기긱긴긷길긺김깁깃깅깆깊까깍깎깐깔깖깜깝깟깠깡깥깨깩깬깰깸깹깻깼깽꺄꺅꺌꺼꺽꺾껀껄껌껍껏껐껑께껙껜껨껫껭껴껸껼꼇꼈꼍꼐꼬꼭꼰꼲꼴꼼꼽꼿꽁꽂꽃꽈꽉꽐꽜꽝꽤꽥꽹꾀꾄꾈꾐꾑꾕꾜꾸꾹꾼꿀꿇꿈꿉꿋꿍꿎꿔꿜꿨꿩꿰꿱꿴꿸뀀뀁뀄뀌뀐뀔뀜뀝뀨끄끅끈끊끌끎끓끔끕끗끙끝끼끽낀낄낌낍낏낑나낙낚난낟날낡낢남납낫났낭낮낯낱낳내낵낸낼냄냅냇냈냉냐냑냔냘냠냥너넉넋넌널넒넓넘넙넛넜넝넣네넥넨넬넴넵넷넸넹녀녁년녈념녑녔녕녘녜녠노녹논놀놂놈놉놋농높놓놔놘놜놨뇌뇐뇔뇜뇝뇟뇨뇩뇬뇰뇹뇻뇽누눅눈눋눌눔눕눗눙눠눴눼뉘뉜뉠뉨뉩뉴뉵뉼늄늅늉느늑는늘늙늚늠늡늣능늦늪늬늰늴니닉닌닐닒님닙닛닝닢다닥닦단닫달닭닮닯닳담답닷닸당닺닻닿대댁댄댈댐댑댓댔댕댜더덕덖던덛덜덞덟덤덥덧덩덫덮데덱덴델뎀뎁뎃뎄뎅뎌뎐뎔뎠뎡뎨뎬도독돈돋돌돎돐돔돕돗동돛돝돠돤돨돼됐되된될됨됩됫됴두둑둔둘둠둡둣둥둬뒀뒈뒝뒤뒨뒬뒵뒷뒹듀듄듈듐듕드득든듣들듦듬듭듯등듸디딕딘딛딜딤딥딧딨딩딪따딱딴딸땀땁땃땄땅땋때땍땐땔땜땝땟땠땡떠떡떤떨떪떫떰떱떳떴떵떻떼떽뗀뗄뗌뗍뗏뗐뗑뗘뗬또똑똔똘똥똬똴뙈뙤뙨뚜뚝뚠뚤뚫뚬뚱뛔뛰뛴뛸뜀뜁뜅뜨뜩뜬뜯뜰뜸뜹뜻띄띈띌띔띕띠띤띨띰띱띳띵라락란랄람랍랏랐랑랒랖랗래랙랜랠램랩랫랬랭랴략랸럇량러럭런럴럼럽럿렀렁렇레렉렌렐렘렙렛렝려력련렬렴렵렷렸령례롄롑롓로록론롤롬롭롯롱롸롼뢍뢨뢰뢴뢸룀룁룃룅료룐룔룝룟룡루룩룬룰룸룹룻룽뤄뤘뤠뤼뤽륀륄륌륏륑류륙륜률륨륩륫륭르륵른를름릅릇릉릊릍릎리릭린릴림립릿링마막만많맏말맑맒맘맙맛망맞맡맣매맥맨맬맴맵맷맸맹맺먀먁먈먕머먹먼멀멂멈멉멋멍멎멓메멕멘멜멤멥멧멨멩며멱면멸몃몄명몇몌모목몫몬몰몲몸몹못몽뫄뫈뫘뫙뫼묀묄묍묏묑묘묜묠묩묫무묵묶문묻물묽묾뭄뭅뭇뭉뭍뭏뭐뭔뭘뭡뭣뭬뮈뮌뮐뮤뮨뮬뮴뮷므믄믈믐믓미믹민믿밀밂밈밉밋밌밍및밑바박밖밗반받발밝밞밟밤밥밧방밭배백밴밸뱀뱁뱃뱄뱅뱉뱌뱍뱐뱝버벅번벋벌벎범법벗벙벚베벡벤벧벨벰벱벳벴벵벼벽변별볍볏볐병볕볘볜보복볶본볼봄봅봇봉봐봔봤봬뵀뵈뵉뵌뵐뵘뵙뵤뵨부북분붇불붉붊붐붑붓붕붙붚붜붤붰붸뷔뷕뷘뷜뷩뷰뷴뷸븀븃븅브븍븐블븜븝븟비빅빈빌빎빔빕빗빙빚빛빠빡빤빨빪빰빱빳빴빵빻빼빽뺀뺄뺌뺍뺏뺐뺑뺘뺙뺨뻐뻑뻔뻗뻘뻠뻣뻤뻥뻬뼁뼈뼉뼘뼙뼛뼜뼝뽀뽁뽄뽈뽐뽑뽕뾔뾰뿅뿌뿍뿐뿔뿜뿟뿡쀼쁑쁘쁜쁠쁨쁩삐삑삔삘삠삡삣삥사삭삯산삳살삵삶삼삽삿샀상샅새색샌샐샘샙샛샜생샤샥샨샬샴샵샷샹섀섄섈섐섕서석섞섟선섣설섦섧섬섭섯섰성섶세섹센셀셈셉셋셌셍셔셕션셜셤셥셧셨셩셰셴셸솅소속솎손솔솖솜솝솟송솥솨솩솬솰솽쇄쇈쇌쇔쇗쇘쇠쇤쇨쇰쇱쇳쇼쇽숀숄숌숍숏숑수숙순숟술숨숩숫숭숯숱숲숴쉈쉐쉑쉔쉘쉠쉥쉬쉭쉰쉴쉼쉽쉿슁슈슉슐슘슛슝스슥슨슬슭슴습슷승시식신싣실싫심십싯싱싶싸싹싻싼쌀쌈쌉쌌쌍쌓쌔쌕쌘쌜쌤쌥쌨쌩썅써썩썬썰썲썸썹썼썽쎄쎈쎌쏀쏘쏙쏜쏟쏠쏢쏨쏩쏭쏴쏵쏸쐈쐐쐤쐬쐰쐴쐼쐽쑈쑤쑥쑨쑬쑴쑵쑹쒀쒔쒜쒸쒼쓩쓰쓱쓴쓸쓺쓿씀씁씌씐씔씜씨씩씬씰씸씹씻씽아악안앉않알앍앎앓암압앗았앙앝앞애액앤앨앰앱앳앴앵야약얀얄얇얌얍얏양얕얗얘얜얠얩어억언얹얻얼얽얾엄업없엇었엉엊엌엎에엑엔엘엠엡엣엥여역엮연열엶엷염엽엾엿였영옅옆옇예옌옐옘옙옛옜오옥온올옭옮옰옳옴옵옷옹옻와왁완왈왐왑왓왔왕왜왝왠왬왯왱외왹왼욀욈욉욋욍요욕욘욜욤욥욧용우욱운울욹욺움웁웃웅워웍원월웜웝웠웡웨웩웬웰웸웹웽위윅윈윌윔윕윗윙유육윤율윰윱윳융윷으윽은을읊음읍읏응읒읓읔읕읖읗의읜읠읨읫이익인일읽읾잃임입잇있잉잊잎자작잔잖잗잘잚잠잡잣잤장잦재잭잰잴잼잽잿쟀쟁쟈쟉쟌쟎쟐쟘쟝쟤쟨쟬저적전절젊점접젓정젖제젝젠젤젬젭젯젱져젼졀졈졉졌졍졔조족존졸졺좀좁좃종좆좇좋좌좍좔좝좟좡좨좼좽죄죈죌죔죕죗죙죠죡죤죵주죽준줄줅줆줌줍줏중줘줬줴쥐쥑쥔쥘쥠쥡쥣쥬쥰쥴쥼즈즉즌즐즘즙즛증지직진짇질짊짐집짓징짖짙짚짜짝짠짢짤짧짬짭짯짰짱째짹짼쨀쨈쨉쨋쨌쨍쨔쨘쨩쩌쩍쩐쩔쩜쩝쩟쩠쩡쩨쩽쪄쪘쪼쪽쫀쫄쫌쫍쫏쫑쫓쫘쫙쫠쫬쫴쬈쬐쬔쬘쬠쬡쭁쭈쭉쭌쭐쭘쭙쭝쭤쭸쭹쮜쮸쯔쯤쯧쯩찌찍찐찔찜찝찡찢찧차착찬찮찰참찹찻찼창찾채책챈챌챔챕챗챘챙챠챤챦챨챰챵처척천철첨첩첫첬청체첵첸첼쳄쳅쳇쳉쳐쳔쳤쳬쳰촁초촉촌촐촘촙촛총촤촨촬촹최쵠쵤쵬쵭쵯쵱쵸춈추축춘출춤춥춧충춰췄췌췐취췬췰췸췹췻췽츄츈츌츔츙츠측츤츨츰츱츳층치칙친칟칠칡침칩칫칭카칵칸칼캄캅캇캉캐캑캔캘캠캡캣캤캥캬캭컁커컥컨컫컬컴컵컷컸컹케켁켄켈켐켑켓켕켜켠켤켬켭켯켰켱켸코콕콘콜콤콥콧콩콰콱콴콸쾀쾅쾌쾡쾨쾰쿄쿠쿡쿤쿨쿰쿱쿳쿵쿼퀀퀄퀑퀘퀭퀴퀵퀸퀼큄큅큇큉큐큔큘큠크큭큰클큼큽킁키킥킨킬킴킵킷킹타탁탄탈탉탐탑탓탔탕태택탠탤탬탭탯탰탱탸턍터턱턴털턺텀텁텃텄텅테텍텐텔템텝텟텡텨텬텼톄톈토톡톤톨톰톱톳통톺톼퇀퇘퇴퇸툇툉툐투툭툰툴툼툽툿퉁퉈퉜퉤튀튁튄튈튐튑튕튜튠튤튬튱트특튼튿틀틂틈틉틋틔틘틜틤틥티틱틴틸팀팁팃팅파팍팎판팔팖팜팝팟팠팡팥패팩팬팰팸팹팻팼팽퍄퍅퍼퍽펀펄펌펍펏펐펑페펙펜펠펨펩펫펭펴편펼폄폅폈평폐폘폡폣포폭폰폴폼폽폿퐁퐈퐝푀푄표푠푤푭푯푸푹푼푿풀풂품풉풋풍풔풩퓌퓐퓔퓜퓟퓨퓬퓰퓸퓻퓽프픈플픔픕픗피픽핀필핌핍핏핑하학한할핥함합핫항해핵핸핼햄햅햇했행햐향허헉헌헐헒험헙헛헝헤헥헨헬헴헵헷헹혀혁현혈혐협혓혔형혜혠혤혭호혹혼홀홅홈홉홋홍홑화확환활홧황홰홱홴횃횅회획횐횔횝횟횡효횬횰횹횻후훅훈훌훑훔훗훙훠훤훨훰훵훼훽휀휄휑휘휙휜휠휨휩휫휭휴휵휸휼흄흇흉흐흑흔흖흗흘흙흠흡흣흥흩희흰흴흼흽힁히힉힌힐힘힙힛힝') parser.add_argument('--data', type=str, default='./data/train_data.csv') opt = parser.parse_args() train(opt, data_type='All_data')

test.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 4041 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115116 117 118 119 120121122	# -*- coding: utf-8 -*- import tensorflow as tf from model import VGG_FeatureExtractor from dataset import DataGenerator import argparse def evaluate(opt): test_datagen = DataGenerator(opt.test_data, opt, False) opt.num_class = len(opt.character) model = VGG_FeatureExtractor(opt.num_class) model.summary() if opt.saved_model == '': print('set path of save model!') exit() model.load_weights(opt.saved_model) model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss='sparse_categorical_crossentropy', metrics=['acc']) history = model.evaluate(test_datagen) return history def predict(opt): test_datagen = DataGenerator(opt.test_data, opt, False, False) opt.num_class = len(opt.character) model = VGG_FeatureExtractor(opt.num_class) model.summary() if opt.saved_model == '': print('set path of save model!') exit() model.load_weights(opt.saved_model) model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss='sparse_categorical_crossentropy', metrics=['acc']) pred = model.predict(test_datagen) y_pred = tf.argmax(pred, axis=1).numpy() #print(y_pred) print([test_datagen.idx_to_char[idx] for idx in y_pred]) return y_pred if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--test_data', type=str, default='./data/test2') parser.add_argument('--batch_size', type=int, default=1) parser.add_argument('--saved_model', type=str, default='./saved_model/total_data/best_loss_0.04_model.h5') parser.add_argument('--character', type=str, default='가각간갇갈갉갊감갑값갓갔강갖갗같갚갛개객갠갤갬갭갯갰갱갸갹갼걀걋걍걔걘걜거걱건걷걸걺검겁것겄겅겆겉겊겋게겐겔겜겝겟겠겡겨격겪견겯결겸겹겻겼경곁계곈곌곕곗고곡곤곧골곪곬곯곰곱곳공곶과곽관괄괆괌괍괏광괘괜괠괩괬괭괴괵괸괼굄굅굇굉교굔굘굡굣구국군굳굴굵굶굻굼굽굿궁궂궈궉권궐궜궝궤궷귀귁귄귈귐귑귓규균귤그극근귿글긁금급긋긍긔기긱긴긷길긺김깁깃깅깆깊까깍깎깐깔깖깜깝깟깠깡깥깨깩깬깰깸깹깻깼깽꺄꺅꺌꺼꺽꺾껀껄껌껍껏껐껑께껙껜껨껫껭껴껸껼꼇꼈꼍꼐꼬꼭꼰꼲꼴꼼꼽꼿꽁꽂꽃꽈꽉꽐꽜꽝꽤꽥꽹꾀꾄꾈꾐꾑꾕꾜꾸꾹꾼꿀꿇꿈꿉꿋꿍꿎꿔꿜꿨꿩꿰꿱꿴꿸뀀뀁뀄뀌뀐뀔뀜뀝뀨끄끅끈끊끌끎끓끔끕끗끙끝끼끽낀낄낌낍낏낑나낙낚난낟날낡낢남납낫났낭낮낯낱낳내낵낸낼냄냅냇냈냉냐냑냔냘냠냥너넉넋넌널넒넓넘넙넛넜넝넣네넥넨넬넴넵넷넸넹녀녁년녈념녑녔녕녘녜녠노녹논놀놂놈놉놋농높놓놔놘놜놨뇌뇐뇔뇜뇝뇟뇨뇩뇬뇰뇹뇻뇽누눅눈눋눌눔눕눗눙눠눴눼뉘뉜뉠뉨뉩뉴뉵뉼늄늅늉느늑는늘늙늚늠늡늣능늦늪늬늰늴니닉닌닐닒님닙닛닝닢다닥닦단닫달닭닮닯닳담답닷닸당닺닻닿대댁댄댈댐댑댓댔댕댜더덕덖던덛덜덞덟덤덥덧덩덫덮데덱덴델뎀뎁뎃뎄뎅뎌뎐뎔뎠뎡뎨뎬도독돈돋돌돎돐돔돕돗동돛돝돠돤돨돼됐되된될됨됩됫됴두둑둔둘둠둡둣둥둬뒀뒈뒝뒤뒨뒬뒵뒷뒹듀듄듈듐듕드득든듣들듦듬듭듯등듸디딕딘딛딜딤딥딧딨딩딪따딱딴딸땀땁땃땄땅땋때땍땐땔땜땝땟땠땡떠떡떤떨떪떫떰떱떳떴떵떻떼떽뗀뗄뗌뗍뗏뗐뗑뗘뗬또똑똔똘똥똬똴뙈뙤뙨뚜뚝뚠뚤뚫뚬뚱뛔뛰뛴뛸뜀뜁뜅뜨뜩뜬뜯뜰뜸뜹뜻띄띈띌띔띕띠띤띨띰띱띳띵라락란랄람랍랏랐랑랒랖랗래랙랜랠램랩랫랬랭랴략랸럇량러럭런럴럼럽럿렀렁렇레렉렌렐렘렙렛렝려력련렬렴렵렷렸령례롄롑롓로록론롤롬롭롯롱롸롼뢍뢨뢰뢴뢸룀룁룃룅료룐룔룝룟룡루룩룬룰룸룹룻룽뤄뤘뤠뤼뤽륀륄륌륏륑류륙륜률륨륩륫륭르륵른를름릅릇릉릊릍릎리릭린릴림립릿링마막만많맏말맑맒맘맙맛망맞맡맣매맥맨맬맴맵맷맸맹맺먀먁먈먕머먹먼멀멂멈멉멋멍멎멓메멕멘멜멤멥멧멨멩며멱면멸몃몄명몇몌모목몫몬몰몲몸몹못몽뫄뫈뫘뫙뫼묀묄묍묏묑묘묜묠묩묫무묵묶문묻물묽묾뭄뭅뭇뭉뭍뭏뭐뭔뭘뭡뭣뭬뮈뮌뮐뮤뮨뮬뮴뮷므믄믈믐믓미믹민믿밀밂밈밉밋밌밍및밑바박밖밗반받발밝밞밟밤밥밧방밭배백밴밸뱀뱁뱃뱄뱅뱉뱌뱍뱐뱝버벅번벋벌벎범법벗벙벚베벡벤벧벨벰벱벳벴벵벼벽변별볍볏볐병볕볘볜보복볶본볼봄봅봇봉봐봔봤봬뵀뵈뵉뵌뵐뵘뵙뵤뵨부북분붇불붉붊붐붑붓붕붙붚붜붤붰붸뷔뷕뷘뷜뷩뷰뷴뷸븀븃븅브븍븐블븜븝븟비빅빈빌빎빔빕빗빙빚빛빠빡빤빨빪빰빱빳빴빵빻빼빽뺀뺄뺌뺍뺏뺐뺑뺘뺙뺨뻐뻑뻔뻗뻘뻠뻣뻤뻥뻬뼁뼈뼉뼘뼙뼛뼜뼝뽀뽁뽄뽈뽐뽑뽕뾔뾰뿅뿌뿍뿐뿔뿜뿟뿡쀼쁑쁘쁜쁠쁨쁩삐삑삔삘삠삡삣삥사삭삯산삳살삵삶삼삽삿샀상샅새색샌샐샘샙샛샜생샤샥샨샬샴샵샷샹섀섄섈섐섕서석섞섟선섣설섦섧섬섭섯섰성섶세섹센셀셈셉셋셌셍셔셕션셜셤셥셧셨셩셰셴셸솅소속솎손솔솖솜솝솟송솥솨솩솬솰솽쇄쇈쇌쇔쇗쇘쇠쇤쇨쇰쇱쇳쇼쇽숀숄숌숍숏숑수숙순숟술숨숩숫숭숯숱숲숴쉈쉐쉑쉔쉘쉠쉥쉬쉭쉰쉴쉼쉽쉿슁슈슉슐슘슛슝스슥슨슬슭슴습슷승시식신싣실싫심십싯싱싶싸싹싻싼쌀쌈쌉쌌쌍쌓쌔쌕쌘쌜쌤쌥쌨쌩썅써썩썬썰썲썸썹썼썽쎄쎈쎌쏀쏘쏙쏜쏟쏠쏢쏨쏩쏭쏴쏵쏸쐈쐐쐤쐬쐰쐴쐼쐽쑈쑤쑥쑨쑬쑴쑵쑹쒀쒔쒜쒸쒼쓩쓰쓱쓴쓸쓺쓿씀씁씌씐씔씜씨씩씬씰씸씹씻씽아악안앉않알앍앎앓암압앗았앙앝앞애액앤앨앰앱앳앴앵야약얀얄얇얌얍얏양얕얗얘얜얠얩어억언얹얻얼얽얾엄업없엇었엉엊엌엎에엑엔엘엠엡엣엥여역엮연열엶엷염엽엾엿였영옅옆옇예옌옐옘옙옛옜오옥온올옭옮옰옳옴옵옷옹옻와왁완왈왐왑왓왔왕왜왝왠왬왯왱외왹왼욀욈욉욋욍요욕욘욜욤욥욧용우욱운울욹욺움웁웃웅워웍원월웜웝웠웡웨웩웬웰웸웹웽위윅윈윌윔윕윗윙유육윤율윰윱윳융윷으윽은을읊음읍읏응읒읓읔읕읖읗의읜읠읨읫이익인일읽읾잃임입잇있잉잊잎자작잔잖잗잘잚잠잡잣잤장잦재잭잰잴잼잽잿쟀쟁쟈쟉쟌쟎쟐쟘쟝쟤쟨쟬저적전절젊점접젓정젖제젝젠젤젬젭젯젱져젼졀졈졉졌졍졔조족존졸졺좀좁좃종좆좇좋좌좍좔좝좟좡좨좼좽죄죈죌죔죕죗죙죠죡죤죵주죽준줄줅줆줌줍줏중줘줬줴쥐쥑쥔쥘쥠쥡쥣쥬쥰쥴쥼즈즉즌즐즘즙즛증지직진짇질짊짐집짓징짖짙짚짜짝짠짢짤짧짬짭짯짰짱째짹짼쨀쨈쨉쨋쨌쨍쨔쨘쨩쩌쩍쩐쩔쩜쩝쩟쩠쩡쩨쩽쪄쪘쪼쪽쫀쫄쫌쫍쫏쫑쫓쫘쫙쫠쫬쫴쬈쬐쬔쬘쬠쬡쭁쭈쭉쭌쭐쭘쭙쭝쭤쭸쭹쮜쮸쯔쯤쯧쯩찌찍찐찔찜찝찡찢찧차착찬찮찰참찹찻찼창찾채책챈챌챔챕챗챘챙챠챤챦챨챰챵처척천철첨첩첫첬청체첵첸첼쳄쳅쳇쳉쳐쳔쳤쳬쳰촁초촉촌촐촘촙촛총촤촨촬촹최쵠쵤쵬쵭쵯쵱쵸춈추축춘출춤춥춧충춰췄췌췐취췬췰췸췹췻췽츄츈츌츔츙츠측츤츨츰츱츳층치칙친칟칠칡침칩칫칭카칵칸칼캄캅캇캉캐캑캔캘캠캡캣캤캥캬캭컁커컥컨컫컬컴컵컷컸컹케켁켄켈켐켑켓켕켜켠켤켬켭켯켰켱켸코콕콘콜콤콥콧콩콰콱콴콸쾀쾅쾌쾡쾨쾰쿄쿠쿡쿤쿨쿰쿱쿳쿵쿼퀀퀄퀑퀘퀭퀴퀵퀸퀼큄큅큇큉큐큔큘큠크큭큰클큼큽킁키킥킨킬킴킵킷킹타탁탄탈탉탐탑탓탔탕태택탠탤탬탭탯탰탱탸턍터턱턴털턺텀텁텃텄텅테텍텐텔템텝텟텡텨텬텼톄톈토톡톤톨톰톱톳통톺톼퇀퇘퇴퇸툇툉툐투툭툰툴툼툽툿퉁퉈퉜퉤튀튁튄튈튐튑튕튜튠튤튬튱트특튼튿틀틂틈틉틋틔틘틜틤틥티틱틴틸팀팁팃팅파팍팎판팔팖팜팝팟팠팡팥패팩팬팰팸팹팻팼팽퍄퍅퍼퍽펀펄펌펍펏펐펑페펙펜펠펨펩펫펭펴편펼폄폅폈평폐폘폡폣포폭폰폴폼폽폿퐁퐈퐝푀푄표푠푤푭푯푸푹푼푿풀풂품풉풋풍풔풩퓌퓐퓔퓜퓟퓨퓬퓰퓸퓻퓽프픈플픔픕픗피픽핀필핌핍핏핑하학한할핥함합핫항해핵핸핼햄햅햇했행햐향허헉헌헐헒험헙헛헝헤헥헨헬헴헵헷헹혀혁현혈혐협혓혔형혜혠혤혭호혹혼홀홅홈홉홋홍홑화확환활홧황홰홱홴횃횅회획횐횔횝횟횡효횬횰횹횻후훅훈훌훑훔훗훙훠훤훨훰훵훼훽휀휄휑휘휙휜휠휨휩휫휭휴휵휸휼흄흇흉흐흑흔흖흗흘흙흠흡흣흥흩희흰흴흼흽힁히힉힌힐힘힙힛힝') opt = parser.parse_args() #evaluate(opt) y_pred = predict(opt)

utils.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 3637383940414243444546474849505152535455565758596061626364	# -*- coding: utf-8 -*- import tensorflow as tf import os import json class MyCallback(tf.keras.callbacks.Callback): def __init__(self, opt): self.opt = opt self.max_acc = -1 self.min_loss = 100000 def on_epoch_end(self, epoch, logs={}): val_acc = logs.get('val_acc') val_loss = logs.get('val_loss') if os.path.isdir(self.opt.save_path) == False: os.mkdir(self.opt.save_path) self.model.save_weights(os.path.join(self.opt.save_path, f'{epoch+1}_epoch_model.h5')) if val_acc is not None: if val_acc > self.max_acc: print('\n--- save best acc model weights ---') self.max_acc = val_acc self.model.save_weights(os.path.join(self.opt.save_path, f'best_acc_{logs.get("val_acc"):.2f}_model.h5')) if logs.get('val_acc') >= 0.99: print('Reach 99% validation accuracy !') if val_loss is not None: if val_loss < self.min_loss: print('\n--- save best loss model weights ---') self.min_loss = val_loss self.model.save_weights(os.path.join(self.opt.save_path, f'best_loss_{logs.get("val_loss"):.2f}_model.h5')) def getImageAndLabelFromJson(path, json_path, character): #file_paths = [os.path.join(path, f'{file}') for file in files] file_paths = [] labels = [] with open(json_path, 'r', encoding='utf-8') as f: data_json = json.load(f) for data in data_json['annotations']: if data['attributes']['type'] == '글자(음절)': file = os.path.join(path, f"{data['image_id']}.png") if os.path.exists(file) and data['text'] in character: file_paths.append(file) #if data['text'] in character: labels.append(data['text']) #else: # labels.append('?') return file_paths, labels if __name__ == '__main__': path = './data/AI_HUB/printed_data_info.json' with open(path, 'r', encoding='utf-8') as f: data_json = json.load(f) print(data_json['annotations'][546938])

json 파일을 읽어 학습된 모델을 테스트 하여 학습된 모델의 loss값을 계산한다.

startpy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 4041 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115116 117 118 119 120121122	# -*- coding: utf-8 -*- import tensorflow as tf from model import VGG_FeatureExtractor from dataset import DataGenerator import argparse def evaluate(opt): test_datagen = DataGenerator(opt.test_data, opt, False) opt.num_class = len(opt.character) model = VGG_FeatureExtractor(opt.num_class) model.summary() if opt.saved_model == '': print('set path of save model!') exit() model.load_weights(opt.saved_model) model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss='sparse_categorical_crossentropy', metrics=['acc']) history = model.evaluate(test_datagen) return history def predict(opt): test_datagen = DataGenerator(opt.test_data, opt, False, False) opt.num_class = len(opt.character) model = VGG_FeatureExtractor(opt.num_class) model.summary() if opt.saved_model == '': print('set path of save model!') exit() model.load_weights(opt.saved_model) model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss='sparse_categorical_crossentropy', metrics=['acc']) pred = model.predict(test_datagen) y_pred = tf.argmax(pred, axis=1).numpy() #print(y_pred) print([test_datagen.idx_to_char[idx] for idx in y_pred]) return y_pred if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--test_data', type=str, default='./data/test2') parser.add_argument('--batch_size', type=int, default=1) parser.add_argument('--saved_model', type=str, default='./saved_model/total_data/best_loss_0.04_model.h5') parser.add_argument('--character', type=str, default='가각간갇갈갉갊감갑값갓갔강갖갗같갚갛개객갠갤갬갭갯갰갱갸갹갼걀걋걍걔걘걜거걱건걷걸걺검겁것겄겅겆겉겊겋게겐겔겜겝겟겠겡겨격겪견겯결겸겹겻겼경곁계곈곌곕곗고곡곤곧골곪곬곯곰곱곳공곶과곽관괄괆괌괍괏광괘괜괠괩괬괭괴괵괸괼굄굅굇굉교굔굘굡굣구국군굳굴굵굶굻굼굽굿궁궂궈궉권궐궜궝궤궷귀귁귄귈귐귑귓규균귤그극근귿글긁금급긋긍긔기긱긴긷길긺김깁깃깅깆깊까깍깎깐깔깖깜깝깟깠깡깥깨깩깬깰깸깹깻깼깽꺄꺅꺌꺼꺽꺾껀껄껌껍껏껐껑께껙껜껨껫껭껴껸껼꼇꼈꼍꼐꼬꼭꼰꼲꼴꼼꼽꼿꽁꽂꽃꽈꽉꽐꽜꽝꽤꽥꽹꾀꾄꾈꾐꾑꾕꾜꾸꾹꾼꿀꿇꿈꿉꿋꿍꿎꿔꿜꿨꿩꿰꿱꿴꿸뀀뀁뀄뀌뀐뀔뀜뀝뀨끄끅끈끊끌끎끓끔끕끗끙끝끼끽낀낄낌낍낏낑나낙낚난낟날낡낢남납낫났낭낮낯낱낳내낵낸낼냄냅냇냈냉냐냑냔냘냠냥너넉넋넌널넒넓넘넙넛넜넝넣네넥넨넬넴넵넷넸넹녀녁년녈념녑녔녕녘녜녠노녹논놀놂놈놉놋농높놓놔놘놜놨뇌뇐뇔뇜뇝뇟뇨뇩뇬뇰뇹뇻뇽누눅눈눋눌눔눕눗눙눠눴눼뉘뉜뉠뉨뉩뉴뉵뉼늄늅늉느늑는늘늙늚늠늡늣능늦늪늬늰늴니닉닌닐닒님닙닛닝닢다닥닦단닫달닭닮닯닳담답닷닸당닺닻닿대댁댄댈댐댑댓댔댕댜더덕덖던덛덜덞덟덤덥덧덩덫덮데덱덴델뎀뎁뎃뎄뎅뎌뎐뎔뎠뎡뎨뎬도독돈돋돌돎돐돔돕돗동돛돝돠돤돨돼됐되된될됨됩됫됴두둑둔둘둠둡둣둥둬뒀뒈뒝뒤뒨뒬뒵뒷뒹듀듄듈듐듕드득든듣들듦듬듭듯등듸디딕딘딛딜딤딥딧딨딩딪따딱딴딸땀땁땃땄땅땋때땍땐땔땜땝땟땠땡떠떡떤떨떪떫떰떱떳떴떵떻떼떽뗀뗄뗌뗍뗏뗐뗑뗘뗬또똑똔똘똥똬똴뙈뙤뙨뚜뚝뚠뚤뚫뚬뚱뛔뛰뛴뛸뜀뜁뜅뜨뜩뜬뜯뜰뜸뜹뜻띄띈띌띔띕띠띤띨띰띱띳띵라락란랄람랍랏랐랑랒랖랗래랙랜랠램랩랫랬랭랴략랸럇량러럭런럴럼럽럿렀렁렇레렉렌렐렘렙렛렝려력련렬렴렵렷렸령례롄롑롓로록론롤롬롭롯롱롸롼뢍뢨뢰뢴뢸룀룁룃룅료룐룔룝룟룡루룩룬룰룸룹룻룽뤄뤘뤠뤼뤽륀륄륌륏륑류륙륜률륨륩륫륭르륵른를름릅릇릉릊릍릎리릭린릴림립릿링마막만많맏말맑맒맘맙맛망맞맡맣매맥맨맬맴맵맷맸맹맺먀먁먈먕머먹먼멀멂멈멉멋멍멎멓메멕멘멜멤멥멧멨멩며멱면멸몃몄명몇몌모목몫몬몰몲몸몹못몽뫄뫈뫘뫙뫼묀묄묍묏묑묘묜묠묩묫무묵묶문묻물묽묾뭄뭅뭇뭉뭍뭏뭐뭔뭘뭡뭣뭬뮈뮌뮐뮤뮨뮬뮴뮷므믄믈믐믓미믹민믿밀밂밈밉밋밌밍및밑바박밖밗반받발밝밞밟밤밥밧방밭배백밴밸뱀뱁뱃뱄뱅뱉뱌뱍뱐뱝버벅번벋벌벎범법벗벙벚베벡벤벧벨벰벱벳벴벵벼벽변별볍볏볐병볕볘볜보복볶본볼봄봅봇봉봐봔봤봬뵀뵈뵉뵌뵐뵘뵙뵤뵨부북분붇불붉붊붐붑붓붕붙붚붜붤붰붸뷔뷕뷘뷜뷩뷰뷴뷸븀븃븅브븍븐블븜븝븟비빅빈빌빎빔빕빗빙빚빛빠빡빤빨빪빰빱빳빴빵빻빼빽뺀뺄뺌뺍뺏뺐뺑뺘뺙뺨뻐뻑뻔뻗뻘뻠뻣뻤뻥뻬뼁뼈뼉뼘뼙뼛뼜뼝뽀뽁뽄뽈뽐뽑뽕뾔뾰뿅뿌뿍뿐뿔뿜뿟뿡쀼쁑쁘쁜쁠쁨쁩삐삑삔삘삠삡삣삥사삭삯산삳살삵삶삼삽삿샀상샅새색샌샐샘샙샛샜생샤샥샨샬샴샵샷샹섀섄섈섐섕서석섞섟선섣설섦섧섬섭섯섰성섶세섹센셀셈셉셋셌셍셔셕션셜셤셥셧셨셩셰셴셸솅소속솎손솔솖솜솝솟송솥솨솩솬솰솽쇄쇈쇌쇔쇗쇘쇠쇤쇨쇰쇱쇳쇼쇽숀숄숌숍숏숑수숙순숟술숨숩숫숭숯숱숲숴쉈쉐쉑쉔쉘쉠쉥쉬쉭쉰쉴쉼쉽쉿슁슈슉슐슘슛슝스슥슨슬슭슴습슷승시식신싣실싫심십싯싱싶싸싹싻싼쌀쌈쌉쌌쌍쌓쌔쌕쌘쌜쌤쌥쌨쌩썅써썩썬썰썲썸썹썼썽쎄쎈쎌쏀쏘쏙쏜쏟쏠쏢쏨쏩쏭쏴쏵쏸쐈쐐쐤쐬쐰쐴쐼쐽쑈쑤쑥쑨쑬쑴쑵쑹쒀쒔쒜쒸쒼쓩쓰쓱쓴쓸쓺쓿씀씁씌씐씔씜씨씩씬씰씸씹씻씽아악안앉않알앍앎앓암압앗았앙앝앞애액앤앨앰앱앳앴앵야약얀얄얇얌얍얏양얕얗얘얜얠얩어억언얹얻얼얽얾엄업없엇었엉엊엌엎에엑엔엘엠엡엣엥여역엮연열엶엷염엽엾엿였영옅옆옇예옌옐옘옙옛옜오옥온올옭옮옰옳옴옵옷옹옻와왁완왈왐왑왓왔왕왜왝왠왬왯왱외왹왼욀욈욉욋욍요욕욘욜욤욥욧용우욱운울욹욺움웁웃웅워웍원월웜웝웠웡웨웩웬웰웸웹웽위윅윈윌윔윕윗윙유육윤율윰윱윳융윷으윽은을읊음읍읏응읒읓읔읕읖읗의읜읠읨읫이익인일읽읾잃임입잇있잉잊잎자작잔잖잗잘잚잠잡잣잤장잦재잭잰잴잼잽잿쟀쟁쟈쟉쟌쟎쟐쟘쟝쟤쟨쟬저적전절젊점접젓정젖제젝젠젤젬젭젯젱져젼졀졈졉졌졍졔조족존졸졺좀좁좃종좆좇좋좌좍좔좝좟좡좨좼좽죄죈죌죔죕죗죙죠죡죤죵주죽준줄줅줆줌줍줏중줘줬줴쥐쥑쥔쥘쥠쥡쥣쥬쥰쥴쥼즈즉즌즐즘즙즛증지직진짇질짊짐집짓징짖짙짚짜짝짠짢짤짧짬짭짯짰짱째짹짼쨀쨈쨉쨋쨌쨍쨔쨘쨩쩌쩍쩐쩔쩜쩝쩟쩠쩡쩨쩽쪄쪘쪼쪽쫀쫄쫌쫍쫏쫑쫓쫘쫙쫠쫬쫴쬈쬐쬔쬘쬠쬡쭁쭈쭉쭌쭐쭘쭙쭝쭤쭸쭹쮜쮸쯔쯤쯧쯩찌찍찐찔찜찝찡찢찧차착찬찮찰참찹찻찼창찾채책챈챌챔챕챗챘챙챠챤챦챨챰챵처척천철첨첩첫첬청체첵첸첼쳄쳅쳇쳉쳐쳔쳤쳬쳰촁초촉촌촐촘촙촛총촤촨촬촹최쵠쵤쵬쵭쵯쵱쵸춈추축춘출춤춥춧충춰췄췌췐취췬췰췸췹췻췽츄츈츌츔츙츠측츤츨츰츱츳층치칙친칟칠칡침칩칫칭카칵칸칼캄캅캇캉캐캑캔캘캠캡캣캤캥캬캭컁커컥컨컫컬컴컵컷컸컹케켁켄켈켐켑켓켕켜켠켤켬켭켯켰켱켸코콕콘콜콤콥콧콩콰콱콴콸쾀쾅쾌쾡쾨쾰쿄쿠쿡쿤쿨쿰쿱쿳쿵쿼퀀퀄퀑퀘퀭퀴퀵퀸퀼큄큅큇큉큐큔큘큠크큭큰클큼큽킁키킥킨킬킴킵킷킹타탁탄탈탉탐탑탓탔탕태택탠탤탬탭탯탰탱탸턍터턱턴털턺텀텁텃텄텅테텍텐텔템텝텟텡텨텬텼톄톈토톡톤톨톰톱톳통톺톼퇀퇘퇴퇸툇툉툐투툭툰툴툼툽툿퉁퉈퉜퉤튀튁튄튈튐튑튕튜튠튤튬튱트특튼튿틀틂틈틉틋틔틘틜틤틥티틱틴틸팀팁팃팅파팍팎판팔팖팜팝팟팠팡팥패팩팬팰팸팹팻팼팽퍄퍅퍼퍽펀펄펌펍펏펐펑페펙펜펠펨펩펫펭펴편펼폄폅폈평폐폘폡폣포폭폰폴폼폽폿퐁퐈퐝푀푄표푠푤푭푯푸푹푼푿풀풂품풉풋풍풔풩퓌퓐퓔퓜퓟퓨퓬퓰퓸퓻퓽프픈플픔픕픗피픽핀필핌핍핏핑하학한할핥함합핫항해핵핸핼햄햅햇했행햐향허헉헌헐헒험헙헛헝헤헥헨헬헴헵헷헹혀혁현혈혐협혓혔형혜혠혤혭호혹혼홀홅홈홉홋홍홑화확환활홧황홰홱홴횃횅회획횐횔횝횟횡효횬횰횹횻후훅훈훌훑훔훗훙훠훤훨훰훵훼훽휀휄휑휘휙휜휠휨휩휫휭휴휵휸휼흄흇흉흐흑흔흖흗흘흙흠흡흣흥흩희흰흴흼흽힁히힉힌힐힘힙힛힝') opt = parser.parse_args() #evaluate(opt) y_pred = predict(opt)

결과를 빠르게 눈으로 확인하기 위하여 streamlit 사용하여 웹 앱으로 구현을 하였고,
왼쪽 canvas에 글자를 쓰면 오른쪽에 모델이 feeding되는 이미지가 나타나고, 예측을 시작한다.

예측 결과는 실시간으로 나타나며, 아래에 높은 확률을 가진 5개의 결과를 내림차순으로 보여준다.

결과

학습

약 98%의 training/validation accuracy로 학습을 완료했으며 이 과정에서 minimum loss는 0.0262였다.

22 eopch에서 best accuracy와 minimum loss의 결과를 얻었으며, 그 이후로는 과적합 경향이 보인다.

3. 실패 원인 분석

3.1 Tesseract OCR의 문제점

Tesseract OCR을 사용하면서 알아낸 문제점이 몇 가지 있었다. 첫 번째로 위에 기술한 것과 같이 사용하려는 PC에 Tesseract OCR 프로그램이 깔려 있어야한다는 점이었다. pyinstaller로 프로그램으로 만들어도 Tesseract OCR을 설치하고, path를 추가해야지만 동작하였다. 두 번째 문제는 Tesseract OCR의 학습 방법이었다. 데이터 학습을 하려면 이미지 및 텍스트를 BOX 파일 형식으로 변환하여야 학습이 가능했다. 이 과정에서 이미지는 일반적인 jpg, png 파일이 아닌 오직 tif 파일 형식만이 BOX 형식으로 변환시킬 수 있었다. 그래서 데이터 학습을 하기 위해 png -> tif -> BOX 순으로 변환을 진행해야했다. 여기서 끝나는게 아닌 BOX 파일을 traindata로 변형시켜야하여 시간을 많이 사용해야했다.  그리고 이미지를 하나씩 반복하여 변환 작업을 해야하며, 적용할 때도 하나씩 추가 해야하는 문제가 있었다. 이로 인하여 학습파일을 추가할 때마다 용량이 커지고 캡쳐된 것을 TXT로 변환하는데 걸리는 시간이 굉장히 오래 걸렸다.

3.2 위와 이어지는 학습 데이터 적용의 문제

위와 이어지는 학습 데이터 적용에서의 문제점도 있었다. 팀원들과 정보수집으로 구한 데이터 형식을 활용할 수가 없었다. 크롤링으로 구한 TXT파일은 활용이 가능했지만, AI HUB 등에서 구한 약410만개 가량, 200GB 용량의 이미지를 사용할 수가 없었다. 하나하나 작업을 하는 것은 시간이 너무 오래 걸렸고 용량을 축소시킬 수 없는 문제점을 맞이하여 모든 이미지를 사용하는 것을 포기하였다. 우리는 확보한 데이터를 이용하기 위해 서칭을 통하여 Tesseract OCR 전용 학습 파일이 아닌 일반적으로 사용되는 데이터 모델을 만들 수 있었다. 그렇게 완성된 것이 음절 이미지를 이용한 데이터 모델이고 그것을 활용하는 것을 보여주기 위하여 음절 인식기에 사용하였다.

3.3 속단하여 발생한 문제점

이 부분이 위의 문제를 야기하게 된 가장 큰 원인이라고 생각한다. 세세하게 알아보고 결정해야 하는 문제를 속단하여 위의 문제들을 직면하게 되었다. 그리고 일이 생각처럼 잘 풀리지 않자, 반복되는 판단 오류로 원래 계획을 달성하지 못하고 미완성하는 결과를 맞이하게 되었다.

4. 마무리

4.1 향후 추진 계획

일단 방학기간 동안 우리의 원래 계획을 완수할 예정이다. 미완성된 부분이나 미흡한 부분을 완성시키고 보완할 예정이다. 프로그램을 만들며 합쳐진 영역 캡쳐 기능을 분리하여 캡쳐를 따로 할 수 있도록 수정하고(전체 화면 캡쳐, 부분 영역 캡쳐 지원 예정), 이 오류에서 영감을 얻어 이미지를 TXT로 변환시키는 기능에서 부분 영역 캡쳐를 지원하는 것을 고려 중에 있다.

그리고 현재 한글과 영어 혼합 문자에서 인식률이 굉장히 낮은 문제점이 있는데 우리가 가지고 있는 데이터를 활용하여 인식률을 개선시킬 예정이다. 위의 내용들을 진행하기 위하여 현재 Tesseract OCR을 배제하고 OCR을 직접 개발하거나 더 나은 OCR 오픈소스를 찾아 활용할 예정이며, 현재는 새로 발견한 오픈소스 OCR을 찾아 실험에 있다. 그리고 UI를 업그레이드하여 부족하다고 느낀 사용자 편의성을 개선할 계획이다.

4.2 정리

팀프로젝트를 진행해본 적이 처음은 아니다. 1학년 때부터 4학년인 지금까지 꽤나 여러 번의 팀프로젝트를 진행하였는데 이렇게 책임감이 많이 느껴지는 프로젝트는 처음이었다. 팀원 개개인이 내뱉고 실행한 말과 행동들이 결과로 돌아오는 것이 프로젝트의 작은 부분부터 몸소 느껴졌다. 각자 맡은 바의 역할을 기간을 두어 정보수집 및 분석을 하고 회의를 진행하였지만 잘되고 있다고 생각한 부분이 사실은 전혀 진행이 되지 않았다는 사실을 알게 되었을 때 우리 팀은 시련에 빠졌다.

우리는 이번 학기에 딥러닝에 대하여 처음 교육을 받아보아 사실상 아는 것이 없이 이 프로젝트를 시작하게 되었다. 그 과정에서 필요한 정보가 굉장히 많았고 우리가 생각하고 수집한 것보다 더 많은 정보를 필요로 한다는 것도 알게 되었다. 직접 코딩을 겪어보며 난관도 많았고 비슷한 코드를 활용해도 전혀 안 될 때가 많았다. 그래도 우리는 현재 상황에서 최선을 다하기 위하여 나름대로의 결과물을 만들어 냈다. 그 결과 아무것도 보여줄 수 없을 것만 같던 우리의 프로젝트는 우리가 그 동안 무엇을 했으며 목적이 무엇인지는 보여줄 수 있었다고 생각한다.

계획서와 결과물을 놓고 보자면 우리 팀은 실패하였지만 굉장히 값진 실패라고 생각한다. 요즘은 실패할 수 있는 경험을 하는 것이 어렵다고 느끼고 있었다. 그런데 우리는 비교적 일찍 실패를 경험하고 우리가 부족한 것이 무엇인지, 간과했던 부분이 있는지, 그리고 오만하지는 않았는지, 우리를 되돌아볼 수 있었고 모든 것을 100% 완수한 것보다 값지게 얻어간 부분이 있다고 느낀다. 굉장히 좋은 경험이었고 향후 추진 계획에서 말한바와 같이 계획을 완수하여 좋은 결과물을 보여줄 예정이다.

5. 부록

5.1 사용자 매뉴얼

Scanner Cloud

1. exe 파일 실행
	exe 파일을 실행한 첫 화면입니다. 기본 폰트 사이즈는 20, 기본 Language 는 eng로 설정 되어 있는 것을 확인 할 수있습니다.

2. 이미지 파일 선택
	Open Image 버튼을 눌러 원하는 이미지 파일을 선택 합니다.

3. 변환 할 텍스트 선택
	바꾸고 싶은 폰트 사이즈와 Language를 고른 후, 열린 이미지에서 변환 하고 싶은 부분을 드래그해서 선택 합니다.

4. txt 저장
	변환 된 텍스트를 txt 파일로 저장 합니다.

5. wordcloud 변환
	Wordcloud 버튼을 눌러 변환 할 txt 파일을 선택합니다. Wordcloud 파일이 생성된 걸 확인 할 수 있습니다.

한글 음절 인식기 - 1
	start.py가 있는 폴더로 이동 후 streamlit run start.py를 터미널에 입력해줍니다.

한글 음절 인식기 - 2
	URL을 따라가 나오는 창에서 마우스로 음절을 그려주면 밑에 정확도를 유추해주는 창이 나타납니다. 지우개 표시를 눌러 지울 수 있고 화살표를 이용하여 한 획을 지울 수 있습니다.