71

This is something I've pseudo-solved many times and have never quite found a solution for.

The problem is to come up with a way to generate N colors, that are as distinguishable as possible where N is a parameter.

Rann Lifshitz
  • 4,040
  • 4
  • 22
  • 42
Louis Brandy
  • 19,028
  • 3
  • 38
  • 29
  • Last I checked [JFreeChart](http://www.jfree.org/jfreechart/) has this precise algorithm and as it is open source you can check out what it does. I do know that the colors I get do not seem to be randomly spaced along some circle or sphere, but rather chosen more specifically. – Kathy Van Stone Sep 30 '09 at 18:00

8 Answers8

28

My first thought on this is "how to generate N vectors in a space that maximize distance from each other."

You can see that the RGB (or any other scale you use that forms a basis in color space) are just vectors. Take a look at Random Point Picking. Once you have a set of vectors that are maximized apart, you can save them in a hash table or something for later, and just perform random rotations on them to get all the colors you desire that are maximally apart from each other!

Thinking about this problem more, it would be better to map the colors in a linear manner, possibly (0,0,0) → (255,255,255) lexicographically, and then distribute them evenly.

I really don't know how well this will work, but it should since, let us say:

n = 10

we know we have 16777216 colors (256^3).

We can use Buckles Algorithm 515 to find the lexicographically indexed color.\frac {\binom {256^3} {3}} {n} * i. You'll probably have to edit the algorithm to avoid overflow and probably add some minor speed improvements.

Rann Lifshitz
  • 4,040
  • 4
  • 22
  • 42
nlucaroni
  • 47,556
  • 6
  • 64
  • 86
  • 1
    This is incorrect because the RGB color space is not perceptually uniform – adrienlucca.net Jan 13 '15 at 17:24
  • I agree that sounds logical. RGB mostly makes purple and orange hybrids and relatively rarey makes blue green hybrids... the color scale is uniform from infra red to deep blue, so have to choose points equally spaced along it. need a rainbow based algo. – bandybabboon Sep 14 '15 at 12:46
  • Please consider upvoting/following the StackExchange Color Theory site: https://area51.stackexchange.com/proposals/110687/color-theory – Adi Shavit Jun 22 '17 at 08:14
20

It would be best to find colors maximally distant in a "perceptually uniform" colorspace, e.g. CIELAB (using Euclidean distance between L*, a*, b* coordinates as your distance metric) and then converting to the colorspace of your choice. Perceptual uniformity is achieved by tweaking the colorspace to approximate the non-linearities in the human visual system.

Jeff Holt
  • 2,940
  • 3
  • 22
  • 29
Liudvikas Bukys
  • 5,790
  • 3
  • 25
  • 36
  • This is probably the best solution as it is quite straightforward. However there are other color-difference formulae to consider, like CIE2000 or even CIECAM – adrienlucca.net Jan 13 '15 at 17:25
11

Some related resources:

ColorBrewer - Sets of colours designed to be maximally distinguishable for use on maps.

Escaping RGBland: Selecting Colors for Statistical Graphics - A technical report describing a set of algorithms for generating good (i.e. maximally distinguishable) colour sets in the hcl colour space.

hadley
  • 102,019
  • 32
  • 183
  • 245
10

Here is some code to allocate RGB colors evenly around a HSL color wheel of specified luminosity.

class cColorPicker
{
public:
    void Pick( vector<DWORD>&v_picked_cols, int count, int bright = 50 );
private:
    DWORD HSL2RGB( int h, int s, int v );
    unsigned char ToRGB1(float rm1, float rm2, float rh);
};
/**

  Evenly allocate RGB colors around HSL color wheel

  @param[out] v_picked_cols  a vector of colors in RGB format
  @param[in]  count   number of colors required
  @param[in]  bright  0 is all black, 100 is all white, defaults to 50

  based on Fig 3 of http://epub.wu-wien.ac.at/dyn/virlib/wp/eng/mediate/epub-wu-01_c87.pdf?ID=epub-wu-01_c87

*/

void cColorPicker::Pick( vector<DWORD>&v_picked_cols, int count, int bright )
{
    v_picked_cols.clear();
    for( int k_hue = 0; k_hue < 360; k_hue += 360/count )
        v_picked_cols.push_back( HSL2RGB( k_hue, 100, bright ) );
}
/**

  Convert HSL to RGB

  based on http://www.codeguru.com/code/legacy/gdi/colorapp_src.zip

*/

DWORD cColorPicker::HSL2RGB( int h, int s, int l )
{
    DWORD ret = 0;
    unsigned char r,g,b;

    float saturation = s / 100.0f;
    float luminance = l / 100.f;
    float hue = (float)h;

    if (saturation == 0.0) 
    {
      r = g = b = unsigned char(luminance * 255.0);
    }
    else
    {
      float rm1, rm2;

      if (luminance <= 0.5f) rm2 = luminance + luminance * saturation;  
      else                     rm2 = luminance + saturation - luminance * saturation;
      rm1 = 2.0f * luminance - rm2;   
      r   = ToRGB1(rm1, rm2, hue + 120.0f);   
      g = ToRGB1(rm1, rm2, hue);
      b  = ToRGB1(rm1, rm2, hue - 120.0f);
    }

    ret = ((DWORD)(((BYTE)(r)|((WORD)((BYTE)(g))<<8))|(((DWORD)(BYTE)(b))<<16)));

    return ret;
}


unsigned char cColorPicker::ToRGB1(float rm1, float rm2, float rh)
{
  if      (rh > 360.0f) rh -= 360.0f;
  else if (rh <   0.0f) rh += 360.0f;

  if      (rh <  60.0f) rm1 = rm1 + (rm2 - rm1) * rh / 60.0f;   
  else if (rh < 180.0f) rm1 = rm2;
  else if (rh < 240.0f) rm1 = rm1 + (rm2 - rm1) * (240.0f - rh) / 60.0f;      

  return static_cast<unsigned char>(rm1 * 255);
}

int _tmain(int argc, _TCHAR* argv[])
{
    vector<DWORD> myCols;
    cColorPicker colpick;
    colpick.Pick( myCols, 20 );
    for( int k = 0; k < (int)myCols.size(); k++ )
        printf("%d: %d %d %d\n", k+1,
        ( myCols[k] & 0xFF0000 ) >>16,
        ( myCols[k] & 0xFF00 ) >>8,
        ( myCols[k] & 0xFF ) );

    return 0;
}
ravenspoint
  • 19,093
  • 6
  • 57
  • 103
5

Isn't it also a factor which order you set up the colors?

Like if you use Dillie-Os idea you need to mix the colors as much as possible. 0 64 128 256 is from one to the next. but 0 256 64 128 in a wheel would be more "apart"

Does this make sense?

svrist
  • 7,042
  • 7
  • 44
  • 67
3

I've read somewhere the human eye can't distinguish between less than 4 values apart. so This is something to keep in mind. The following algorithm does not compensate for this.

I'm not sure this is exactly what you want, but this is one way to randomly generate non-repeating color values:

(beware, inconsistent pseudo-code ahead)

//colors entered as 0-255 [R, G, B]
colors = []; //holds final colors to be used
rand = new Random();

//assumes n is less than 16,777,216
randomGen(int n){
   while (len(colors) < n){
      //generate a random number between 0,255 for each color
      newRed = rand.next(256);
      newGreen = rand.next(256);
      newBlue = rand.next(256);
      temp = [newRed, newGreen, newBlue];
      //only adds new colors to the array
      if temp not in colors {
         colors.append(temp);
      }
   }
}

One way you could optimize this for better visibility would be to compare the distance between each new color and all the colors in the array:

for item in color{
   itemSq = (item[0]^2 + item[1]^2 + item[2]^2])^(.5);
   tempSq = (temp[0]^2 + temp[1]^2 + temp[2]^2])^(.5);
   dist = itemSq - tempSq;
   dist = abs(dist);
}
//NUMBER can be your chosen distance apart.
if dist < NUMBER and temp not in colors {
   colors.append(temp);
}

But this approach would significantly slow down your algorithm.

Another way would be to scrap the randomness and systematically go through every 4 values and add a color to an array in the above example.

helloandre
  • 10,541
  • 8
  • 47
  • 64
3

To achieve "most distinguishable" we need to use a perceptual color space like Lab (or any other perceptually linear color space) other than RGB. Also, we can quantize this space to reduce the size of the space.

Generate the full 3D space with all possible quantized entries and run the K-means algorithm with K=N. The resulting centers/"means" should be approximately most distinguishable from each other.

Tanjim Ahmed Khan
  • 650
  • 1
  • 9
  • 21
Adi Shavit
  • 16,743
  • 5
  • 67
  • 137
3
function random_color($i = null, $n = 10, $sat = .5, $br = .7) {
    $i = is_null($i) ? mt_rand(0,$n) : $i;
    $rgb = hsv2rgb(array($i*(360/$n), $sat, $br));
    for ($i=0 ; $i<=2 ; $i++) 
        $rgb[$i] = dechex(ceil($rgb[$i]));
    return implode('', $rgb);
}

function hsv2rgb($c) { 
    list($h,$s,$v)=$c; 
    if ($s==0) 
        return array($v,$v,$v); 
    else { 
        $h=($h%=360)/60; 
        $i=floor($h); 
        $f=$h-$i; 
        $q[0]=$q[1]=$v*(1-$s); 
        $q[2]=$v*(1-$s*(1-$f)); 
        $q[3]=$q[4]=$v; 
        $q[5]=$v*(1-$s*$f); 
        return(array($q[($i+4)%6]*255,$q[($i+2)%6]*255,$q[$i%6]*255)); //[1] 
    } 
}

So just call the random_color() function where $i identifies the color, $n the number of possible colors, $sat the saturation and $br the brightness.

vvvvv
  • 25,404
  • 19
  • 49
  • 81
Mauro
  • 3,946
  • 2
  • 27
  • 41
  • Can you explain what "i" is in this case? The question asked for N numbers. What is the "i" paramater? – CodeGuy Jan 14 '13 at 07:39
  • On `random_color()`, `$i` is the "seed" to generate the hue, should be a number from 0 to `$n`, if you input no seed (NULL), the function picks a random one. `$n` is the amount of possible colors for a given saturation and brightness i.e. the number of colors in the palette. We're basically splitting the 360 hue degrees into `$n` and using `$i` as a multiplier. In other words, higher `$n` will give you more colors, lower `$n` will give you less colors but more different to each other. `$i` will identify the color and will always be the same if you keep using this function. I hope that helps. – Mauro Jan 14 '13 at 18:35
  • I see! Thanks for the explanation. One more thing...any suggestions for what to do if I have a background color and I want to be as far away from that as possible for all the colors? – CodeGuy Jan 15 '13 at 05:16
  • You need to add 180 degrees to the hue of your color maintaining saturation and value. Post a new question for this, paste the link here and I'll explain further! – Mauro Jan 15 '13 at 16:37