우리는 졸업 프로젝트의 주제로 공연 문화의 기록과 알림서비스 제공 웹을 개발하고자한다. 우선 알림 서비스를 위해 티켓사이트(인터파크, 멜론, yes24 등)에서 공연 정보를 크롤링을 통해 얻을 것이다.
또한 OpenCV와 OCR을 통해 실제 티켓에서 정보를 읽어와 공연 후기 기록을 편리하게 하도록 할 것이다.
크롤링은 Python의 Beautiful Soup을 import하여 테스트했다. 시작하기 전에 requests와 beautifulsoup4패키지를 설치해 주어야한다.
pip install requests beautifulsoup4
Beautiful soup말고 selenium을 사용하는 것도 좋다. selenium은 동적인 동작을 곁들여서 크롤링을 할 수 있게 도와주는 라이브러리다. 예를 들어 beautiful soup은 크롤링하려는 page마다 url을 사용해야한다면, selenium은 마우스에서 다음페이지 버튼을 누르면 알아서 다음 페이지를 크롤링 해준다.selenium도 사용할 것이기 때문에 설치한다.
pip install selenium
selenium만으로 동적 크롤링을 하는 것이 아니라 web driver라는 가상의 브라우저 프로그램과 연동해서 위 기능을 구현할 수 있기 때문에 web driver도 설치해주어야한다.
chrome driver
이 사이트에 들어가서
latest stable release를 클릭하여
chromedriver win32.zip(window 경우)를 설치한다.
나중에 chrmoedriver.exe파일 경로를 입력할 것이기 때문에 사용하기 쉬운 장소에 옮겨두고 경로를 기억해두는 편이 좋다.
이렇게 하면 기본적인 세팅은 끝난난다.
크롤링을 하기위해서는 크롤링을할 웹 문서하 어떤 구조로 되어있는지 파악하는 것이 중요한다. Chrome에서 F12를 누르면 html구조를 볼 수 있다.
내가 크롤링할 페이지는 멜론 티켓오픈 페이지 있는 사진과 같이 li 단위로 필요한 오픈날짜 정보가 있음을 확인 할 수 있다. 이를 잘 기억해 두었다가 코드를 짜면 된다.
from msilib.schema import tables
from bs4 import BeautifulSoup
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager #절대경로를 따로 입력할 필요를 줄여준다.
import requests
import re
import pymysql
#멜론티켓 크롤링
driver=webdriver.Chrome(ChromeDriverManager().install()) #웹드라이버 절대 경로 정보
url='https://ticket.melon.com/csoon/index.htm#orderType=0&pageIndex=1&schGcode=GENRE_ALL&schText=&schDt='
driver.get(url) #url을 얻어온다.
soup=BeautifulSoup(driver.page_source,'html.parser')
info_list=[]
body=soup.find('div',{'class':'box_ticket_cont'})
for li in body.find_all('li')[0:]: #li가 여러개 이기 때문에 for문으로 받아서 정보를 처리한다.
dateStr=li.select('span.date')[0].text.strip()
date= re.sub('\((월|화|수|목|금|토|일)\)', '', dateStr).replace('.','-')
title=li.select('a.tit')[0].text.strip()
link=url+li.find('a')['href']
info_list.append((title,date,link,"Melon"))
#db연결
conn = pymysql.connect(host='localhost', user='', password='', db='melonticket',charset='utf8')
sql = "INSERT INTO open_info (title, date, link, site) VALUES (%s, %s, %s, %s)"
with conn:
with conn.cursor() as cur:
for title, date, link, site in info_list:
cur.execute(sql, (title, date, link, site))
conn.commit()
이 코드를 통해 멜론 사이트에 접속해 오픈 날짜 정보를 얻어 올 수가 있다. Web driver의 절대 경로를 따로 입력하지 않게 from webdriver_manager.chrome import ChromeDriverManager를 사용해주었다. 만약 절대 경로를 입력하고 싶으면 다음과 같이 코드를 작성하면 된다.
chromedriver = 'C:\dev_python\Webdriver\chromedriver.exe'
#selenum의 webdriver에 앞서 설치한 chromedirver를 연동한다.
driver = webdriver.Chrome(chromedriver)
이렇게 해서 db에 크롤링한 정보가 들어가는 것까지 확인을 마쳤다.
https://webnautes.tistory.com/779
OCR 개발에는 주로 Tesseract OCR과 EasyOCR이 사용되는데 EasyOCR은 gpu가 사용된다. 그렇기 때문에 Tesseract OCR을 테스트 해봤다.(레퍼런스도 훨씬 많고 gpu setting을 할 필요가 없다는 장점이 있다.)
우선 Tesseract_OCR에 가서 windows용 Tesseract를 설치한다.
64bit용 exe을 설치하면 된다.
korean을 인식할 것이기 때문에 language에서 Korean을 꼭 체크해준다.
설치를 완료한 이유 환경변수를 설정해주어야하는데 시스템 속성에서 path 환경변수에 TesseractOCR이 다운된 경로를 등록해주면 된다.
cmd창에서 다음과 같이 뜨면 설치 성공이다.
더 자세한 설치방법을 보고싶으면 여기로가면 된다.
cmd에서 실행하면 기본적으로 돌아가는 걸 확인 할 수 있다.
tesseract imagename outputbase [-l lang] [--oem ocrenginemode] [--psm pagesegmode] [configfiles...]
위의 포맷을 맞춰 실행하면 OCR 결과를 얻을 수 있다.
내가 분석하고자 하는 티켓은 한국어와 영어가 같이 있어 다음과 같은 명령어로 실행했다.
tesseract "이미지 경로" stdout -l kor+eng --psm 4
여기서 psm은 이미지를 어떤 방식으로 추출할지 결정하는 것이다.
tesseract의 기본 train data만으로 ocr 데이터를 얻으면 다음과 같이 오류가 많음을 확인할 수 있다. fine tuning을 위해 학습을 시킬 수 있는데 그 방법은 여기서 참고하면 된다.
tesseract자체가 두가지 언어를 같이 학습시키고 인식하는것에 한계가 있기 때문에 더 이상 학습을 진행하지 않고 EasyOCR을 이용하기로 결정했다.
https://joyhong.tistory.com/79
https://github.com/guiem/train-tesseract
EasyOCR은 Tesseract OCR과 달리 여러개의 언어를 동시 인식하는 것이 가능해 영어와 한국어를 동시에 인식을 해야하는 프로젝트에 적합한 무료 OCR 개발 서비스이다. Tesseract와 달리 GPU를 지원하므로 교내에서 지원받은 GPU를 사용할 예정이다.
pip install easyocr
easyocr을 설치하면 easyocr을 사용할 수 있다. 이렇게 하면 기본적인 세팅은 끝난 것이다.
추가적인 세팅 설명을 알고싶다면 여기로 가면 된다.
import easyocr
import numpy as np
from PIL import ImageFont, ImageDraw, Image
import cv2
import random
import matplotlib.pyplot as plt
reader = easyocr.Reader(['ko', 'en'], gpu=False)
result = reader.readtext("C:/Users/82102/Desktop/data7.jpg")
img = cv2.imread("C:/Users/82102/Desktop/data7.jpg")
img = Image.fromarray(img)
font = ImageFont.truetype("fonts/HMKMRHD.TTF",40)
draw = ImageDraw.Draw(img)
np.random.seed(42)
COLORS = np.random.randint(0, 255, size=(255, 3),dtype="uint8")
for i in result :
x = i[0][0][0]
y = i[0][0][1]
w = i[0][1][0] - i[0][0][0]
h = i[0][2][1] - i[0][1][1]
color_idx = random.randint(0,255)
color = [int(c) for c in COLORS[color_idx]]
draw.rectangle(((x, y), (x+w, y+h)), outline=tuple(color), width=2)
draw.text((int((x + x + w) / 2) , y-2),str(i[1]), font=font, fill=tuple(color),)
plt.imshow(img)
plt.show()
EasyOCR을 실행하면 다음과 같이 결과를 확인할 수 있다. 이미지의 좌표값을 함께 읽기때문에 이미지 박스를 그려 결과를 쉽게 대조할 수 있다. 오타가 있지만 tesseract ocr과 비교했을때 훨씬 정밀도가 높아졌다.
OCR을 인식을 잘하게하기 위해서 contour 인식을 해야한다. 다음 블로그를 참고하여 코드를 작성했다.
from matplotlib import pyplot as plt
from imutils.perspective import four_point_transform
from imutils.contours import sort_contours
import imutils
from easyocr import Reader
import cv2
import requests
import numpy as np
from PIL import ImageFont, ImageDraw, Image
def plt_imshow(title='image', img=None, figsize=(8 ,5)):
plt.figure(figsize=figsize)
if type(img) == list:
if type(title) == list:
titles = title
else:
titles = []
for i in range(len(img)):
titles.append(title)
for i in range(len(img)):
if len(img[i].shape) <= 2:
rgbImg = cv2.cvtColor(img[i], cv2.COLOR_GRAY2RGB)
else:
rgbImg = cv2.cvtColor(img[i], cv2.COLOR_BGR2RGB)
plt.subplot(1, len(img), i + 1), plt.imshow(rgbImg)
plt.title(titles[i])
plt.xticks([]), plt.yticks([])
plt.show()
else:
if len(img.shape) < 3:
rgbImg = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
else:
rgbImg = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(rgbImg)
plt.title(title)
plt.xticks([]), plt.yticks([])
plt.show()
def make_scan_image(image, width, ksize=(5,5), min_threshold=75, max_threshold=200):
image_list_title = []
image_list = []
image = imutils.resize(image, width=width)
ratio = org_image.shape[1] / float(image.shape[1])
# 이미지를 grayscale로 변환하고 blur를 적용
# 모서리를 찾기위한 이미지 연산
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, ksize, 0)
edged = cv2.Canny(blurred, min_threshold, max_threshold)
image_list_title = ['gray', 'blurred', 'edged']
image_list = [gray, blurred, edged]
# contours를 찾아 크기순으로 정렬
cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)
cnts = sorted(cnts, key=cv2.contourArea, reverse=True)
findCnt = None
# 정렬된 contours를 반복문으로 수행하며 4개의 꼭지점을 갖는 도형을 검출
for c in cnts:
peri = cv2.arcLength(c, True)
approx = cv2.approxPolyDP(c, 0.02 * peri, True)
# contours가 크기순으로 정렬되어 있기때문에 제일 첫번째 사각형을 영역으로 판단하고 break
if len(approx) == 4:
findCnt = approx
break
# 만약 추출한 윤곽이 없을 경우 오류
if findCnt is None:
raise Exception(("Could not find outline."))
output = image.copy()
cv2.drawContours(output, [findCnt], -1, (0, 255, 0), 2)
image_list_title.append("Outline")
image_list.append(output)
# 원본 이미지에 찾은 윤곽을 기준으로 이미지를 보정
transform_image = four_point_transform(org_image, findCnt.reshape(4, 2) * ratio)
plt_imshow(image_list_title, image_list)
plt_imshow("Transform", transform_image)
return transform_image
def putText(cv_img, text, x, y, color=(0, 0, 0), font_size=2):
# Colab이 아닌 Local에서 수행 시에는 gulim.ttc 를 사용하면 됩니다.
# font = ImageFont.truetype("fonts/gulim.ttc", font_size)
font = ImageFont.truetype('/usr/share/fonts/truetype/nanum/NanumGothicBold.ttf', font_size)
img = Image.fromarray(cv_img)
draw = ImageDraw.Draw(img)
draw.text((x, y), text, font=font, fill=color)
cv_img = np.array(img)
return cv_img
url ='https://postfiles.pstatic.net/MjAyMjA1MTNfMjg0/MDAxNjUyNDI3NjE5MjUw.afSWVWPelX1Lslwma_zJt63aPUP8baWB_S4zcf1ChAQg.AdfEyvT97Q3kKlMipXYia1t8UFLbX8qT1Kjn0QdfZTEg.JPEG.hyunk0928/1.jpg?type=w773'
image_nparray = np.asarray(bytearray(requests.get(url).content), dtype=np.uint8)
org_image = cv2.imdecode(image_nparray, cv2.IMREAD_COLOR)
plt_imshow("orignal image", org_image)
ticket1_image = make_scan_image(org_image, width=200, ksize=(5, 5), min_threshold=20, max_threshold=100)
langs = ['ko', 'en']
print("[INFO] OCR'ing input image...")
reader = Reader(lang_list=langs, gpu=False)
results = reader.readtext(ticket1_image)
# loop over the results
for (bbox, text, prob) in results:
print("[INFO] {:.4f}: {}".format(prob, text))
(tl, tr, br, bl) = bbox
tl = (int(tl[0]), int(tl[1]))
tr = (int(tr[0]), int(tr[1]))
br = (int(br[0]), int(br[1]))
bl = (int(bl[0]), int(bl[1]))
# 추출한 영역에 사각형을 그리고 인식한 글자를 표기합니다.
cv2.rectangle(ticket1_image, tl, br, (0, 255, 0), 2)
ticket1_image = putText(ticket1_image, text, tl[0], tl[1] - 60, (0, 255, 0), 50)
# cv2.putText(business_card_image, text, (tl[0], tl[1] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)
plt_imshow("Image", ticket1_image, figsize=(16,10))
위 코드를 실행하여 차례대로 전처리가 이루어진 결과는 다음과 같다.
위 사진은 원본이고 contour를 인식하기 위해 다음 단계를 거친다.
최종적으로 변환된 사진은 다음과 같다.
contour를 인식해서 EasyOCR을 적용하면 주면에 배경이 있어도 인식할 수 있다.
하지만 CONTOUR를 인식하여 잘라내는 전처리는 색상대비가 선명할때만 정밀도가 높아진다는 한계가 있다. 그렇기때문에 완벽하게 CONTOUR를 따는 것이 아닌 어느정도 누끼만 따는 방법으로 전처리를 할 예정이다.
https://yunwoong.tistory.com/76
EasyOCR Fine-tuning 과정으로 다음 블로그를 참고하였다.
EasyOCR을 학습시키기 위해서는 다음 단계를 거친다.
- 학습 데이터 만들기
- 한글 클래스 적용하기
- 모델 설정하기
- 학습시키기
- 결과 테스트
학습 데이터는 .lmdb 형태로 변환 시켜야한다.
우리는 총 두가지 부류의 학습데이터를 만들었다. 직접 모은 티켓 정보와 ai hub에서 얻은 인쇄체, 인쇄체 증강 데이터이다.
먼저 학습 데이터를 만들기 위해 다음 git을 clone한다.
git clone https://github.com/clovaai/deep-text-recognition-benchmark
data는 다음과 같은 folder 구조여야한다.
├── gt.txt
└── test
├── word_1.png
├── word_2.png
├── word_3.png
└── ...
이때 gt.txt는 다음과 같이 이미지 경로와 text내용이 {imagepath}\t{label}\n
구조로 작성되어야한다.
예시)
test/word_1.png Tiredness
test/word_2.png kills
test/word_3.png A
...
*주의: ai hub 데이터를 이용하기 위해선 json으로 되어있는 text정보를 파일 이름과 내용으로 추출해야한다. 다음 코드를 이용하여 id와 text를 추출해 txt파일을 만들었다.
import json
file_path="./printed_data_info.json"
with open(file_path, 'r', encoding="UTF8") as file:
json_data = json.load(file)
for data in json_data['annotations']:
f = open("./printed_data/"+data['id']+".gt.txt", 'w', encoding="UTF8")
f.write(data['text'])
f.close()
이렇게 만든 txt파일들은 다음 코드로 합쳐서 gt.txt를 만들면 된다.
import glob
import os
path = os.path.dirname(os.path.realpath(__file__))
os.chdir(path)
if os.path.exists("gt.txt"):
os.remove("gt.txt")
else:
print("The file does not exist")
read_files = glob.glob("*.txt")
print(read_files)
with open("gt.txt", "wb") as outfile:
for f in read_files:
i = 0
line = "images/"+os.path.splitext(f)[0]+".png"
i += 1
outfile.write(line.encode('utf-8'))
outfile.write("\t".encode('utf-8'))
with open(f, "rb") as infile:
outfile.write(infile.read())
outfile.write("\n".encode('utf-8'))
학습 데이터가 될 이미지와 txt파일을 만들었다면 다음 구문을 이용해 lmdb포맷을 변환한다.
# deep-text-recognition-benchmark 프로젝트에서 사용할 학습데이터 포맷으로의 변환
# deep-text-recognition-benchmark 프로젝트 root에서 실행
# training 데이터 변환
(venv) $ create_lmdb_dataset.py \
--gtFile "[PATH]/gt.txt" \
--inputPath "[PATH]/training" \
--outputPath "[PATH]/training"
# validation 데이터 변환
(venv) $ create_lmdb_dataset.py \
--gtFile "[PATH]/gt.txt" \
--inputPath "[PATH]/validation" \
--outputPath "[PATH]/validation"
# test 데이터 변환
(venv) $ create_lmdb_dataset.py \
--gtFile "[PATH]/gt.txt" \
--inputPath "[PATH]/test" \
--outputPath "[PATH]/test"
deep-text-recognition-benchmark 프로젝트의 train.py파일을 열고 256 line에 다음 구문을 추가한다.
# deep-text-recognition-benchmark 프로젝트 'train.py'의 285라인에 아래 코드를 삽입한다.
# 아래 character는 EasyOCR 프로젝트 './easyocr/config.py'의 'korean_g2' > 'character'에 해당한다.
opt.character = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~가각간갇갈감갑값강갖같갚갛개객걀거걱건걷걸검겁것겉게겨격겪견결겹경곁계고곡곤곧골곰곱곳공과관광괜괴굉교구국군굳굴굵굶굽궁권귀규균그극근글긁금급긋긍기긴길김깅깊까깎깐깔깜깝깥깨꺼꺾껍껏껑께껴꼬꼭꼴꼼꼽꽂꽃꽉꽤꾸꿀꿈뀌끄끈끊끌끓끔끗끝끼낌나낙낚난날낡남납낫낭낮낯낱낳내냄냉냐냥너넉널넓넘넣네넥넷녀녁년념녕노녹논놀놈농높놓놔뇌뇨누눈눕뉘뉴늄느늑는늘늙능늦늬니닐님다닥닦단닫달닭닮담답닷당닿대댁댐더덕던덜덤덥덧덩덮데델도독돈돌돕동돼되된두둑둘둠둡둥뒤뒷드득든듣들듬듭듯등디딩딪따딱딴딸땀땅때땜떠떡떤떨떻떼또똑뚜뚫뚱뛰뜨뜩뜯뜰뜻띄라락란람랍랑랗래랜램랫략량러럭런럴럼럽럿렁렇레렉렌려력련렬렵령례로록론롬롭롯료루룩룹룻뤄류륙률륭르른름릇릎리릭린림립릿마막만많말맑맘맙맛망맞맡맣매맥맨맵맺머먹먼멀멈멋멍멎메멘멩며면멸명몇모목몰몸몹못몽묘무묵묶문묻물뭄뭇뭐뭣므미민믿밀밉밌및밑바박밖반받발밝밟밤밥방밭배백뱀뱃뱉버번벌범법벗베벤벼벽변별볍병볕보복볶본볼봄봇봉뵈뵙부북분불붉붐붓붕붙뷰브블비빌빗빚빛빠빨빵빼뺨뻐뻔뻗뼈뽑뿌뿐쁘쁨사삭산살삶삼상새색샌생서석섞선설섬섭섯성세센셈셋션소속손솔솜솟송솥쇄쇠쇼수숙순술숨숫숲쉬쉽슈스슨슬슴습슷승시식신싣실싫심십싱싶싸싹쌀쌍쌓써썩썰썹쎄쏘쏟쑤쓰쓸씀씌씨씩씬씹씻아악안앉않알앓암압앗앙앞애액야약얇양얗얘어억언얹얻얼엄업없엇엉엌엎에엔엘여역연열엷염엽엿영옆예옛오옥온올옮옳옷와완왕왜왠외왼요욕용우욱운울움웃웅워원월웨웬위윗유육율으윽은을음응의이익인일읽잃임입잇있잊잎자작잔잖잘잠잡장잦재쟁저적전절젊점접젓정젖제젠젯져조족존졸좀좁종좋좌죄주죽준줄줌줍중쥐즈즉즌즐즘증지직진질짐집짓징짙짚짜짝짧째쨌쩌쩍쩐쪽쫓쭈쭉찌찍찢차착찬찮찰참창찾채책챔챙처척천철첫청체쳐초촉촌총촬최추축춘출춤춥춧충취츠측츰층치칙친칠침칭카칸칼캐캠커컨컬컴컵컷켓켜코콜콤콩쾌쿠퀴크큰클큼키킬타탁탄탈탑탓탕태택탤터턱털텅테텍텔템토톤톱통퇴투툼퉁튀튜트특튼튿틀틈티틱팀팅파팎판팔패팩팬퍼퍽페펴편펼평폐포폭표푸푹풀품풍퓨프플픔피픽필핏핑하학한할함합항해핵핸햄햇행향허헌험헤헬혀현혈협형혜호혹혼홀홍화확환활황회획횟효후훈훌훔훨휘휴흉흐흑흔흘흙흡흥흩희흰히힘"
혹은 학습 구문을 실행할때 '--character'파라미터를 전달해도 된다.
deep-text-recognition-benchmark프로젝트에는 Pre-trained 모델이 제공되지 않기때문에 EasyOCR Model Hub에서 korean_g2를 다운받아 모델로 사용하고자 한다.
다운로드한 모델은 '[Path]/pre_trained_model'에 저장하면 된다. 나는 deep-text-recognition-benchmark폴더가 있는 곳에 같이 생성하였다.
다음 코드를 실행시키면 모델이 학습하기 시작한다. 오랜 시간이 걸리므로 gpu를 사용하는 것을 추천한다.
# deep-text-recognition-benchmark 프로젝트를 이용한 모델 학습
# deep-text-recognition-benchmark 프로젝트 root에서 실행
(venv) $ python3 train.py \
--train_data "./workspace/step3/training" \
--valid_data "./workspace/step3/validation" \
--select_data / \
--batch_ratio 1 \
--Transformation None \
--FeatureExtraction "VGG" \
--SequenceModeling "BiLSTM" \
--Prediction "CTC" \
--input_channel 1 \
--output_channel 256 \
--hidden_size 256 \
--saved_model "./workspace/pre_trained_model/korean_g2.pth" \
--FT
# character 적용을 위한 소스코드 수정을 피하고 싶은 경우
(venv) $ python3 train.py \
--train_data "./workspace/step3/training" \
--valid_data "./workspace/step3/validation" \
--select_data / \
--batch_ratio 1 \
--Transformation None \
--FeatureExtraction "VGG" \
--SequenceModeling "BiLSTM" \
--Prediction "CTC" \
--input_channel 1 \
--output_channel 256 \
--hidden_size 256 \
--character " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~가각간갇갈감갑값강갖같갚갛개객걀거걱건걷걸검겁것겉게겨격겪견결겹경곁계고곡곤곧골곰곱곳공과관광괜괴굉교구국군굳굴굵굶굽궁권귀규균그극근글긁금급긋긍기긴길김깅깊까깎깐깔깜깝깥깨꺼꺾껍껏껑께껴꼬꼭꼴꼼꼽꽂꽃꽉꽤꾸꿀꿈뀌끄끈끊끌끓끔끗끝끼낌나낙낚난날낡남납낫낭낮낯낱낳내냄냉냐냥너넉널넓넘넣네넥넷녀녁년념녕노녹논놀놈농높놓놔뇌뇨누눈눕뉘뉴늄느늑는늘늙능늦늬니닐님다닥닦단닫달닭닮담답닷당닿대댁댐더덕던덜덤덥덧덩덮데델도독돈돌돕동돼되된두둑둘둠둡둥뒤뒷드득든듣들듬듭듯등디딩딪따딱딴딸땀땅때땜떠떡떤떨떻떼또똑뚜뚫뚱뛰뜨뜩뜯뜰뜻띄라락란람랍랑랗래랜램랫략량러럭런럴럼럽럿렁렇레렉렌려력련렬렵령례로록론롬롭롯료루룩룹룻뤄류륙률륭르른름릇릎리릭린림립릿마막만많말맑맘맙맛망맞맡맣매맥맨맵맺머먹먼멀멈멋멍멎메멘멩며면멸명몇모목몰몸몹못몽묘무묵묶문묻물뭄뭇뭐뭣므미민믿밀밉밌및밑바박밖반받발밝밟밤밥방밭배백뱀뱃뱉버번벌범법벗베벤벼벽변별볍병볕보복볶본볼봄봇봉뵈뵙부북분불붉붐붓붕붙뷰브블비빌빗빚빛빠빨빵빼뺨뻐뻔뻗뼈뽑뿌뿐쁘쁨사삭산살삶삼상새색샌생서석섞선설섬섭섯성세센셈셋션소속손솔솜솟송솥쇄쇠쇼수숙순술숨숫숲쉬쉽슈스슨슬슴습슷승시식신싣실싫심십싱싶싸싹쌀쌍쌓써썩썰썹쎄쏘쏟쑤쓰쓸씀씌씨씩씬씹씻아악안앉않알앓암압앗앙앞애액야약얇양얗얘어억언얹얻얼엄업없엇엉엌엎에엔엘여역연열엷염엽엿영옆예옛오옥온올옮옳옷와완왕왜왠외왼요욕용우욱운울움웃웅워원월웨웬위윗유육율으윽은을음응의이익인일읽잃임입잇있잊잎자작잔잖잘잠잡장잦재쟁저적전절젊점접젓정젖제젠젯져조족존졸좀좁종좋좌죄주죽준줄줌줍중쥐즈즉즌즐즘증지직진질짐집짓징짙짚짜짝짧째쨌쩌쩍쩐쪽쫓쭈쭉찌찍찢차착찬찮찰참창찾채책챔챙처척천철첫청체쳐초촉촌총촬최추축춘출춤춥춧충취츠측츰층치칙친칠침칭카칸칼캐캠커컨컬컴컵컷켓켜코콜콤콩쾌쿠퀴크큰클큼키킬타탁탄탈탑탓탕태택탤터턱털텅테텍텔템토톤톱통퇴투툼퉁튀튜트특튼튿틀틈티틱팀팅파팎판팔패팩팬퍼퍽페펴편펼평폐포폭표푸푹풀품풍퓨프플픔피픽필핏핑하학한할함합항해핵핸햄햇행향허헌험헤헬혀현혈협형혜호혹혼홀홍화확환활황회획횟효후훈훌훔훨휘휴흉흐흑흔흘흙흡흥흩희흰히힘"
--saved_model "./workspace/pre_trained_model/korean_g2.pth" \
--FT
'./saved_models/None-VGG-BiLSTM-CTC-Seed1111' 디렉토리에 저장된 'best_accuracy.pth' 파일을 복사해 'custom.pth'로 파일명을 변경한뒤 따로 폴더를 만들어 custom.yaml과 custom.py를 같은 폴더상에 위치하게 해준다.
custom.yaml
# korean_g2 (None-VGG-BiLSTM-CTC)
imgH: 32
lang_list: ['ko']
network_params:
input_channel: 1
output_channel: 256
hidden_size: 256
character_list: " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~가각간갇갈감갑값강갖같갚갛개객걀거걱건걷걸검겁것겉게겨격겪견결겹경곁계고곡곤곧골곰곱곳공과관광괜괴굉교구국군굳굴굵굶굽궁권귀규균그극근글긁금급긋긍기긴길김깅깊까깎깐깔깜깝깥깨꺼꺾껍껏껑께껴꼬꼭꼴꼼꼽꽂꽃꽉꽤꾸꿀꿈뀌끄끈끊끌끓끔끗끝끼낌나낙낚난날낡남납낫낭낮낯낱낳내냄냉냐냥너넉널넓넘넣네넥넷녀녁년념녕노녹논놀놈농높놓놔뇌뇨누눈눕뉘뉴늄느늑는늘늙능늦늬니닐님다닥닦단닫달닭닮담답닷당닿대댁댐더덕던덜덤덥덧덩덮데델도독돈돌돕동돼되된두둑둘둠둡둥뒤뒷드득든듣들듬듭듯등디딩딪따딱딴딸땀땅때땜떠떡떤떨떻떼또똑뚜뚫뚱뛰뜨뜩뜯뜰뜻띄라락란람랍랑랗래랜램랫략량러럭런럴럼럽럿렁렇레렉렌려력련렬렵령례로록론롬롭롯료루룩룹룻뤄류륙률륭르른름릇릎리릭린림립릿마막만많말맑맘맙맛망맞맡맣매맥맨맵맺머먹먼멀멈멋멍멎메멘멩며면멸명몇모목몰몸몹못몽묘무묵묶문묻물뭄뭇뭐뭣므미민믿밀밉밌및밑바박밖반받발밝밟밤밥방밭배백뱀뱃뱉버번벌범법벗베벤벼벽변별볍병볕보복볶본볼봄봇봉뵈뵙부북분불붉붐붓붕붙뷰브블비빌빗빚빛빠빨빵빼뺨뻐뻔뻗뼈뽑뿌뿐쁘쁨사삭산살삶삼상새색샌생서석섞선설섬섭섯성세센셈셋션소속손솔솜솟송솥쇄쇠쇼수숙순술숨숫숲쉬쉽슈스슨슬슴습슷승시식신싣실싫심십싱싶싸싹쌀쌍쌓써썩썰썹쎄쏘쏟쑤쓰쓸씀씌씨씩씬씹씻아악안앉않알앓암압앗앙앞애액야약얇양얗얘어억언얹얻얼엄업없엇엉엌엎에엔엘여역연열엷염엽엿영옆예옛오옥온올옮옳옷와완왕왜왠외왼요욕용우욱운울움웃웅워원월웨웬위윗유육율으윽은을음응의이익인일읽잃임입잇있잊잎자작잔잖잘잠잡장잦재쟁저적전절젊점접젓정젖제젠젯져조족존졸좀좁종좋좌죄주죽준줄줌줍중쥐즈즉즌즐즘증지직진질짐집짓징짙짚짜짝짧째쨌쩌쩍쩐쪽쫓쭈쭉찌찍찢차착찬찮찰참창찾채책챔챙처척천철첫청체쳐초촉촌총촬최추축춘출춤춥춧충취츠측츰층치칙친칠침칭카칸칼캐캠커컨컬컴컵컷켓켜코콜콤콩쾌쿠퀴크큰클큼키킬타탁탄탈탑탓탕태택탤터턱털텅테텍텔템토톤톱통퇴투툼퉁튀튜트특튼튿틀틈티틱팀팅파팎판팔패팩팬퍼퍽페펴편펼평폐포폭표푸푹풀품풍퓨프플픔피픽필핏핑하학한할함합항해핵핸햄햇행향허헌험헤헬혀현혈협형혜호혹혼홀홍화확환활황회획횟효후훈훌훔훨휘휴흉흐흑흔흘흙흡흥흩희흰히힘"
custom.py
import torch.nn as nn
class Model(nn.Module):
def __init__(self, input_channel, output_channel, hidden_size, num_class):
super(Model, self).__init__()
""" FeatureExtraction """
self.FeatureExtraction = VGG_FeatureExtractor(input_channel, output_channel)
self.FeatureExtraction_output = output_channel
self.AdaptiveAvgPool = nn.AdaptiveAvgPool2d((None, 1))
""" Sequence modeling"""
self.SequenceModeling = nn.Sequential(
BidirectionalLSTM(self.FeatureExtraction_output, hidden_size, hidden_size),
BidirectionalLSTM(hidden_size, hidden_size, hidden_size))
self.SequenceModeling_output = hidden_size
""" Prediction """
self.Prediction = nn.Linear(self.SequenceModeling_output, num_class)
def forward(self, input, text):
""" Feature extraction stage """
visual_feature = self.FeatureExtraction(input)
visual_feature = self.AdaptiveAvgPool(visual_feature.permute(0, 3, 1, 2))
visual_feature = visual_feature.squeeze(3)
""" Sequence modeling stage """
contextual_feature = self.SequenceModeling(visual_feature)
""" Prediction stage """
prediction = self.Prediction(contextual_feature.contiguous())
return prediction
class BidirectionalLSTM(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(BidirectionalLSTM, self).__init__()
self.rnn = nn.LSTM(input_size, hidden_size, bidirectional=True, batch_first=True)
self.linear = nn.Linear(hidden_size * 2, output_size)
def forward(self, input):
"""
input : visual feature [batch_size x T x input_size]
output : contextual feature [batch_size x T x output_size]
"""
try: # multi gpu needs this
self.rnn.flatten_parameters()
except: # quantization doesn't work with this
pass
recurrent, _ = self.rnn(input) # batch_size x T x input_size -> batch_size x T x (2*hidden_size)
output = self.linear(recurrent) # batch_size x T x output_size
return output
class VGG_FeatureExtractor(nn.Module):
def __init__(self, input_channel, output_channel=256):
super(VGG_FeatureExtractor, self).__init__()
self.output_channel = [int(output_channel / 8), int(output_channel / 4),
int(output_channel / 2), output_channel]
self.ConvNet = nn.Sequential(
nn.Conv2d(input_channel, self.output_channel[0], 3, 1, 1), nn.ReLU(True),
nn.MaxPool2d(2, 2),
nn.Conv2d(self.output_channel[0], self.output_channel[1], 3, 1, 1), nn.ReLU(True),
nn.MaxPool2d(2, 2),
nn.Conv2d(self.output_channel[1], self.output_channel[2], 3, 1, 1), nn.ReLU(True),
nn.Conv2d(self.output_channel[2], self.output_channel[2], 3, 1, 1), nn.ReLU(True),
nn.MaxPool2d((2, 1), (2, 1)),
nn.Conv2d(self.output_channel[2], self.output_channel[3], 3, 1, 1, bias=False),
nn.BatchNorm2d(self.output_channel[3]), nn.ReLU(True),
nn.Conv2d(self.output_channel[3], self.output_channel[3], 3, 1, 1, bias=False),
nn.BatchNorm2d(self.output_channel[3]), nn.ReLU(True),
nn.MaxPool2d((2, 1), (2, 1)),
nn.Conv2d(self.output_channel[3], self.output_channel[3], 2, 1, 0), nn.ReLU(True))
def forward(self, input):
return self.ConvNet(input)
이제 EasyOCR 프로젝트의 root에서 시작되는 run.py코드를 돌려주면 학습된 모델을 이용해 학습이 가능하다. (다음코드에서 경로만 적절히 수정해 적용하면 된다.)
from easyocr.easyocr import *
# GPU 설정
os.environ['CUDA_VISIBLE_DEVICES'] = '0,1'
def get_files(path):
file_list = []
files = [f for f in os.listdir(path) if not f.startswith('.')] # skip hidden file
files.sort()
abspath = os.path.abspath(path)
for file in files:
file_path = os.path.join(abspath, file)
file_list.append(file_path)
return file_list, len(file_list)
if __name__ == '__main__':
# # Using default model
# reader = Reader(['ko'], gpu=True)
# Using custom model
reader = Reader(['ko'], gpu=True,
model_storage_directory='./train-easyocr/deep-text-recognition-benchmark/Finished_models/data3',
user_network_directory='./train-easyocr/deep-text-recognition-benchmark/Finished_models/data3',
recog_network='custom')
files, count = get_files('./train-easyocr/deep-text-recognition-benchmark/ticket_images')
for idx, file in enumerate(files):
filename = os.path.basename(file)
result = reader.readtext(file)
# ./easyocr/utils.py 733 lines
# result[0]: bbox
# result[1]: string
# result[2]: confidence
for (bbox, string, confidence) in result:
print("filename: '%s', confidence: %.4f, string: '%s'" % (filename, confidence, string))
# print('bbox: ', bbox)
현재까지 총 4가지의 data set을 가지고 모델을 학습시켰다.
결과에 사용된 티켓
티켓 데이터 355개
문장 데이터(인쇄체 증강데이터) 41575개
2-2. 단어+문장 데이터(인쇄체 증강데이터) 513718개
티켓+문장 데이터 41930개
기본 인쇄체 데이터 303057개
결과적으로 실제 티켓 데이터를 학습시킨것이 5개중 가장 나은 결과를 보였다. 하지만 여전히 만족스러운 성능 결과를 보이고 있지는 않기 때문에 실제 EasyOCR의 모델을 기본모델을 Fine-tuning할 수 있는 새로운 방법을 찾았다.
korean_gz.pth에 학습 시키는 것이 아닌 original easyocr에 티켓 데이터를 덮씌워서 학습 시키기로 하였다.
EasyOCR root에 숨은 디렉토리 .EasyOCR/model에 있는 craft_mlt_52K.pth를 기본 모델로 세팅하였다.
traingin code를 다음과 같이 수정하면 된다.
!python3 train.py \
--train_data "../result1/training" \
--valid_data "../result1/validation" \
--select_data / \
--batch_ratio 1 \
--Transformation None \
--FeatureExtraction "VGG" \
--SequenceModeling "BiLSTM" \
--Prediction "CTC" \
--input_channel 1 \
--output_channel 256 \
--hidden_size 256 \
--saved_model "../trained_model/craft_mlt_25k.pth" \
--exp_name "default_and_data1" \
--FT
이후 만들어진 best_accuracy.pth의 이름을 craft_mlt_52K.pth로 변경하고 .EasyOCR/model에 넣고 다음 코드를 돌려 결과를 확인해 보았다.
import easyocr
reader = easyocr.Reader(['ko','en'], gpu = True)
result = reader.readtext('./src/data8.jpg', detail = 0)
for i in result:
print(i)
다음 사진을 EasyOCR로 읽었을때
학습 이전에는
2019년; 대한민국이 기다려온 영웅이 온다!
1
YR:
ANIVIAJARY
MUSICAL
2019-08-11 (일) 18.30
A석
예술의전당 오페라극장
일반
40,000원
3층 B늘록
2열
13번
경감확
T14-9484810{1/1)
신용카드
주최: SBS (주)에이롬
제작: (주 에이름
문의: {주)에이람 02-2250-5941
주차할인바코드
블록을 늘록으로 읽었는데 우리의 티켓 데이터를 학습한 것은
2019년,대한민국이 기다려온엉웅이 온다'
I
f[#RS
##NIYERSARY
#OSICA
2019-08-11 (일) 18330
A석
예술의전당 오페라극장
일반
40,000원
3층 B블록
2열
13번
유
T147948481011)
신용카드
주최: 5BS (주g에이블
제작' (주에이콩
문의: '주 애이블 0-2250-5947
주차보인바코드
블록으로 잘 읽었다. 유의미한 변화를 보였음을 확인 할 수 있었다. 필요한 일시, 좌석, 장소를 Fine-tuning하여 잘 읽었기 때문에 앞으로 위 방법을 이용하고 추가적으로 학습시킬 예정이다.
https://davelogs.tistory.com/94
https://github.com/clovaai/deep-text-recognition-benchmark
The store is working towards a more sustainable retail model by reducing packaging waste
https://onlyup-game.io/
안녕하세요 게시물 정말 잘 읽어 보았습니다. 그런데 마지막에 .EasyOCR/model경로에 학습된 모델을 craft_mlt_25k.pth로 변경하여 적용한다 하셨는데 저는 왜인지 옮기고 코드를 실행하면 craft_mlt_25k.pth모델을 다시 다운 받더군요. 혹시 왜 그런지 알려주실수 있을까요?