Tesseract.js란

Google의 Tesseract OCR 엔진을 WASM으로 포팅한 것. 브라우저에서 서버 없이 OCR이 가능하다.

정확도가 Google Vision API만큼은 아니지만, 데이터시트 같은 깔끔한 문서에서는 충분히 쓸만하다.

설치

npm install tesseract.js

첫 실행 시 언어 데이터를 다운로드한다. 영어 기준 약 10MB.

기본 사용법

import Tesseract from 'tesseract.js'

const result = await Tesseract.recognize(
  imageSource,  // 이미지 URL, File, Blob, Canvas 등
  'eng',        // 언어
  {
    logger: m => console.log(m)  // 진행 상황
  }
)

console.log(result.data.text)

서비스로 감싸기

매번 워커를 생성하면 느리다. 워커를 재사용하는 서비스를 만들자:

// services/ocrService.js
import { createWorker } from 'tesseract.js'

let worker = null

export async function initOCR() {
  if (worker) return worker
  
  worker = await createWorker('eng', 1, {
    logger: m => {
      if (m.status === 'recognizing text') {
        console.log(`OCR 진행: ${Math.round(m.progress * 100)}%`)
      }
    }
  })
  
  return worker
}

export async function recognizeText(imageSource) {
  const w = await initOCR()
  const result = await w.recognize(imageSource)
  return result.data.text
}

export async function terminateOCR() {
  if (worker) {
    await worker.terminate()
    worker = null
  }
}

Figure/Table 인식 로직

OCR로 텍스트를 뽑은 후, 정규식으로 패턴을 찾는다:

export function parseRegionType(ocrText) {
  const text = ocrText.toLowerCase().trim()
  
  // Figure 패턴
  const figureMatch = text.match(/fig(?:ure)?\.?\s*(\d+)/i)
  if (figureMatch) {
    return {
      type: 'Figure',
      number: figureMatch[1],
      confidence: 'high'
    }
  }
  
  // Table 패턴
  const tableMatch = text.match(/table\.?\s*(\d+)/i)
  if (tableMatch) {
    return {
      type: 'Table',
      number: tableMatch[1],
      confidence: 'high'
    }
  }
  
  // 애매한 경우
  if (text.includes('fig')) {
    return { type: 'Figure', number: null, confidence: 'low' }
  }
  if (text.includes('table')) {
    return { type: 'Table', number: null, confidence: 'low' }
  }
  
  return { type: 'Unknown', number: null, confidence: 'none' }
}

캡션 추출

Figure 번호 뒤에 오는 텍스트가 보통 캡션이다:

export function extractCaption(ocrText) {
  // "Figure 1. Block Diagram" 에서 "Block Diagram" 추출
  const captionMatch = ocrText.match(/(?:fig(?:ure)?|table)\.?\s*\d+[.:]\s*(.+)/i)
  
  if (captionMatch) {
    return captionMatch[1].trim()
  }
  
  return null
}

전체 인식 플로우

export async function analyzeRegion(imageBlob) {
  // OCR 실행
  const text = await recognizeText(imageBlob)
  
  // 타입 파싱
  const typeInfo = parseRegionType(text)
  
  // 캡션 추출
  const caption = extractCaption(text)
  
  // 파일명 생성
  const filename = generateFilename(typeInfo)
  
  return {
    ocrText: text,
    type: typeInfo.type,
    number: typeInfo.number,
    caption: caption,
    filename: filename,
    confidence: typeInfo.confidence
  }
}

function generateFilename(typeInfo) {
  if (typeInfo.type === 'Unknown') {
    return `image_${Date.now()}`
  }
  
  const num = typeInfo.number || 'x'
  return `${typeInfo.type.toLowerCase()}_${num}`
}

Vue 컴포넌트에서 사용

<script setup>
import { ref } from 'vue'
import { analyzeRegion } from '@/services/ocrService'

const regions = ref([])
const isAnalyzing = ref(false)

async function handleCapture(captureData) {
  isAnalyzing.value = true
  
  try {
    // OCR 분석
    const analysis = await analyzeRegion(captureData.blob)
    
    // 영역 데이터 저장
    regions.value.push({
      id: Date.now(),
      imageUrl: captureData.imageUrl,
      blob: captureData.blob,
      rect: captureData.rect,
      page: captureData.page,
      ...analysis
    })
  } finally {
    isAnalyzing.value = false
  }
}
</script>

<template>
  <div v-if="isAnalyzing" class="analyzing">
    분석 중...
  </div>
</template>

OCR 성능 최적화

1. 이미지 전처리

OCR 전에 이미지를 흑백으로 변환하면 정확도가 올라간다:

function preprocessImage(canvas) {
  const ctx = canvas.getContext('2d')
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
  const data = imageData.data
  
  // 그레이스케일 변환
  for (let i = 0; i < data.length; i += 4) {
    const gray = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114
    data[i] = data[i + 1] = data[i + 2] = gray
  }
  
  ctx.putImageData(imageData, 0, 0)
  return canvas
}

2. 영역 제한

전체 이미지 대신 텍스트가 있을 것 같은 영역만 OCR:

// 상단 20%만 OCR (Figure/Table 레이블은 보통 위에 있음)
const partialHeight = Math.min(100, canvas.height * 0.2)
const partialCanvas = document.createElement('canvas')
partialCanvas.width = canvas.width
partialCanvas.height = partialHeight

const ctx = partialCanvas.getContext('2d')
ctx.drawImage(canvas, 0, 0, canvas.width, partialHeight, 0, 0, canvas.width, partialHeight)

const text = await recognizeText(partialCanvas)

3. 워커 풀

여러 영역을 동시에 분석하려면 워커 풀을 사용:

import { createScheduler, createWorker } from 'tesseract.js'

const scheduler = createScheduler()

// 워커 2개 생성
for (let i = 0; i < 2; i++) {
  const worker = await createWorker('eng')
  scheduler.addWorker(worker)
}

// 병렬 처리
const results = await Promise.all(
  images.map(img => scheduler.addJob('recognize', img))
)

한글 인식

한글 데이터시트도 있다면:

const worker = await createWorker('eng+kor')

다만 언어 데이터가 추가로 필요하고, 인식 속도가 느려진다.

다음 글에서

분석한 영역들을 IndexedDB에 저장하고, 최종적으로 Markdown + 이미지를 ZIP으로 내보내는 기능을 구현한다.