///
Search
Duplicate
✏️

MMOCR 프레임워크에서 한글 사용하기

created
2022/11/24 03:12
last edited
2024/03/12 15:46
difficulty
문과: 어려움
이과: 보통
1 more property
MMOCR 프레임워크에서 한글을 사용하기 위해서는 두 가지 노력이 필요합니다. 첫 번째는 MMOCR의 recognition 모델이 학습을 할 때 한글 문자 그 자체를 인식 할 수 있도록 만들어야 하는 것이고, 두 번째는 MMOCR 모델의 추론 출력 시각화 시 한글이 깨져 보이는 문제를 해결하는 것입니다.

MMOCR 이 한글 문자 그 자체를 인식할 수 있도록 만들기

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

MMOCR 모델의 추론을 시각화했을 때 한글이 깨져 보이는 문제 해결하기

mmocr/ocr.py 에 명세된 MMOCR API 를 사용하면 detection + recognition 결과를 시각화하고 파일로 저장할 수 있습니다. 하지만 정상적으로 한글을 학습한 모델의 경우에도 다음과 같이 <unk> 로 시각화되는 것을 확인할 수 있습니다.
왜 이런 문제가 발생할까요? MMOCR 이 시각화를 위해 사용하는 저수준 라이브러리는 MMEngine 입니다. MMEngine 은 다시 matplotlib 을 호출해서 사용합니다. 이때 matplotlib 에서 한글을 지원하지 않는 폰트를 사용하기 때문입니다. 위와 같이 한글이 ‘깨져’ 보이는 경우 다음과 같은 콘솔 로그가 출력됩니다.
/usr/local/lib/python3.8/dist-packages/mmengine/visualization/utils.py:238: UserWarning: Glyph 48373 (\N{HANGUL SYLLABLE BOG}) missing from current font. s, (width, height) = canvas.print_to_buffer() /usr/local/lib/python3.8/dist-packages/mmengine/visualization/utils.py:238: UserWarning: Glyph 54788 (\N{HANGUL SYLLABLE HYEON}) missing from current font. s, (width, height) = canvas.print_to_buffer() /usr/local/lib/python3.8/dist-packages/mmengine/visualization/utils.py:238: UserWarning: Glyph 51456 (\N{HANGUL SYLLABLE JUN}) missing from current font. s, (width, height) = canvas.print_to_buffer() /usr/local/lib/python3.8/dist-packages/mmengine/visualization/utils.py:238: UserWarning: Glyph 48276 (\N{HANGUL SYLLABLE BEOM}) missing from current font. s, (width, height) = canvas.print_to_buffer() /usr/local/lib/python3.8/dist-packages/mmengine/visualization/utils.py:238: UserWarning: Glyph 54868 (\N{HANGUL SYLLABLE HWA}) missing from current font. s, (width, height) = canvas.print_to_buffer()
Python
복사
콘솔 출력
한글 폰트를 설치합니다. 가장 무난한 나눔고딕을 설치합니다. 제 경우에는 도커 환경에서 작업이 이루어지고 있기 때문에 도커파일에 설치 스크립트를 작성했습니다.
# 한글 시각화를 위한 나눔폰트 설치 및 matplotlib 폰트 캐시 제거 RUN wget http://cdn.naver.com/naver/NanumFont/fontfiles/NanumFont_TTF_ALL.zip &&\ unzip NanumFont_TTF_ALL.zip -d NanumFont &&\ rm -f NanumFont_TTF_ALL.zip &&\ mv NanumFont /usr/share/fonts/ &&\ fc-cache -f -v &&\ rm -rf ~/.cache/matplotlib
Python
복사
Dockerfile
apt-get install 을 두고 wget 으로 압축파일을 다운로드받아 사용하는 이유는 우분투 개발 환경과 달리 배포 환경이 데비안 리눅스이기 때문입니다. 나눔고딕과 같은 서드파티 패키지 저장소를 탐색하고 다운로드받을 때 데비안 리눅스에서는 별도의 설정이 필요한데, 이런 과정을 피하기 위해 파일을 다운로드받는 전략을 사용합니다.
설치된 폰트를 matplotlib 에서 사용할 수 있는 상태로 만들 수 있는 방법은 다양합니다. 관련 내용은 다른 블로그들에 자세히 설명되어 있으니 생략하도록 하겠습니다. matplotlib 에서 나눔고딕 폰트를 적절히 불러올 수 있음을 확신하기 위해 ocr.py 의 가장 상단에 아래와 같이 matplotlib 피규어를 그려 보았습니다. 한글이 깨지지 않으면 정상적으로 한글을 지원하는 폰트가 설치된 것입니다.
하지만 지금 상태까지 왔다고 하더라도 MMOCR 의 API 를 사용했을 때 한글이 정상적으로 시각화되는 것은 아닙니다. 앞서 MMOCR 은 MMEngine 이라는 저수준 라이브러리를 사용한다고 했습니다. MMEngine 이 사용하는 시각화 함수가 font_families 인자에 어떠한 값도 주어지지 않는 경우 'sans-serif' 로 초기화하고 있기 때문입니다.
@VISUALIZERS.register_module() class Visualizer(ManagerMixin): # ... @master_only def draw_texts(self, ... font_families: Union[str, List[str]] = 'sans-serif', ...) -> 'Visualizer': # ... check_type_and_length('font_families', font_families, (str, list), num_text) font_families = value2list(font_families, str, num_text)
Python
복사
mmengine/visualization/visualizer.py
기본값 산세리프를 사용하는 대신 나눔고딕을 사용하려면 config 파일을 아래와 같이 변경하면 됩니다.
visualizer = dict( type='TextRecogLocalVisualizer', name='visualizer', font_families='NanumGothic', vis_backends=vis_backends)
Python
복사
configs/textrecog/_base_/default_runtime.py
그리고 다시 ocr.py 를 실행하면 아래와 같은 결과를 얻을 수 있습니다.

외전

1567
pull
이 기능은 제가 작성한 커밋에 의해 활성화되었습니다. 아래는 어떤 과정을 거쳐 커밋을 생성했는지 과정을 소개합니다. 아래 내용은 MMOCR 의 내부구현을 어느정도 알고 있다는 것을 전제로 작성되었습니다.
우선, 텍스트를 그려내는 함수가 어떻게 호출되는지 파악할 필요가 있었습니다. 이미지로 추론 결과를 시각화하는 것은 BaseMMOCRInferencer 클래스의 visualize 메서드의 역할입니다.
class BaseMMOCRInferencer(BaseInferencer): # ... def visualize(self, inputs: InputsType, preds: PredType, show: bool = False, wait_time: int = 0, draw_pred: bool = True, pred_score_thr: float = 0.3, img_out_dir: str = '') -> List[np.ndarray]: # ... self.visualizer.add_datasample( img_name, img, pred, show=show, wait_time=wait_time, draw_gt=False, draw_pred=draw_pred, pred_score_thr=pred_score_thr, out_file=out_file, ) # ...
Python
복사
mmocr/apis/inferencers/base_mmocr_inferencer.py
이때 add_datasample 메서드를 호출하는 self.visualizer 변수의 타입은 TextRecogLocalVisualizer 입니다. TextRecogLocalVisualizeradd_datasample 메서드를 확인해 보면, 해당 메서드는 시각화할 이미지를 적절한 형태로 만들어 내는 역할을 수행합니다.
@VISUALIZERS.register_module() class TextRecogLocalVisualizer(BaseLocalVisualizer): # ... def add_datasample(self, name: str, image: np.ndarray, data_sample: Optional['TextRecogDataSample'] = None, draw_gt: bool = True, draw_pred: bool = True, show: bool = False, wait_time: int = 0, pred_score_thr: float = None, out_file: Optional[str] = None, step=0) -> None: """Visualize datasample and save to all backends. - If GT and prediction are plotted at the same time, they are displayed in a stitched image where the left image is the ground truth and the right image is the prediction. .... """ # ... cat_images = [image] if draw_gt and data_sample is not None and 'gt_text' in data_sample: gt_text = data_sample.gt_text.item cat_images.append(self._draw_instances(image, gt_text)) if (draw_pred and data_sample is not None and 'pred_text' in data_sample): pred_text = data_sample.pred_text.item cat_images.append(self._draw_instances(image, pred_text)) cat_images = self._cat_image(cat_images, axis=0) # ... if out_file is not None: mmcv.imwrite(cat_images[..., ::-1], out_file) def _draw_instances(self, image: np.ndarray, text: str) -> np.ndarray: # ... self.draw_texts( text, np.array([width / 2, height / 2]), colors=self.gt_color, font_sizes=font_size, vertical_alignments='center', horizontal_alignments='center') # ...
Python
복사
mmocr/visualization/textrecog_visualizer.py
적절하게 생성된 이미지는 cat_images 리스트에 담겨 mmcv.imwrite 와 같은 파일 입출력 api 로 보내진다는 것을 알 수 있습니다. 여기서 실질적으로 텍스트를 그리는 부분을 찾아야 하기 때문에 _draw_instances 메서드를 확인해 봅니다.
@VISUALIZERS.register_module() class Visualizer(ManagerMixin): # ... @master_only def draw_texts(self, ... font_families: Union[str, List[str]] = 'sans-serif', ...) -> 'Visualizer': # ... check_type_and_length('font_families', font_families, (str, list), num_text) font_families = value2list(font_families, str, num_text)
Python
복사
mmengine/visualization/visualizer.py
_draw_instances 메서드에서는 draw_texts 메서드를 호출하고 있습니다. draw_texts 메서드는 Visualizer 클래스에 정의되어 있습니다. 앞서 텍스트를 그리는 부분을 찾아야 한다고 말했습니다. 여기에 제가 글의 앞부분에서 잠깐 언급했던, font_families 인자에 어떠한 값도 주어지지 않는 경우 'sans-serif' 로 초기화하는 부분을 확인할 수 있습니다.
@VISUALIZERS.register_module() class Visualizer(ManagerMixin): # ... @master_only def draw_texts(self, ... font_families: Union[str, List[str]] = 'sans-serif', ...) -> 'Visualizer': # ... check_type_and_length('font_families', font_families, (str, list), num_text) font_families = value2list(font_families, str, num_text)
Python
복사
mmengine/visualization/visualizer.py
그래서 matplotlib 의 폰트 설정을 바꾸기 위해 print(plt.rcParams['font.family']) 을 수정하거나, matplotlibrc 를 수정하더라도, 소용없이 항상 sans-serif 로 한글을 그려낸다는 것을 확인할 수 있습니다. 새로운 커밋을 생성하기 전에, 이 문제가 맞는지 확인해볼 필요가 있습니다.
@master_only def draw_texts( self, ... font_families: Union[str, List[str]] = 'NanumGothicCoding', ...) -> 'Visualizer':
Python
복사
mmengine/visualization/visualizer.py
위와 같이 변경하고, ocr.py 를 실행하면 다음과 같이 정상적인 시각화 결과를 얻을 수 있습니다.
문제를 발견했으니, 이제 제대로 고쳐 볼 차례입니다. configs/textrecog/_base_/default_runtime.py 파일을 다음과 같이 작성하여 TextRecogLocalVisualizer 클래스의 생성자를 제어할 수 있습니다.
visualizer = dict( type='TextRecogLocalVisualizer', name='visualizer', vis_backends=vis_backends)
Python
복사
configs/textrecog/_base_/default_runtime.py
제가 원하는 것은 config 파일을 아래와 같이 변경했을 때,
visualizer = dict( type='TextRecogLocalVisualizer', name='visualizer', vis_backends=vis_backends, font_families='NanumGothic')
Python
복사
configs/textrecog/_base_/default_runtime.py
draw_text 함수에 아래와 같이 산세리프가 아니라 나눔고딕이 전달되는 것입니다
@VISUALIZERS.register_module() class TextRecogLocalVisualizer(BaseLocalVisualizer): # ... def _draw_instances(self, image: np.ndarray, text: str) -> np.ndarray: """Draw text on image. Args: image (np.ndarray): The image to draw. text (str): The text to draw. Returns: np.ndarray: The image with text drawn. """ height, width = image.shape[:2] empty_img = np.full_like(image, 255) self.set_image(empty_img) font_size = 0.5 * width / (len(text) + 1) self.draw_texts( text, np.array([width / 2, height / 2]), colors=self.gt_color, font_sizes=font_size, vertical_alignments='center', horizontal_alignments='center', font_families=self.font_families) text_image = self.get_image() return text_image
Python
복사
self.draw_texts() 를 호출하는 클래스는 다른 목적의 Visualizer 클래스들에서도 발견됩니다. 이 부분들도 마찬가지로 수정해 주어야 합니다. draw_texts() 를 호출하는 클래스와 메서드를 정리하면 다음과 같습니다.
클래스 이름
사용하는 함수
비고
조부모 클래스
Visualizer
draw_texts
부모 클래스
BaseLocalVisualizer
TextRecogLocalVisualizer
_draw_instances
draw_texts 호출
클래스 이름
사용하는 함수
비고
조부모 클래스
Visualizer
draw_texts
부모 클래스
BaseLocalVisualizer
get_labels_image
draw_texts 호출
KIELocalVisualizer
_draw_instances
get_labels_image 호출
@VISUALIZERS.register_module() class BaseLocalVisualizer(Visualizer): # ... # ... def get_labels_image(self, image: np.ndarray, labels: Union[np.ndarray, torch.Tensor], bboxes: Union[np.ndarray, torch.Tensor], colors: Union[str, Sequence[str]] = 'k', font_size: Union[int, float] = 10, auto_font_size: bool = False) -> np.ndarray: # ... self.draw_texts( labels, (bboxes[:, :2] + bboxes[:, 2:]) / 2, vertical_alignments='center', horizontal_alignments='center', colors='k', font_sizes=font_size) # ... # ...
Python
복사
mmocr/visualization/base_visualizer.py
@VISUALIZERS.register_module() class KIELocalVisualizer(BaseLocalVisualizer): # ... def _draw_instances( self, image: np.ndarray, bbox_labels: Union[np.ndarray, torch.Tensor], bboxes: Union[np.ndarray, torch.Tensor], polygons: Sequence[np.ndarray], edge_labels: Union[np.ndarray, torch.Tensor], texts: Sequence[str], class_names: Dict, is_openset: bool = False, arrow_colors: str = 'g', ) -> np.ndarray: # ... classes_image = np.full(empty_shape, 255, dtype=np.uint8) bbox_classes = [class_names[int(i)]['name'] for i in bbox_labels] classes_image = self.get_labels_image(classes_image, bbox_classes, bboxes) # ... if is_openset: edge_image = np.full(empty_shape, 255, dtype=np.uint8) edge_image = self._draw_edge_label(edge_image, edge_labels, bboxes, texts, arrow_colors) cat_image.append(edge_image) return self._cat_image(cat_image, axis=1) # ... def _draw_edge_label(self, image: np.ndarray, edge_labels: Union[np.ndarray, torch.Tensor], bboxes: Union[np.ndarray, torch.Tensor], texts: Sequence[str], arrow_colors: str = 'g') -> np.ndarray: # ... if key_texts: self.draw_texts( key_texts, (bboxes[key_index, :2] + bboxes[key_index, 2:]) / 2, colors='k', horizontal_alignments='center', vertical_alignments='center') if val_texts: self.draw_texts( val_texts, (bboxes[val_index, :2] + bboxes[val_index, 2:]) / 2, colors='k', horizontal_alignments='center', vertical_alignments='center') # ... # ...
Python
복사
mmocr/visualization/kie_visualizer.py
KIELocalVisualizer, TextRecogLocalVisualizer 은 모두 BaseLocalVisualizer 를 상속받아 만들어졌습니다. 그래서 self.draw_texts 에 전달될 폰트에 대한 정보는 BaseLocalVisualizer 또는 그의 부모인 Visualizer 에서 관리되어야 합니다. 하지만 Visualizer 은 폰트의 크기같은 정보들을 직접 다루는 것 같지 않았고, 너무 낮은 추상화 수준이라는 생각이 들었습니다. 따라서 BaseLocalVisualizer 클래스에 font_families 라는 멤버 변수(클래스 애트리뷰트)를 추가해 주는 작업이 필요하다고 생각했습니다.
그런데 TextRecogLocalVisualizer 클래스의 생성자는 font_families 인자를 받지 못합니다.
@VISUALIZERS.register_module() class TextRecogLocalVisualizer(BaseLocalVisualizer): """MMOCR Text Detection Local Visualizer. Args: name (str): Name of the instance. Defaults to 'visualizer'. image (np.ndarray, optional): The origin image to draw. The format should be RGB. Defaults to None. vis_backends (list, optional): Visual backend config list. Defaults to None. save_dir (str, optional): Save file dir for all storage backends. If it is None, the backend storage will not save any data. gt_color (str or tuple[int, int, int]): Colors of GT text. The tuple of color should be in RGB order. Or using an abbreviation of color, such as `'g'` for `'green'`. Defaults to 'g'. pred_color (str or tuple[int, int, int]): Colors of Predicted text. The tuple of color should be in RGB order. Or using an abbreviation of color, such as `'r'` for `'red'`. Defaults to 'r'. """ def __init__(self, name: str = 'visualizer', image: Optional[np.ndarray] = None, vis_backends: Optional[Dict] = None, save_dir: Optional[str] = None, gt_color: Optional[Union[str, Tuple[int, int, int]]] = 'g', pred_color: Optional[Union[str, Tuple[int, int, int]]] = 'r') -> None: super().__init__( name=name, image=image, vis_backends=vis_backends, save_dir=save_dir) self.gt_color = gt_color self.pred_color = pred_color
Python
복사
mmocr/visualization/textrecog_visualizer.py
BaseLocalVisualizer 클래스도 마찬가지입니다. BaseLocalVisualizer 클래스에는 아예 생성자를 찾을 수 없는데, 이는 MMEngine 의 복잡한 메커니즘에 의해 자동적으로 클래스가 관리되기 때문입니다.
그래서 저는 TextRecogLocalVisualizer 의 생성자를 다음과 같이 수정하고
@VISUALIZERS.register_module() class TextRecogLocalVisualizer(BaseLocalVisualizer): # ... def __init__(self, name: str = 'visualizer', image: Optional[np.ndarray] = None, vis_backends: Optional[Dict] = None, save_dir: Optional[str] = None, gt_color: Optional[Union[str, Tuple[int, int, int]]] = 'g', pred_color: Optional[Union[str, Tuple[int, int, int]]] = 'r', **kwargs) -> None: super().__init__( name=name, image=image, vis_backends=vis_backends, save_dir=save_dir, **kwargs) self.gt_color = gt_color self.pred_color = pred_color
Python
복사
mmocr/visualization/textrecog_visualizer.py
BaseLocalVisualizer 의 생성자를 추가하여 self.font_families 를 저장하는 로직을 추가했습니다.
@VISUALIZERS.register_module() class BaseLocalVisualizer(Visualizer): # ... def __init__(self, name: str = 'visualizer', font_families: Union[str, List[str]] = 'sans-serif', **kwargs) -> None: super().__init__(name=name, **kwargs) self.font_families = font_families
Python
복사
mmocr/visualization/base_visualizer.py
글을 쓰는 데 참고한 자료입니다.
글을 쓰는 데 반영된 생각들입니다.
1.
작성 중입니다.
이 글은 다음 글로 이어집니다.
1.
작성 중입니다.
바로가기
다빈치 작업실 : 블로그 홈
생각 완전체 : 글 그 자체. 블로그 포스팅