23
\$\begingroup\$

I'm creating a demo of 20 balls bouncing each other, with a background filled with a solid color. Color for each ball is chosen randomly with randint(0, 255) for each R, G, B tuple component.

Problem is some balls end up with a color that is very similar to background, making them hard to see. I would like to avoid that, either by rolling another color if the chosen one is too similar (within a given threshold), or by moving it away from the background color.

How to calculate a similarity index to use as a threshold? Or how to transform a color to make it, say, less blue-ish?

Obvious disclaimer: I know nothing about color theory, so I don't know if the above concepts even exists!

I'm coding in python, but I'm looking for concepts and strategies, so pseudo-code is fine (or just the theory).

EDIT:

To clarify a few points:

  • Each ball have its own random color. I'd like them to remain as random as possible, and to explore as much as possible of the color space (be it RGB or HSV, pygame accepts both formats for defining color)

  • I'd just like to avoid each color of being "too close" to background. Problem is not just about hue: I'm perfectly OK with a light-blue ball against a dark-blue background (or a "full-blue" against a "white-ish" blue, etc)

  • For now, I don't care if a ball ends up with a color similar (or even identical) to another ball. Avoiding that is a bonus, but it's a minor issue.

  • Background color is a constant, single RGB color. For now I'm using "basic" blue (0, 0, 255), but I'm not settled with that. So consider the background to be of an arbitrary color (with arbitrary hue, brightness, saturation, etc).

EDIT2:

TL,DR version: Just give a way to create X random colors so that none "fades in too much" against a background of arbitrary (but single and constant) color

\$\endgroup\$
12
  • 11
    \$\begingroup\$ stackoverflow.com/questions/5392061/… \$\endgroup\$
    – user15805
    Aug 7, 2014 at 10:03
  • 1
    \$\begingroup\$ Why not just put an outline around each ball? \$\endgroup\$
    – ashes999
    Aug 7, 2014 at 10:25
  • 1
    \$\begingroup\$ Then there might be a problem with making an outline color distinct from both background AND balls color ;) \$\endgroup\$
    – Kromster
    Aug 7, 2014 at 10:27
  • 1
    \$\begingroup\$ @AlexM.: hard to believe there's no easy solution to "generate random colors avoiding an arbitrary one" \$\endgroup\$
    – MestreLion
    Aug 7, 2014 at 11:33
  • 1
    \$\begingroup\$ @MestreLion just use black for the outline and generate colours with a minimum RGB value of 128. It's That Easy. \$\endgroup\$
    – ashes999
    Aug 7, 2014 at 16:51

6 Answers 6

32
\$\begingroup\$

You can use the fact that colors make up a (three dimensional) color space and calculate a distance in this color space. You then need to define a metric in this color space to find the distance between two colors.

E.g. the distance in a euclidean space between two points x = (x1, x2, x3) and y = (y1, y2, y3) is given by d(x,y) = sqrt( (y1-x1) * (y1-x1) + (y2-x2) * (y2-x2) + (y3-x3) * (y3-x3) ).

Now you can calculate the distance between a balls color and your background color and if it is too small, create a new random color and test again.

You will find that neither RGB nor even HSV are good color spaces for this task. The wikipedia article on color difference employs the Lab color space and gives several possible metrics.

There is also a diskussion on stackoverflow on the topic.

\$\endgroup\$
5
  • \$\begingroup\$ That's a very nice approach, I could do that :) Thanks! Also, care to elaborate a bit on how to find an adequate "minumum distance" threshold? If RGB is not adequate, how to convert to Lab? I'm not striving for accurate perception metrics, so any "good enough" defaults will do. \$\endgroup\$
    – MestreLion
    Aug 7, 2014 at 12:02
  • 1
    \$\begingroup\$ Love this answer, well thought out, good explanation of how to do the task whilst providing the equations and references needed. Good first answer. \$\endgroup\$ Aug 7, 2014 at 12:02
  • 1
    \$\begingroup\$ If you are looking for a "good enough" approximation of human perception, you may want to look into the YUV color space, which has a direct conversion from RGB. \$\endgroup\$
    – trm
    Aug 7, 2014 at 18:10
  • 5
    \$\begingroup\$ Save a bit of time, and don't take the square root (expensive). Just make your minimum color distance value that you compare to the square. \$\endgroup\$ Aug 8, 2014 at 20:46
  • 1
    \$\begingroup\$ @MestreLion The Lab* color space was invented to be perceptually uniform, meaning that the perceived distance between two colors is the same as the actual distance in the color space. Since RGB colors are device-dependant, you can't directly convert between Lab and RGB. However, if you use a specific absolute RGB color space, you can convert them. See this article: en.wikipedia.org/wiki/SRGB_color_space and this article: en.wikipedia.org/wiki/Lab_color_space \$\endgroup\$
    – jwir3
    Aug 12, 2014 at 17:49
8
\$\begingroup\$

The United Nations web accessibility standards page (http://www.un.org/webaccessibility/1_visual/13_colourcontrast.shtml) does indicate a standard for ensuring proper text contrast on websites, keeping in mind that some users are colorblind. This might be particularly important for you since some of your users may be colorblind as well (would be hard to tell a red ball from a green background if they have the same luminance).

The UN page points to the Juicy Studios Luminosity Colour Contrast Ratio Analyser at (http://juicystudio.com/services/luminositycontrastratio.php#specify) which states text on background must have a contrast ratio of 4.5:1, except for what I believe to be similar to your case:

  • Large Text: Large-scale text and images of large-scale text have a contrast ratio of at least 3:1

Now what's a contrast ratio? Following hyperlinks further (this is fun!) we get to a W3 page that defines contrast ratio as:

Contrast Ratio = (L1 + 0.05)/(L2 + 0.05)

Where L1 and L2 are the relative luminences of the colors. L1 is the more luminant color, L2 is the less luminant color. Again, we follow the link to the same W3 page that specifies:

L = 0.2126 * R + 0.7152 * G + 0.0722 * B where R, G and B are defined as:

if RsRGB <= 0.03928 then R = RsRGB/12.92 else R = ((RsRGB+0.055)/1.055) ^ 2.4
if GsRGB <= 0.03928 then G = GsRGB/12.92 else G = ((GsRGB+0.055)/1.055) ^ 2.4
if BsRGB <= 0.03928 then B = BsRGB/12.92 else B = ((BsRGB+0.055)/1.055) ^ 2.4
and RsRGB, GsRGB, and BsRGB are defined as:

RsRGB = R8bit/255
GsRGB = G8bit/255
BsRGB = B8bit/255

NOTE: The caret symbol ^ above refers to exponentiation (x²)

An we have a pretty good framework for a contrast algorithm (that's colorblind friendly!)

  • Determine R, G, B values for each color
  • Determine Luminance for each color
  • If the Contrast Ratio of the more luminant color to the least luminant color is less than 3, reroll!
\$\endgroup\$
7
  • 2
    \$\begingroup\$ Might want to note that ^ is supposed to be exponentiation. To us crazy C-like users it means xor (I genuinely thought you were trying to do a bitwise operation on floating point values for a minute). \$\endgroup\$
    – Pharap
    Aug 8, 2014 at 0:18
  • \$\begingroup\$ That's an amazing approach, I like it! :) \$\endgroup\$
    – MestreLion
    Aug 8, 2014 at 0:22
  • \$\begingroup\$ And don't worry about ^, I would never think about a bitwise XOR in this context. (or in any actually. C'mon C phreaks, ^ is the de-facto simbol for exponentiation in pseudo-codes (although very few languages actually use it... in python it's **) \$\endgroup\$
    – MestreLion
    Aug 8, 2014 at 0:24
  • 1
    \$\begingroup\$ Just fixing typos. Plus, if one person has trouble with the ^, chances are somebody else in the future could too. Doesn't hurt to be specific. \$\endgroup\$ Aug 8, 2014 at 0:34
  • 2
    \$\begingroup\$ @John No worries, . I'm not sure where the confusion stems from (i.e. which language used ^ for which purpose first). I only know ^ is sometimes used for exponentiation because of my experience with VB (I don't know of any other languages that use it as exponentiation, but there probably are some). Just as a heads-up though, it's the caret symbol, not the carrot symbol. Might want to change it before someone makes some god awful joke about programming with vegetables (particularly one involving square roots). \$\endgroup\$
    – Pharap
    Aug 8, 2014 at 0:36
2
\$\begingroup\$

Using RGB values by generating a random 0-255 value for each component might not only create colors that are similar to the background, you can also end up with very similar colors for the balls.

I'd probably choose a less random approach and create colors that are guaranteed to be different instead. You could create a color for each ~32 degrees in the hue range and alternate the brightness or saturation (use HSV color-model instead of RGB).

So something like this:

numColors = 20;
stepSize = 360 / (numColors / 2 + 1); // hue step size
for(i = 0; i < numColors; i++){
    color = HSV(i / 2 * stepSize, 0.5 + (i % 2) * 0.5, 1.0) 
}

which will create 20 colors, nicely distributed. This assumes you're using integer math, so i = 0 and i = 1 will create the same hue value.

Of course this doesn't scale too well.. if you have 100 color values to create, the hue spacing might not be enough and you'll end up with similar colors again. In that case you could also vary the V (brightness) value.

I don't know what your background color is like, but HSV makes it also easier to compare colors (just compare the 3 components and don't forget that HUE is circular (0 == 360)).

UPDATE:

Since I didn't really answer your question but rather provided another approach, here's how an algorithm for an arbitrary background and completely random colored balls could look like (this is a brute-force approach but should work fine for your use-case):

// background color, currently equals RGB(0, 0, 255)
bg = HSV(240, 1.0, 1.0)

// number of colors to create is equal to the amount of balls
numColors = balls.length

// set threshold to compare colors. you can tweak this to your liking
threshold = 0.15

for(i = 0; i < numColors; i++){
    do {
        // assuming rnd returns a random float 0-1 inclusive
        color = HSV(rnd() * 360, rnd(), rnd())
        // calculate delta values, normalize hue to 0-1
        dH = (180 - abs(abs(bg.h - color.h) - 180)) / 360
        dS = bg.s - color.s
        dV = bg.v - color.v
        // "distance" between bg and color
        difference = sqrt(dH * dH + dS * dS + dV * dV)
    } while(difference < threshold)

    ball[i].color = color
}
\$\endgroup\$
5
  • \$\begingroup\$ About your algoritm, how is it taking the background color into account? \$\endgroup\$
    – MestreLion
    Aug 7, 2014 at 10:30
  • \$\begingroup\$ It is currently not taking the background color into account... you could tweak it by comparing the generated colors (as described in the last paragraph) and then create a color with a different V (as the current algorithm doesn't alter that). \$\endgroup\$
    – bummzack
    Aug 7, 2014 at 10:37
  • \$\begingroup\$ @MestreLion Added an update with an alternative algorithm \$\endgroup\$
    – bummzack
    Aug 7, 2014 at 11:47
  • \$\begingroup\$ Great, that seems a nice strategy. Brute force is OK, colors are only generate once at initialization, not within game loop. Seems you're using the same approach as René's, with plain euclidean distance in HSV, correct? Any references of a suitable threshold for HSV? Do H, S and V weight the same in distance like in RGB? \$\endgroup\$
    – MestreLion
    Aug 7, 2014 at 12:15
  • \$\begingroup\$ @MestreLion Yes, my second algorithm is pretty much what René explained in his answer. Sadly there's no definitive value for a good threshold since color changes aren't perceived uniformly. We recognize changes in green much faster than in blue... so for green tones a smaller threshold might work but won't be sufficient for other colors etc. As far as I know the Lab color-space is close to human color perception and might be best suited if you don't get desirable results with the HSV colorspace. \$\endgroup\$
    – bummzack
    Aug 7, 2014 at 18:25
1
\$\begingroup\$

Since you din mentioned you got a stationary background, the color of the balls can still be a random but they have to fall at certain ranges that still compliments the background.

Basics. Before we do that you need to know the basics. Consider the following colors:

Black   #000000 rgb(0,0,0)
Red     #FF0000 rgb(255,0,0)
Green   #00FF00 rgb(0,255,0)
Blue    #0000FF rgb(0,0,255)
Yellow  #FFFF00 rgb(255,255,0)
Cyan    #00FFFF rgb(0,255,255)
Pink    #FF00FF rgb(255,0,255)
Gray    #C0C0C0 rgb(192,192,192)
White   #FFFFFF rgb(255,255,255)

Mixture of Colors RGB [(0..255),(0..255),(0..255)] creates new colors as above.

Computing for Negative Colours Computing for negative color is just like transform red in cyan, green in purple, blue in yellow.

Red     #FF0000 rgb(255,0,0) ->     Cyan    #00FFFF rgb(0,255,255)
Green   #00FF00 rgb(0,255,0) ->     Purple   #FF00FF    rgb(255,0,255)
Blue    #0000FF rgb(0,0,255) ->     Yellow  #FFFF00 rgb(255,255,0)

Complementary Color

As per reference on Computing complementary colors: http://serennu.com/colour/rgbtohsl.php

About HSL

HSL expresses colours in terms of their Hue, Saturation and Lightness, giving a number for each of these three attributes of the colour.

The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of the wheel; 0° being red, 180° being red's opposite colour cyan, and so on.

Saturation is the intensity of the colour, how dull or bright it is. The lower the saturation, the duller (greyer) the colour looks. This is expressed as a percentage, 100% being full saturation, the brightest, and 0% being no saturation, grey.

Lightness is how light the colour is. Slightly different to saturation. The more white in the colour the higher its Lightness value, the more black, the lower its Lightness. So 100% Lightness turns the colour white, 0% Lightness turns the colour black, and the "pure" colour would be 50% Lightness.

It's easier to see the difference between Saturation and Lightness than to explain it. If you want to clarify, try viewing the Lightness and Saturation variations on the colour calculator page, choosing quite a bright colour as your starter colour.

So HSL notation looks like this, giving the Hue, Saturation and Lightness values in that order:t

Red: 0° 100% 50% Pale pink: 0° 100% 90% Cyan: 180° 100% 50% Here are the Steps:

  1. Convert your colour to HSL.

  2. Change the Hue value to that of the Hue opposite (e.g., if your Hue is 50°, the opposite one will be at 230° on the wheel — 180° further around).

  3. Leave the Saturation and Lightness values as they were.

  4. Convert this new HSL value back to your original colour notation (RGB or whatever).

Sites like EasyRGB.com can do generic conversion for you from RGB to HSL or vice versa.

Programming Example done in PHP as per reference

Conversion from RGB to HSL

The value above Blue #0000FF rgb(0,0,255) can be presented as Red Hexadecimal 00 + Green Hexadecimal 00 + Blue Hexadecimal FF

$redhex  = substr($hexcode,0,2);
$greenhex = substr($hexcode,2,2);
$bluehex = substr($hexcode,4,2);

It can also be presented as Red Decimal 0 + Green Decimal 0 + Blue Decimal 255

$var_r = (hexdec($redhex)) / 255;
$var_g = (hexdec($greenhex)) / 255;
$var_b = (hexdec($bluehex)) / 255;

Now plug these values into the rgb2hsl routine. Below is my PHP version of EasyRGB.com's generic code for that conversion:

Input is $var_r, $var_g and $var_b from above Output is HSL equivalent as $h, $s and $l — these are again expressed as fractions of 1, like the input values

$var_min = min($var_r,$var_g,$var_b);ttt
$var_max = max($var_r,$var_g,$var_b);
$del_max = $var_max - $var_min;

$l = ($var_max + $var_min) / 2;

if ($del_max == 0)
{
        $h = 0;
        $s = 0;
}
else
{
        if ($l < 0.5)
        {
                $s = $del_max / ($var_max + $var_min);
        }
        else
        {
                $s = $del_max / (2 - $var_max - $var_min);
        };

        $del_r = ((($var_max - $var_r) / 6) + ($del_max / 2)) / $del_max;
        $del_g = ((($var_max - $var_g) / 6) + ($del_max / 2)) / $del_max;
        $del_b = ((($var_max - $var_b) / 6) + ($del_max / 2)) / $del_max;

        if ($var_r == $var_max)
        {
                $h = $del_b - $del_g;
        }
        elseif ($var_g == $var_max)
        {
                $h = (1 / 3) + $del_r - $del_b;
        }
        elseif ($var_b == $var_max)
        {
                $h = (2 / 3) + $del_g - $del_r;
        };

        if ($h < 0)
        {
                $h += 1;
        };

        if ($h > 1)
        {
                $h -= 1;
        };
};

So now we have the colour as an HSL value, in the variables $h, $s and $l. These three output variables are again held as fractions of 1 at this stage, rather than as degrees and percentages. So e.g., cyan (180° 100% 50%) would come out as $h = 0.5, $s = 1, and $l = 0.5.

Next find the value of the opposite Hue, i.e., the one that's 180°, or 0.5, away (I'm sure the mathematicians have a more elegant way of doting this, but):

Calculate the opposite hue, $h2

            $h2 = $h + 0.5;

            if ($h2 > 1)
                    {
                            $h2 -= 1;
                    };

The HSL value of the complementary colour is now in $h2, $s, $l. So we're ready to convert this back to RGB (again, my PHP version of the EasyRGB.com formula). Note the input and output formats are different this time, see my comments at the top of the code:

Input is HSL value of complementary colour, held in $h2, $s, $l as fractions of 1 Output is RGB in normal 255 255 255 format, held in $r, $g, $b Hue is converted using function hue_2_rgb, shown at the end of this code

    if ($s == 0)
    {
            $r = $l * 255;
            $g = $l * 255;
            $b = $l * 255;
    }
    else
    {
            if ($l < 0.5)
            {
                    $var_2 = $l * (1 + $s);
            }
            elset
            {
                    $var_2 = ($l + $s) - ($s * $l);
            };

            $var_1 = 2 * $l - $var_2;
            $r = 255 * hue_2_rgb($var_1,$var_2,$h2 + (1 / 3));
            $g = 255 * hue_2_rgb($var_1,$var_2,$h2);
            $b = 255 * hue_2_rgb($var_1,$var_2,$h2 - (1 / 3));
    };

   // Function to convert hue to RGB, called from above

    function hue_2_rgb($v1,$v2,$vh)
    {
            if ($vh < 0)
            {
                    $vh += 1;
            };

            if ($vh > 1)
            {
                    $vh -= 1;
            };

            if ((6 * $vh) < 1)
            {
                    return ($v1 + ($v2 - $v1) * 6 * $vh);
            };

            if ((2 * $vh) < 1)
            {
                    return ($v2);
            };

            if ((3 * $vh) < 2)
            {
                    return ($v1 + ($v2 - $v1) * ((2 / 3 - $vh) * 6));
            };

            return ($v1);
    };

And after that routine, we finally have $r, $g and $b in 255 255 255 (RGB) format, which we can convert to six digits of hex:

    $rhex = sprintf("%02X",round($r));
    $ghex = sprintf("%02X",round($g));
    $bhex = sprintf("%02X",round($b));

    $rgbhex = $rhex.$ghex.$bhex;

$rgbhex is our answer — the complementary colour in hex.

Since your color background is blue or 0,0,255 the HSL is

Hue (H): 240 degrees / Saturation (S):100% / Lightness (L):4.9%

opposite of 240 is 60 in a circle then convert back to RGB gives a value of #181800

\$\endgroup\$
7
  • \$\begingroup\$ Thanks! But the link talks about generating a "complementary color" (whatever that is) of itself. I'm still clueless on how to use that to solve my problem: make sure that each random color is not similar to the background color. Background color is constant, single color, and there are multiple balls. \$\endgroup\$
    – MestreLion
    Aug 7, 2014 at 10:11
  • \$\begingroup\$ Thanks Krom doing that now. @MestreLion, since the background is constant you can specify a range to your balls that still complements the background. \$\endgroup\$ Aug 7, 2014 at 10:20
  • \$\begingroup\$ Currently Expounding my answer. \$\endgroup\$ Aug 7, 2014 at 10:27
  • \$\begingroup\$ Done. I do not what to change much on the sites explaination because it is explained well enough. Credits to the guys at EasyRGB.com \$\endgroup\$ Aug 7, 2014 at 11:26
  • \$\begingroup\$ Let us continue this discussion in chat. \$\endgroup\$ Aug 7, 2014 at 11:53
1
\$\begingroup\$

I'd like to expand upon @ashes999's idea of creating an outline.

My code is going to be as python-like as I can off the top of my head (I've never written a line of python in my life, but I've glanced over a book once or twice).

def lerp(a,b,value): # assuming your library was not kind enough to give you this for free
    return a + ((b - a) * value);

def drawRandomBall(bgColour,radius,point):
    var ballColour = Colour(random(0,255),random(0,255),random(0,255);
    var outlineColour = Colour(lerp(bgColour.R,255 - ballColour.R,0.5),lerp(bgColour.G,255 - ballColour.G,0.5),lerp(bgColour.B,255 - ballColour.B,0.5));
    fillCircle(point,radius+1,outlineColour);
    fillCircle(point,radius,ballColour);

It may not look particularly pretty and it's not really ideal for production code, but it should suffice as a quick-fix. I haven't tested the code as I don't exactly have a "balls bouncing around the screen" program to hand that I can mutilate to test it.


Edit:

Upon seeing the actual code in use, the situation has flipped on its head, so instead I offer this solution.

Replace lines 276-284 with the following:

# Colour generator
def generateColourNonBG(leeway):
    color = (randint(0,255), randint(0,255), randint(0,255))
    while color[0] >= BG_COLOR[0]-leeway and color[0] =< BG_COLOR[0] + leeway and
    color[1] >= BG_COLOR[1]-leeway and color[1] =< BG_COLOR[1] + leeway and
    color[2] >= BG_COLOR[2]-leeway and color[2] =< BG_COLOR[2] + leeway:
        color = (randint(0,255), randint(0,255), randint(0,255))

# Ball generator
def generateBall():
    Ball(generateColourNonBG(5),
                   radius=randint(10, radius),
                   position=[randint(100, screen.get_size()[0]-radius),
                             randint(100, screen.get_size()[0]-radius)],
                   velocity=[randint(50, 50+vel[0]), randint(50, 50+vel[1])],
                   )                       

# Create the balls
balls = pygame.sprite.Group()
for _ in xrange(BALLS):
    balls.add(generateBall())

In this new simplified version, a factory method spits out the balls, and a specialised method generates the colours. The colour generator tests the randomly generated colour to see if it is within a certain range of closeness to the background colour. A colour is considered too close to the BG colour if all three of its colour channels are within leeway either side of BG. So if leeway is 0, only colours that exactly match BG are refused. I have chosen 5 as the default, which refuses the BG colour and the 5 colours either side. Since BG colour is white, I'm not sure if it will refuse black because the digits wrap round. That can be avoided with use of min and max functions, but I left them out for the sake of brevity.

\$\endgroup\$
8
  • \$\begingroup\$ I'm open to any strategy :) as for a program to mutilate, I can give you that: github.com/MestreLion/rainballs relevant lines would be 279-284 \$\endgroup\$
    – MestreLion
    Aug 7, 2014 at 12:33
  • \$\begingroup\$ I'm using pygame as a library. No Lerp, just basic RGB-HSV-CYMK conversions: pygame.org/docs/ref/color.html \$\endgroup\$
    – MestreLion
    Aug 7, 2014 at 12:35
  • \$\begingroup\$ @MestreLion Oh lordy, now I have to install pygame and python to test it. I've shot myself in the foot with this one XP. Lerping's easy, the lerp function in my answer is all there is to it. (Lerp means 'Linear interpolation'). \$\endgroup\$
    – Pharap
    Aug 7, 2014 at 12:48
  • \$\begingroup\$ @MestreLion Fiddled about a bit, turns out my version of python doesn't like print being called without parentheses. Fixed that, but now I've got errors from pygame not being able to find the right libraries. I'll rewrite my code to fit in with what you've got, but I can't test it until I fix pygame. I'd recommend appending that file to the question if you haven't already (in case anyone is feeling nice enough to write some python for you). \$\endgroup\$
    – Pharap
    Aug 7, 2014 at 13:09
  • \$\begingroup\$ You print is different because you're using Python 3, and I'm still in Python 2. And I'm not sure if pygame was ported to Py3 yet. Bottomline: don't bother :) Question is about colors, you don't need actual balls bouncing around :) \$\endgroup\$
    – MestreLion
    Aug 7, 2014 at 16:23
1
\$\begingroup\$

For a RGB format, this seems to work well enough and is trivial:

diversity_score = (abs(r1 - r2) + abs(g1 - g2) + abs(b1 - b2)) / 765.0

This will give you a score between 0.0 and 1.0. The lower, the harder it is to distinguish two colors.

It should be obvious, but for the sake of completeness: the r, g, b values must be cast to a floating point number first, and it is assumed that their maximum value is 255.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .