AI 튜토리얼

Baidu Unlimited-OCR로 만드는 OCR 파이프라인: 고해상도 이미지와 여러 페이지 PDF 한 번에 읽는 법

Baidu의 30억 파라미터 비전-언어 모델 Unlimited-OCR로 보고서, 스캔 양식, 표, 긴 PDF까지 한 번에 텍스트로 바꾸는 엔드투엔드 파이프라인을 Google Colab에서 단계별로 만들어 봅니다. 코드를 복사해 그대로 실행할 수 있도록 정리했습니다.

📖 난이도: 중급 ⏱ 소요 시간: 약 30분 🛠 Python 3 · GPU 🔄 최종 업데이트: 최근

Unlimited-OCR이 일반 OCR과 뭐가 다른가요?

기존 OCR은 보통 "글자 인식 → 레이아웃 분석 → 표 추출" 같은 단계를 거치며, 단계마다 별도 모델을 붙여야 했습니다. 문서가 길어질수록 단계 사이 오류가 누적되고 처리 속도도 느려집니다.

Unlimited-OCR은 Baidu가 2026년에 공개한 30억 파라미터 비전-언어 모델로, 이미지와 텍스트를 동시에 이해하는 멀티모달 AI입니다. 한 번의 디코딩으로 페이지 전체의 글자, 문단, 표, 페이지 간 참조까지 구조화된 텍스트로 뽑아내기 때문에 별도의 레이아웃 분석기를 둘 필요가 없습니다.

OCR (광학 문자 인식) — 이미지 속 글자를 컴퓨터가 검색·편집 가능한 텍스트로 바꾸는 기술. 쉽게 말해 "그림 속 글자를 워드 파일처럼" 만들어 주는 기술입니다.
비전-언어 모델 (Vision-Language Model) — 이미지와 자연어를 함께 이해하는 AI. 그림을 보면서 "이건 표", "이건 제목" 같은 의미까지 파악합니다.
핵심 포인트: Unlimited-OCR은 "긴 문서를 한 번에" 읽도록 설계된 모델이라, 여러 페이지 PDF와 조밀한 표·각주가 있는 보고서를 처리할 때 특히 강점이 큽니다.

사전 준비: GPU 환경과 라이브러리

이 튜토리얼은 Google Colab 기준이지만, 로컬 GPU(VRAM 12GB 이상)에서도 동일하게 동작합니다. 필요한 라이브러리는 아래 9개입니다.

  • transformers 4.57.1 — Hugging Face 모델 로더
  • PyMuPDF — PDF를 이미지로 변환 (코드에서는 fitz로 import)
  • Pillow, matplotlib — 이미지 생성과 미리보기
  • accelerate — GPU 연산 가속
  • einops, addict, easydict, psutil — 모델 내부 의존성

모델 파일은 약 6GB(BF16 30억 파라미터)라서 첫 실행 시 다운로드가 조금 걸리지만, 한 번 받아 두면 이후에는 캐시에서 즉시 로드됩니다.

: Colab에서는 런타임 → 런타임 유형 변경 → GPU로 먼저 바꿔 주세요. 모델이 GPU 없이는 사실상 동작하지 않습니다.
bfloat16 (BF16) — 머신러닝 전용 16비트 숫자 형식. 메모리를 절반으로 줄이되 정밀도는 거의 유지해 GPU 추론을 빠르게 해 줍니다.

STEP 1. 모델 불러오기

먼저 필요한 패키지를 설치하고, GPU가 감지되면 자동으로 bfloat16 또는 float16 중 하나를 선택합니다. 그다음 Hugging Face에서 baidu/Unlimited-OCR 모델을 받아 GPU로 올립니다.

import subprocess, sys

def pip_install(*pkgs):
    subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", *pkgs])

print(">> Installing dependencies (1-2 min)...")
pip_install(
    "transformers==4.57.1",
    "Pillow", "matplotlib", "einops",
    "addict", "easydict", "pymupdf", "psutil", "accelerate",
)
print(">> Done.")

import os, torch
from transformers import AutoModel, AutoTokenizer

assert torch.cuda.is_available(), "No GPU detected! In Colab: Runtime -> Change runtime type -> GPU."
gpu_name = torch.cuda.get_device_name(0)
print(f">> GPU: {gpu_name}")
use_bf16 = torch.cuda.is_bf16_supported()
DTYPE = torch.bfloat16 if use_bf16 else torch.float16
print(f">> Using dtype: {DTYPE}")

MODEL_NAME = "baidu/Unlimited-OCR"
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True)
model = AutoModel.from_pretrained(
    MODEL_NAME, trust_remote_code=True,
    use_safetensors=True, torch_dtype=DTYPE,
).eval().cuda()
print(">> Model loaded and moved to GPU.")
float16 (FP16) — IEEE 754 표준 16비트 숫자 형식. BF16보다 표현 범위가 좁지만 호환성이 넓어 구형 GPU에서도 안전하게 동작합니다.

이 한 블록으로 (1) 의존성 설치 → (2) GPU 확인 → (3) 데이터 타입 자동 선택 → (4) 모델 로드까지 한 번에 끝납니다.

STEP 2. 샘플 문서 만들기

테스트용 이미지를 미리 준비하지 않아도, Pillow로 제목·본문·표·각주가 들어간 샘플 페이지를 자동 생성할 수 있습니다. 이렇게 만든 3장을 묶어 PDF로도 변환합니다.

from PIL import Image, ImageDraw, ImageFont
import textwrap, os, matplotlib.pyplot as plt

os.makedirs("inputs", exist_ok=True)
os.makedirs("outputs/single_gundam", exist_ok=True)
os.makedirs("outputs/single_base", exist_ok=True)
os.makedirs("outputs/multi_page", exist_ok=True)

def load_font(size):
    for path in ["/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
                 "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"]:
        if os.path.exists(path):
            return ImageFont.truetype(path, size)
    return ImageFont.load_default()

def make_sample_page(path, page_no):
    W, H = 1240, 1754
    img = Image.new("RGB", (W, H), "white"); d = ImageDraw.Draw(img)
    title_f, head_f, body_f = load_font(48), load_font(34), load_font(26)
    d.text((80, 70), f"Quarterly Operations Report - Page {page_no}", fill="black", font=title_f)
    d.line([(80, 145), (W-80, 145)], fill="black", width=3)
    body = ("This document demonstrates Unlimited-OCR's one-shot long-horizon "
            "parsing. The model reads an entire page - headings, paragraphs, "
            "and tables - and emits structured text in a single decoding pass.")
    y = 190
    for line in textwrap.wrap(body, width=72):
        d.text((80, y), line, fill="black", font=body_f); y += 40
    y += 30
    d.text((80, y), f"Table {page_no}: Regional Revenue (USD, millions)", fill="black", font=head_f)
    y += 60
    rows = [["Region","Q1","Q2","Q3"],["North","12.4","13.1","15.0"],
            ["South","9.8","10.2","11.7"],["East","14.3","13.9","16.2"],
            ["West","11.1","12.5","12.9"]]
    col_w, row_h, x0 = 260, 56, 80
    for r,row in enumerate(rows):
        for c,cell in enumerate(row):
            x = x0 + c*col_w
            d.rectangle([x,y,x+col_w,y+row_h], outline="black", width=2)
            d.text((x+14,y+12), cell, fill="black", font=body_f)
        y += row_h
    img.save(path); return path

IMAGE_PATH = make_sample_page("inputs/sample_page_1.png", 1)
PAGE_2     = make_sample_page("inputs/sample_page_2.png", 2)
PAGE_3     = make_sample_page("inputs/sample_page_3.png", 3)
print(f">> Sample pages written: {IMAGE_PATH}, {PAGE_2}, {PAGE_3}")

plt.figure(figsize=(6,8)); plt.imshow(Image.open(IMAGE_PATH))
plt.axis("off"); plt.title("Input document (page 1)"); plt.show()
: 실제 업무 문서를 쓰려면 이 블록을 건너뛰고, Colab 왼쪽 파일 패널에서 PDF를 업로드한 뒤 경로만 바꿔 주면 됩니다.

STEP 3. Gundam 모드로 단일 페이지 OCR

문서 안에 작은 글자나 빽빽한 표가 있다면 Gundam 모드가 정답입니다. 이미지를 잘게 타일로 잘라 부분적으로도 보기 때문에 작은 글자도 놓치지 않습니다.

Gundam 모드 — 페이지 전체를 보는 전역 이미지와 잘게 자른 크롭 타일을 함께 사용하는 추론 모드. 처리 시간은 늘어나지만, 조밀한 레이아웃과 작은 글자를 더 잘 읽습니다.
print("\n" + "="*76)
print("STEP 4: Single image - GUNDAM mode (tiled, high detail)")
print("="*76)

model.infer(
    tokenizer,
    prompt="<image>document parsing.",
    image_file=IMAGE_PATH,
    output_path="outputs/single_gundam",
    base_size=1024, image_size=640, crop_mode=True,
    max_length=32768,
    no_repeat_ngram_size=35, ngram_window=128,
    save_results=True,
)
N-gram (n-그램) — 연속된 n개 단어의 묶음. no_repeat_ngram_size는 똑같은 n-gram이 다시 나오는 걸 막아 줍니다. 긴 글자 출력에서 같은 문장이 계속 반복되는 "퇴화 현상"을 줄여 줘요.

STEP 4. Base 모드로 단일 페이지 OCR

깨끗하게 인쇄된 한 페이지라면 타일 자르기 없이 한 번에 보는 Base 모드가 더 빠릅니다. 결과 정확도와 속도 trade-off를 직접 비교해 보기 위해 같은 페이지를 한 번 더 돌립니다.

Base 모드 — 이미지를 자르지 않고 기본 해상도 한 장으로만 보는 추론 모드. 글자가 크고 깔끔한 PDF, 스캔본에 가장 효율적입니다.
print("\n" + "="*76)
print("STEP 5: Single image - BASE mode (single view, faster)")
print("="*76)

model.infer(
    tokenizer,
    prompt="<image>document parsing.",
    image_file=IMAGE_PATH,
    output_path="outputs/single_base",
    base_size=1024, image_size=1024, crop_mode=False,
    max_length=32768,
    no_repeat_ngram_size=35, ngram_window=128,
    save_results=True,
)

STEP 5. 여러 페이지 PDF 파싱

3장의 샘플 이미지를 PDF로 묶고, PyMuPDF로 각 페이지를 고해상도 PNG로 래스터화한 뒤 infer_multi()에 통째로 넣습니다. 모델이 페이지 사이 맥락까지 한 번에 이어서 읽습니다.

PyMuPDF (fitz) — PDF를 고속으로 읽고 이미지로 변환하는 Python 라이브러리. 코드에서는 import fitz로 사용합니다.
래스터화 (Rasterization) — PDF 페이지를 픽셀 이미지(PNG 등)로 변환하는 과정. 모델은 픽셀 이미지만 입력으로 받기 때문에 PDF를 그대로는 못 읽고, 반드시 이미지로 바꿔 줘야 합니다.
print("\n" + "="*76)
print("STEP 6: Multi-page / PDF parsing")
print("="*76)

import tempfile, fitz

def pdf_to_images(pdf_path, dpi=300):
    doc = fitz.open(pdf_path)
    tmp_dir = tempfile.mkdtemp(prefix="pdf_ocr_")
    mat = fitz.Matrix(dpi/72, dpi/72); paths = []
    for i, page in enumerate(doc):
        out = os.path.join(tmp_dir, f"page_{i+1:04d}.png")
        page.get_pixmap(matrix=mat).save(out); paths.append(out)
    doc.close(); return paths

SAMPLE_PDF = "inputs/sample_doc.pdf"
pdf = fitz.open()
for p in [IMAGE_PATH, PAGE_2, PAGE_3]:
    img_doc = fitz.open(p); rect = img_doc[0].rect
    pdf_bytes = img_doc.convert_to_pdf()
    img_pdf = fitz.open("pdf", pdf_bytes)
    page = pdf.new_page(width=rect.width, height=rect.height)
    page.show_pdf_page(rect, img_pdf, 0)
pdf.save(SAMPLE_PDF); pdf.close()

page_images = pdf_to_images(SAMPLE_PDF, dpi=300)
print(f">> Rasterized {len(page_images)} pages")

model.infer_multi(
    tokenizer,
    prompt="<image>Multi page parsing.",
    image_files=page_images,
    output_path="outputs/multi_page",
    image_size=1024,
    max_length=32768,
    no_repeat_ngram_size=35, ngram_window=1024,
    save_results=True,
)
주의: 여러 페이지 모드에서는 ngram_window를 1024로 키워서 페이지가 넘어가도 같은 문장이 반복되지 않게 해야 안정적인 결과가 나옵니다.

STEP 6. 결과 확인 & 치트시트

출력 폴더에는 .txt, .md, .mmd, .json 등 다양한 형식의 결과가 자동으로 저장됩니다. 마지막으로 어떤 모드를 골라야 하는지 한눈에 보는 요약표를 정리했습니다.

for out_dir in ["outputs/single_gundam", "outputs/single_base", "outputs/multi_page"]:
    for dirpath, _, files in os.walk(out_dir):
        for fn in sorted(files):
            fp = os.path.join(dirpath, fn)
            print(f" {fp} ({os.path.getsize(fp):,} bytes)")

📌 상황별 모드 치트시트

  • 단일 이미지, 글자가 작거나 빽빽할 때 → infer() + gundam (640 + crop_mode=True)
  • 단일 이미지, 깔끔한 인쇄본 → infer() + base (1024, crop_mode=False)
  • 여러 페이지 PDF → infer_multi() + image_size=1024, ngram_window=1024
  • 긴 문서 → max_length=32768no_repeat_ngram 설정을 그대로 유지
  • 내 파일로 테스트 → Colab 사이드바에서 업로드 후 image_file 경로만 변경

자주 묻는 질문(FAQ)

Q1. GPU가 없으면 못 쓰나요?

기본적으로는 GPU가 필요합니다. 다만 torch_dtypetorch.float32로 바꾸고 모델 크기를 양자화하면 16GB 정도의 VRAM이 있는 GPU에서 겨우 돌릴 수 있습니다. CPU만 있다면 처리 시간이 수십 배 길어져 실용적이지 않습니다.

Q2. Gundam 모드와 Base 모드 중 뭘 먼저 써야 하나요?

먼저 Base 모드로 한 번 돌려 보고, 결과에서 글자 누락이나 표 깨짐이 보이면 Gundam 모드로 다시 돌리는 게 가장 효율적입니다. Gundam 모드는 처리 시간이 평균 2~3배 더 걸립니다.

Q3. 한 번에 몇 페이지까지 한 번에 넣을 수 있나요?

공식 안내와 Colab 메모리 한도 기준, 16GB VRAM에서 3~5페이지가 안정적입니다. 더 긴 문서는 PDF를 5페이지 단위로 잘라 infer_multi()를 여러 번 호출하고, 마지막에 텍스트 파일을 이어 붙이는 방식을 추천합니다.

Q4. 결과 파일은 어떤 포맷으로 나오나요?

기본적으로 .txt(순수 텍스트), .md(마크다운), .mmd(멀티모달 마크다운), .json(구조화 데이터) 4종이 같은 폴더에 함께 생성됩니다. 표와 헤딩은 마크다운이나 JSON에서 더 잘 보존됩니다.

Q5. 한국어 문서도 잘 읽나요?

Unlimited-OCR은 영어 학습 비중이 가장 크지만, 한글·일본어·중국어도 학습 데이터에 포함되어 있어 표와 본문 정도는 안정적으로 인식합니다. 다만 띄어쓰기가 없는 구형 한글 인쇄본은 Base 모드보다 Gundam 모드가 더 유리합니다.

Q6. 손글씨나 영수증처럼 비정형 문서도 가능한가요?

손글씨는 정자체 기준으로는 가능하지만, 흘려 쓴 노트나 접히고 구겨진 영수증은 인식률이 떨어집니다. 이런 경우엔 모델을 호출하기 전에 대비·노이즈 제거를 먼저 적용하면 정확도가 크게 올라갑니다.

정리하면: Unlimited-OCR은 "별도의 레이아웃 분석기 없이 한 번에" 긴 문서를 읽는 데 초점을 둔 30억 파라미터 모델입니다. Gundam 모드는 정밀도, Base 모드는 속도, infer_multi()는 페이지 연결성에 강점이 있으니, 문서 성격에 맞춰 골라 쓰면 됩니다.