I'm currently working on some visualization code that includes a colorized output option. I'm surprised by the lack of resources about this topic. In particular, if you want to report some data that naturally has a single quantitative channel and a secondary qualitative channel, the obvious solution is to use the HSV color space, fix the saturation somewhere, use the value for the quantitative channel and the hue for the qualitative channel. To test this scheme, we can just visualize the quantitative channel ranging uniformly from 0 to 1 vertically, while the qualitative channel varies horizontally. The result will look something like this (click to enlarge):
The inadequacy of this scheme is clearest if one compares the cyan column with the blue column. The blue column appears to reach an equivalent level of brightness to the cyan column almost 50% lower.
Solutions are found in the unfortunately somewhat obscure field of color calibration. Specifically, my solution is to use a color space known as L*a*b*, with L* representing the quantitative channel, and a* and b* being distributed in a circle, with the angle representing the qualitative channel and the radius expanding with L* (to maximize the gamut). Let's have a look:
That's better, isn't it? Theoretically, if your monitor is calibrated to sRGB, and you are a standard colorimetric observer (whatever that means), and the observing conditions are optimal, every pixel of each row should appear exactly the same brightness, despite the variation in color (well, up to the precision of 24-bit RGB). If you'd like to create this effect yourself, feel free to borrow my GNU C code below, or if you're doing Web-based work, have a look at Gregor Aisch's Javascript port.
/* Copyright (c) David Dalrymple 2011 */
#define TAU 6.283185307179586476925287 // also known as "two pi" to the unenlightened
/* Convert from L*a*b* doubles to XYZ doubles
* Formulas drawn from http://en.wikipedia.org/wiki/Lab_color_space
*/
void lab2xyz(double* x, double* y, double* z, double l, double a, double b) {
double finv(double t) {
return (t>(6.0/29.0))?(t*t*t):(3*(6.0/29.0)*(6.0/29.0)*(t-4.0/29.0));
}
double sl = (l+0.16)/1.16;
double ill[3] = {0.9643,1.00,0.8251}; //D50
*y = ill[1] * finv(sl);
*x = ill[0] * finv(sl + (a/5.0));
*z = ill[2] * finv(sl - (b/2.0));
}
/* Convert from XYZ doubles to sRGB bytes
* Formulas drawn from http://en.wikipedia.org/wiki/Srgb
*/
void xyz2rgb(unsigned char* r, unsigned char* g, unsigned char* b, double x, double y, double z) {
double rl = 3.2406*x - 1.5372*y - 0.4986*z;
double gl = -0.9689*x + 1.8758*y + 0.0415*z;
double bl = 0.0557*x - 0.2040*y + 1.0570*z;
int clip = (rl < 0.0 || rl > 1.0 || gl < 0.0 || gl > 1.0 || bl < 0.0 || bl > 1.0);
if(clip) {
rl = (rl<0.0)?0.0:((rl>1.0)?1.0:rl);
gl = (gl<0.0)?0.0:((gl>1.0)?1.0:gl);
bl = (bl<0.0)?0.0:((bl>1.0)?1.0:bl);
}
//Uncomment the below to detect clipping by making clipped zones red.
//if(clip) {rl=1.0;gl=bl=0.0;}
double correct(double cl) {
double a = 0.055;
return (cl<=0.0031308)?(12.92*cl):((1+a)*pow(cl,1/2.4)-a);
}
*r = (unsigned char)(255.0*correct(rl));
*g = (unsigned char)(255.0*correct(gl));
*b = (unsigned char)(255.0*correct(bl));
}
/* Convert from LAB doubles to sRGB bytes
* (just composing the above transforms)
*/
void lab2rgb(unsigned char* R, unsigned char* G, unsigned char* B, double l, double a, double b) {
double x,y,z;
lab2xyz(&x,&y,&z,l,a,b);
xyz2rgb(R,G,B,x,y,z);
}
/* Convert from a qualitative parameter c and a quantitative parameter l to a 24-bit pixel
* These formulas were invented by me to obtain maximum contrast without going out of gamut
* if the parameters are in the range 0-1
*/
void cl2pix(void* rgb, double c, double l) {
unsigned char* ptr = (unsigned char*)rgb;
double L = l*0.61+0.09; //L of L*a*b*
double angle = TAU/6.0-c*TAU;
double r = l*0.311+0.125; //~chroma
double a = sin(angle)*r;
double b = cos(angle)*r;
lab2rgb(ptr,ptr+1,ptr+2,L,a,b);
}