tesseract.js를 이용한 비디오 OCR 가이드

Browser 환경에서 Video OCR 수행에 대한 기록

tesseract.js를 이용한 비디오 OCR 가이드

개요

비디오내에 표기된 시간과 비디오의 현재 재생시간의 오차를 파악하여 특정 비디오 시간대를 추론할 수 있는 함수 구현과정에서 사용한 tesseract.js 라이브러리를 이용한 OCR과정에 대한 내용을 공유하고자 합니다.

💡
OCR (Optical Character Recognition)
스캔된 이미지 분석을 통해서 이미지에 쓰인 문자데이터를 기계가 읽을 수 있는 데이터로 변환하는 작업

진행과정

  • 캔버스를 통한 비디오 프레임 데이터 수집
  • tesseract.js API를 이용한 문자 영역 추출 및 이미지 전처리
  • 수집된 OCR 데이터를 기반으로 특정 화면시간대에 비디오 시간 추론하는 알고리즘 작성
0:00
/10:15

10분짜리 타이머가 지속되는 샘플비디오를 통해서 OCR을 통한 시간추론 기능을 작성합니다.

캔버스를 통한 비디오 프레임 데이터 수집

먼저 비디오의 전체 시간에서 특정구간 프레임을 수집하기위한 함수를 정의합니다.

const $video = document.querySelector("#video");
const $canvas = document.querySelector("#canvas");

// 비디오 시간 변경 후 발생하는 이벤트까지 대기하는 함수
const waitChangeTime = () =>
  new Promise((res, rej) =>
    $video.addEventListener("seeked", res, { once: true })
  );

// 비디오를 프레임구간을 순회하면서 비디오의 재생시간을 이동 및 반환하는 제너레이터 함수
const videoAsyncIterable = async function* () {
    const FRAME_COUNT = 10;
    const cycle = $video.duration / FRAME_COUNT;
    let i = FRAME_COUNT;

    while (i >= 0) {
      $video.currentTime = cycle * i; // 비디오 시간을 변경
      await waitChangeTime(); // 비디오 시간이 변경됨에 따라서 발생하는 이벤트를 기다림
      yield Math.min(cycle * i, $video.duration); // 비디오 시간을 반환
      i--;
    }
};

// 비디오와 OCR이미지 위치정보를 전달하면 해당시간대 비디오를 캡쳐하여 이미지데이터로 반환
const getFrameImageByVideo = ($video, crop) => {
    const ctx = $canvas.getContext("2d");
    $canvas.width = $video.width;
    $canvas.height = $video.height;
    ctx.drawImage($video, ...crop, 0, 0, $canvas.width, $canvas.height);
    const imageData = ctx.getImageData(0, 0, $canvas.width, $canvas.height);


    ctx.putImageData(imageData, 0, 0);
    return imageData;
};

비디오 태그의 loadeddata 이벤트에 프레임을 수집하도록 실행되는 코드를 작성해줍니다.

const $video = document.querySelector("#video");
const $canvas = document.querySelector("#canvas");

const onload = async () => {

  for await (const time of videoAsyncIterable()) {
    const imageData = getFrameImageByVideo($video, [150, 210, 1000, 300]); // 비디오 화면에서 글자영역만 따로 추출
  }
  
};


$video.addEventListener("loadeddata", onload); // 비디오 로드시 실행

해당 코드를 실행을 하면 아래와같이 비디오 시간이 끝에서부터 시작구간까지 10프레임을 순회하며, 비디오 구간이 변경될때마다 캔버스에서 해당프레임을 캡쳐하여 글자영역만 크롭되어 렌더링되는걸 확인할 수 있습니다.

코드 실행 캡쳐본

tesseract.js 라이브러리를 이용한 문자 추출 및 이미지 전처리

OCR을 수행하기위한 기존코드에 tesseract 스크립트 추가 및 OCR을 수행하는 함수를 작성해줍니다.

// script 추가 <script src="https://cdnjs.cloudflare.com/ajax/libs/tesseract.js/5.1.0/tesseract.min.js"></script>

const ocr = async (imageData) => {
  const canvas = new OffscreenCanvas(imageData.width, imageData.height); 
  const ctx = canvas.getContext("2d");
  ctx.putImageData(imageData, 0, 0);

  const { data } = await Tesseract.recognize(canvas, "eng");

  return data.text.replace(/\s+/g, ""); //불필요한 공백 제거
}

작성한 OCR 함수를 onload함수에 추가하여 코드를 실행하고 console을 통해서 OCR 결과를 확인합니다.

//... 

const onload = async () => {
  
  for await (const time of videoAsyncIterable()) {
    const imageData = getFrameImageByVideo($video, [150, 210, 1000, 300]); // 비디오 화면에서 글자영역만 따로 추출
    const ocrResult = await ocr(imageData);
    console.log("OCR RESULT: ", ocrResult);
  }
  
};

//... 
코드 실행 결과화면 콘솔

위와 같이 OCR 글자인식은 되었으나 글자가 제대로 인식이 안되거나 일부만 되는등의 부정확한 결과를 보입니다.

OCR 수행시에 좋은 결과를 얻기위해서는 글자 영역과 다른 배경이 명확하게 구분되도록 이미지 처리(Image processing) 작업이 선행되어야합니다.
예제에서는 Thresholding 알고리즘을 수행하는 코드를 추가하여, 글자영역을 제외한 배경이 대한 구분이 명확해지도록 이미지데이터를 조작합니다.

// ...
// thresholdFilter 함수 추가
const thresholdFilter = (pixels, level = 0.5) => {
  const thresh = Math.floor(level * 255);
  for (let i = 0; i < pixels.length; i += 4) {
    const r = pixels[i];
    const g = pixels[i + 1];
    const b = pixels[i + 2];
    const gray = 0.2126 * r + 0.7152 * g + 0.0722 * b;
    let val;
    if (gray >= thresh) {
      val = 255;
    } else {
      val = 0;
    }
    pixels[i] = pixels[i + 1] = pixels[i + 2] = val;
  }
};

// getFrameImageByVideo 함수 수정
const getFrameImageByVideo = ($video, crop) => {
    const ctx = $canvas.getContext("2d");
    $canvas.width = $video.width;
    $canvas.height = $video.height;
    ctx.drawImage($video, ...crop, 0, 0, $canvas.width, $canvas.height);
    const imageData = ctx.getImageData(0, 0, $canvas.width, $canvas.height);

    thresholdFilter(imageData.data, 0.95);

    ctx.putImageData(imageData, 0, 0);
    return imageData;
};

특정 화면시간대에 비디오 시간 추론하는 알고리즘 작성

수집된 OCR 데이터를 기반으로 특정 시간의 분과 초를 입력받아서 해당 시간을 출력하는 화면이 어느재생시간대에 해당하는지에 대한 기능을 구현해봅니다.

기존에 구현했던 onload 함수를 수정합니다.

// 입력을 받을 수 있는 input을 추가합니다.
const $form = document.querySelector("#time-input"); 
const $formSubmit = document.querySelector("#time-input-submit");
const $minute = document.querySelector("#minute");
const $second = document.querySelector("#second");

// ...

const onload = async () => {
  const ocrResults = [];

  for await (const videoTime of videoAsyncIterable()) {
    const imageData = getFrameImageByVideo($video, [150, 210, 1000, 300]);
    const ocrResult = await ocr(imageData);
    // OCR된 텍스트를 초단위 숫자로 변경하여 배열에 재생시간초와 OCR시간초 저장
    ocrResults.push({
      ocrTime: ocrResult
        .split(":")
        .map(Number)
        .reduce((acc, cur, i) => acc + cur * Math.pow(60, 1 - i), 0),
      videoTime,
    });
  }

  $form.addEventListener("submit", async (e) => {
    e.preventDefault();
    const minute = parseInt($minute.value, 10) * 60;
    const second = parseInt($second.value, 10);

    // 입력한 시간을 초로 변환
    const givenTime = minute + second;

    // 입력한 시간사이의 가장 가까운 시간중 큰 시간과 작은 시간을 찾음
    let closestSmaller = null;
    let closestLarger = null;

    ocrResults
      .sort((a, b) => a.videoTime - b.videoTime)
      .forEach((entry) => {
        if (
          entry.ocrTime <= givenTime &&
          (!closestSmaller || entry.ocrTime > closestSmaller.ocrTime)
        ) {
          closestSmaller = entry;
        }

        if (
          entry.ocrTime >= givenTime &&
          (!closestLarger || entry.ocrTime < closestLarger.ocrTime)
        ) {
          closestLarger = entry;
        }
      });

    // 시간대 사이의 비율을 계산
    const percentageDifference =
      (givenTime - closestSmaller.ocrTime) /
      (closestSmaller === closestLarger
        ? 1
        : closestLarger.ocrTime - closestSmaller.ocrTime);

    // 계산 결과를 출력
    console.log(givenTime);
    console.log("Closest Smaller:", closestSmaller);
    console.log("Closest Larger:", closestLarger);
    console.log("Percentage Difference:", percentageDifference);

    // 비디오 시간을 계산된 비율로 변경
    video.currentTime =
      closestSmaller.videoTime +
      (closestLarger.videoTime - closestSmaller.videoTime) *
        percentageDifference;
  });
};

코드를 수정후 기능을 테스트해봅니다.

분과 초를 입력하면 해당시간에 맞게 출력되는 화면 시간대로 비디오가 이동되는 모습

이렇게 브라우저에서 tesseract.js 라이브러리와 Video OCR을 통해서 특정 텍스트에 시간대로 이동할 수 있는 기능을 구현해보았습니다.
브라우저에서 OCR을 수행하면 서버의 자원을 사용하지않고, 클라이언트의 자원만 가지고도 필요에 따라서 다양한 케이스에 적용이 가능합니다.
필자의 경우에는 매장을 관제하는 CCTV 시스템에서 특정 제품의 제조시간대로 이동하는 등의 기능에 적용해본 경험이 있습니다.

전체 코드는 아래 Codepen을 통해서 확인하실 수 있습니다.

전체 코드