It is rather involved. The short answer is, converting Munsell codes into RGB involves interpolation of empirical data in 3D that is highly non-linear. The only data set that is publicly available was collected in the 1930's. Free or inexpensive programs that I have found on the net have proved to be flawed. I wrote my own. But I am jumping ahead. Let's start with the basics.
Munsell codes are different in kind than those other codes, xyY, Lab, and RGB. Munsell notation describes the color of an object - what people experience when they view the object. (Isaac Newton was the first to realize that color is in the eye of the beholder.) Munsell conducted extensive experiments with human subjects and ingenious devices.
The other codes, i.e. xyY, Lab*, and RGB, describe light that has bounced off an object and passed through a convolultion with a rather simple mathematical model of a human eye. Some google-terms are "illuminant," "tri-stimulus," and "CIE standard observer."
Munsell describes the colors of objects as they are perceived under a wide variety of illuminants. Another google-term is "chromatic adaptation." Chromatic adaptation in the brain is automatic if the lighting is not too weird. It is really quite remarkable. Take a piece of typing paper outside under a blue sky. The paper looks white. Take it indoors and look at it under incandescent (yellowish) lights. It still looks white! Munsell tapped into that astonishing processing power empirically. Munsell codes also preserve perceived hue at different chromas. A sky-blue and a powder-blue that Munsell assigns the same hue notation, e.g. 5RP, will appear to the typical human with normal eyesight to be the same hue. More on that in the footnote.
CIE xyY, Lab*, and RGB mean nothing unless an illuminant is specified. Chromatic adaptation for illuminants in the mathematical model is computationally difficult. (Rough but simple approximations can be done using the "Bradford matrices.") The RGB that we use is by default "sRGB," which specifies an illuminant called D65. D65 is something like a cloudless day at noon. The Lab numbers listed by the OP are probably relative to D50, which is more like afternoon or morning light. The xyY numbers might be relative to D50, or they might be relative to an old standard called C. I am not going to check. C was the light from a standard lighting fixture that was relatively inexpensive to build in the 1930's. It is obsolete. But C plays a key role in the answer to the question.
In the 1930's, color scientists were developing the mathematical models. One of the things they did was to take a standard Munsell Book of Color, shine illuminant-C light on the colored chips in the book, and record the data in xyY format. That data-set, called the "Munsell Renotation Data," is the only one that is freely available. Others surely exist, but they are closely held secrets.
Good news though. The data set works good. The Munsell authority today is a company called Gretag Macbeth. I imagine they have voluminous data related to the color-chips they sell. The only numbers I know of that they publish are the D50 Lab and D65 sRGB numbers for a small set of colors on their "Color Checker" cards. I wrote an interpolator based on the old renotation data. It agrees with the numbers for the Color Checker card almost exactly. I regret to inform that so far I have only written code for the conversion that goes the opposite direction from what the OP requested (a year ago, as I type this). It goes from sRGB to Munsell. I click on an image, and the program displays the sRGB and Munsell notations for the area clicked upon. I use it for oil painting.
Footnote: CIE has a standard that is analogous to Munsell. It is called LCh subscripted with a,b. It is Lab* in polar coordinates. The hues are in degrees. Chroma numbers are approximately 5 times the C in Munsell HVC. LCh has its problems though. If you have ever used a photo editor to pump up the vividness of the sky, only to see the blue turn to purple, the program was probably using LCh. When I started writing my program, I was unaware that Bruce Lindloom had done work that parallels what I was doing. His web site was invaluable to me as I finished the project. He designed a space he calls UPLab, which is LCh straightened out to align with Munsell. I had already re-invented LCh and (essentially) UPLab before I discovered Mr. Linbloom's site, but his knowledge of the subject far exceeds mine.
Well, this answer has become its own beast. Many new versions, it was getting stupid long. Many thanks to all of the great many contributors to this answer. But, in order to keep it simple for the masses. I archived all the versions/history of this answer's evolution to my github. And started it over clean on StackOverflow here with the newest version. A special thanks goes out to Mike 'Pomax' Kamermans for this version. He gave me the new math.
This function (pSBC
) will take a HEX or RGB web color. pSBC
can shade it darker or lighter, or blend it with a second color, and can also pass it right thru but convert from Hex to RGB (Hex2RGB) or RGB to Hex (RGB2Hex). All without you even knowing what color format you are using.
This runs really fast, probably the fastest, especially considering its many features. It was a long time in the making. See the whole story on my github. If you want the absolutely smallest and fastest possible way to shade or blend, see the Micro Functions below and use one of the 2-liner speed demons. They are great for intense animations, but this version here is fast enough for most animations.
This function uses Log Blending or Linear Blending. However, it does NOT convert to HSL to properly lighten or darken a color. Therefore, results from this function will differ from those much larger and much slower functions that use HSL.
jsFiddle with pSBC
github > pSBC Wiki
Features:
- Auto-detects and accepts standard Hex colors in the form of strings. For example:
"#AA6622"
or "#bb551144"
.
- Auto-detects and accepts standard RGB colors in the form of strings. For example:
"rgb(123,45,76)"
or "rgba(45,15,74,0.45)"
.
- Shades colors to white or black by percentage.
- Blends colors together by percentage.
- Does Hex2RGB and RGB2Hex conversion at the same time, or solo.
- Accepts 3 digit (or 4 digit w/ alpha) HEX color codes, in the form #RGB (or #RGBA). It will expand them. For Example:
"#C41"
becomes "#CC4411"
.
- Accepts and (Linear) blends alpha channels. If either the
c0
(from) color or the c1
(to) color has an alpha channel, then the returned color will have an alpha channel. If both colors have an alpha channel, then the returned color will be a linear blend of the two alpha channels using the percentage given (just as if it were a normal color channel). If only one of the two colors has an alpha channel, this alpha will just be passed thru to the returned color. This allows one to blend/shade a transparent color while maintaining the transparency level. Or, if the transparency levels should blend as well, make sure both colors have alphas. When shading, it will pass the alpha channel straight thru. If you want basic shading that also shades the alpha channel, then use rgb(0,0,0,1)
or rgb(255,255,255,1)
as your c1
(to) color (or their hex equivalents). For RGB colors, the returned color's alpha channel will be rounded to 3 decimal places.
- RGB2Hex and Hex2RGB conversions are implicit when using blending. Regardless of the
c0
(from) color; the returned color will always be in the color format of the c1
(to) color, if one exists. If there is no c1
(to) color, then pass 'c'
in as the c1
color and it will shade and convert whatever the c0
color is. If conversion only is desired, then pass 0
in as the percentage (p
) as well. If the c1
color is omitted or a non-string
is passed in, it will not convert.
- A secondary function is added to the global as well.
pSBCr
can be passed a Hex or RGB color and it returns an object containing this color information. Its in the form: {r: XXX, g: XXX, b: XXX, a: X.XXX}. Where .r
, .g
, and .b
have range 0 to 255. And when there is no alpha: .a
is -1. Otherwise: .a
has range 0.000 to 1.000.
- For RGB output, it outputs
rgba()
over rgb()
when a color with an alpha channel was passed into c0
(from) and/or c1
(to).
- Minor Error Checking has been added. It's not perfect. It can still crash or create jibberish. But it will catch some stuff. Basically, if the structure is wrong in some ways or if the percentage is not a number or out of scope, it will return
null
. An example: pSBC(0.5,"salt") == null
, where as it thinks #salt
is a valid color. Delete the four lines which end with return null;
to remove this feature and make it faster and smaller.
- Uses Log Blending. Pass
true
in for l
(the 4th parameter) to use Linear Blending.
Code:
// Version 4.0
const pSBC=(p,c0,c1,l)=>{
let r,g,b,P,f,t,h,i=parseInt,m=Math.round,a=typeof(c1)=="string";
if(typeof(p)!="number"||p<-1||p>1||typeof(c0)!="string"||(c0[0]!='r'&&c0[0]!='#')||(c1&&!a))return null;
if(!this.pSBCr)this.pSBCr=(d)=>{
let n=d.length,x={};
if(n>9){
[r,g,b,a]=d=d.split(","),n=d.length;
if(n<3||n>4)return null;
x.r=i(r[3]=="a"?r.slice(5):r.slice(4)),x.g=i(g),x.b=i(b),x.a=a?parseFloat(a):-1
}else{
if(n==8||n==6||n<4)return null;
if(n<6)d="#"+d[1]+d[1]+d[2]+d[2]+d[3]+d[3]+(n>4?d[4]+d[4]:"");
d=i(d.slice(1),16);
if(n==9||n==5)x.r=d>>24&255,x.g=d>>16&255,x.b=d>>8&255,x.a=m((d&255)/0.255)/1000;
else x.r=d>>16,x.g=d>>8&255,x.b=d&255,x.a=-1
}return x};
h=c0.length>9,h=a?c1.length>9?true:c1=="c"?!h:false:h,f=this.pSBCr(c0),P=p<0,t=c1&&c1!="c"?this.pSBCr(c1):P?{r:0,g:0,b:0,a:-1}:{r:255,g:255,b:255,a:-1},p=P?p*-1:p,P=1-p;
if(!f||!t)return null;
if(l)r=m(P*f.r+p*t.r),g=m(P*f.g+p*t.g),b=m(P*f.b+p*t.b);
else r=m((P*f.r**2+p*t.r**2)**0.5),g=m((P*f.g**2+p*t.g**2)**0.5),b=m((P*f.b**2+p*t.b**2)**0.5);
a=f.a,t=t.a,f=a>=0||t>=0,a=f?a<0?t:t<0?a:a*P+t*p:0;
if(h)return"rgb"+(f?"a(":"(")+r+","+g+","+b+(f?","+m(a*1000)/1000:"")+")";
else return"#"+(4294967296+r*16777216+g*65536+b*256+(f?m(a*255):0)).toString(16).slice(1,f?undefined:-2)
}
Usage:
// Setup:
let color1 = "rgb(20,60,200)";
let color2 = "rgba(20,60,200,0.67423)";
let color3 = "#67DAF0";
let color4 = "#5567DAF0";
let color5 = "#F3A";
let color6 = "#F3A9";
let color7 = "rgb(200,60,20)";
let color8 = "rgba(200,60,20,0.98631)";
// Tests:
/*** Log Blending ***/
// Shade (Lighten or Darken)
pSBC ( 0.42, color1 ); // rgb(20,60,200) + [42% Lighter] => rgb(166,171,225)
pSBC ( -0.4, color5 ); // #F3A + [40% Darker] => #c62884
pSBC ( 0.42, color8 ); // rgba(200,60,20,0.98631) + [42% Lighter] => rgba(225,171,166,0.98631)
// Shade with Conversion (use "c" as your "to" color)
pSBC ( 0.42, color2, "c" ); // rgba(20,60,200,0.67423) + [42% Lighter] + [Convert] => #a6abe1ac
// RGB2Hex & Hex2RGB Conversion Only (set percentage to zero)
pSBC ( 0, color6, "c" ); // #F3A9 + [Convert] => rgba(255,51,170,0.6)
// Blending
pSBC ( -0.5, color2, color8 ); // rgba(20,60,200,0.67423) + rgba(200,60,20,0.98631) + [50% Blend] => rgba(142,60,142,0.83)
pSBC ( 0.7, color2, color7 ); // rgba(20,60,200,0.67423) + rgb(200,60,20) + [70% Blend] => rgba(168,60,111,0.67423)
pSBC ( 0.25, color3, color7 ); // #67DAF0 + rgb(200,60,20) + [25% Blend] => rgb(134,191,208)
pSBC ( 0.75, color7, color3 ); // rgb(200,60,20) + #67DAF0 + [75% Blend] => #86bfd0
/*** Linear Blending ***/
// Shade (Lighten or Darken)
pSBC ( 0.42, color1, false, true ); // rgb(20,60,200) + [42% Lighter] => rgb(119,142,223)
pSBC ( -0.4, color5, false, true ); // #F3A + [40% Darker] => #991f66
pSBC ( 0.42, color8, false, true ); // rgba(200,60,20,0.98631) + [42% Lighter] => rgba(223,142,119,0.98631)
// Shade with Conversion (use "c" as your "to" color)
pSBC ( 0.42, color2, "c", true ); // rgba(20,60,200,0.67423) + [42% Lighter] + [Convert] => #778edfac
// RGB2Hex & Hex2RGB Conversion Only (set percentage to zero)
pSBC ( 0, color6, "c", true ); // #F3A9 + [Convert] => rgba(255,51,170,0.6)
// Blending
pSBC ( -0.5, color2, color8, true ); // rgba(20,60,200,0.67423) + rgba(200,60,20,0.98631) + [50% Blend] => rgba(110,60,110,0.83)
pSBC ( 0.7, color2, color7, true ); // rgba(20,60,200,0.67423) + rgb(200,60,20) + [70% Blend] => rgba(146,60,74,0.67423)
pSBC ( 0.25, color3, color7, true ); // #67DAF0 + rgb(200,60,20) + [25% Blend] => rgb(127,179,185)
pSBC ( 0.75, color7, color3, true ); // rgb(200,60,20) + #67DAF0 + [75% Blend] => #7fb3b9
/*** Other Stuff ***/
// Error Checking
pSBC ( 0.42, "#FFBAA" ); // #FFBAA + [42% Lighter] => null (Invalid Input Color)
pSBC ( 42, color1, color5 ); // rgb(20,60,200) + #F3A + [4200% Blend] => null (Invalid Percentage Range)
pSBC ( 0.42, {} ); // [object Object] + [42% Lighter] => null (Strings Only for Color)
pSBC ( "42", color1 ); // rgb(20,60,200) + ["42"] => null (Numbers Only for Percentage)
pSBC ( 0.42, "salt" ); // salt + [42% Lighter] => null (A Little Salt is No Good...)
// Error Check Fails (Some Errors are not Caught)
pSBC ( 0.42, "#salt" ); // #salt + [42% Lighter] => #a5a5a500 (...and a Pound of Salt is Jibberish)
// Ripping
pSBCr ( color4 ); // #5567DAF0 + [Rip] => [object Object] => {'r':85,'g':103,'b':218,'a':0.941}
The picture below will help show the difference in the two blending methods:
Micro Functions
If you really want speed and size, you will have to use RGB not HEX. RGB is more straightforward and simple, HEX writes too slow and comes in too many flavors for a simple two-liner (IE. it could be a 3, 4, 6, or 8 digit HEX code). You will also need to sacrifice some features, no error checking, no HEX2RGB nor RGB2HEX. As well, you will need to choose a specific function (based on its function name below) for the color blending math, and if you want shading or blending. These functions do support alpha channels. And when both input colors have alphas it will Linear Blend them. If only one of the two colors has an alpha, it will pass it straight thru to the resulting color. Below are two liner functions that are incredibly fast and small:
const RGB_Linear_Blend=(p,c0,c1)=>{
var i=parseInt,r=Math.round,P=1-p,[a,b,c,d]=c0.split(","),[e,f,g,h]=c1.split(","),x=d||h,j=x?","+(!d?h:!h?d:r((parseFloat(d)*P+parseFloat(h)*p)*1000)/1000+")"):")";
return"rgb"+(x?"a(":"(")+r(i(a[3]=="a"?a.slice(5):a.slice(4))*P+i(e[3]=="a"?e.slice(5):e.slice(4))*p)+","+r(i(b)*P+i(f)*p)+","+r(i(c)*P+i(g)*p)+j;
}
const RGB_Linear_Shade=(p,c)=>{
var i=parseInt,r=Math.round,[a,b,c,d]=c.split(","),P=p<0,t=P?0:255*p,P=P?1+p:1-p;
return"rgb"+(d?"a(":"(")+r(i(a[3]=="a"?a.slice(5):a.slice(4))*P+t)+","+r(i(b)*P+t)+","+r(i(c)*P+t)+(d?","+d:")");
}
const RGB_Log_Blend=(p,c0,c1)=>{
var i=parseInt,r=Math.round,P=1-p,[a,b,c,d]=c0.split(","),[e,f,g,h]=c1.split(","),x=d||h,j=x?","+(!d?h:!h?d:r((parseFloat(d)*P+parseFloat(h)*p)*1000)/1000+")"):")";
return"rgb"+(x?"a(":"(")+r((P*i(a[3]=="a"?a.slice(5):a.slice(4))**2+p*i(e[3]=="a"?e.slice(5):e.slice(4))**2)**0.5)+","+r((P*i(b)**2+p*i(f)**2)**0.5)+","+r((P*i(c)**2+p*i(g)**2)**0.5)+j;
}
const RGB_Log_Shade=(p,c)=>{
var i=parseInt,r=Math.round,[a,b,c,d]=c.split(","),P=p<0,t=P?0:p*255**2,P=P?1+p:1-p;
return"rgb"+(d?"a(":"(")+r((P*i(a[3]=="a"?a.slice(5):a.slice(4))**2+t)**0.5)+","+r((P*i(b)**2+t)**0.5)+","+r((P*i(c)**2+t)**0.5)+(d?","+d:")");
}
Want more info? Read the full writeup on github.
PT
(P.s. If anyone has the math for another blending method, please share.)
Best Answer
Maybe you already look into this wikipedia article, but to make it clear.
There is a difference between HSL and HSB (aka HSV).
So you can't take the (B)rightness from the color class and use it like a (L)uminosity.
To get back from the Color class provided values
GetHue()
,GetSaturation()
andGetBrightness()
to a normal color you should give this extension method a chance.Update
So just to make things clear. My code above and the three methods within the Color class mentioned above are using the HSB (aka HSV) color model, but Photoshop uses the HSL color model.
In your comment you wrote that the parameters
Hue = 0
,Saturation = 1
andBrightness = 1
give you with the code above a red color and white in Photoshop. When you take a closer look at the differences of these modes this makes absolutely sense:The HSL cylinder
(source: wikimedia.org)
The HSB cylinder
(source: wikimedia.org)
As you can see, Photoshop and the .Net framework (including my extension function) are using different coloring models. So you should check if you find somewhere an implementation of the other coloring model, a transformation or something else that gives you the results you need.