오늘은 최근 NCSOFT에서 공개한 VARCO-VISION-2.0-1.7B-OCR 에 대해서 간략한 소개 및 실습을 해볼 것이다.
VARCO-VISION-2.0-1.7B-OCR은 이전에 출시한 VARCO-VISION-2.0-1.7B 모델에서 파생된 OCR(광학 문자 인식)에 특화되어 있는 Vision-Language Model(VLM)이다.
NCSOFT/VARCO-VISION-2.0-1.7B-OCR · Hugging Face
VARCO-VISION-2.0-1.7B-OCR Introduction VARCO-VISION-2.0-1.7B-OCR is a lightweight yet powerful OCR-specialized model derived from VARCO-VISION-2.0-1.7B, designed to deliver efficient and accurate text recognition in real-world scenarios. Unlike conventiona
huggingface.co
이 모델은 한국어와 영어를 지원하며, 텍스트와 텍스트의 위치를 바운딩 박스와 함께 제공한다.
NCSOFT/VARCO-VISION-2.0-1.7B-OCR 모델은 CC BY-NC-SA 4.0 라이선스를 따르기 때문에
이 모델을 사용하는 경우, 비상업적인 목적으로만 사용해야 하며 동일한 라이선스 조건 하에 2차 저작물 배포가 가능하다.
우선 허깅페이스에 공개된 벤치마크점수는 아래와 같이 우수한 성적을 보였다.
| Benchmark | CLOVA OCR | EasyOCR | VARCO-VISION-2.0-1.7B-OCR |
|---|---|---|---|
| CORD | 93.9 | 77.8 | 95.6 |
| ICDAR2013 | 94.4 | 85.0 | 95.5 |
| ICDAR2015 | 84.1 | 57.9 | 75.4 |
또한 공개된 예시에서 한글과 영어 모두에서 우수한 것을 볼 수 있었다.

이제 이 모델을 이용하여 간단한 실습을 진행해 보자.
나는 PDF파일에 OCR을 적용해 보기 위해서 다음과 같은 과정을 거쳤다.
- PDF 로드
- PDF를 페이지별로 분할하여, 각 페이지를 이미지화
- 각 이미지를 배치형태로 모델에 입력
- 모델 response를 후처리함수를 통해 텍스트와 바운딩 박스 분리
- 바운딩 박스 좌표를 해당 페이지 이미지에 오버레이
- 오버레이 된 이미지들 병합하여 pdf로 생성
PDF를 이미지로 변환하기 위해서는 pdf2image 라이브러리를 사용했고, 모델은 vLLM을 이용하여 서빙했다.
매우 단순한 작업이었는데, 의외의 난관이었던 것은 모델의 response가 내 예상과 달랐다는 것이다.
의도한 것인지는 잘 모르겠지만,
나는 당연히 모델의 출력이 {텍스트} {바운딩박스좌표} 이런 식으로 텍스트와 좌표가 분리되어서 나올 줄 알았는데,{텍스트}{바운딩박스좌표} 이런식으로 공백이 없이 붙여서 나왔다.
텍스트 0.1,0.2,0.3,0.4 # 예상형식
텍스트0.1,0.2,0.3,0.4 # 실제형식

모든 좌표는 정규화된 좌표값으로 1보다 작기 때문에 후처리함수를 이용하여 0.이라는 문자를 기준으로 문자열을 끊어서 텍스트와 좌표를 분리하였다. (아래 코드에서 parse_md 함수참고)
최종적으로 아래와 같이 ocr이 매우 잘 적용된 것을 볼 수 있었다.


import os
import re
import base64
import requests
import shutil
import argparse
from pdf2image import convert_from_path
from PIL import Image, ImageDraw, ImageFont
from loguru import logger
import concurrent.futures
# --- 유틸 함수 ---
def encode_image_to_base64(image_path: str) -> str:
with open(image_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode("utf-8")
def pdf_to_images(pdf_path):
return convert_from_path(pdf_path, dpi=300)
def save_images_as_pdf(image_list, output_pdf_path):
if not image_list:
return
image_list[0].save(output_pdf_path, save_all=True, append_images=image_list[1:])
# --- 바운딩박스 파싱 및 그리기 ---
def parse_md(md_path):
bboxes = []
with open(md_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line:
continue
idx = line.find('0.')
if idx == -1:
continue
text = line[:idx]
coords_str = line[idx:]
coords = [c.strip() for c in coords_str.split(',')]
if len(coords) < 4:
continue
try:
coords_float = [float(c) for c in coords[:4]]
bboxes.append((text, *coords_float))
except Exception:
continue
return bboxes
def draw_bboxes(img, bboxes):
draw = ImageDraw.Draw(img)
try:
font = ImageFont.truetype("NanumGothic.ttf", 24)
except:
font = ImageFont.load_default()
w, h = img.size
for (text, x1, y1, x2, y2) in bboxes:
x0 = int(x1 * w)
y0 = int(y1 * h)
x1p = int(x2 * w)
y1p = int(y2 * h)
box = [x0, y0, x1p, y1p]
try:
draw.rectangle(box, outline='yellow', width=4)
draw.text((x0, y0-30), text, fill='blue', font=font)
except Exception:
continue
return img
# --- VLM 단일 페이지 처리 ---
def vlm_ocr_single(image_path: str, model: str, base_url: str, page_num: int) -> str:
"""단일 이미지에 대해 VLM OCR API 호출 및 결과 반환."""
try:
base64_image = encode_image_to_base64(image_path)
url = f"{base_url}/v1/chat/completions"
headers = {"Content-Type": "application/json"}
data = {
"model": model,
"messages": [
{
"role": "user",
"content": [
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{base64_image}"
}
},
{
"type": "text",
"text": "<ocr>"
}
]
}
],
"temperature": 0.0
}
response = requests.post(url, headers=headers, json=data)
response.raise_for_status()
result = response.json()
content = result["choices"][0]["message"]["content"]
return content if content else f"*페이지 {page_num}에서 텍스트를 추출할 수 없습니다.*"
except Exception as e:
print(f"[VLM ERROR page {page_num}] {e}")
return f"*페이지 {page_num} - 처리 오류: {str(e)}*"
# --- 병렬 처리 함수 ---
def process_page(args):
idx, img_path, model, base_url = args
md_result = vlm_ocr_single(img_path, model, base_url, idx)
return idx, md_result
def main(pdf_path, output_dir, vlm_model, base_url):
os.makedirs(output_dir, exist_ok=True)
pdf_copy_path = os.path.join(output_dir, os.path.basename(pdf_path))
shutil.copy2(pdf_path, pdf_copy_path)
bbox_dir = os.path.join(output_dir, "bbox")
page_dir = os.path.join(output_dir, "page")
page_bbox_dir = os.path.join(output_dir, "page_bbox")
os.makedirs(bbox_dir, exist_ok=True)
os.makedirs(page_dir, exist_ok=True)
os.makedirs(page_bbox_dir, exist_ok=True)
images = pdf_to_images(pdf_path)
bbox_images = []
pdf_base = os.path.splitext(os.path.basename(pdf_path))[0]
page_env = os.environ.get("OCR_ONLY_PAGE_LIST")
if hasattr(main, "page_list"):
page_list = main.page_list
elif page_env:
page_list = [int(x) for x in page_env.split(",") if x.strip().isdigit()]
else:
page_list = list(range(1, len(images)+1))
# 이미지 저장 및 경로 수집
img_paths = []
for idx, img in enumerate(images, 1):
if idx not in page_list:
continue
temp_img_path = os.path.join(page_dir, f"page_{idx}.jpg")
img.save(temp_img_path)
img_paths.append((idx, temp_img_path, vlm_model, base_url))
# 병렬 처리로 OCR 수행
results = []
with concurrent.futures.ThreadPoolExecutor() as executor:
for idx, md_result in executor.map(process_page, img_paths):
md_path = os.path.join(bbox_dir, f"bbox_{idx}.md")
with open(md_path, "w", encoding="utf-8") as f:
f.write(md_result)
bboxes = parse_md(md_path)
img = Image.open(os.path.join(page_dir, f"page_{idx}.jpg"))
img_with_bbox = draw_bboxes(img.copy(), bboxes)
bbox_img_path = os.path.join(page_bbox_dir, f"{idx}_bbox.png")
img_with_bbox.save(bbox_img_path)
bbox_images.append(img_with_bbox.convert("RGB"))
results.append((idx, md_result))
pdf_out_path = os.path.join(output_dir, f"{pdf_base}_ocr.pdf")
save_images_as_pdf(bbox_images, pdf_out_path)
print(f"모든 페이지 바운딩박스 PDF 저장 완료: {pdf_out_path}")
if __name__ == "__main__":
base_url = "http://localhost:8000"
model = 'NCSOFT/VARCO-VISION-2.0-1.7B-OCR'
parser = argparse.ArgumentParser(description="PDF OCR + BBox + PDF merge (병렬)")
parser.add_argument("--data", required=True, help="입력 PDF 경로")
parser.add_argument("--dir", required=True, help="출력 폴더 경로")
parser.add_argument("--page", required=False, help="처리할 페이지 번호(1부터 시작, 쉼표로 구분)")
args = parser.parse_args()
if args.page:
page_list = [int(x) for x in args.page.split(",") if x.strip().isdigit()]
main.page_list = page_list
main(args.data, args.dir, model, base_url)'알쓸신잡' 카테고리의 다른 글
| macOS sshfs로 다른 시스템 마운트하기 (0) | 2025.09.14 |
|---|---|
| Langfuse 소개 및 설치 (0) | 2025.07.10 |
| TypeError: Can't instantiate abstract class OpenAI with abstract method _prepare_chat_with_tools (0) | 2025.05.20 |
| Use Ollama with any GGUF Model on Hugging Face Hub (0) | 2025.04.16 |
| Dev tunnels command-line reference (0) | 2025.04.11 |