|
The problem of locating contrasting colour of a given colour is very important not only in the WEB (have you never seen html pages with unreadable text over unreal background?) but also in user interface (sunken and raisen edges, contour of buttons, etc.). So a lot of people have tryed to resolve it. But... this problem is a physiological problem (see Ewald Hering works) not a mathematical problem! Hence, there is'nt an exact formula to determine it. The human's color sensibility is not linear and is much variables. So, cannot we do anything? No, if we take into consideration the LUMINANCE. It is
Y = (30*Red + 59*Green + 11*Blue)/255
in integer unit of colour components (i.e. 0 < Red,Green,Blue <= 255) and assume values from 0 to 100 (it is, really, a percentage).
So, if we must simply choose a color that contrast with another, we can use this step-function
if Y < 50 then color is white<br />
else color is black<br />
This work well with any color (I believe).
But, if we also take into consideration the chromatic aspect, we must use the HUE. It is the attribute which tells us whether the colour is red, green, etc., and assume values from 0° to 360° (degrees) into the HSL (or HSB) colour space. For a given colour with HUE H (say 130°, green), the contrasting colour will have HUE
<br />
H' = (H + 180)mod360 (in degrees)<br />
that is, in the example, 310° (magenta). In the HSL or HSB colour spaces, ther are two other values that identify a given colour, SATURATION and BRIGHTNESS (or VALUE). This values aren't bind to corresponding perception, so luminance value is different from others. For example, if we have a colour RGB=(102,102,255) with HSB=(240°,60%,100%) and Y=46%, the algorithm used by alucardx give a contrasting colour with RGB=(153,153,0), HSB=(60°,100%,60%) and Y=53%. 53 is too close to original colour luminance, therefore it don't catch the eyes (a text coloured with it over background coloured with original colour is unreadable). But, if we adjust Saturation and Brightness (Hue is fixed) so that the difference from original colour luminance and the contrasting colour luminance be over a certain value (say 30), work is done. With difference threshold of 30, I have obtained RGB=(255,255,102), HSB=(60°,60%,100%), Y=93% (very good). I tested many other values using an appropriate program and result was appreciable.
A coloured life at all
The Ghost in the Mind Machine
|
|
|
|
|
Can you give us some pseudocode? You kinda lost me in that second part of your post.
Thanks
|
|
|
|
|
These are the functions used for computing contrasting color. They are a bit more complex than the function presented in the article, but they work better.;)
COLORREF SfxColorRGBtoHSB(COLORREF color)
{
int nHue, nSat, nBri;
SfxRGBtoHSB(GetRValue(color), GetGValue(color), GetBValue(color),
&nHue, &nSat, &nBri);
return HSB(nHue,nSat,nBri);
}
COLORREF SfxColorHSBtoRGB(COLORREF color)
{
int nRed, nGreen, nBlue;
SfxHSBtoRGB(GetHValue(color), GetSValue(color), GetLValue(color),
&nRed, &nGreen, &nBlue);
return RGB(nRed,nGreen,nBlue);
}
void SfxRGBtoHSB(int nRed, int nGreen, int nBlue, int *nHue, int *nSat, int *nBri)
{
double dRed, dGreen, dBlue;
double dHue, dSat, dBri;
double dMin, dMax, dDelta;
dRed = (double)nRed/255.0;
dGreen = (double)nGreen/255.0;
dBlue = (double)nBlue/255.0;
dMin = min(dRed,dGreen);
dMin = min(dMin,dBlue);
dMax = max(dRed,dGreen);
dMax = max(dMax,dBlue);
dBri = dMax;
if( dMax == dMin )
{
*nSat = 0;
*nHue = HUE_UNDEF;
*nBri = (int)(dBri * 100.0);
return;
}
dDelta = dMax - dMin;
dSat = dDelta / dMax;
if( dRed == dMax ) dHue = (dGreen - dBlue) / dDelta;
else if( dGreen == dMax ) dHue = 2.0 + (dBlue - dRed) / dDelta;
else dHue = 4.0 + (dRed - dGreen) / dDelta;
dHue *= 40.0;
if( dHue < 0 ) dHue += 240.0;
*nHue = (int)dHue;
*nSat = (int)(dSat * 100.0);
*nBri = (int)(dBri * 100.0);
}
void SfxHSBtoRGB(int nHue, int nSat, int nBri, int *nRed, int *nGreen, int *nBlue)
{
int nSector;
double dFract, dVal1, dVal2;
double dRed, dGreen, dBlue;
double dHue, dSat, dBri;
if( (nSat == 0) || (nHue == HUE_UNDEF) )
{
*nRed = *nGreen = *nBlue = (nBri * 255)/100;
return;
}
dHue = (double)nHue;
dSat = (double)nSat/100.0;
dBri = (double)nBri/100.0;
dHue /= 40.0;
nSector = (int)floor(dHue);
dFract = dHue - floor(dHue);
if( !(nSector & 1) ) dFract = 1.0 - dFract;
dVal1 = dBri * (1.0 - dSat);
dVal2 = dBri * (1.0 - dSat * dFract);
switch( nSector )
{
case 0: dRed = dBri; dGreen = dVal2; dBlue = dVal1; break;
case 1: dRed = dVal2; dGreen = dBri; dBlue = dVal1; break;
case 2: dRed = dVal1; dGreen = dBri; dBlue = dVal2; break;
case 3: dRed = dVal1; dGreen = dVal2; dBlue = dBri; break;
case 4: dRed = dVal2; dGreen = dVal1; dBlue = dBri; break;
case 5: dRed = dBri; dGreen = dVal1; dBlue = dVal2; break;
}
*nRed = (int)(dRed * 255.0);
*nGreen = (int)(dGreen * 255.0);
*nBlue = (int)(dBlue * 255.0);
}
COLORREF SfxContrastingColor(COLORREF color, int nThreshold)
{
int nOrigLum, nCalcLum, nLoop;
int nHue, nSat, nBri;
int nRed, nGreen, nBlue;
nRed = GetRValue(color);
nGreen = GetGValue(color);
nBlue = GetBValue(color);
nOrigLum = LUMINANCE(nRed,nGreen,nBlue);
SfxRGBtoHSB(nRed, nGreen, nBlue, &nHue, &nSat, &nBri);
if( nHue == HUE_UNDEF )
{
nRed = nGreen = nBlue = 0;
if( nBri < 50 ) nRed = nGreen = nBlue = 255;
}
else
{
nHue = (nHue + 120) % 240;
nLoop = 20;
while( nLoop )
{
SfxHSBtoRGB(nHue, nSat, nBri, &nRed, &nGreen, &nBlue);
nCalcLum = LUMINANCE(nRed,nGreen,nBlue);
if( abs(nOrigLum - nCalcLum) >= nThreshold ) break;
if( nOrigLum <= 50 )
{
nSat -= 5;
if( nSat < 0 ) nSat += 5;
nBri += 10;
if( nBri > 100 ) nBri = 100;
}
else
{
nSat += 5;
if( nSat > 100 ) nSat = 100;
nBri -= 5;
if( nBri < 10 ) nBri = 10;
}
nLoop--;
}
}
return RGB(nRed,nGreen,nBlue);
}
int SfxGetLuminance(COLORREF crRGB)
{
return LUMINANCE(GetRValue(crRGB),GetGValue(crRGB),GetBValue(crRGB));
}
#define HSB(h,s,b) ((COLORREF)(((BYTE)(b)|((WORD)((BYTE)(s))<<8))|(((DWORD)(BYTE)(h))<<16)))
#define LUMINANCE(r,g,b) ((30*r+59*g+11*b)/255)
#define GetLValue(hsb) ((BYTE)(hsb))
#define GetSValue(hsb) ((BYTE)(((WORD)(hsb)) >> 8))
#define GetHValue(hsb) ((BYTE)((DWORD)(hsb)>>16))
#define HUE_UNDEF 0x000000FF
If you have some questions or new idea, please send me.
The Ghost in the Mind Machine
|
|
|
|
|
Correct me if I'm wrong, but when you say:
Quote: ...LUMINANCE. It is
Y = (30*Red + 59*Green + 11*Blue)/255
you surely meant:
Y = (30*Red + 59*Green + 11*Blue)/100
since 30+59+11 add up to 100 and you want a percentage.
Cheers
Markus
|
|
|
|
|
#define abs(x) ((x) < 0 ? (-(x)) : (x))
abs is a "macro".
#define TOLERANCE 0x40
TOLERANCE is a "symbolic constant".
Old dog learning new tricks!
|
|
|
|
|
thanks for pointing out the mistake. anyway, my skills are self taught, without any proper tutorials or course. i learned c++ through "experimenting", exploring and by studying others' work.
|
|
|
|
|
You can also do this using a RGB Color cube and a distance function. See google for more info.
Todd Smith
|
|
|
|
|
Why not simply take black or white as contrasting color?
If you have a light background color, use black text.
If you have a dark background color, use white text.
Of course you need some tuning to determine what is exactly light or dark, but I'm sure it should also work in most of the cases.
Your algorithm seems to give text that is not very clear on some backgrounds. Try purple for example.
Some other colors could probably also give psychedelic effects.
A second problem is that this method does not always give readable text if the output is converted to grayscale. E.g. run the demo executable, change the background color to 255,128,0. Then take a screen-print, paste it in PaintShop and choose 'Greyscale'. The text becomes unreadable now.
I'm not 100% sure but the same effect could happen when printing this on a non-color printer.
Enjoy life, this is not a rehearsal !!!
|
|
|
|
|
i already stated in the first place my method is not perfect.
the reason why it gives unclear text after converting to greyscale is because it already gives unclear text before converting to greyscale.
anyway, i designed my method without considering anything about greyscale.
|
|
|
|
|
the greyscale test is important for colorblindness usability.
|
|
|
|
|
That's what I did once I needed showing text on a non-fixed background color.
The following function calculates, first of all, the luminance value of a color (which is the gray value that this color would take on a B&W image). If it's greater than a threshold (the greater, the brighter), the returned color is black, otherwise white.
<br />
COLORREF GetReadableTextColor(COLORREF crBackground)<br />
{<br />
BYTE Y = (BYTE) <br />
(<br />
0.299 * GetRValue(crBackground) + <br />
0.587 * GetGValue(crBackground) + <br />
0.114 * GetBValue(crBackground)<br />
);<br />
<br />
return (Y > 140) ? 0x0 : 0xFFFFFF;<br />
}<br />
··--oOOo--(*)(·)--o00o--·· Xah
|
|
|
|
|
does ur code always work?
|
|
|
|
|
Why not just XOR with 0x808080? No need to check for overflows and the colours always contrast (since there's always a difference of 128 between the original and contrasting values for each colour channel). It may not look pretty, but it works perfectly.
|
|
|
|
|
I didn't try this, but this could be a good alternative either.
By the way, your name should be spelled 'Kippensoep' in the new Dutch spelling. Isn't it?
Enjoy life, this is not a rehearsal !!!
|
|
|
|
|
Pfah.. that new Dutch spelling... I refuse to eat anything called "pannenkoeken" or "kippensoep". "Pannekoeken" and "kippesoep" taste much better (although not together).
|
|
|
|
|
You can always eat 'paddestoelen'. That is still without the N.
Enjoy life, this is not a rehearsal !!!
|
|
|
|
|
Your method fails in the face of pure colors, such as (255,0,0) and (220,10,10).
I'm trying to improve my method if i have the time.
Meanwhile, I hope someone could come up with a "perfect" solution so that we can share.
|
|
|
|
|
Nope, it doesn't fail in those cases.
255,0,0 xor 128,128,128 = 127,128,128 (medium grey)
220,10,10 xor 128,128,128 = 92,138,138 (yellowish grey)
It really does work in all cases.
|
|
|
|
|
nah, you claimed that your method is perfect, however, the medium grey doesn't contrast well at all with the red background.
a better contrast in this case is 255,0,0 (red) xor 255,255,255 = 0,255,255 (cyan)
see the image below for the reason why your method doesn't work for pure colors.
http://2respect.netfirms.com/Untitled.gif [^]
Now do you still persist your method is perfect and "work in all cases"?
|
|
|
|
|
"Directly downloading images is not permitted on the Netfirms FREE plan. If you are the owner of this site, either ensure that this image is embedded in a web page, or upgrade to one of the Netfirms premium plans."
Now do you still persist your method is perfect and "work in all cases"?
Yes. It is not perfectly contrasting. If you want that, there are better ways (see below). It is simple, though, and a certain (i.e. constant) level of contrast (being 221 units) is guaranteed. That's perfect in my book. (I meant a perfect method of getting a contrasting colour, not a method for getting a perfectly contrasting colour).
To get the maximum level of contrast, use this (pseudocode):
result = black;
if (source.red < 128) result.red = 255;
if (source.green < 128) result.green = 255;
if (source.blue < 128) result.blue = 255;
That'll always give you maximum contrast (ranging from 211 to 443) by ensuring it's a corner point on the colour cube that is as far away from the orginal colour as possible.
|
|
|
|
|
|
since there's always a difference of 128 between the original and contrasting values for each colour channel
Examples:
RGB=(128,128,128) your method would result in contrast color RGB=(127,127,127)
same with any color close to 128
153 would result in contrast value of 102
135 would result in 120
etc...
Steve
|
|
|
|
|
That's not a difference of 128, that's a difference of 1. Recheck the thing I posted:
128 would result in 0
152 would result in 25
135 would result in 7
(and vice versa, of course).
Wat XOR 0x80 does is simply subtract 128 if the channel value is above 128, and it adds 128 if the value is below that threshold.
|
|
|
|
|
Assuming a color in a COLORREF (format 0xaaBBGGRR) value :
COLORREF l_nColor = 0x00FF7F1F;
Getting the dark and light values :
COLORREF l_nColorDark = (l_nColor >> 1) & 0x007F7F7F);
[EDIT=Fix from Paolo Messina]
COLORREF l_nColorLight = ((l_nColor >> 1) & 0x007F7F7F) + 0x00808080);
[/EDIT]
Now getting the grey values (very accurate 16 bits fixed point version) :
COLORREF l_nColorGrey = ( (((int) (l_nColor & 0x000000FF) >> 0) * 0x00004C8B)
+ (((int) (l_nColor & 0x0000FF00) >> 8) * 0x0000941F)
+ (((int) (l_nColor & 0x00FF0000) >> 16) * 0x00001D2F)
) >> 16;
l_nColorGrey = (l_nColorGrey << 0)
| (l_nColorGrey << 8)
| (l_nColorGrey << 16)
;
This is also how we can get other interresting values, such HSL (Hue, Saturation, Light) :
COLORREF l_nColorHue = ( (((int) (l_nColor & 0x000000FF) >> 0) * 0x00008000)
+ (((int) (l_nColor & 0x0000FF00) >> 8) * 0xFFFF94D1)
+ (((int) (l_nColor & 0x00FF0000) >> 16) * 0xFFFFEB30)
) >> 16
+ 128;
COLORREF l_nColorSat = ( (((int) (l_nColor & 0x000000FF) >> 0) * 0xFFFFD4D1)
+ (((int) (l_nColor & 0x0000FF00) >> 8) * 0xFFFFAB30)
+ (((int) (l_nColor & 0x00FF0000) >> 16) * 0x00008000)
) >> 16
+ 128;
COLORREF l_nColorLight = l_nColorGrey;
Now the contrast can be get from the difference between two grey values.
Imagine you have a dark color, thus the grey value will be below 128. To get a good contrast, write in white above the color.
Imagine you have a light color, thus the grey value will be above 128. To get a good contrast, write in black above the color.
Kochise
In Code we trust !
|
|
|
|
|
Hi Kochise,
Just a small fix... in this line:
COLORREF l_nColorLight = ((l_nColor >> 1) & 0x007F7F7F) + 0x007F7F7F); you probably meant to write:
COLORREF l_nColorLight = ((l_nColor >> 1) & 0x007F7F7F) + 0x00808080); That is you add 128 to each component, instead of 127, so that max value is 255, not 254.
Paolo
------
Why spend 2 minutes doing it by hand when you can spend all night plus most of the following day writing a system to do it for you? - (Chris Maunder)
|
|
|
|
|