FOLLOW-UP ANSWER
I'm posting this as a followup answer to not only clarify my initial answer (which I also just edited), but also to add code snippets of the various concepts. Each step in the R′G′B′to Y process is important, and also must be in the order described or the results will fail.
DEFINITIONS:
sRGB: sRGB is a tristimulus color model which is the standard for the Web, and used on most computer monitors. It uses the same primaries and white point as Rec709, the standard for HDTV. sRGB differs from Rec709 only in the transfer curve, often referred to as gamma.
Gamma: This is a curve used with various methods of image coding for storage and transmission. It is often similar to the perception curve of human vision. In digital, gamma's effect is to give more weight to the darker areas of an image such that they are defined by more bits in order to avoid artifacts such as "banding".
Luminance: (notated L or Y): a linear measure or representation of light (i.e. NO gamma curve). As a measure it is usually cd/m2. As a representation, it's Y as in CIEXYZ, and commonly 0 (black) to 100 (white). Luminance features spectral weighting, based on human perception of different wavelengths of light. However, luminance is linear in terms of lightness/darkness - that is if 100 photons of light measures 10, then 20 would be 200 photons of light.
L* (aka Lstar): Perceptual Lightness, as defined by CIELAB (L*a*b*) Where luminance is linear in terms of the quantity of light, L* is based on perception, and so is nonlinear in terms of light quantities, with a curve intended to match the human eye's photopic vision (approx. gamma is ^0.43).
Luminance vs L:* 0 and 100 are the same in both luminance (written Y or L) and Lightness (written L*), but in the middle they are very different. What we identify as middle grey is in the very middle of L* at 50, but that relates to 18.4 in Luminance (Y). In sRGB that's #777777 or 46.7%.
Contrast: The term for defining a difference between two L or two Y values. There are multiple methods and standards for contrast. One common method is Weber contrast, which is ΔL/L. Contrast is usually stated as a ratio (3:1) or a percentage (70%).
DERIVING LUMINANCE (Y) FROM sRGB
STEP ZERO (un - HEX)
If needed, convert a HEX color value to a triplet of integer values where #00 = 0
and #FF = 255
.
STEP ONE (8 bit to decimal)
Convert 8 bit sRGB values to decimal by dividing by 255:
?? R′decimal = R′8bit / 255 ?? ?? G′decimal = G′8bit / 255 ?? ?? B′decimal = B′8bit / 255
If your sRGB values are 16 bit then convert to decimal by dividing by 65535.
STEP TWO (Linearize, Simple Version)
Raise each color channel to the power of 2.2, the same as an sRGB display. This is fine for most applications. But if you need to make multiple ound trips into and out of sRGB gamma encoded space, then use the more accurate versions below.
?? R′^2.2 = Rlin ?? G′^2.2 = Glin ?? B′^2.2 = Blin
STEP TWO (Linearize, Accurate Version)
Use this version instead of the simple ^2.2 version above if you are doing image manipulations and multiple round trips in and out of gamma encoded space.
function sRGBtoLin(colorChannel) {
// Send this function a decimal sRGB gamma encoded color value
// between 0.0 and 1.0, and it returns a linearized value.
if ( colorChannel <= 0.04045 ) {
return colorChannel / 12.92;
} else {
return Math.pow((( colorChannel + 0.055)/1.055),2.4));
}
}
EDIT TO ADD CLARIFICATION: the sRGB linearization I cited above above uses the correct threshold from the official IEC standard, while the old WCAG2 math uses an incorrect threshold (a known, open bug). Nevertheless, the threshold difference does not affect the WCAG 2 results, which are instead plagued by other factors.
STEP THREE (Spectrally Weighted Luminance)
The normal human eye has three types of cones that are sensitive to red, green, and blue light. But our spectral sensitivity is not uniform, as we are most sensitive to green (555 nm), and blue is a distant last place. Luminance is spectrally weighted to reflect this using the following coefficients:
?? Rlin * 0.2126 + Glin * 0.7152 + Blin * 0.0722 = Y = L
Multiply each linearized color channel by their coefficient and sum them all together to find L, Luminance.
STEP FOUR (Contrast Determination)
There are many different means to determine contrast, and various standards as well. Some equations work better than others depending on the specific application.
WCAG 2.x
The current web page contrast guideline listed in the WCAG 2.0 and 2.1 is simple contrast with an offset:
?? C = ((Llighter + 0.05) / (Ldarker + 0.05)) : 1
This gives a ratio, and the WCAG specifies 3:1 for non-text, and 4.5:1 for text to meet the "AA" level.
However, it is a weak example for a variety of reasons. I'm on record as pointing out the flaws in a current GitHub issue (WCAG #695) and have been researching alternatives.
EDIT TO ADD (Jan 2021):
The replacement to the old WCAG 2 contrast is the APCA:
"Advanced Perceptual Contrast Algorithm"
A part of the new WCAG 3. It is a substantial leap forward. While stable I still consider it beta, and because it is a bit more complicated, probably better to link to the SAPC/APCA GitHub repo for the time being.
Some other previously developed contrast methods in the literature:
Modified Weber
The Hwang/Peli Modified Weber provides a better assessment of contrast as it applies to computer monitors / sRGB.
?? C = (Llighter – Ldarker) / (Llighter + 0.1)
Note that I chose the flare factor of 0.1 instead of 0.05 based on some recent experiments. That value is TBD though, and a different value might be better.
LAB Difference
Another alternative that I happen to like more than others is converting the linearized luminance (L) to L* which is Perceptual Lightness, then just subtracting one from the other to find the difference.
Convert Y to L*:
function YtoLstar(Y) {
// Send this function a luminance value between 0.0 and 1.0,
// and it returns L* - perceptual lightness
if ( Y <= (216/24389) { // The CIE standard states 0.008856 but 216/24389 is the intent for 0.008856451679036
return Y * (24389/27); // The CIE standard states 903.3, but 24389/27 is the intent, making 903.296296296296296
} else {
return Math.pow(Y,(1/3)) * 116 - 16;
}
}
Once you've converted L to L*, then a useful contrast figure is simply:
??? C = Llighter – Ldarker**
The results here may need to be scaled to be similar to other methods. A scaling of about 1.6 or 1.7 seems to work well.
There are a number of other methods for determining contrast, but these are the most common. Some applications though will do better with other contrast methods. Some others are Michaelson Contrast, Perceptual Contrast Length (PCL), and Bowman/Sapolinski.
ALSO, if you are looking for color differences beyond the luminance or lightness differences, then CIELAB has some useful methods in this regard.
SIDE NOTES:
Averaging RGB No Bueno!
OP 2x2p mentioned a commonly cited equation for making a greyscale of a color as:
??? GRAY = round((R + G + B) / 3);
He pointed out how inaccurate it seemed, and indeed — it is completely wrong. The spectral weighting of R, G, and B is substantial and cannot be overlooked. GREEN is a higher luminance than BLUE by an ORDER OF MAGNITUDE. You cannot just sum all three channels together and divide by three and get anything close to the actual luminance of a particular color.
I believe the confusion over this may have come from a color control known as HSI (Hue, Saturation, Intensity). But this control is not (and never intended to be) perceptually uniform!!! HSI, like HSV, are just "conveniences" for manipulating color values in a computer. Neither are perceptually uniform, and the math they use is strictly for supporting an "easy" way to adjust color values in software.
OP's Sample Colors
2x2p posted his code using '#318261','#9d5fb0' as test colors. Here's how they look on my spreadsheet, along with each value in every step along the process of conversion (using the "accurate" sRGB method):
Both are close to middle grey of #777777. Notice also that while the luminance L is just 18, the perceptual lightness L* is 50.