So, How Do Monitors Display Greys?
Simple – the same way they display colour. As explained in a older post, screens rely on three primary colours: red, green, and blue. Bright red, for example, is mixed using the following values:
Should you change the red value to 0%, you’re left with black; change all the values to 100% and you get white. Black and white lie at either end of the greyscale gamut – and as you’ve probably figured out already – three equivalent RGB values will result in some shade of grey. For example:
CSS Colour Values
CSS allows for various means of denoting RGB colour. Red is one of the standard CSS colour names, so you could simply type “
red”. Alternatively, CSS allows you to write it out as:
rgb(100%, 0%, 0%)
Of course, the keyword “
red” refers to a very specific shade of red, which may not suit your design. If you wished for a slightly darker red, you could use
rgb(90%,0%,0%). Other named colours include: blue, which is
rgb(0%,100%,0%); or orange, which is
rgb(100%,60%,0%); and so forth. In these examples, each RGB value is represented using percentile measurements, but there are more popular ways to describe colour when programmings websites and applications. Hexadecimal representation is likely to be the format you encounter most. In hexadecimal, red is:
There are six digits and a hash symbol (
F is actually a digit, but more on that later). Or more correctly speaking – there are three pairs of digits representing red, green, and blue, respectively:
Notice that instead of 100% for red, there’s an
FF; and that the 0%’s are now padded to
00 in order to fill two places. We can therefore surmise that 100% is equal to FF, which is actually equal to 255 … but, why not 100, or 99?
To explain this as simply as possible: humans use a base 10 (decimal) counting system. This system works nicely for counting on ten fingers – but using base 16 (hexadecimal) is like counting on sixteen fingers. To make up those extra digits (fingers?), hexadecimal adds a few letters. Below is a comparison: the first line counts up using decimal; the line below it’s the equivalent in hexadecimal:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ... 255
0 1 2 3 4 5 6 7 8 9 A B C D E F 10 11 12 ... FF
Using just two digits, hexadecimal can therefore represent a maximum value of 255, whereas decimal tops-out at 99.
RGB 0–255 Notation
rgb(0-255,0-255,0-255). These are decimal values, as opposed to hexadecimal or percentile. To see behaviour this in action, consider the following HTML:
console.log(style.color) returns the colour value – originally specified in hexadecimal as
#FF0000– logging the following line in the developer console:
rgb(255, 0, 0)
The Averaging Method
Having extracted the RGB value – in this case a red of
rgb(255,0,0) – the next step is to convert it to a grey. It has been established that greys are an equal mix of red, green, and blue, and for this method all three values are added together then averaged out. The formula can be expressed as:
(red + green + blue) ÷ 3 = grey
Substituting in the red values results in:
(255 + 0 + 0) ÷ 3 = 85
Therefore, the greyscale equivalent of
rgb(85,85,85), as presented using the pair of swatches below:
div elements, each with a background-colour corresponding to its source pixel. Pixel art is well-suited to the task at hand – allowing for more discernible areas of colour – so to see this script in action, let’s begin with this image of Nyan Cat:
Firstly, the image is a bit small. However, the script accepts a parameter for scaling, and in this case I’ve used a factor of 3. The averaged conversion has then been placed alongside the original colours to provide a comparison:
The result is satisfactory, although there’s room for improvement – most notably, the purple and blue bands of the rainbow appear in the same shade of grey. This is, in some cases, unavoidable. After all, each RGB channel has 256 (0-255) levels, and as there are three, that’s 16,777,216 (256 × 256 × 256) possible colours to be converted to just 256 possible greys. As a case in point, here’s the math behind how the purple and blue land up the same:
- The purple RGB value is
(102 + 51 + 255) ÷ 3 = 136
Therefore, the averaged grey RGB value is
rgb(136, 136, 136)
- The blue RGB value is
(0 + 153 + 255) ÷ 3 = 136
Therefore, the averaged grey RGB value is also
rgb(136, 136, 136)
To take things a step further, I decided to throw a few more colours into the image, substituting the standard background for a rainbow gradient. In this case the shortcomings of the averaging method are more apparent – just observe how the greyscale background is seemingly comprised of the same shades of grey repeating themselves:
This issue has something to do with how the human eye is more sensitive to certain colours, but to address this, the next method takes human physiology into account to provide improved results.
The Luminosity Method
The luminosity method incorporates the same coefficients as those implemented in GIMP’s (an open source Photoshop alternative) greyscale algorithm:
(red × 0.3 + green × 0.59 + blue × 0.11) = grey
‘Coefficients’ in this context refer to: the weighting of the red, green, and blue channels when converting to a shade of grey. Recall that the averaging method blended the RGB channels using a straight-forward 1:1:1 ratio – that’s 1 part red, to every 1 part green, and 1 part blue, respectively. However, because the human eye is most sensitive to green, and least sensitive to blue, the ratio should be 0.89 : 1.77 : 0.33. The averaged and luminosity pie charts below illustrate this concept as applied for the colour
As you can see, the green ‘slice’ is heavily weighted in the luminosity (right) pie chart, occupying around 213 degrees. So even though it contains a fraction of the total green that could be applied, green is as prominent as the blue. Factoring in this coefficient produces more accurate results, as seen in the luminosity conversion below. Take note of the background rainbow in particular – especially how the blue section of spectrum is clearly darker than the green:
It’s probably simplest to test this out using something like JSFiddle. Start by adding the following HTML code:
The red background colour is now converted to a shade of grey (if you’re using JSFiddle, don’t forget to click “Run” again).
You may have noticed the script’s provision for the
rgb() CSS colour notation. RGBA takes a fourth value between 0 and 1 to indicate a level of opacity (alpha). This is maintained if necessary, so that a semi-opaque colour, i.e.
rgba(253,55,55,0.75), is converted to a semi-opaque grey.
This is a simple example, but you can fiddle with it some more – or better yet, checkout the GitHub repo for something more fully-featured.
This post covered two common formulae for converting colour to greyscale. However, there are more, and if you’re interested in reading further, I highly recommend Tanner Helland’s blog post on the topic.