이미지에서 색상 추출하기

이미지에서 주요 색상을 추출하는 방법과 이를 라이브러리화하는 과정을 소개해요.

Image Colors 데모 Image Colors 라이브러리 동작 데모


시작은 단순한 아이디어

“이미지에서 색상을 뽑아낼 수 있으면 편하겠다. 그리고 그 이미지의 키 컬러를 뽑아내면 좋겠다.”라는 생각으로 시작했어요.

이미지에서 색상을 추출하는 사이트는 많았지만, 대부분 스포이드 방식(특정 픽셀의 컬러값만 가져옴)이거나 전체 색상을 쭉 나열해서 핵심을 파악하기 어려웠습니다. 그래서 이미지의 핵심 색상(palette)을 자동으로 뽑아주는 라이브러리를 만들기로 했습니다.


첫 번째 도전: 성능 최적화

가장 먼저 부딪힌 문제는 성능이었어요. 처음에는 모든 픽셀을 분석했는데, 4K 이미지(3840 × 2160)는 8,294,400 픽셀이나 됩니다.

모든 픽셀을 분석하는 대신 일정 간격으로 픽셀을 샘플링하는 방식을 도입했습니다.

1
2
3
4
5
6
7
const samplingRate = 10; // 10픽셀마다 샘플링
for (let y = 0; y < height; y += samplingRate) {
  for (let x = 0; x < width; x += samplingRate) {
    // 샘플링된 픽셀만 처리
  }
}
// 4K 이미지도 82,944 픽셀만 처리! (약 100배 성능 향상)

사용자가 샘플링 비율을 조절할 수 있게 하여 성능과 정확도의 균형을 맞출 수 있도록 했습니다.


두 번째 도전: 다양한 환경 지원

하나의 라이브러리로 브라우저와 Node.js 환경을 모두 지원하고 싶었는데, 이미지 처리 방식이 완전히 달랐어요. 브라우저는 Canvas API와 HTMLImageElement를, Node.js는 Sharp나 Jimp를 사용합니다.

어댑터 패턴으로 환경별 차이를 추상화했습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface ImageAdapter {
  loadImage(source: string | Buffer): Promise<ImageData>;
  getPixelData(image: ImageData): Uint8ClampedArray;
}

class BrowserAdapter implements ImageAdapter {
  async loadImage(source: string): Promise<ImageData> {
    const img = new Image();
    img.src = source;
    await img.decode();

    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d')!;
    canvas.width = img.width;
    canvas.height = img.height;
    ctx.drawImage(img, 0, 0);

    return ctx.getImageData(0, 0, img.width, img.height);
  }
}

어댑터 패턴 덕분에 코어 로직은 환경에 상관없이 일관되게 유지할 수 있었습니다.


세 번째 도전: 색상 정확도

K-means 알고리즘을 사용해 색상을 군집화했는데, 초기 중심점(centroid)이 무작위로 선택되어 결과가 매번 달라지는 문제가 발생했어요.

K-means는 데이터를 K개의 그룹으로 나누는 클러스터링 알고리즘입니다. 색상 추출에서는 RGB 값을 3차원 공간의 점으로 간주하여 유사한 색상끼리 묶습니다.

해결책으로 알고리즘을 여러 번 실행하여 가장 낮은 분산을 가진 결과를 선택하고, 사람의 시각과 유사하게 색상 차이를 계산하는 CIEDE2000 알고리즘을 적용했습니다.

1
2
3
4
5
function colorDistance(color1: RGB, color2: RGB): number {
  const lab1 = rgbToLab(color1);
  const lab2 = rgbToLab(color2);
  return ciede2000(lab1, lab2);
}

네 번째 도전: 타입 안정성

JavaScript로 시작했다가 타입 관련 버그로 고생했습니다. TypeScript로 전환하면서 컴파일 타임에 타입 에러를 발견할 수 있게 되었고, 코드의 가독성과 유지보수성도 크게 향상됐어요.

1
2
3
4
5
6
7
8
9
10
11
12
13
interface ExtractOptions {
  k?: number;
  sampling?: number;
  quality?: 'low' | 'medium' | 'high';
}

function extractColors(
  image: ImageData,
  options: ExtractOptions = {}
): RGB[] {
  const { k = 5, sampling = 10, quality = 'medium' } = options;
  // 타입 안전성 보장
}

배운 점

성능은 처음부터 고려하기: 나중에 최적화하려면 전체 구조를 뜯어고쳐야 할 수도 있습니다. 알고리즘 선택과 데이터 처리 방식은 초기에 잘 정해야 해요.

확장성을 염두에 두기: 처음부터 여러 환경을 고려해두면 나중에 수고를 덜 수 있습니다.

사용자 경험 고려: 샘플링 비율이나 색상 개수를 조절할 수 있게 하고, 합리적인 기본값을 설정하고, 에러 메시지를 명확하게 쓰는 것이 중요합니다.


참고 자료