把图片交给 AI请它总结“这是什么配色”会得到很有气氛的词。可如果后续要生成主题变量仍需要一个稳定的数值底座到底哪些颜色出现了多少次我会先从像素提取少量代表色再让模型帮忙命名与讨论用途。八叉树要合并颜色不能合并掉像素八叉树把 RGB 每一位组合成 0 到 7 的分支。每插入一个像素沿途节点都累加数量与通道总和。减少颜色时折叠一个节点的子树用该节点累计的均值作为代表色。无论树怎样折叠输出色桶的数量总和都应该等于输入像素数。这个不变量很重要。若把根节点或只剩一个孩子的节点排除在归并之外某些实现会把未统计的内部节点当成叶子返回一张看似正常、实际上丢了像素的调色板。五个测试像素——黑、白、红、白、黑——足够暴露这类问题。明确“最多”不承诺“恰好”下面每次折叠最深层的一个节点优先处理样本少的节点。它是教学用的八叉树量化不是最小感知误差算法一次折叠可能跨过目标数量所以maxColors2可能只剩一个颜色。若产品必须恰好给出两色需要另一种拆分或聚类策略不能偷偷复制色桶凑数。Nano Banana 2.1 的图像编辑说明适合了解后续参考图工作流但提取的 RGB 也不能保证生成图严格遵守。应把色值、占比、命名提案分开让模型解释色彩关系不替代原始统计。运行node palette.js验证像素总数守恒、色数上限、单色输入和重复像素加权。示例直接使用 RGB 数组未测试图片解码、透明合成、浏览器或真实模型颜色距离也没有做感知空间校准。保留这份账本创意建议才有可以回头核对的依据。可独立运行的核心例子function palette(pixels,maxColors) { if(!Number.isInteger(maxColors)||maxColors1||!pixels.length)throw Error(数量无效); const nodedepth({depth,count:0,sum:[0,0,0],children:new Map()}); const rootnode(0); for(const rgb of pixels) { if(rgb.length!3||rgb.some(v!Number.isInteger(v)||v0||v255))throw Error(RGB无效); let nroot; for(let depth0;depth8;depth) { n.count;rgb.forEach((v,i)n.sum[i]v); if(depth8)break; const bit7-depth,k((rgb[0]bit1)2)|((rgb[1]bit1)1)|(rgb[2]bit1); if(!n.children.has(k))n.children.set(k,node(depth1)); nn.children.get(k); } } const leavesnn.children.size?[...n.children.values()].flatMap(leaves):[n]; while(leaves(root).lengthmaxColors) { const candidates[]; function visit(n) { if(!n.children.size)return; if([...n.children.values()].every(c!c.children.size))candidates.push(n); else n.children.forEach(visit); } visit(root); candidates.sort((a,b)b.depth-a.depth||a.count-b.count); if(!candidates.length)throw Error(归并无进展); candidates[0].children.clear(); } return leaves(root).map(n({rgb:n.sum.map(vMath.round(v/n.count)),count:n.count})); } const px[[0,0,0],[255,255,255],[255,0,0],[255,255,255],[0,0,0]]; const outpalette(px,2); if(out.length2||out.reduce((s,c)sc.count,0)!5)throw Error(像素丢失); const onepalette([[12,34,56],[12,34,56]],8); if(one.length!1||one[0].count!2||one[0].rgb.join(,)!12,34,56)throw Error(单色失败); const weightedpalette([[0,0,0],[0,0,0],[255,255,255]],1)[0]; if(weighted.rgb[0]!85||weighted.count!3)throw Error(加权错误); console.log(JSON.stringify(out));