Image Processing 4. Desaturation

2 min read June 14, 2025 #image processing #javascript

Next up is the Desaturation filter, which converts a color image to grayscale.

logo4

Homework#

The previous homework assignment was to output each individual image channel as shades of gray. I also provided a hint: gray is a combination of all three colors in equal proportions. Thus, the solution was simply to duplicate the brightness of one channel to the other two:

// Red
for (let i = 0; i < dst.length; i++) {
  let r = src[i] & 0xFF;
  dst[i] = 0xFF000000 | (r << 16) | (r << 8) | r;
}
// Green
for (let i = 0; i < dst.length; i++) {
  let g = (src[i] >> 8) & 0xFF;
  dst[i] = 0xFF000000 | (g << 16) | (g << 8) | g;
}
// Blue
for (let i = 0; i < dst.length; i++) {
  let b = (src[i] >> 16) & 0xFF;
  dst[i] = 0xFF000000 | (b << 16) | (b << 8) | b;
}

Solution example: CodePen

hmwrk1.jpg

Desaturation#

The essence of this filter is to convert a color image to grayscale while preserving its original brightness. An immediate intuitive solution is to sum the color components and divide by 3:

for (let i = 0; i < dst.length; i++) {
  let r = src[i] & 0xFF;
  let g = (src[i] >> 8) & 0xFF;
  let b = (src[i] >> 16) & 0xFF;
  let gray = (r + g + b) / 3;
  dst[i] = 0xFF000000 | (gray << 16) | (gray << 8) | gray;
}

This is probably the fastest method, but it isn't very suitable for human perception. Humans don't perceive all colors uniformly (I'm not talking about vision impairments). Our eyes are most sensitive to green and least sensitive to blue. This means that to accurately preserve brightness in a desaturated image, we must consider perceptual sensitivity to each color component.

For the NTSC television standard:
Gray = 0.3R + 0.59G + 0.11B

for (let i = 0; i < dst.length; i++) {
  let r = src[i] & 0xFF;
  let g = (src[i] >> 8) & 0xFF;
  let b = (src[i] >> 16) & 0xFF;
  let gray = (r * 0.3 + g * 0.59 + b * 0.11);
  dst[i] = (src[i] & 0xFF000000) | (gray << 16) | (gray << 8) | gray;
}

For the sRGB color profile (profiles are a topic for another article):
Gray = 0.2126R + 0.7152G + 0.0722B

for (let i = 0; i < dst.length; i++) {
  let r = src[i] & 0xFF;
  let g = (src[i] >> 8) & 0xFF;
  let b = (src[i] >> 16) & 0xFF;
  let gray = (r * 0.2126 + g * 0.7152 + b * 0.0722);
  dst[i] = (src[i] & 0xFF000000) | (gray << 16) | (gray << 8) | gray;
}

grayscale_alg.png

The first option with the arithmetic mean is noticeably darker than the others. Differences between NTSC and sRGB coefficients are subtle at first glance: in sRGB, Pikachu's cheeks and tongue appear darker, which, in my opinion, better matches the brightness of the original color image.

grayscale.gif

Few more examples:
grayscale2.gif grayscale3.gif

Which algorithm to use depends on the task: