30
\$\begingroup\$

Possible Duplicate:
Making a symbol appear on any colour

I have to mark a certain item in a way that will make it stick-out in the background. I need it to be surrounded with the color that contrasts the background as much as possible so it will pop out and easily noticeable by the player.

Lets say I know the background is color 'x', how do I find 'y' such that it will be very contrasting to 'x' and easy to notice in a background where 'x' is a dominant color?

I first thought about inverting color 'x' and then I noticed that when 'x' is a medium shade of gray, if I invert 'x' to get 'y', then 'y' is also a medium shade of gray which does not work.

\$\endgroup\$
9
  • 5
    \$\begingroup\$ The "most contrasting color" is often going to be really ugly, is that really what you want, or do you just want a sufficiently contrasting color? \$\endgroup\$
    – user744
    Oct 8, 2012 at 13:28
  • \$\begingroup\$ If you can address the concern you mention that would be exquisite. I was thinking of lowering the bar and thought it would be difficult to answer. It would be much better if it was sufficiently contrasting while taking into account looks. \$\endgroup\$
    – AturSams
    Oct 8, 2012 at 13:52
  • 1
    \$\begingroup\$ A-ha, I knew I had answered this question before, I just couldn't find it on my answers list searching for "color". UKians... \$\endgroup\$
    – user744
    Oct 8, 2012 at 14:20
  • \$\begingroup\$ I decided to take time testing both answers. :) \$\endgroup\$
    – AturSams
    Oct 8, 2012 at 19:53
  • 3
    \$\begingroup\$ Not to diss the great answers below, but - have you considered just using an outline (or in your case, an additional outline)? e.g. take a look how the text here is clearly visible over both light and dark backgrounds, due to it having an outline in a color that strongly contrasts with the text color. Will also make your coloring choice consistent, which is a plus imo. Just a thought. And if you need to really make it stick out, you can animate that outline! \$\endgroup\$
    – Oak
    Oct 8, 2012 at 22:02

5 Answers 5

49
\$\begingroup\$

A quick and easy way - though not 100% precise one - is to consider just the five extreme points white, black, red, green and blue.

First, let's transform RGB into linear space. Officially this is usually done by this formula (assuming the source data is in sRGB, which is the default for most graphic card operations on 8-bit data and nearly every image you see displayed on the web):

enter image description here

... where a = 0.055 for C = {R, G, B}

Or, if you just want a quick approximation, use Clinear = Csrgb2.2.

Black or White?

You can then calculate two helpful values, luminance Y and saturation S. Useful formulas (for our linear-space RGB values) are:

Y = 0.2126 R + 0.7152 G + 0.0722 B

S = (max(R, G, B) - min(R, G, B)) / max(R, G, B)

The second one is the calculation from the HSV colour space.

If the saturation is low enough (pick any value you like; something between 0.3 and 0.5 would work fine), check the luminance; if that's > 0.5, your contrasting colour is black, else if it's < 0.5 the colour is white. For exactly Y = 0.5, both work.

Red, Green or Blue?

For colours with high saturation, you can calculate a hue H and decide according to it. Since I'm lazy, I'll just copy the calculation from Wikipedia (hue is named H2):

enter image description here

enter image description here

If your hue is between 60° and 180°, your colour is green, if it's between 180° and 300°, it's blue, else it's red. Extend it to yellow, turquoise and magenta for more variation, or just use the opposite of the just-calculated hue at maximum saturation and lightness.

\$\endgroup\$
9
  • \$\begingroup\$ +1 This is an amazingly good answer, the main reason I picked Snake's was simplicity, I don't fully understand the part with the (alpha) (Beta) (H2) (C2).. \$\endgroup\$
    – AturSams
    Oct 8, 2012 at 14:52
  • \$\begingroup\$ @ArthurWulfWhite Ignore the C2, and both apha and beta are just used to calculate the hue (H2) - that's all you need. They represent the coordinates in which the colour you're using is on the 2D plane below. \$\endgroup\$ Oct 8, 2012 at 15:02
  • \$\begingroup\$ This is really very cool stuff. So H2 is basically the Hue value? I really love applied mathematics and this seems like a shining example to me. \$\endgroup\$
    – AturSams
    Oct 8, 2012 at 17:26
  • \$\begingroup\$ In the first part, how do I calculate the sRGB from regular RGB and what is a value? \$\endgroup\$
    – AturSams
    Oct 14, 2012 at 23:32
  • \$\begingroup\$ @ArthurWulfWhite: You don't calculate it; "sRGB" is a color space, and you can assume nearly every image you find is already in this color space unless specifically defined otherwise. Quick starting points for further reading: Wikipedia W3C \$\endgroup\$ Oct 15, 2012 at 4:59
11
\$\begingroup\$

The most contrasting color would be the color that is as far as possible from color X. It's easy to get it this way (assuming 0,0,0=black and 1,1,1=white -- floating point colors):

y = rgb_color( 
    x.r > 0.5 ? 0 : 1,
    x.g > 0.5 ? 0 : 1,
    x.b > 0.5 ? 0 : 1
);

The result is quite ugly though, so you might want to consider a few more things:

1) generate only black / white colors:

v = ( x.r + x.g + x.b ) / 3 > 0.5 ? 0 : 1;
y = rgb_color( v, v, v );

2) change the values "0" and "1" to something less sharp

3) work in the HSV color space:

y = hsv_color( 
    ( x.h + 0.5 ) % 1,
    x.s > 0.5 ? 0 : 1,
    x.v > 0.5 ? 0 : 1
);
\$\endgroup\$
9
  • \$\begingroup\$ That is exactly what I just thought myself and as Joe Wreschnig and you mentioned it is not very pretty. It does answer the question perfectly. Working in hsv sounds interesting, however, I fear that both 0 and 1 are red in hsv so for hue I would have to do a (x+0.5)%1.0 kind of thing. and also I will have to keep value slightly above and bellow 0 and 1 to make it less sharp. \$\endgroup\$
    – AturSams
    Oct 8, 2012 at 14:05
  • \$\begingroup\$ @ArthurWulfWhite You're right about hue, I fixed it in my answer. \$\endgroup\$
    – snake5
    Oct 8, 2012 at 14:15
  • 7
    \$\begingroup\$ What you first propose is not even the most contrasting color, RGB is not perceptually uniform nor split along what are thought to be the opponent colors in our visual processing. Change your recommendation from HSV to HSL and your third option is an okay answer, though. But Martin's is waaaay better. \$\endgroup\$
    – user744
    Oct 8, 2012 at 14:15
  • \$\begingroup\$ @JoeWreschnig: Given that I use way more calculations and don't derive many more benefits in terms of results out of them (aside from correct luminance values and thus better B&W/colour threshold), I'm not sure "better" is the right word. More extensible and more easily tweakable, maybe. :) \$\endgroup\$ Oct 8, 2012 at 14:29
  • 1
    \$\begingroup\$ @snake5 I really like this answer the best, it is very simple and concise. I learned a lot from Martin Sojka's answer about converting bases too. I will pick this as the accepted answer cause it is well thought out and a quick fix :) \$\endgroup\$
    – AturSams
    Oct 8, 2012 at 14:54
11
\$\begingroup\$

Just to distill Martin Sojka's excellent answer into something simple to apply, here's how to decide whether black or white text would have higher contrast on a given background color (R, G, B) in the sRGB color space:

const float gamma = 2.2;
float L = 0.2126 * pow( R, gamma )
        + 0.7152 * pow( G, gamma )
        + 0.0722 * pow( B, gamma );

boolean use_black = ( L > pow( 0.5, gamma ) );

This assumes that R, G and B are floating-point numbers ranging from 0.0 to 1.0. If what you have is, say, integers from 0 to 255, convert them to floats and divide them by 255.

(I would not suggest using colored text, both because the human eye has much poorer spatial resolution for color than for luminance, and also because combinations of highly saturated complementary colors tend to be irritating to look at.)

Note that, if L is close to 0.52.2 ≈ 0.2, small changes in the background color could cause the most contrasting text color to flip from black to white or vice versa. To avoid this happening too frequently, you could save the previous text color and only change it if L moves too far from the threshold:

if ( L > 0.3 ) {
    use_black = true;
} else if ( L < 0.1 ) {
    use_black = false;
} else {
    // keep previous text color
}

Ps. If you wanted an even quicker approximation, you could round the exponent gamma down to 2.0, allowing you to replace the pow() with a simple multiplication:

float L = 0.2126 * R*R + 0.7152 * G*G + 0.0722 * B*B;

This approximate formula is still within ±0.05 of the correct luminance calculated with the official piecewise formula given by Martin (or with the gamma = 2.2 approximation used above, which itself is within ±0.01 of the official formula), and so more than close enough for this purpose. Besides, you can mostly compensate for the error simply by adjusting the threshold slightly.

Finally, note that, if the background has a very busy texture, it's possible that no single color may stand out from it very effectively. In such cases, you may want to consider using combinations of colors, such as black text with a white outline.


Ps. Earlier versions of this answer suggested using the uncorrected threshold L > 0.5. However, this suggestion ignores the fact that the gamma correction used in the RGB color space approximates the perceptual non-linearity of the human eye, so that the shade of gray that looks about halfway between black and white is indeed close to RGB(0.5, 0.5, 0.5). Thus, to get an accurate measure of perceptual luminance, we really need to apply a final inverse gamma correction to the linear luminance L — or, equivalently, just apply gamma to the thresholds as well.

This perceptual non-linearity also explains why just linearly mixing the uncorrected RGB components into a luma value, and basing the decision on that, also works reasonably well:

float luma = 0.2126 * R + 0.7152 * G + 0.0722 * B;
boolean use_black = ( luma > 0.5 );

This formula does tend to overestimate the brightness of highly saturated colors; for example, for pure green it gives luma = 0.7152, whereas properly applying gamma correction would give pow(L, gamma) ≈ 0.478. Fortunately, for saturated colors, either black or white will provide a decent contrast anyway.

\$\endgroup\$
3
\$\begingroup\$

Since there is some subjectivity to any answer here, I'll offer one that is easy to understand and easy to compute. Whether it suits you and your needs is up to you.

Imagine color space represented by R, G, and B values each on the interval [0,1]. Imagine the RGB value being a vector in color space, where that vector is confined to a cube. For any RGB value in that space, the most contrasting value would be geometrically as far away from that value while still being on that cube.

Offset the cube, so it is centered at the origin, with new values r, g, and b on the interval [-0.5, 0.5]. Do the by subtracting 0.5 from all of the original RGB values. Now compute the sign of each component of rgb and multiply by -0.5. This will put you at one of the vertices of the offset color space cube. Add 0.5 to each value to restore the values to the original color space cube.

\$\endgroup\$
0
\$\begingroup\$

It depends how you invert the color, the standard way of doing so is something like:

newColor = new Color(255-oldColor.r, 255-oldColor.g, 255-oldColor.b);

Which means that something grey like (128,128,128) will turn into something very grey (127, 127, 127). You may want to try inverting the hue instead. Convert your color to HSV mode and invert the hue color.hue = 360-color.hue.

You could additionally invert the saturation.

Ultimately, it sounds like you're the one choosing the original colors here. So just choose colors that invert nicely.

Another option would be to check for colors that won't change much. Something like:

if((255-oldColor.r)-oldColor.r < 10)
    newColor.r = 0;
\$\endgroup\$
1
  • 2
    \$\begingroup\$ If I convert the hue of a gray color, it would stay gray. \$\endgroup\$
    – AturSams
    Oct 8, 2012 at 13:49

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