AI OCR 시스템의 내부를 열어보는 글이다. 개요가 "무엇을 하는가"라면, 이 글은 "어떻게 하는가"를 다룬다. 이미지 전처리부터 좌표계 함정까지, 실제 구현에서 부딪힌 문제와 해결을 정리했다.
전처리: 항상 하지 않는다
좋은 이미지에 샤프닝을 걸면 오히려 노이즈가 생긴다. 그래서 전처리는 문제가 있을 때만 적용한다. Hough Line으로 기울기를 측정하고, Laplacian 분산으로 초점을 진단하고, 그레이스케일 range로 대비를 확인한다. 조건을 충족하는 항목만 보정한다. 고품질 PDF 스캔이라면? 아무것도 하지 않는다.
Auto-crop: 보이지 않는 계약
책상 위에서 찍은 사진의 배경을 OpenCV로 제거한다. 이 과정에서 모든 bbox 좌표의 기준이 바뀐다. crop된 이미지 기준으로 좌표가 계산되고, 검수 화면 API도 동일한 crop을 적용한다. 이 "보이지 않는 계약"이 깨지면 bbox가 어긋난다.
문서가 이미지의 80% 이상을 차지하면 crop을 건너뛴다. 풀페이지 스캔에서 불필요한 crop은 오히려 해가 된다.
Bbox 스냅핑: 두 AI를 잇는 다리
Gemini가 추출한 값("KS 먹는 샘물2L X 6")과 PaddleOCR이 감지한 텍스트 영역("KS먹는샘물2L×6")을 텍스트 유사도로 매칭한다. 3-pass 알고리즘으로 동작한다.
Pass 1은 엄격한 1:1 greedy 매칭(threshold 0.5). Pass 2는 미매칭 필드에 대해 relaxed 매칭(threshold 0.35, 영역 재사용 허용). Pass 3은 품목_1, 품목_2 같은 번호 필드의 수직 순서를 검증하고 위반 시 재할당한다.
핵심은 텍스트 일치가 거리보다 5배 중요하다는 스코어링이다. 비뚤어진 사진에서도 텍스트만 맞으면 정확히 매칭된다. 영수증에 "12,990"이 3번 나와도 배타적 할당으로 각각 다른 품목에 연결된다.
신뢰도: 5가지 시그널
Gemini는 대부분의 필드에 0.95 이상의 confidence를 준다. 이 값을 그대로 쓰면 실제로는 틀린 필드도 자동 승인된다. 그래서 5가지 코드 시그널로 보정한다:
- OCR 매칭률 — bbox 스냅 시 텍스트 유사도가 낮으면 감점
- Bbox 유무 — 좌표를 찾지 못한 필드는 신뢰도가 낮다
- 정규화 성공 — 날짜/금액 변환이 실패하면 원본이 이상한 것
- 특수문자 비율 — 비정상적으로 높으면 OCR 오류 가능성
- 값 길이 — 1자 이하 값은 의미가 불분명
이 보정은 Gemini 호출 없이 코드만으로 동작한다. 추가 비용 제로.
실제로 겪은 좌표 함정들
좌표 파이프라인에서 가장 어려운 건 디버깅이다. 몇 가지 실제 사례:
PaddleOCR의 UVDoc 문제. v5에서 문서 곡면 보정(UVDoc)을 켜면 내부적으로 이미지를 변형한다. 감지 좌표가 변형된 이미지 기준이 되어 원본과 ~20px 불일치가 발생한다. 디버깅으로 "픽셀 94%가 변경되었다"는 걸 발견하고 use_doc_unwarping=False로 해결했다.
Gemini box_2d 요청의 함정. 좌표를 함께 요청하면 품목 누락이 발생한다. 코스트코 영수증에서 17개 중 3개가 사라졌다. 원인은 출력 토큰 소진.
다운스케일 해상도 문제. bbox 스냅핑용 이미지를 1024px로 줄이면 밀집 한글 영수증에서 인식이 깨진다. 1536px로 올려서 해결.
PDF의 두 얼굴
하나의 PDF에 텍스트 페이지와 스캔 이미지 페이지가 섞일 수 있다. 페이지 단위로 분류하여 텍스트 페이지는 PyMuPDF로 직접 추출(OCR 불필요), 스캔 페이지만 이미지 변환 후 OCR을 돌린다. 17페이지 혼합 PDF에서 4페이지에만 OCR을 실행하여 처리 시간과 메모리를 절약한다.
이 모든 내용을 인터랙티브 HTML 문서로 정리했다.