So if you want distance between 2 colors (r0,g0,b0)
and (r1,g1,b1)
to detect closest color regardless of its intensity (that is what base color means in this case) you should
- Normalize color vectors to common size
- Compute distance
- Scale the result back
// variables
int r0,g0,b0,c0;
int r1,g1,b1,c1,d;
// color sizes
c0=sqrt(r0*r0+g0*g0+b0*b0);
c1=sqrt(r1*r1+g1*g1+b1*b1);
// distance between normalized colors
d = sqrt((r0*c1-r1*c0)^2 + (g0*c1-g1*c0)^2 + (b0*c1-b1*c0)^2) / (c0*c1);
This approach will get unstable comparing dark colors so you can add simple condition like
if (c0<treshold) color is dark
And compare such color only agains the shades of gray or return unknown color. Our vision works similarly we can not safely recognize dark colors ...
Anyway HSV color space is much better for color comparison because it better resembles human color recognition. so convert RGB -> HSV
and compute distance ignoring the value V
which is the intensity of color...
In HSV you need to handle H
as a periodic full circle value so the change can be only half of circle big. S
tells you if it is color or grayscale which has to be handled separately and V
is the intensity.
// variables
int h0,s0,v0;
int h1,s1,v1,d,q;
q=h1-h0;
if (q<-128) q+=256; // use shorter angle
if (q>+128) q-=256; // use shorter angle
q*=q; d =q;
q=s1-s0; q*=q; d+=q;
if (s0<32) // grayscales
{
d=0; // ignore H,S
if (s1>=32) continue; // compare only to gray-scales so ignore this color
}
q=v1-v0; q*=q; d+=q;
Some things to compare ...
You should do a visual check of your source to actually see what is happening otherwise you will go in circles without any results. For example I just now coded this in C++/VCL/mine image class:
union color
{
DWORD dd; WORD dw[2]; byte db[4];
int i; short int ii[2];
color(){}; color(color& a){ *this=a; }; ~color(){}; color* operator = (const color *a) { dd=a->dd; return this; }; /*color* operator = (const color &a) { ...copy... return this; };*/
};
enum{ // this is inside my picture:: class
_x=0, // dw
_y=1,
_b=0, // db
_g=1,
_r=2,
_a=3,
_v=0, // db
_s=1,
_h=2,
};
void rgb2hsv(color &c)
{
double r,g,b,min,max,del,h,s,v,dr,dg,db;
r=c.db[picture::_r]; r/=255.0;
g=c.db[picture::_g]; g/=255.0;
b=c.db[picture::_b]; b/=255.0;
min=r; if (min>g) min=g; if(min>b) min=b;
max=r; if (max<g) max=g; if(max<b) max=b;
del=max-min;
v=max;
if (del<=1e-10) { h=0; s=0; } // grayscale
else{
s=del/max;
dr=(((max-r)/6.0)+(del/2.0))/del;
dg=(((max-g)/6.0)+(del/2.0))/del;
db=(((max-b)/6.0)+(del/2.0))/del;
if (fabs(r-max)<1e-10) h=db-dg;
else if (fabs(g-max)<1e-10) h=(1.0/3.0)+dr-db;
else if (fabs(b-max)<1e-10) h=(2.0/3.0)+dg-dr;
if (h<0.0) h+=1.0;
if (h>1.0) h-=1.0;
}
c.db[picture::_h]=h*255.0;
c.db[picture::_s]=s*255.0;
c.db[picture::_v]=v*255.0;
}
void hsv2rgb(color &c)
{
int i;
double r,g,b,h,s,v,vh,v1,v2,v3,f;
h=c.db[picture::_h]; h/=255.0;
s=c.db[picture::_s]; s/=255.0;
v=c.db[picture::_v]; v/=255.0;
if (s<=1e-10) { r=v; g=v; b=v; } // grayscale
else{
vh=h*6.0;
if (vh>=6.0) vh=0.0;
f=floor(vh); i=f;
v1=v*(1.0-s);
v2=v*(1.0-s*( vh-f));
v3=v*(1.0-s*(1.0-vh+f));
if (i==0) { r=v ; g=v3; b=v1; }
else if (i==1) { r=v2; g=v ; b=v1; }
else if (i==2) { r=v1; g=v ; b=v3; }
else if (i==3) { r=v1; g=v2; b=v ; }
else if (i==4) { r=v3; g=v1; b=v ; }
else { r=v ; g=v1; b=v2; }
}
c.db[picture::_r]=r*255.0;
c.db[picture::_g]=g*255.0;
c.db[picture::_b]=b*255.0;
}
struct _base_color
{
DWORD rgb,hsv;
const char *nam;
_base_color(DWORD _rgb,const char *_nam){ nam=_nam; rgb=_rgb; color c; c.dd=rgb; rgb2hsv(c); hsv=c.dd; }
_base_color(){};
_base_color(_base_color& a){};
~_base_color(){};
_base_color* operator = (const _base_color *a){};
//_base_color* operator = (const _base_color &a);
};
const _base_color base_color[]=
{
// 0x00RRGGBB
_base_color(0x00000000,"Black"),
_base_color(0x00808080,"Gray"),
_base_color(0x00C0C0C0,"Silver"),
_base_color(0x00FFFFFF,"White"),
_base_color(0x00800000,"Maroon"),
_base_color(0x00FF0000,"Red"),
_base_color(0x00808000,"Olive"),
_base_color(0x00FFFF00,"Yellow"),
_base_color(0x00008000,"Green"),
_base_color(0x0000FF00,"Lime"),
_base_color(0x00008080,"Teal"),
_base_color(0x0000FFFF,"Aqua"),
_base_color(0x00000080,"Navy"),
_base_color(0x000000FF,"Blue"),
_base_color(0x00800080,"Purple"),
_base_color(0x00FF00FF,"Fuchsia"),
_base_color(0x00000000,"")
};
void compare_colors()
{
picture pic0;
int h0,s0,v0,h1,s1,v1,x,y,i,d,i0,d0;
int r0,g0,b0,r1,g1,b1,c0,c1,q,xx;
color c;
pic0.resize(256*4,256);
pic0.pf=_pf_rgba;
for (y=0;y<256;y++)
for (x=0;x<256;x++)
{
// get color from image
c=pic0.p[y][x];
xx=x;
r0=c.db[picture::_r];
g0=c.db[picture::_g];
b0=c.db[picture::_b];
rgb2hsv(c);
h0=c.db[picture::_h];
s0=c.db[picture::_s];
v0=c.db[picture::_v];
// naive RGB
xx+=256;
for (i0=-1,d0=-1,i=0;base_color[i].nam[0];i++)
{
// compute distance
c.dd=base_color[i].rgb;
r1=c.db[picture::_r];
g1=c.db[picture::_g];
b1=c.db[picture::_b];
// no need for sqrt
d=((r1-r0)*(r1-r0))+((g1-g0)*(g1-g0))+((b1-b0)*(b1-b0));
// remember closest match
if ((d0<0)||(d0>d)) { d0=d; i0=i; }
}
pic0.p[y][xx].dd=base_color[i0].rgb;
// normalized RGB
xx+=256;
c0=sqrt((r0*r0)+(g0*g0)+(b0*b0));
if (!c0) i0=0; else
for (i0=-1,d0=-1,i=1;base_color[i].nam[0];i++)
{
// compute distance
c.dd=base_color[i].rgb;
r1=c.db[picture::_r];
g1=c.db[picture::_g];
b1=c.db[picture::_b];
c1=sqrt((r1*r1)+(g1*g1)+(b1*b1));
// no need for sqrt
q=((r0*c1)-(r1*c0))/4; q*=q; d =q;
q=((g0*c1)-(g1*c0))/4; q*=q; d+=q;
q=((b0*c1)-(b1*c0))/4; q*=q; d+=q;
d/=c1*c0; d<<=16; d/=c1*c0;
// remember closest match
if ((d0<0)||(d0>d)) { d0=d; i0=i; }
}
pic0.p[y][xx].dd=base_color[i0].rgb;
// HSV
xx+=256;
for (i0=-1,d0=-1,i=0;base_color[i].nam[0];i++)
{
// compute distance
c.dd=base_color[i].hsv;
h1=c.db[picture::_h];
s1=c.db[picture::_s];
v1=c.db[picture::_v];
// no need for sqrt
q=h1-h0;
if (q<-128) q+=256; // use shorter angle
if (q>+128) q-=256; // use shorter angle
q*=q; d =q;
q=s1-s0; q*=q; d+=q;
if (s0<32) // grayscales
{
d=0; // ignore H,S
if (s1>=32) continue; // compare only to grayscales
}
q=v1-v0; q*=q; d+=q;
// remember closest match
if ((d0<0)||(d0>d)) { d0=d; i0=i; }
}
pic0.p[y][xx].dd=base_color[i0].rgb;
}
pic0.bmp->Canvas->Brush->Style=bsClear;
pic0.bmp->Canvas->Font->Color=clBlack;
x =256; pic0.bmp->Canvas->TextOutA(5+x,5,"Naive RGB");
x+=256; pic0.bmp->Canvas->TextOutA(5+x,5,"Normalized RGB");
x+=256; pic0.bmp->Canvas->TextOutA(5+x,5,"HSV");
pic0.bmp->Canvas->Brush->Style=bsSolid;
//pic0.save("colors.png");
}
You can ignore the pic0
stuff it is only pixel access to image. I added few quirks in RGB distance equation to shift the sub-results so they fit inside 32 bit int
s to avoid overflows. As an input I use this image:
And for each pixel is then corresponding base color from LUT found. This is the result:
On the Left is the source image, then naive RGB comparison, then Normalized RGB comparison (can not distinguish between the same color shades) and on the right the HSV comparison.
For the normalized RGB the colors found are alway the first in the LUT from the same color but different intensities. The comparison selects the darker only because they are first in the LUT.
As I mentioned before dark and grayscale colors are problem with this and should be handled separately. If you got similar results and still wrong detection then you need to add more base colors to cover the gaps. If you do not have similar results at all then you got most likely a problem with:
- wrong HSV or RGB ranges mine are
<0,255>
for each channel
overflow somewhere
when multiplying numbers the bits used are summed !!! So
8bit * 8bit * 8bit * 8bit = 32bit
and if the numbers are signed you re in trouble ... if on 32bit variables just like me in the example above so you need to shift the range a bit or use FPU on <0.0,1.0>
intervals.
Just to be sure I added also mine HSV/RGB conversions in case you got some problem there.
And here the original HSV generated conversion: