이미지에서 색상 추출하기
이미지에서 주요 색상을 추출하는 라이브러리를 개발하며 겪은 기술적 도전과 해결 과정을 공유합니다.
이미지에서 주요 색상을 추출하는 방법과 이를 라이브러리화하는 과정을 소개해요.
시작은 단순한 아이디어
“이미지에서 색상을 뽑아낼 수 있으면 편하겠다. 그리고 그 이미지의 키 컬러를 뽑아내면 좋겠다.”라는 생각으로 시작했어요.
이미지에서 색상을 추출하는 사이트는 많았지만, 대부분 스포이드 방식(특정 픽셀의 컬러값만 가져옴)이거나 전체 색상을 쭉 나열해서 핵심을 파악하기 어려웠습니다. 그래서 이미지의 핵심 색상(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;
// 타입 안전성 보장
}
배운 점
성능은 처음부터 고려하기: 나중에 최적화하려면 전체 구조를 뜯어고쳐야 할 수도 있습니다. 알고리즘 선택과 데이터 처리 방식은 초기에 잘 정해야 해요.
확장성을 염두에 두기: 처음부터 여러 환경을 고려해두면 나중에 수고를 덜 수 있습니다.
사용자 경험 고려: 샘플링 비율이나 색상 개수를 조절할 수 있게 하고, 합리적인 기본값을 설정하고, 에러 메시지를 명확하게 쓰는 것이 중요합니다.
