브라우저 환경에서 이미지를 데이터로 활용하기

브라우저 환경에서 서버에서 받아온 Image를 통해 데이터로 활용했던 기록

브라우저 환경에서 이미지를 데이터로 활용하기

개요

브라우저에서 이미지를 ImageData API를 통해 데이터화하고, 이를 참조 및 합성하는 방법에 대해 다룹니다. 본문에서는 동일한 비율로 베이스 이미지 위에 컬러맵 이미지가 어느 정도 비율을 차지하는지 계산하고, 이미지를 합성하여 시각화하는 과정을 설명합니다.

진행과정

  • 베이스 이미지 및 파란색 컬러맵 이미지를 OffScreencanvas API 통하여
    ImageData로 변환
  • 이미지 데이터 연산을 위해서 변환된 이미지 데이터에 대해서 데이터 압축수행
  • 각 압축된 이미지 데이터를 비교하여, 베이스 이미지 위에 얼마나 컬러맵이 존재하는지 비율 계산
  • 두 이미지 데이터를 참조하여 이미지를 합성 후 시각화

먼저 코드에서 사용할 기능 함수를 미리 정의합니다.

이미지 로드 함수

const loadImage = (src) => {
    return fetch(src)
        .then((r) => {
        if (!r.ok) throw new Error("Network response was not ok");

        return r.blob();
        })
        .then((blob) => createImageBitmap(blob));
};

이미지 데이터 압축 함수

const getCompressedImageData = (imageData, scale) => {
    const compressSize = Math.floor(1 / scale);
    const compressWidth = Math.ceil(imageData.width / compressSize);
    const compressHeight = Math.ceil(imageData.height / compressSize);

    const tempData = new Uint8ClampedArray(
        compressWidth * compressHeight * 4
    );

    for (let y = 0; y < compressHeight; y++) {
        for (let x = 0; x < compressWidth; x++) {
          const index = y * compressWidth * 4 + x * 4;
          const rgba = imageData.data.slice(
              (y * compressSize * imageData.width + x * compressSize) * 4,
              (y * compressSize * imageData.width + x * compressSize) * 4 + 4
          );
          
          tempData.set(rgba, index);
        }
    }

    return new ImageData(tempData, compressWidth, compressHeight);
};

사용할 이미지 불러오기 및 압축

이미지를 불러온 후 OffscreenCanvas API를 이용하여 ImageData를 추출하고 1/4로 압축합니다.

const baseImageSource = `https://w-log.dev/content/images/2024/05/baseImage-1.png`;
const colorImageSource = `https://w-log.dev/content/images/2024/05/colorImage-1.png`;

const offCanvas = new OffscreenCanvas(640, 360);
const ctx = offCanvas.getContext("2d");

const [baseImageData, colorImageData] = await Promise.all(
    [baseImageSource, colorImageSource].map(async (src) => {
        const bitmap = await loadImage(src);
        offCanvas.width = bitmap.width;
        offCanvas.height = bitmap.height;
        ctx.clearRect(0, 0, 9999, 9999);
        ctx.drawImage(bitmap, 0, 0);

        return getCompressedImageData(
            ctx.getImageData(0, 0, bitmap.width, bitmap.height),
            0.25 // 이미지 데이터를 1/4로 압축
        );
    })
);

//...
💡
이미지 데이터를 압축하는 이유
이미지 데이터는 작은 이미지라도 가로 크기(width) * 세로 크기(height) 개수만큼의 픽셀 정보(Raster)를 가지고있기때문에 자바스크립 메인스레드에서 연산하기 부담스러운 크기를 가집니다.
예제에서는 압축을 통해 사이즈를 줄이지만 웹 워커등을 통해서 다른 스레드에서 연산을 수행하는 등 다양한 최적화 방법이 존재합니다.

이미지 데이터를 비교하여 파란색 영역의 비율 구하기

이미지 전체 데이터 영역에서 색상정보가 없는 부분을 제외한 색상(blue)영역의 비율을 계산합니다.

const calculateFillArea = (baseImageData, colorImageData) => {
  let total = 0;
  let progress = 0;

  for (let y = 0; y < baseImageData.height; y++) {
    for (let x = 0; x < baseImageData.width; x++) {
      const index = (y * baseImageData.width + x) * 4;
      const baseRgba = baseImageData.data.slice(index, index + 4);
      const colorRgba = colorImageData.data.slice(index, index + 4);

      if (baseRgba[3] === 255) total += 1;
      if (baseRgba[3] === 255 && colorRgba[3] === 255) progress += 1;
    }
  }

  return ((progress / total) * 100).toFixed(2) + "%";
};

const fillPercentage = calculateFillArea(baseImageData, colorImageData); // 49.56%

// ...

이미지 데이터를 비교하여 파란색영역을 합성하기

const compositeImageData = (baseImageData, colorImageData) => {
  const tempData = new Uint8ClampedArray(
    baseImageData.width * baseImageData.height * 4
  );

  for (let y = 0; y < baseImageData.height; y++) {
    for (let x = 0; x < baseImageData.width; x++) {
      const index = (y * baseImageData.width + x) * 4;
      const baseRgba = baseImageData.data.slice(index, index + 4);
      const colorRgba = colorImageData.data.slice(index, index + 4);

      if (baseRgba[3] === 255 && colorRgba[3] === 255) {
        tempData.set(colorRgba, index);
      } else {
        tempData.set(baseRgba, index);
      }
    }
  }

  return new ImageData(
    tempData,
    baseImageData.width,
    baseImageData.height
  );
};

const compositedImageData = compositeImageData(baseImageData, colorImageData);

canvas.width = compositedImageData.width;
canvas.height = compositedImageData.height;
canvas.getContext("2d").putImageData(compositedImageData, 0, 0);

//...

이미지 데이터를 조작하여 합성하는 코드

결과물

결과화면

완성된 결과물 코드

위와 같이 브라우저에서 Canvas를 활용하여 이미지를 데이터화시켜 조작 및 계산이 가능했습니다. 이 과정은 실제로 AI 모델에서 수행하는 영역 분할(Segmentation) 이미지를 실시간으로 통신하여 시각화하는 과정에서 사용된 방식입니다.

AI 모델은 이미지 내의 객체나 영역을 분할(Segmentation)하여 각 영역을 구분합니다. 이러한 Segmentation 결과를 브라우저로 전송하고 시각화할 때, 이미지 데이터의 크기와 연산 부담이 문제가 될 수 있습니다.

따라서 본 예제에서는 데이터 압축을 통해 이미지 픽셀의 일부 정보를 손실시키더라도 데이터 크기를 줄여서 효율적으로 처리했으며, 이외에도 실시간 통신을 위해서 웹 워커(Web Worker)를 사용하여 메인 스레드(Main Thread)에서 연산을 분리하는등의 브라우저 성능 저하를 최소화 할 수 있는 다양한 최적화 방법이 존재합니다.
이러한 접근 방식은 실시간 시각화가 필요한 다양한 사례에 유용하게 사용할 수 있을 것이라고 생각이됩니다.

다음 포스팅에서는 손실된 색상 이미지 픽셀에 대한 보간법을 다루어, 압축으로 인한 품질 저하를 최소화하는 방법을 소개하겠습니다.