VARCO-VISION-2.0-1.7B-OCR 소개 및 실습

2025. 7. 29. 15:33·알쓸신잡

오늘은 최근 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

 

또한 공개된 예시에서 한글과 영어 모두에서 우수한 것을 볼 수 있었다.

https://huggingface.co/NCSOFT/VARCO-VISION-2.0-1.7B-OCR

 

이제 이 모델을 이용하여 간단한 실습을 진행해 보자.

나는 PDF파일에 OCR을 적용해 보기 위해서 다음과 같은 과정을 거쳤다.

  1. PDF 로드
  2. PDF를 페이지별로 분할하여, 각 페이지를 이미지화
  3. 각 이미지를 배치형태로 모델에 입력
  4. 모델 response를 후처리함수를 통해 텍스트와 바운딩 박스 분리
  5. 바운딩 박스 좌표를 해당 페이지 이미지에 오버레이
  6. 오버레이 된 이미지들 병합하여 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)
728x90

'알쓸신잡' 카테고리의 다른 글

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
'알쓸신잡' 카테고리의 다른 글
  • macOS sshfs로 다른 시스템 마운트하기
  • Langfuse 소개 및 설치
  • TypeError: Can't instantiate abstract class OpenAI with abstract method _prepare_chat_with_tools
  • Use Ollama with any GGUF Model on Hugging Face Hub
창빵맨
창빵맨
  • 창빵맨
    Let's be Developers
    창빵맨
    로그인/로그아웃
  • 전체
    오늘
    어제
    • 분류 전체보기 (471)
      • 알쓸신잡 (79)
      • ML & DL (85)
        • Computer v.. (22)
        • NLP (22)
        • 파이썬 머신러닝 완.. (3)
        • 개념정리 (38)
      • 리눅스 (21)
      • 프로젝트 (29)
        • 산불 발생 예측 (6)
        • 음성비서 (12)
        • pdf 병합 프로그.. (0)
        • 수위 예측 (5)
        • 가짜 뉴스 분류 (5)
        • 전력사용량 예측 (1)
      • 코딩테스트 (217)
        • 프로그래머스[Pyt.. (17)
        • 프로그래머스[Fai.. (3)
        • 백준[Python] (160)
        • 이것이취업을위한코딩.. (18)
        • 파이썬 알고리즘 (19)
      • 데이터분석실습 (25)
        • 데이터 과학 기반의.. (18)
        • 헬로 데이터 과학 (7)
      • 메모장 (0)
      • 잡담 (4)
  • Blog

    • 🏠 Home
    • ✏️글쓰기
    • 💻 관리자

    Personal

    GITHUB
    Instagram
  • 공지사항

  • 인기 글

  • 태그

    이코테
    파이썬
    dp
    그리디
    백준
    이분탐색
    이것이취업을위한코딩테스트다
    DFS
    나동빈
    BFS
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3

HOME

HOME

  • Profile
  • Light
상단으로

티스토리툴바