개발 일지/도트 팔레트 추출

도트 이미지에서 색상 팔레트 추출하기

JiWoo. 2025. 6. 15. 18:11

최근 들어서 개발에 소홀해지는 경우가 조금씩 발생하고 있어 정신차리고 개발을 계속 이어 나가보려고 합니다.

이번 이야기는 이미지에서 색상 팔레트를 추출하는 방법을 이야기해 보려고 합니다.

색상 추출을 하는 이유

픽셀 팔레트 프로젝트, 이전 게시글에서도 이야기한 것처럼 도트 이미지를 업로드하면

해당 이미지에서 사용된 색상들을 팔레트로 추출해 주는 서비스를 제공하기 위해서 필요한 기능입니다.

색상을 추출하는 방법

색상을 추출하는데 필요한 것은 Canvas API 로 지원하는 getImageData() 메서드를 활용해 구현할 것입니다.

 

이번 프로젝트에서는 이미지를 그냥 <img /> 태그로 관리하는 것이 아닌 Canvas 를 활용했습니다.

일반 img 태그로 관리할 경우 작은 이미지에 대한 스케일 처리를 하기가 힘들기 때문에 Canvas를 활용해야 합니다.

이미지 데이터 가져와 화면에 렌더링하기

우선 이전에 다루지는 않았지만, 업스케일링 처리를 하는 과정에서 file을 입력받고 이미지 파일일 경우,

Canvas에 그려내 Preview 화면을 구현하였습니다.

 

이제는 Preview 화면을 구현하기 위해 사용되었던 로직 내에서 메모리에 저장된 이미지 파일을 그대로

색상을 추출할 때 재사용할 예정입니다.

 

먼저 코드를 살펴봅시다.

const $canvas = document.getElementById("preview");
const ctx = $canvas.getContext("2d");

const $fileInput = document.getElementById("file");

const changeFileInput = (e) => {
  const file = e.target.files[0]; // 입력된 파일 가져오기
  const url = URL.createObjectURL(file); // data url 생성
  
  // 이미지 파일 유효성 검증
  if(/^(image\/?)(:?png|jpg|jpeg)/i.test(file.type)) {
    const image = new Image();
    
    image.onload = () => {
      $canvas.classList.remove("unloaded");
      $canvas.width = image.width;
      $canvas.height = image.height;
      ctx.drawImage(image, 0, 0); // Canvas 위에 이미지 그려내기
    }
    
    // 유효성 검증 후, data url 이미지 처리
    image.src = url;
  }
};

const initFileInput = () => {
  $fileInput.addEventListener("change", changeFileInput);
};

document.addEventListener("DOMContentLoaded", initFileInput);

먼저 Preview 를 만들기 위해서는 이미지 파일을 받아와야 합니다.

이미지 파일을 받는 방법은 화면에 작성해 둔 input 태그에 id로 정보를 가져오고 change event 를 작성합니다.

입력된 이미지 파일 데이터 가져오기

change 이벤트를 통해 type이 file인 input에 변화가 생기면 함수를 실행합니다.

현재 input 의 상태를 이루고 있는 데이터를 다룰 수가 있는데 파일 상태를 저장하고 있는 files 필드에 데이터를 꺼내옵니다.

const file = e.target.files[0];

그 후, 정규식을 작성하여 원하는 이미지 파일 확장자를 검사하고 이미지 처리를 진행합니다.

이미지 파일을 data url 형식으로 변환하고 변환된 데이터를 Image 객체를 생성하여 src 필드에 입력해 줍니다.

하지만,,

image 객체에 입력된 이미지는 로드되는 시간이 소요됩니다.

 

이미지 다운로드가 되어 처리된 이미지의 경우 메모리에 남아 있기 때문에 바로 로드가 될 수는 있지만,

처음 받은 이미지의 경우 새로운 처리가 필요하기 때문에 필요한 타이밍에 이미지가 로드되지 않아 문제가 발생할 수 있습니다.

 

이런 경우를 해결할 수 있는 방법이 바로 Image 객체onload 메서드를 활용하는 것입니다.

const image = new Image();

image.onload = () => {
  // bla bla bla any codes ..
}

image.src = url;

 

이런 식으로 데이터를 읽고 처리하는데 발생하는 시간을 생각해서 코드를 작성해 주어야 합니다.

ctx.drawImage(image, 0, 0);

마지막으로, canvas의 context 를 활용하여 drawImage() 메서드를 통해 이미지를 그려내면 끝납니다.

본격적인 색상 데이터 추출

본격적으로 context에 그려낸 이미지를 활용하여 이미지 데이터를 가져올 예정입니다.

데이터를 가져오게 되면 rgba 데이터를 가져올 수 있는 준비가 완료되기 때문에 코드를 추가적으로 작성해 주도록 하겠습니다.

Canvas 이미지 데이터 읽어오기

Canvas는 context를 통해 Canvas에 그려진 데이터를 읽어올 수 있습니다.

const imageData = ctx.getImageData(0, 0, image.width, image.height);

이미지 데이터는 위에 작성된 코드로 가져올 수 있는데 이 이미지 데이터를 가지고 rgb 값을 뽑아낼 수 있도록 준비하겠습니다.

RGB 값 추출을 위한 객체 생성

이제 마지막 단계로 색상 팔레트 추출을 위한 객체를 작성해 봅시다.

class Palette {
  constructor(imageData) {
    this.data = imageData.data;
    this.colors = new Set();
  }
  
  extract() {}
  
  render() {}
}

저는 팔레트 추출을 담당하는 객체를 생성해서 추출과 팔레트 렌더링을 함께 할 수 있는 메서드들을 작성해 보려고 합니다.

 

참조할 객체 변수는 data와 colors 로 선언하였습니다.

변수 설명

data: imageData를 가져온 경우, 픽셀 별 rgba 데이터를 가지고 있는 data 프로퍼티를 담아주는 변수입니다.

colors: new Set() 객체를 활용하여 색상을 중복 없이 담아내기 위해 작성하고 컬러 값들을 저장하기 위한 변수입니다.

extract 메서드 구현하기

extract 메서드는 이미지 데이터에서 픽셀 별 rgb 데이터를 추출하는 역할을 담당합니다.

하지만, rgb 데이터를 추출하기 위해서는 imageData의 data 프로퍼티의 구조를 파악해야 합니다.

getImageData 메서드를 통해 출력한 imageData의 형태는 이렇습니다.

Unit8ClampedArray

여기서 data 프로퍼티에 담겨진 Uint8ClampedArray 타입의 데이터가 확인이 되는데,

이 데이터는 범위가 0부터 255까지의 값을 저장할 수 있고, 0보다 작다면 0, 255보다 크다면 자동으로 255 값으로 저장이 됩니다.

 

0 ~ 255의 값으로 총 (2^8 = 256가지) 의 형태를 가진 8비트 크기의 부호 없는 정수를 관리하는 형태이죠.

 

rgb의 값은 0부터 255 사이로 표현이 되고, 그 값을 위에 타입을 가진 배열로 저장하고 있을 거란 추측이 됩니다.

RGB 값 추출 방법

해당 배열은 RGBA ( red, green, blue, alpha ) 의 값을 1차원 배열의 형태로 색상 값을 순서대로 저장합니다.

따라서 0 ~ 4 인덱스 위치 값을 꺼내 나열하면 사진에 있는 숫자를 기준으로 rgba(200, 184, 171, 255) 의 색상이 됩니다.

 

그렇다면 이 색상을 쭉 하나씩 꺼내보려면 어떤 방식으로 접근하는 게 좋을까?

일단 간단한 방법으로 반복문을 사용하는 것입니다.

 

또 한번 코드를 살펴봅시다.

class Palette {
  constructor(imageData) {
    this.data = imageData.data;
    this.colors = new Set();
  }
  
  extract() {
    for(let i = 0; i < this.data.length; i += 4) {
      const r = this.data[i];
      const g = this.data[i + 1];
      const b = this.data[i + 2];
      const a = this.data[i + 3];
      this.colors.add(`rgba(${r}, ${g}, ${b}, ${a})`);
    }
  }
  
  render() {}
}

extract() 메서드를 호출하면 반복문을 통해 1차원 배열을 4개의 간격으로 데이터를 꺼내오는 역할을 하도록 설정합니다.

 

첫 반복에는 0의 위치의 red, 1의 green, 2의 blue, 3의 alpha를 가져오는 것으로 색상을 추출합니다.

다음 반복에는 4의 크기를 더해 4의 위치부터 red .. 이런 식으로 데이터를 가공하면 될 것 같습니다.

render 메서드 구현하기

render 메서드는 이름 그대로 화면에 렌더링하는 역할을 담당합니다.

 

그러면 어떤 내용을 렌더링하는 것인가?

그 내용은 바로 추출한 색상을 화면에 나타내 도트 이미지에 어떤 색상을 사용했는 지 보여주는 것입니다.

색상을 나열할 레이아웃 생성

주석으로 표기한 위치에 색상을 표기할 태그를 작성해 줍니다.

<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="./src/styles/index.css" />
    <title>Pixel Palette</title>
  </head>
  <body>
    <div class="container">
      <div class="wrapper">
        <!-- <div class="title-box">
          <h1 class="title">Pixel Palette</h1>
        </div> -->
        <div class="preview-box">
          <canvas id="preview" class="unloaded" width="300" height="300"></canvas>
        </div>
        <div class="input-box">
          <input id="file" type="file" />
        </div>
        <div id="palette-box"></div> <!-- 색상을 표기할 위치 -->
      </div>
    </div>
    <script src="./src/javascript/palette.js"></script>
    <script src="./src/javascript/file.js"></script>
  </body>
</html>

render 동작 처리

render 메서드는 일반적인 방법과 같기에 설명은 길게 하지 않겠습니다.

const $paletteBox = document.getElementById("palette-box");

class Palette {
  constructor(imageData) {
    this.data = imageData.data;
    this.colors = new Set();
  }

  extract() {
    // ... extract codes
  }

  // 추출 이미지 DOM 렌더링
  render() {
    $paletteBox.innerHTML = "";

    for (let color of this.colors.values()) {
      const $item = document.createElement("div");
      $item.style.width = `10px`;
      $item.style.height = `10px`;
      $item.style.backgroundColor = color;

      $paletteBox.append($item);
    }
  }
}

color는 이미 this.colors에 저장이 되었기 때문에 그대로 꺼내와 하나씩 나열해 주면 render의 역할은 끝납니다.

 

Set 객체의 values 메서드는 순회 가능한 iterable 데이터를 반환하기에 컬러 값을 하나씩 꺼내 처리할 수 있습니다.

따라서, 순회할 요소를 반환하는 iterable 데이터를 for ... of 루프로 처리합니다.

iterable 참고 자료

 

iterable 객체

 

ko.javascript.info

이어서 색상 추출

image.onload = () => {
  $canvas.classList.remove("unloaded");
  $canvas.width = image.width;
  $canvas.height = image.height;
  ctx.drawImage(image, 0, 0);
  
  const imageData = ctx.getImageData(0, 0, image.width, image.height);
  const palette = new Palette(imageData);
  palette.extract();
  palette.render();
}

결과

업로드한 이미지는 Preview를 통해 보여주고, 하단에 사용된 색상을 쭉 나열하는 식으로 처리하였습니다.

마무리

현재는 색상 데이터가 많으면 꾸겨지는 현상이 일어나고 있어 레이아웃 형태를 새로 잡고

각 색상 별 Cluster 기능을 제공하여 정렬된 색상 팔레트를 제공할 예정입니다.

 

또한, 각 색상 별로 사용된 개수와 색상 정보를 띄울 수 있는 공간을 마련하여 픽셀 아트를 만드는 경우,

도움을 줄 수 있는 서비스를 제공하려고 노력할 예정입니다.

 

별 내용 없지만 .. 긴 글 읽어주신 것에 대해 감사합니다.