I found that there are colorspaces such as

CIELAB and

CIELUV that are

*perceptually uniform*, that is, the human perceptual difference between colors in these spaces is the euclidean distance between the colors. So one way to solve this problem would be to choose maximally equidistant points in these spaces, but I don't think this is easy, as these colorspaces don't have simple shapes.

What I ended up returning to was the original problem of generating a perceptually uniform rainbow of n colors. First, I needed a perceptual distance metric. One such metric could be to just convert colors to the CIELAB colorspace and calculate their euclidean distance, but I ended up using the simpler metric proposed here:

http://www.compuphase.com/cmetric.htmUsing this distance metric, for a given n, I perform binary search over perceptual distances to find the perceptual distance that evenly divided the rainbow into n divisions. To find each successive hue with perceptual distance d from the previous, I use binary search over hues.

Here are the results...

Rainbow with linear hues:

Attachment:

rainbow.png [ 466 Bytes | Viewed 958 times ]
Rainbow with perceptually uniform hues:

Attachment:

perceptual.png [ 520 Bytes | Viewed 958 times ]
This approach seems to have shrunk the purply-reddish part of the spectrum, like I was hoping it would. It also seems to have shrunk the green and the blue parts of the spectrum too, which I hadn't noticed in my maps as being a problem, but looking at the original rainbow, it seems like this should be helpful too. Maybe a different distance metric would have even better results?

Here's the code:

**Code:**

#!/usr/bin/env python

from math import sqrt

import sys

BLACK = 0.0, 0.0, 0.0

WHITE = 1.0, 1.0, 1.0

def distance(rgb1, rgb2):

r1, g1, b1 = rgb1

r2, g2, b2 = rgb2

rmean = (r1 + r2) / 2.0

rdiff = (r1 - r2) * 256

gdiff = (g1 - g2) * 256

bdiff = (b1 - b2) * 256

rweight = 2 + rmean

gweight = 4.0

bweight = 2 + (1.0 - rmean)

return sqrt(rweight * rdiff * rdiff +

gweight * gdiff * gdiff +

bweight * bdiff * bdiff)

MAX_DIST = distance(BLACK, WHITE)

def rgb_to_hex(rgb):

return '#%.2x%.2x%.2x' % tuple(round(x * 255) for x in rgb)

def hue_to_rgb(h):

i = int(h * 6.0)

f = h * 6.0 - i

g = 1.0 - f

i %= 6

if i == 0:

return 1.0, f, 0.0

elif i == 1:

return g, 1.0, 0.0

elif i == 2:

return 0.0, 1.0, f

elif i == 3:

return 0.0, g, 1.0

elif i == 4:

return f, 0.0, 1.0

else: # i == 5

return 1.0, 0.0, g

def next_hue(h, desired_distance):

h = max(0.0, min(1.0, h))

rgb1 = hue_to_rgb(h)

# Perform binary search over hues to find hue with desired perceptual

# distance from h...

lo = h

hi = min(1.0, h + 0.5)

while hi - lo >= 1e-12:

mid = (lo + hi) / 2

rgb2 = hue_to_rgb(mid)

dist = distance(rgb1, rgb2)

if dist <= desired_distance:

lo = mid

if dist >= desired_distance:

hi = mid

return hi

def rainbow(n):

return [hue_to_rgb(float(i) / n) for i in xrange(n)]

def perceptual_rainbow(n):

n = max(0, n)

# Perform binary search over perceptual distance to find perceptual

# distance that evenly divides all hues into n divisions...

hues = [0.0] * (n + 1)

lo = 0.0

hi = MAX_DIST

while hi - lo >= 1e-12:

mid = (lo + hi) / 2

for i in xrange(1, n + 1):

h = next_hue(hues[i - 1], mid)

hues[i] = h

if hues[n] == 1.0:

hi = mid

else:

lo = mid

return [hue_to_rgb(hues[i]) for i in xrange(n)]

def usage():

sys.stderr.write('Usage: %s N\n' % sys.argv[0])

sys.exit(1)

def main():

if len(sys.argv) != 2:

usage()

try:

n = int(sys.argv[1])

except ValueError:

usage()

else:

for rgb in perceptual_rainbow(n):

print rgb_to_hex(rgb)

if __name__ == '__main__':

main()

Example usage:

**Code:**

$ python rainbow.py 10

#ff0000

#ff7200

#ffe500

#78ff00

#00ff55

#00ffd9

#0091ff

#001fff

#9200ff

#ff00a2