人们提供的参考链接很好,这个问题有几个解决方案,但是由于我最近一直在研究这个问题(完全不知道其他人是如何解决的),我提供了我的方法简单的英语:
首先,意识到(人类感知的)颜色是 3 维的。这基本上是因为人眼有 3 种不同的受体:红色、绿色和蓝色。同样,您的显示器也有红色、绿色和蓝色像素元素。可以使用其他表示,如色调、饱和度、亮度 (HSL),但基本上所有表示都是 3 维的。
这意味着 RGB 颜色空间是一个立方体,具有红色、绿色和蓝色轴。从 24 位源图像来看,这个立方体在每个轴上有 256 个离散级别。将图像减少到 8 位的一种简单方法是简单地减少每个轴的级别。例如,一个 8x8x4 立方体调色板具有 8 个红色和绿色级别,4 个蓝色级别,通过取红色和绿色值的高 3 位以及蓝色值的高 2 位很容易创建。这很容易实现,但有几个缺点。在生成的 256 调色板中,根本不会使用许多条目。如果图像具有使用非常细微的颜色偏移的细节,这些偏移将随着颜色捕捉到相同的调色板条目而消失。
自适应调色板方法不仅需要考虑图像中的平均/常见颜色,还需要考虑颜色空间的哪些区域具有最大的差异。也就是说,与具有数千个像素完全相同的浅绿色阴影的图像相比,具有数千种微妙浅绿色阴影的图像需要不同的调色板,因为后者理想情况下会对该颜色使用单个调色板条目。
为此,我采用了一种方法,可以生成 256 个存储桶,每个存储桶包含完全相同数量的不同值。因此,如果原始图像包含 256000 种不同的 24 位颜色,则该算法会产生 256 个桶,每个桶包含 1000 个原始值。这是通过使用存在的不同值的中值(而不是平均值)对颜色空间进行二进制空间分区来实现的。
在英文中,这意味着我们首先将整个颜色立方体分成小于中值红色值的像素的一半和大于中值红色值的一半。然后,将每个结果的一半除以绿色值,然后除以蓝色,依此类推。每个分割都需要一个位来指示像素的下半部分或上半部分。经过 8 次拆分后,方差已被有效地拆分为色彩空间中 256 个同等重要的簇。
在伪代码中:
// count distinct 24-bit colors from the source image
// to minimize resources, an array of arrays is used
paletteRoot = {colors: [ [color0,count],[color1,count], ...]} // root node has all values
for (i=0; i<8; i++) {
colorPlane = i%3 // red,green,blue,red,green,blue,red,green
nodes = leafNodes(paletteRoot) // on first pass, this is just the root itself
for (node in nodes) {
node.colors.sort(colorPlane) // sort by red, green, or blue
node.lo = { colors: node.colors[0..node.colors.length/2] }
node.hi = { colors: node.colors[node.colors.length/2..node.colors.length] }
delete node.colors // free up space! otherwise will explode memory
node.splitColor = node.hi.colors[0] // remember the median color used to partition
node.colorPlane = colorPlane // remember which color this node split on
}
}
您现在有 256 个叶节点,每个叶节点都包含与原始图像相同数量的不同颜色,并在空间上聚集在颜色立方体中。要为每个节点分配一种颜色,请使用颜色计数找到加权平均值。加权是改善感知颜色匹配的优化,但不是那么重要。确保独立平均每个颜色通道。结果非常好。请注意,蓝色比红色和绿色少分一次是有意的,因为眼睛中的蓝色受体对细微变化的敏感度低于红色和绿色。
还有其他可能的优化。通过使用 HSL,您可以将更高的量化放在亮度维度而不是蓝色。此外,上述算法会略微降低整体动态范围(因为它最终会平均颜色值),因此动态扩展生成的调色板是另一种可能性。