The RGB values stored in digital images are not a linear representation of light intensity. They are gamma-encoded — compressed through a power curve — to match the non-linear sensitivity of human vision and to make more efficient use of limited bit depth. Before performing any mathematically correct color operation, you must reverse this encoding through gamma expansion.
Why Gamma Exists
Human vision is more sensitive to differences in dark tones than bright tones. Gamma encoding allocates more of the available bit range to darker values, where our eyes can tell the difference. An 8-bit channel storing linear light would show visible banding in shadows; gamma-encoded, it distributes those 256 steps more evenly across our perception.
The sRGB Transfer Function
The standard sRGB gamma curve is not a simple power function. It has a linear segment near zero and a power segment for the rest:
function srgbToLinear(c: number): number {
// c is 0–1 (sRGB encoded)
return c <= 0.04045
? c / 12.92
: Math.pow((c + 0.055) / 1.055, 2.4)
}
function linearToSrgb(c: number): number {
// c is 0–1 (linear light)
return c <= 0.0031308
? c * 12.92
: 1.055 * Math.pow(c, 1 / 2.4) - 0.055
}Impact on Distance Calculations
If you calculate Euclidean distance directly on gamma-encoded values, dark color differences are exaggerated and bright color differences are compressed. A distance of 10 between two dark grays will look much more significant than a distance of 10 between two light yellows. Linearizing first ensures that equal distances correspond to equal physical differences in light energy.
When You Can Skip It
For palette extraction via color grouping (as used in our Studio tools), the goal is perceptual similarity rather than physical accuracy. In this case, working directly in sRGB and using a generous distance threshold is often sufficient and much faster. The physical error is acceptable because the grouping threshold already absorbs it.
