# Generate RGB color set with highest diversity

Asked by At

I'm trying to create an algorithm that will output a set of different RGB color values, that should be as distinct as possible. For Example:

following a set of 3 colors:

1. (255, 0, 0) [Red]
2. (0, 255, 0) [Green]
3. (0, 0, 255) [Blue]

the next 3 colors would be:

1. (255, 255, 0) [Yellow]
2. (0, 255, 255) [Cyan]
3. (255, 0, 255) [Purple]

The next colors should be in-between the new intervals. Basically, my idea is to traverse the whole color spectrum systematic intervals similar to this: A set of 13 colors should include the color in between 1 and 7, continue that pattern infinitely.

I'm currently struggling to apply this pattern to an algorithm to RGB values as it does not seem trivial to me. I'm thankful for any hints that can point me to a solution.

## 1 Answers On

Here is my attempt. You didn't specify a language, so I'll write it in Python (not optimized, so as to be easily translated to other languages)

``````n_colors = 25
rw, gw, bw = 2, 4, 1
n_global_moves = 32

class Color:
max_weighted_square_distance = (rw * rw + gw * gw + bw * bw) * 255 * 255

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

def weighted_square_distance(self, other):
rd = rw * (self.r - other.r)
gd = gw * (self.g - other.g)
bd = bw * (self.b - other.b)
return rd*rd + gd*gd + bd*bd

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:
min_wsd = -1
else:
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  min_wsd < new_min_wsd:
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"rw, gw, bw: {rw}, {gw}, {bw}")
print(f"n_global_moves: {n_global_moves}")
print(colors)
``````

The colors are first set all to grey, i.e., put in the center of a color cube, and then moved in the color cube in such a way as to hopefully maximize the minimum distance between colors.

To save CPU time the square of the distance is used instead of the distance itself, which would require the calculation of a square root.

To compensate for the different sensitivity of the cones in our eyes, we use different weights (`rw`, `gw` and `bw`) in the 3 color directions when calculating the Euclidean distance. This causes the color cube to effectively become a color cuboid.

Colors are moved one at a time, by a maximum of 1 in each of the 3 directions, to one of the adjacent colors that maximizes the minimum distance from the other colors. By so doing, the global minimum distance is (approximately) maximized.

The “global move” phases are needed in order to overcome situations where no color would move, but forcing all colors to move to a position which is not much worse than their current one causes the whole to find a better configuration with the subsequent regular moves. This can best be seen with 3 colors and no global moves, setting `rw`, `gw` and `bw` all to `1`: the configuration becomes

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

while, by adding 2 global moves, the configuration becomes the expected one

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

The algorithm produces one of 8 solutions. The other 7 are produced by replacing `c` with `255-c`, where `c` is `r`, `g` or `b`, in all combinations.

The algorithm makes a simplifying assumption regarding color “distances”. I don't know whether a warp of the color cuboid exists that would cause the perceived distance between 2 colors to be equal to the Euclidean distance. If there is, moving colors within this warp would produce better results.

I have not corrected for the monitor's gamma, because it's purpose is precisely that of altering the spacing of the brightness in order to compensate for the eyes' different sensitivity at high and low brightness. Of course, the screen gamma may not do this precisely, and a (screen dependent!) modification of the gamma would yield better results, but the standard gamma is a good starting point.

This is the output of the algorithm for 25 colors: WARNING

After posting this answer I found the Wikipedia article on color difference. The weights I have used may be appropriate for the components' contribution to brightness, but not for the perception of color differences. Furthermore a better formula is available (without completely changing the color space), so I will have to fix these two aspects.