Vì sao K-means là lựa chọn hợp lý cho color palette
Bài toán của color palette rất rõ ràng: từ một tập pixel đã được sampling, gom chúng thành một số nhóm màu đại diện. Đây chính xác là bài toán clustering cơ bản. K-means giải quyết đúng thứ ta cần: chia dữ liệu thành K cụm sao cho các điểm trong cùng cụm gần nhau nhất.
Điểm quan trọng là: với tool UI, K-means không cần phải “chuẩn sách giáo khoa”. Chúng ta không cần hội tụ tuyệt đối, không cần distance metric phức tạp. Chỉ cần kết quả nhìn hợp lý với mắt người dùng.
Vì sao dùng RGB thay vì LAB hay HSL
RGB không phải không gian màu hoàn hảo về mặt cảm nhận thị giác, nhưng nó có ba lợi thế cực lớn trong bối cảnh này: dữ liệu ảnh vốn đã ở RGB, tính toán cực nhanh và đơn giản, và sai số cảm nhận là chấp nhận được khi mục tiêu chỉ là bảng màu tổng quát.
Quan trọng hơn, phần “cảm nhận” của palette sẽ được xử lý thêm ở các bước sau như lọc màu quá tối/sáng hoặc sắp xếp theo dominance. Không cần dồn hết gánh nặng vào clustering.
Cài đặt K-means tối giản cho palette màu
Dưới đây là một phiên bản K-means cố tình giữ cho đơn giản. Không dependency, không class phức tạp, chỉ đủ để chạy nhanh và ổn định trong trình duyệt.
// K-means clustering đơn giản trên không gian RGB
// samples: danh sách { r, g, b }
// k: số lượng màu mong muốn
// maxIter: giới hạn số vòng lặp để tránh tốn CPU
function kmeansColors(samples, k, maxIter) {
if (!samples.length || k <= 0) return [];
// Không cho k lớn hơn số sample
k = Math.min(k, samples.length);
maxIter = maxIter || 12;
// Khởi tạo centroid ngẫu nhiên từ sample có sẵn
const centroids = [];
const usedIndexes = new Set();
while (centroids.length < k) {
const idx = Math.floor(Math.random() * samples.length);
if (usedIndexes.has(idx)) continue;
usedIndexes.add(idx);
centroids.push({
r: samples[idx].r,
g: samples[idx].g,
b: samples[idx].b
});
}
let labels = new Array(samples.length).fill(0);
for (let iter = 0; iter < maxIter; iter++) {
// Bước assign: gán mỗi sample vào centroid gần nhất
for (let i = 0; i < samples.length; i++) {
let bestIdx = 0;
let bestDist = Infinity;
const s = samples[i];
for (let c = 0; c < centroids.length; c++) {
const cd = centroids[c];
const dr = s.r - cd.r;
const dg = s.g - cd.g;
const db = s.b - cd.b;
const dist = dr * dr + dg * dg + db * db;
if (dist < bestDist) { bestDist = dist; bestIdx = c; } } labels[i] = bestIdx; } // Bước update: tính lại centroid từ các sample đã gán const sums = new Array(k).fill(0).map(() => ({
r: 0, g: 0, b: 0, count: 0
}));
for (let i = 0; i < samples.length; i++) {
const lbl = labels[i];
const s = samples[i];
const bucket = sums[lbl];
bucket.r += s.r;
bucket.g += s.g;
bucket.b += s.b;
bucket.count += 1;
}
let changed = false;
for (let c = 0; c < k; c++) { if (sums[c].count === 0) continue; const newR = sums[c].r / sums[c].count; const newG = sums[c].g / sums[c].count; const newB = sums[c].b / sums[c].count; const cd = centroids[c]; // Nếu centroid không đổi đáng kể, coi như hội tụ if ( Math.round(cd.r) !== Math.round(newR) || Math.round(cd.g) !== Math.round(newG) || Math.round(cd.b) !== Math.round(newB) ) { cd.r = newR; cd.g = newG; cd.b = newB; changed = true; } cd.count = sums[c].count; } // Không còn thay đổi thì dừng sớm để tiết kiệm CPU if (!changed) break; } // Làm tròn giá trị RGB và trả về kèm dominance return centroids.map(c => ({
r: Math.round(c.r),
g: Math.round(c.g),
b: Math.round(c.b),
count: c.count || 0
}));
}
Giới hạn vòng lặp và dừng sớm: chìa khóa hiệu năng
Hai chi tiết nhỏ nhưng cực kỳ quan trọng trong code trên là maxIter và cơ chế dừng sớm khi centroid không thay đổi. Với palette màu, thường chỉ cần 5–8 vòng lặp là đủ hội tụ. Việc giới hạn ở 12 vòng giúp đảm bảo tool không bao giờ “đi quá xa” và gây tốn CPU vô ích.
Đây là tư duy rất khác với code học thuật: thay vì theo đuổi hội tụ tuyệt đối, ta theo đuổi trải nghiệm mượt.
Đủ dùng là thắng
K-means RGB trong ví dụ này không phải là cài đặt hoàn hảo nhất, nhưng nó đạt được ba mục tiêu quan trọng nhất: nhanh, dễ kiểm soát và cho kết quả nhìn hợp lý. Phần còn lại của “chất lượng palette” nên được xử lý ở các bước sau như lọc màu cực đoan, sắp xếp theo dominance hay hue.
Kết luận
Trong bối cảnh color palette tool chạy client-side, K-means RGB đơn giản là một lựa chọn rất thực dụng. Nó tránh được over-engineering, giữ cho code dễ bảo trì và quan trọng nhất là mang lại trải nghiệm mượt cho người dùng. Khi làm tool cho con người, đủ dùng đúng chỗ luôn tốt hơn hoàn hảo sai chỗ.
Bình luận