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