生成具有最高多样性的 RGB 颜色集

Generate RGB color set with highest diversity

我正在尝试创建一种算法来输出一组不同的 RGB 颜色值,这些颜色值应该尽可能不同。 例如:

以下一组 3 种颜色:

  1. (255, 0, 0) [红色]
  2. (0, 255, 0) [绿色]
  3. (0, 0, 255) [蓝色]

接下来的 3 种颜色是:

  1. (255, 255, 0) [黄色]
  2. (0, 255, 255) [青色]
  3. (255, 0, 255) [紫色]

接下来的颜色应该在新间隔之间。基本上,我的想法是遍历类似于这样的整个色谱系统间隔:

一组13种颜色应该包括1到7之间的颜色,无限延续该模式。

我目前正在努力将这种模式应用到 RGB 值的算法中,因为它对我来说似乎并不微不足道。我很感谢能给我指出解决方案的任何提示。

CompuPhase 的 Wikipedia article on color difference is worth reading, and so is the article on a “low-cost approximation” 链接在其中。我将基于后者进行尝试。

你没有指定语言,所以我会用优化Python来写(参考文章中已经存在的整数优化除外) , 以使其易于翻译成其他语言。

n_colors = 25
n_global_moves = 32

class Color:
    max_weighted_square_distance = (((512 + 127) * 65025) >> 8) + 4 * 65025 + (((767 - 127) * 65025) >> 8)

    def __init__(self, r, g, b):
        self.r, self.g, self.b = r, g, b

    def weighted_square_distance(self, other):
        rm = (self.r + other.r) // 2  # integer division
        dr =  self.r - other.r
        dg =  self.g - other.g
        db =  self.b - other.b
        return (((512 + rm) * dr*dr) >> 8) + 4 * dg*dg + (((767 - rm) * db*db) >> 8)

    def min_weighted_square_distance(self, index, others):
        min_wsd = self.max_weighted_square_distance
        for i in range(0, len(others)):
            if i != index:
                wsd = self.weighted_square_distance(others[i])
                if  min_wsd > wsd:
                    min_wsd = wsd
        return min_wsd

    def is_valid(self):
        return 0 <= self.r <= 255 and 0 <= self.g <= 255 and 0 <= self.b <= 255

    def add(self, other):
        return Color(self.r + other.r, self.g + other.g, self.b + other.b)

    def __repr__(self):
        return f"({self.r}, {self.g}, {self.b})"


colors = [Color(127, 127, 127) for i in range(0, n_colors)]

steps = [Color(dr, dg, db) for dr in [-1, 0, 1]
                           for dg in [-1, 0, 1]
                           for db in [-1, 0, 1] if dr or dg or db]  # i.e., except 0,0,0
moved = True
global_move_phase = False
global_move_count = 0
while moved or global_move_phase:
    moved = False
    for index in range(0, len(colors)):
        color = colors[index]
        if global_move_phase:
            best_min_wsd = -1
        else:
            best_min_wsd = color.min_weighted_square_distance(index, colors)
        for step in steps:
            new_color = color.add(step)
            if new_color.is_valid():
                new_min_wsd = new_color.min_weighted_square_distance(index, colors)
                if  best_min_wsd < new_min_wsd:
                    best_min_wsd = new_min_wsd
                    colors[index] = new_color
                    moved = True
    if not moved:
        if  global_move_count < n_global_moves:
            global_move_count += 1
            global_move_phase = True
    else:
        global_move_phase = False

print(f"n_colors: {n_colors}")
print(f"n_global_moves: {n_global_moves}")
print(colors)

首先将颜色全部设置为灰色,即放在 RGB 颜色立方体的中心,然后在颜色立方体中移动,希望使颜色之间的最小距离最大化。

为了节省 CPU 时间,使用距离的平方而不是距离本身,这需要计算平方根。

颜色一次移动一个,在 3 个方向的每个方向上最多移动 1 个,到相邻颜色之一,使与其他颜色的最小距离最大化。通过这样做,全局最小距离(近似)最大化。

为了克服没有颜色会移动的情况,需要“全局移动”阶段,但是迫使所有颜色移动到一个不比当前位置差多少的位置会导致整体找到更好的配置随着随后的常规动作。最好用 3 种颜色看,没有全局移动,将加权平方距离修改为简单的 rd*rd+gd*gd+bd*bd:配置变为

[(2, 0, 0), (0, 253, 255), (255, 255, 2)]

同时,通过添加 2 个全局移动,配置变为预期的配置

[(0, 0, 0), (0, 255, 255), (255, 255, 0)]

该算法只产生几种可能的解决方案中的一种。不幸的是,由于使用的度量不是欧几里得,因此不可能简单地翻转 8 种可能组合中的 3 维(即,将 r255-r and/or 替换为相同的 g and/or b) 以获得等效的解决方案。最好按照尝试颜色移动步骤的顺序引入随机性,并改变随机种子。

我没有校正显示器的gamma,因为它的目的恰恰是改变亮度的间距,以补偿眼睛在高亮度和低亮度下的不同敏感度。当然,屏幕伽玛曲线偏离理想值,(取决于系统!)伽玛修改会产生更好的结果,但标准伽玛是一个很好的起点。

这是 25 种颜色的算法输出:

请注意,前 8 种颜色(底行和上一行的前 3 种颜色)靠近 RGB 立方体的角(它们不是因为 non-Euclidean 指标)。

先问一下,你是要留在sRGB,遍历每一个RGB组合吗?

或者(这是我的假设)你真的想要彼此 "farthest" 的颜色吗?由于您使用了术语“distinct”,因此我将介绍查找 颜色差异。

塑造你的看法

sRGB 是指您的 display/output 的色彩空间。虽然伽马曲线在感知上 "sorta" 是均匀的,但整体 sRGB 色彩空间却不是,它的目的是为显示器建模而不是人类感知。

要根据感知确定颜色之间的 "maximum distance",您需要一个感知模型,使用感知均匀的色彩空间或使用颜色外观模型 (CAM)。

由于您只需要 sRGB 值作为结果,那么使用统一的色彩空间可能就足够了,例如 CIELAB 或 CIELUV。由于这些使用笛卡尔坐标,(L*a*b*) 中两种颜色之间的差异只是欧几里得距离。

如果您想使用极坐标(即色调角),那么您可以跳过 CIELAB,进入 CIELCh。


怎么做

我建议 Bruce Lindbloom's site 学习相关数学。

简化步骤:

  1. 通过从三个颜色通道中的每一个中移除伽马曲线来线性化 sRGB。
  2. 将线性化值转换为 CIE XYZ(使用 D65,无自适应)
  3. 将 XYZ 转换为 L* a* b*
  4. 找对面:
    一种。现在通过绘制一条通过 0 的线来找到 "opposite" 颜色,使该线与零两边的距离相等。或者
    b. ALT:再做一次从 LAB 到 CIELCh 的变换,然后通过将色调旋转 180 度找到相反的结果。然后转换回 LAB。
  5. 将 LAB 转换为 XYZ。

  6. 将 XYZ 转换为 sRGB。

  7. 将sRGB伽马曲线添加回每个通道。

继续使用 sRGB?

如果您不太关心感知均匀性,那么您可以只使用 sRGB,尽管结果会不太准确。在这种情况下,您需要做的就是取每个通道相对于 255 的差异(即反转每个通道):

差异有什么区别?

以下是上述两种方法的一些比较:

对于起始颜色#0C5490 sRGB 差异方法:

相同的起始颜色,但使用 CIELAB L* C* h*(并且只是将色调旋转 180 度,不调整 L*)。

起始颜色#DFE217,sRGB差异法:

在 CIELAB LCh 中,只需将色调旋转 180:

再次在 LCh 中,但这次也将 L* 调整为 (100 - L*firstcolor)

现在您会注意到这些色调角度发生了很大变化 — 事实上,虽然 LAB 是 "somewhat uniform,",但它在蓝色区域非常松散。

看看数字:

它们的色相、色度、a、b 似乎大不相同...但它们创建的 HEX 颜色值相同!所以是的,即使是 CIELAB 也有不准确的地方(尤其是蓝色)。

如果您想要更高的准确性,请尝试 CIECAM02