Skip to content

Commit 6c8e88d

Browse files
committed
refactor: improve performance and accuracy for images with few colors
1 parent 01dc0f3 commit 6c8e88d

3 files changed

Lines changed: 38 additions & 31 deletions

File tree

src/pipeline.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,39 @@ export function extractPalette(
188188
});
189189
}
190190

191-
// OKLCH quantization path
192191
let quantized: Array<{ color: [number, number, number]; population: number }>;
193-
if (opts.colorSpace === 'oklch') {
192+
193+
// Short-circuit check: collect up to opts.colorCount + 1 unique colors
194+
const seenColors = new Set<string>();
195+
const uniqueColors: Array<[number, number, number]> = [];
196+
197+
for (const color of pixelArray) {
198+
const key = color.join(',');
199+
if (!seenColors.has(key)) {
200+
seenColors.add(key);
201+
uniqueColors.push(color);
202+
203+
// The image contains more distinct colors than requested,
204+
// so we can stop searching for unique colors as we know we will have to use the quantizer.
205+
if (uniqueColors.length > opts.colorCount) break;
206+
}
207+
}
208+
209+
// If unique colors <= maxColors, return them directly with population counts
210+
if (uniqueColors.length <= opts.colorCount) {
211+
// Count populations for unique colors
212+
const countMap = new Map<string, number>();
213+
for (const color of pixelArray) {
214+
const key = color.join(',');
215+
countMap.set(key, (countMap.get(key) || 0) + 1);
216+
}
217+
quantized = uniqueColors.map((color) => ({
218+
color,
219+
population: countMap.get(color.join(','))!,
220+
}));
221+
}
222+
// OKLCH quantization path
223+
else if (opts.colorSpace === 'oklch') {
194224
const scaled = pixelsRgbToOklchScaled(pixelArray);
195225
quantized = paletteOklchScaledToRgb(
196226
quantizer.quantize(scaled, opts.colorCount),

src/quantizers/mmcq.ts

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -346,29 +346,6 @@ function quantize(
346346
): Array<{ color: [number, number, number]; population: number }> {
347347
if (!pixels.length || maxColors < 2 || maxColors > 256) return [];
348348

349-
// Short-circuit: if unique colors <= maxColors, return them directly
350-
const seenColors = new Set<string>();
351-
const uniqueColors: Array<[number, number, number]> = [];
352-
for (const color of pixels) {
353-
const key = color.join(',');
354-
if (!seenColors.has(key)) {
355-
seenColors.add(key);
356-
uniqueColors.push(color);
357-
}
358-
}
359-
if (uniqueColors.length <= maxColors) {
360-
// Count populations for unique colors
361-
const countMap = new Map<string, number>();
362-
for (const color of pixels) {
363-
const key = color.join(',');
364-
countMap.set(key, (countMap.get(key) || 0) + 1);
365-
}
366-
return uniqueColors.map((color) => ({
367-
color,
368-
population: countMap.get(color.join(','))!,
369-
}));
370-
}
371-
372349
const histo = getHisto(pixels);
373350

374351
// Get the initial vbox from the pixels

test/node-test.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,22 +60,22 @@ describe('getColor()', function() {
6060
expect(isValidRGB(color)).to.be.true;
6161
});
6262

63-
it('returns near-black for black.png', async function() {
63+
it('returns perfect black for black.png', async function() {
6464
const color = await getColor(imgPath('black.png'));
6565
expect(isColorObject(color)).to.be.true;
66-
expect(isCloseTo(color, [0, 0, 0])).to.be.true;
66+
expect(color.array()).to.deep.equal([0, 0, 0]);
6767
});
6868

69-
it('returns near-red for red.png', async function() {
69+
it('returns perfect red for red.png', async function() {
7070
const color = await getColor(imgPath('red.png'));
7171
expect(isColorObject(color)).to.be.true;
72-
expect(isCloseTo(color, [255, 0, 0])).to.be.true;
72+
expect(color.array()).to.deep.equal([255, 0, 0]);
7373
});
7474

75-
it('returns valid Color for white.png', async function() {
75+
it('returns perfect white for white.png', async function() {
7676
const color = await getColor(imgPath('white.png'));
7777
expect(isColorObject(color)).to.be.true;
78-
expect(isCloseTo(color, [255, 255, 255])).to.be.true;
78+
expect(color.array()).to.deep.equal([255, 255, 255]);
7979
});
8080

8181
it('returns valid Color for transparent.png', async function() {

0 commit comments

Comments
 (0)