Math in… Colors
Have you ever looked up a color and found something like rgb(200, 50, 25) or #C83219? How do those represent colors?
Cones and human vision
If you’ve ever covered one eye, you might have noticed it becomes much more difficult to tell how far away things are. Your ability to sense depth visually (depth perception) is based on taking data from two sources (two eyes) and reconciling them. Your ability to sense color similarly depends on several data measurements.
Human eyes contain two types of photoreceptors: rods and cones. Rods detect light and are helpful for seeing in the dark, but don’t tell us much about color. Cones, on the other hand, come in three varieties: S (short wavelength), M (medium wavelength), and L (long wavelength).
While light comes in a wide range of wavelengths, visible light occupies a small slice of that range — about 380-750 nanometers:
The visible spectrum shown within the full electromagnetic spectrum, with select wavelengths indicated in nanometers. (Source: Philip Ronan, via Wikipedia.)
This range is the combined range of wavelengths detectable by your cones. Each cone has a wavelength it is particularly strong at detecting, performing well near that wavelength and more poorly away from it. S cone sensitivity peaks around 440 nm, M cone sensitivity peaks around 540 nm, and L cone sensitivity peaks around 570 nm:
Sensitivity of S, M, and L cones. (Source: Ben Rudiak-Gould, via Wikipedia.)
To see a color, your brain patches together the information it gets from the three types of cones. For example, if light is detected as medium-strong by an L cone, weak by an M cone, and absent by an S cone, your brain interprets it as some shade of red. Exactly what shade of red would depend on the exact strengths.
The RGB color model and pixels
Centuries of experimenting with color have taught us that we can often get a color we want by mixing other colors in the right proportions. For example, if you started with red, blue, and yellow paints, you likely wouldn’t have too much trouble making some versions of green, orange, and purple.
People have similarly found that by mixing red, green, and blue light, we can recreate most of the colors of light that we care about, since the three colors work nicely together, each interacting with the three cone types in different ways. While this RGB (red, green, blue) color model dates back to at least the mid-1800s and played a key role in early color photography, we see it every day in pixels.
A pixel, or smallest 2D digital display unit, is really an array of 3 light emitters — one red, one green, and one blue:
RGB pixel arrays in various devices. (Source: Pengo, via Wikipedia.)
At its most basic, you can think about a pixel as being in a state where each of these three colors is either on or off. For example, we could write 010 to represent a pixel in the state with red off, green on, and blue off. In this notation, there are 8 possible states for a pixel:
We typically call the resulting colors black, red, green, blue, cyan, magenta, yellow, and white. We could think of each of these colors as having coordinates in 3D space. For example, cyan could be more formally written as (0,1,1). In this 3D space, our 8 colors form this color cube:
The 8 colors with 0 and 1 coordinates in RGB-space.
(Each cube is painted with three slightly different colors for depth, but each represents a single hue in the color space.)
With more control over our RGB emitters than simply turning each color on or off, we might be able to set each to dim — halfway between on and off. While (0,0,0) is black and (0,1,0) is green, (0,½,0) would be a darker green. Here are the colors we wind up with if we allow for a dimmed, “half on” state:
The 27 colors with 0, ½, and 1 as coordinates in RGB-space.
Can you spot that darker green? The gray block has coordinates (½,½,½), so it’s the color smack in the middle of the cube. We also gained an orange at (1,½,0), a violet at (½,0,1), and many other nice hues by allowing ½ to be a coordinate.
sRGB and hex color codes
We made our coordinates 0, ½, and 1 for a nice example, but since there is a whole a range of brightnesses between fully on and fully off, the only things deciding what values between 0-1 our coordinates could be are hardware limitations. With a fancy enough display on a device with enough memory, we could have colors with arbitrarily precise RGB coordinates.
This was a barrier for some time, and many devices supported 16, 256, or other (now) shockingly low numbers of colors! In the late 1990s, hardware had improved enough to allow for the sRGB standard, which prescribed 16,777,216 colors and is still in use today. Each RGB emitter is allowed to take one of 2⁸ = 256 brightness values, giving 256³ = 16,777,216 colors.
Here’s what (the outside of) our color cube looks like with 20³ = 8,000 colors:
If you’re already having trouble telling the difference between neighboring colors, imagine how subtle things get with 10 or more colors transitioning between each neighboring pair!
In the sRGB scheme, each light emitter has a coordinate between 0 and 1 given by a fraction in {0/255, 1/255, 2/255, …, 253/255, 254/255, 255/255}. Since the denominators are all 255, we might as well just keep track of the numerators, so one standard way to express the color with coordinates (200/255, 50/255, 25/255) is as rgb(200, 50, 25).
Since 256 = 16², another way these colors are expressed is in hexadecimal (base-16), or “hex” for short. We typically express numbers in decimal (base-10), using the digits {0,1,2,3,4,5,6,7,8,9} in positions that represent powers of 10: 10⁰ = 1, 10¹ = 10, 10² = 100, 10³ = 1000, and so on. A number written as 3407 represents
When expressing numbers base-N, we use digits for the numbers {0,1,2, … ,N-1} and powers of N. You might have seen binary at some point, which is base-2 and uses the digits {0,1}.
In base-16, we need digits for {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}, but it would be a bad idea to use 10-15 as written there, since what would a hexadecimal number like 102 represent? Would it be 10⋅16¹+2⋅16⁰=162 or 1⋅16²+0⋅16¹+2⋅16⁰=258?
To fix this ambiguity, for bases greater than 10 we extend our digits 0-9 with letters of the alphabet. Base-12 uses the digits {0,1,2,3,4,5,6,7,8,9,A,B} with the understanding that A represents 10 and B represents 11. Similarly, base-16 uses the digits {0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F}.
Every number 0-255 can be written as a 2-digit hexadecimal number, possibly with leading zeros. Here are three examples, with a subscript of 16 indicating the number is expressed in hexadecimal:
When representing a color in hexadecimal, we write each coordinate (R, G, and B) as a 2-digit hexadecimal number and concatenate them into a 6-digit hexadecimal number, prefixed by ‘#.’ So, by our computations above, the color rgb(200, 50, 25) can be represented as #C83219.
That’s how our screens show us millions of colors and how we communicate them to each other via coordinates.
Limitations of RGB models
While digital implementations of RGB color models miss colors in the gaps “between” coordinates, there are some colors of light that simply cannot be faithfully represented, even when coordinates are allowed to be arbitrarily precise.
When we blend red, green, and blue together in proportions that keep the overall light intensity constant, we can think about the coordinates as satisfying an equation like R+G+B=C for a luminosity constant C, with 0 ≤ R, G, B ≤ 1. In the case that C=1, the coordinates that satisfy this are barycentric coordinates for a triangle. Plotting points in that triangle according to their color lets us see the hues we can get with our RGB color model:
An RGB color triangle at two levels of detail.
In this triangle, red has coordinates (1,0,0), green (0,1,0), and blue (0,0,1). The cyan, magenta, and yellow hues are a little dimmer than we’re used to, since they’re in some sense at “half brightness” at coordinates (0,½,½), (½,0,½), and (½,½,0), respectively. A gray sits at (⅓,⅓,⅓). Anything else we can get with our RGB model will be a brighter or darker version of the colors in this color triangle.
However, there are wavelengths of light corresponding to colors outside this triangle. In our barycentric coordinates, points outside the triangle require at least one coordinate to be negative and others to possibly be greater than 1. Since our RGB model is additive (i.e., based on mixing nonnegative amounts of red, blue, and green light), we are out of luck when trying to make a color that would require a negative coordinate.
Below are (area-normalized) color-matching functions for the visible spectrum based on particular wavelengths of red, green, and blue, derived from experiments. To make a color in the RGB model, you would find its wavelength on the horizontal scale, draw a vertical line through it, and find the points where it intersects each of the red, green, and blue curves, and then multiply each intersection height by a corresponding number — in this case 72.0962 for red, 1.3791 for green, and 1 for blue. The values obtained give the intensities that red, green, and blue need to be mixed at.
(Normalized) CIE 1931 RGB color-matching functions for noted RGB wavelengths. (Source: PAR, via Wikipedia)
Any wavelength where one or more of the curves has negative height cannot be faithfully represented. Red dips below zero for the 435-545 nanometer range, requiring negative negative coordinates. That is a giant chunk of the blue-green spectrum that we can’t accurately display on a screen! The versions of these colors that we showed earlier in the visible light spectrum are redder than they should be.
So, while an RGB model is pretty good at tricking our brain into thinking it’s seeing most of the colors on the visible spectrum, it can’t get a lot of them quite right.
On the other hand, we can make magenta, and magenta is extra-spectral — it isn’t on the visible spectrum of light. While magenta occurs in nature as the color of some flowers, we see those flowers as magenta because they reflect to us a mixture of blue and red light. In this sense, our RGB model hacks our cones in a way that nature already did!
In comparison, yellow is an honest to goodness color of light with an associated wavelength range. If we perceive something as yellow, it can be because it sends us yellow light or because it sends us a mixture of red and green light. Even though the red light and green light don’t literally combine into a new electromagnetic wave with a wavelength corresponding to yellow, we end up seeing both results as the same color.
For more on this topic, check out: