OCR과 레이아웃 인식 - 스캔 문서를 RAG에 활용하기
지난 글에서 엑셀 같은 구조화된 데이터를 RAG에 통합하는 방법을 다뤘습니다. 이번에는 반대로 가장 다루기 까다로운 원본 유형인 스캔된 PDF, 사진으로 찍은 문서를 처리하는 방법을 정리합니다. 지금까지 다룬 파이프라인은 텍스트가 이미 컴퓨터가 읽을 수 있는 형태(디지털 텍스트)라는 것을 전제로 했는데, 스캔 문서는 이 전제부터 깨져 있습니다.
1. 스캔 문서는 왜 기존 방식으로 처리가 안 되는가
디지털로 작성된 PDF는 텍스트 레이어가 그대로 존재해서 바로 추출할 수 있지만, 스캔 문서나 사진은 그저 하나의 큰 이미지일 뿐입니다.
[디지털 PDF]
파일 내부에 "재택근무는 주 2회까지 가능합니다"라는 문자 정보가 그대로 저장됨
→ fitz, pdfplumber 등으로 바로 추출 가능
[스캔 PDF / 사진]
파일 내부에는 픽셀 정보만 있음, 글자라는 개념 자체가 없음
→ 사람 눈에는 글자로 보이지만 컴퓨터에게는 그림일 뿐
→ 별도의 문자 인식(OCR) 과정 없이는 텍스트를 뽑아낼 수 없음
사내 문서 중에는 옛날에 종이로 작성돼서 스캔만 해둔 계약서, 팩스로 받은 문서, 사진으로 찍어 공유한 화이트보드 회의록처럼 텍스트 레이어가 없는 경우가 생각보다 많습니다.
2. OCR(광학 문자 인식)이란
OCR은 이미지 안의 문자 영역을 인식해서 실제 텍스트 문자열로 변환하는 기술입니다. 지난 임베딩 글에서 다룬 "텍스트를 벡터로 바꾸는" 단계 이전에, "이미지를 텍스트로 바꾸는" 전처리 단계가 하나 더 추가되는 셈입니다.
스캔 이미지 → [OCR] → 텍스트 → [기존 파이프라인] → 청킹 → 임베딩 → 벡터 저장
3. Tesseract로 기본 OCR 적용하기
가장 널리 쓰이는 오픈소스 OCR 엔진인 Tesseract로 간단히 시작해볼 수 있습니다.
pip install pytesseract pillow
import pytesseract
from PIL import Image
def ocr_extract(image_path):
image = Image.open(image_path)
text = pytesseract.image_to_string(image, lang="kor+eng")
return text
[입력] 스캔된 계약서 이미지
[출력]
"본 계약서는 갑과 을 사이에 체결된 근무 조건에 관한 것으로,
재택근무는 주 2회까지 팀장 승인 하에..."
lang="kor+eng"처럼 여러 언어를 함께 지정하면 한글과 영문이 섞인 문서도 인식할 수 있습니다.
4. 스캔 품질에 따라 정확도가 크게 갈린다
OCR은 원본 스캔 품질에 매우 민감합니다. 기울어진 스캔, 저해상도, 손글씨가 섞인 문서는 인식률이 크게 떨어집니다.
| 문서 상태 | OCR 인식률 경향 |
|---|---|
| 고해상도 정자세 스캔, 인쇄체 | 높음 (90% 이상) |
| 저해상도, 약간 기울어짐 | 중간, 오탈자 다수 발생 |
| 손글씨, 팩스로 받은 문서 | 낮음, 별도 전처리 필수 |
import cv2
def preprocess_image(image_path):
img = cv2.imread(image_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
denoised = cv2.fastNlMeansDenoising(gray)
_, thresh = cv2.threshold(denoised, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
return thresh
흑백 변환, 노이즈 제거, 이진화 같은 전처리를 OCR 이전에 거치면 인식률이 눈에 띄게 개선되는 경우가 많습니다. 특히 스캔 품질이 낮은 오래된 문서일수록 이 전처리 단계의 효과가 큽니다.
5. Vision LLM으로 OCR 대체하기
Tesseract 같은 전통적 OCR 엔진 대신, 지난 멀티모달 RAG 글에서 다룬 Vision LLM에게 직접 텍스트 추출을 맡기는 방법도 있습니다.
def vision_ocr(image_base64):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "이 이미지에 있는 모든 텍스트를 정확하게 그대로 추출해줘. "
"표라면 행과 열 구조를 유지해서 서술해줘."},
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{image_base64}"}}
]
}]
)
return response.choices[0].message.content
| 구분 | Tesseract(전통 OCR) | Vision LLM |
|---|---|---|
| 비용 | 무료, 로컬 실행 | API 호출 비용 발생 |
| 처리 속도 | 빠름 | 상대적으로 느림 |
| 표·레이아웃 이해 | 단순 텍스트 나열, 구조 손실 쉬움 | 행렬 관계까지 문맥으로 이해 |
| 손글씨·저품질 스캔 | 인식률 낮음 | 상대적으로 견고함 |
문서량이 많고 정형화된 형태라면 Tesseract로 비용을 아끼고, 소량이지만 레이아웃이 복잡하거나 품질이 낮은 문서는 Vision LLM으로 처리하는 방식으로 섞어서 쓰는 것도 실무에서 흔한 선택입니다.
6. 레이아웃 인식 - 문서 구조를 함께 파악하기
단순히 텍스트만 뽑아내는 것을 넘어, 문서 안에서 어디가 제목이고 어디가 본문이고 어디가 표인지 레이아웃 구조까지 파악하면 지난 청킹 글에서 다룬 "구조 기반 분할"을 스캔 문서에도 적용할 수 있습니다.
from unstructured.partition.pdf import partition_pdf
elements = partition_pdf(
filename="scanned_contract.pdf",
strategy="hi_res",
infer_table_structure=True
)
for element in elements:
print(f"[{element.category}] {element.text[:50]}")
[출력 예시]
[Title] 근무 조건 계약서
[NarrativeText] 본 계약서는 갑과 을 사이에 체결된...
[Table] 부서 | 담당자 | 예산(만원) ...
unstructured 같은 라이브러리는 내부적으로 레이아웃 인식 모델을 활용해서, 제목·본문·표·리스트를 카테고리별로 구분해줍니다. 이렇게 구분된 결과를 바탕으로 지난 8편에서 다룬 제목 단위 청킹을 스캔 문서에도 그대로 적용할 수 있습니다.
7. OCR 결과 검증 - 신뢰도 점수 활용하기
OCR은 오탈자나 오인식이 섞일 수 있기 때문에, 신뢰도가 낮은 인식 결과는 그대로 임베딩에 태우지 않고 별도로 표시해두는 것이 안전합니다.
def ocr_with_confidence(image_path):
data = pytesseract.image_to_data(Image.open(image_path), output_type=pytesseract.Output.DICT)
low_confidence_words = [
data["text"][i] for i in range(len(data["text"]))
if data["text"][i].strip() and int(data["conf"][i]) < 60
]
return {
"text": " ".join(data["text"]),
"low_confidence_words": low_confidence_words
}
신뢰도가 낮은 단어가 많은 페이지는 관리자에게 재검토 알림을 보내거나, 검색 결과에 "이 문서는 OCR 인식률이 낮아 오류가 있을 수 있습니다"라는 안내를 함께 붙이는 방식으로 리스크를 관리할 수 있습니다.
8. 정리
- 스캔 문서와 사진은 텍스트 레이어가 없어, 청킹 이전에 OCR로 문자를 인식하는 전처리 단계가 반드시 필요합니다.
- 스캔 품질에 따라 인식률 차이가 크기 때문에, 흑백화·노이즈 제거 같은 이미지 전처리가 정확도에 큰 영향을 줍니다.
- Vision LLM은 전통 OCR보다 레이아웃 이해에 강하고,
unstructured같은 라이브러리로 제목·본문·표 구조까지 함께 인식하면 기존 청킹 전략을 스캔 문서에도 그대로 적용할 수 있습니다.
이번 글로 심화 검색 기법 파트(GraphRAG, 멀티모달 RAG, Query Routing, 구조화 데이터, OCR)를 마무리했습니다. 다음 글부터는 운영·거버넌스 파트로 넘어가서, RAG 시스템에서 어떤 문서가 어떤 답변에 실제로 쓰였는지 추적하는 감사 로그를 다뤄보겠습니다.
'RAG를 활용한 LLM 서비스 구축' 카테고리의 다른 글
| STEP 26 - 버전 관리와 카나리 배포 (0) | 2026.07.23 |
|---|---|
| STEP 25 - RAG 감사 로그 (1) | 2026.07.23 |
| STEP 23 - 구조화된 데이터(엑셀·테이블) RAG에 통합하기 (0) | 2026.07.21 |
| STEP 22 - Query Routing (0) | 2026.07.20 |
| STEP 21 - 멀티모달 RAG (0) | 2026.07.20 |