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

4
我正在尝试创建一个算法,它将输出一组不同的RGB颜色值,这些值应尽可能地不同。例如:
在以下三种颜色的基础上:
  1. (255, 0, 0) [红色]
  2. (0, 255, 0) [绿色]
  3. (0, 0, 255) [蓝色]
下一个三种颜色将是:
  1. (255, 255, 0) [黄色]
  2. (0, 255, 255) [青色]
  3. (255, 0, 255) [紫色]
接下来的颜色应该在新区间之间。基本上,我的想法是以类似于此的系统间隔遍历整个颜色光谱:

enter image description here

一组13种颜色应包括1到7之间的颜色,并无限延续该模式。目前我正在努力将此模式应用于RGB值算法,但对我来说似乎并不容易。感谢任何能指向解决方案的提示。

1
一些值得考虑的想法... 1)黑色和白色是颜色立方体上距离最远的两种颜色,但你没有它们。2)距离最远取决于观察者,红绿色盲非常普遍。3)您可能希望查看HSL颜色空间,并保持饱和度和价值最大化,然后将360色相圆分成您想要的任意数量的颜色 - 这基本上就是您已经直觉地在做的事情。 - Mark Setchell
1
HSL在这里进行了描述 https://en.wikipedia.org/wiki/HSL_and_HSV ,你已经有效地选择了图表下半部分的0/120/240度作为前三个值,然后在图表下半部分添加了60/180/300作为后三个值。https://en.wikipedia.org/wiki/HSL_and_HSV#/media/File:HSL-HSV_hue_and_chroma.svg - Mark Setchell
@MarkSetchell 感谢您的建议!我也在考虑使用HSV,但我仍然想知道如何在RGB空间中解决这个问题。此外,您在第一条评论中是正确的;“最高多样性”可能有点模糊不清。 - T A
1
它的目的是什么?选择会对比很差,无法快速传达信息给读者大脑。因此,我不建议其他评论中提出的解决方案(例如仅更改色调),而是适用于地图。 - Giacomo Catenazzi
看看这个:多波段图像栅格到RGB 简单地将可见光谱分成N个波长/颜色,使您拥有N种不同的颜色(也可以用作多波段渲染的原色,因为它们相加得到白色,并且通过它们的线性组合可以实现任何颜色)。 - Spektre
2个回答

3

这篇关于颜色差异的维基百科文章值得一读,其中链接了CompuPhase的“低成本逼近”文章。我将以后者为基础进行尝试。

你没有指定语言,所以我将使用优化的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)]

通过添加两个全局移动,配置变为预期的配置。

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

该算法只能产生几种可能的解决方案。不幸的是,由于使用的度量不是欧几里得度量,因此不能简单地在8种可能的组合中翻转3个维度(即替换r255-r和/或g和/或b)以获得等效的解决方案。最好是在尝试颜色移动步骤的顺序中引入随机性,并改变随机种子。
我没有纠正显示器的gamma,因为它的目的正是为了改变亮度间距,以补偿眼睛在高亮度和低亮度时的不同敏感度。当然,屏幕伽马曲线偏离理想值,修改伽马(系统相关!)可以产生更好的结果,但标准伽马是一个很好的起点。
这是算法对25种颜色的输出:

output for 25 colors as 5 by 5 color patches

请注意,前8种颜色(底行和上一行的前3种颜色)靠近RGB立方体的角落(它们不在角落上,因为使用了非欧几里得度量)。

1
首先问一下,你想保留sRGB并逐个RGB组合吗?还是(这是我的假设)你实际上想要彼此“最远”的颜色?由于你使用了“distinct”这个词,我将介绍如何找到颜色差异。 建模您的感知
sRGB是指您的显示器/输出的颜色空间。虽然伽马曲线“有点”感知均匀,但整个sRGB颜色空间不是,它更意在模拟显示器而不是人类视觉。
为了确定感知中颜色之间的“最大距离”,您需要一个感知模型,可以使用感知均匀的颜色空间或使用颜色外观模型(CAM)。
由于您只需要sRGB值作为结果,因此使用均匀的颜色空间可能就足够了,例如CIELAB或CIELUV。由于这些使用笛卡尔坐标系,因此两种颜色之间的差异在(L*a*b*)中就是欧几里得距离。

enter image description here

LAB diffrence equation

如果你想要使用极坐标(即色调角度),那么可以在CIELAB的基础上再进一步,转而使用CIELCh。

如何做到这一点

我建议参考布鲁斯·林德布卢姆的网站,了解相关数学知识。

简化步骤如下:

  1. 通过从每个颜色通道中删除伽马曲线来线性化sRGB。
  2. 将线性化值转换为CIE XYZ(使用D65,无适应)。
  3. 将XYZ转换为L * a * b *。
  4. 找到相反色:
    a. 通过绘制一条通过0的线,并使该线距离零的两侧相等,来寻找“相反”的颜色。或者
    b. ALT:再进行一次从LAB到CIELCh的转换,然后通过旋转色调180度来找到相反的颜色。然后转换回LAB。
  5. 将LAB转换为XYZ。

  6. 将XYZ转换为sRGB。

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

保持在sRGB中?

如果你不太关心感知均匀性,那么你可以继续使用sRGB,但结果会不够准确。在这种情况下,你只需要相对于255取每个通道的差值(即反转每个通道)即可:

enter image description here

差异的不同之处是什么?

以下是两种讨论过的方法的比较:

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

enter image description here

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

enter image description here

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

enter image description here

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

enter image description here

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

enter image description here

现在你会注意到这些色相角度的变化很大——事实上,虽然LAB是“有点均匀”的,但在蓝色方面却相当不稳定。
看一下这些数字:

enter image description here

他们在色调、色度、a、b方面似乎有很大的不同...然而它们却创建了相同的HEX颜色值!因此,即使是CIELAB也存在不准确性(尤其是蓝色)。
如果你想要更高的精度,请尝试使用CIECAM02。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接