// Upscale a pose layer smoothly, then snap every pixel to the pose's own k main colours so the tracer sees flat regions. // Soft (WhatsApp/JPEG) outlines otherwise trace as many thin shade-strips that vtracer drops as speckles. // The palette comes from the pose itself (k-means), never from the brand palette: see companion.md on quantising. const sharp = require("module").createRequire("/home/ubuntu/kalki-cli/projects/mybazaar-web/package.json")("sharp"); const [, , input, output, kArg = "8", scaleArg = "4"] = process.argv; const k = +kArg, scale = +scaleArg; (async () => { const meta = await sharp(input).metadata(); const { data, info } = await sharp(input).ensureAlpha() .resize(meta.width * scale, meta.height * scale, { kernel: "lanczos3" }) .raw().toBuffer({ resolveWithObject: true }); const n = info.width * info.height; const samples = []; for (let p = 0; p < n; p += 7) if (data[p * 4 + 3] > 200) samples.push([data[p * 4], data[p * 4 + 1], data[p * 4 + 2]]); // k-means++ style seeding by spreading over the sample, then 15 Lloyd iterations. let centres = [samples[0]]; while (centres.length < k) { let best = null, bestD = -1; for (let s = 0; s < samples.length; s += 13) { const d = Math.min(...centres.map((c) => (c[0] - samples[s][0]) ** 2 + (c[1] - samples[s][1]) ** 2 + (c[2] - samples[s][2]) ** 2)); if (d > bestD) { bestD = d; best = samples[s]; } } centres.push(best); } const nearest = (r, g, b) => { let bi = 0, bd = Infinity; centres.forEach((c, i) => { const d = (c[0] - r) ** 2 + (c[1] - g) ** 2 + (c[2] - b) ** 2; if (d < bd) { bd = d; bi = i; } }); return bi; }; for (let it = 0; it < 15; it++) { const acc = centres.map(() => [0, 0, 0, 0]); for (const [r, g, b] of samples) { const a = acc[nearest(r, g, b)]; a[0] += r; a[1] += g; a[2] += b; a[3]++; } centres = centres.map((c, i) => (acc[i][3] ? [acc[i][0] / acc[i][3], acc[i][1] / acc[i][3], acc[i][2] / acc[i][3]] : c)); } centres = centres.map((c) => c.map(Math.round)); for (let p = 0; p < n; p++) { const i = p * 4; if (data[i + 3] < 128) { data[i + 3] = 0; continue; } const c = centres[nearest(data[i], data[i + 1], data[i + 2])]; data[i] = c[0]; data[i + 1] = c[1]; data[i + 2] = c[2]; data[i + 3] = 255; } await sharp(data, { raw: { width: info.width, height: info.height, channels: 4 } }).png().toFile(output); console.log(`${input.split("/").pop()}: ${k} colours ${centres.map((c) => "#" + c.map((v) => v.toString(16).padStart(2, "0")).join("")).join(" ")}`); })();