Image Programming in JavaScript: Converting to Monochrome

In part 1 of this series, we looked at how each pixel of an image is composed of three parts; red, green and blue, and showed how to make histograms to give a summary of each. Towards the end, I showed that they can be averaged in different ways to create a single histogram. In this article, we're going to look at the idea of mixing colors in more depth and show how we can use it to turn color images into monochrome in a variety of ways.

Some Terminology

In the last article, we talked about how each pixel is composed of a red, a green, and a blue component, and how each of those has a value between 0 and 255. What I didn't tell you then was that it's often useful to consider just the red components of pixels in an image, just the green components, or just the blue components.

A channel of an image I1 is an image I2 composed entirely of one component of I1

So when we talk about the red channel of an image, we're talking about the image that results from simply dropping the green and blue components of each of its pixels. It's best to see it in action:

The first image above can be entirely reconstructed from the last three monochrome images, using the first for the red channel, the second for the green channel, and the third for the blue. By the end of the article, we'll show the code for the demo that was used to convert the color image above into each of the monochrome ones.

Monochrome in Black and White

Before we discuss converting a color image into monochrome, it will help to understand a bit more about what we mean by monochrome.

Conceptually, we can consider each pixel of a monochrome image to consist of just one byte of information, instead of three bytes for red, green and blue as in a color image. This byte represents the brightness of a pixel, where 0 is black (no brightness) and 255 is white (no brightness). The values in between represent the grays, from the dark low numbers to the light high colors.

Since monochrome images are composed of values other than (r, g, b), let's update the working definition of a digital image that we used last time:

A Digital Image is a sequence of pixels, each of which is composed of one or more channels. The value of each channel represents its strength in that pixel.

So a monochrome image consists of only one channel, that representing the brightness of each pixel.

The Real World Intrudes

We've got a nice new mental model of a monochrome image, but the real world, as it tends to do, will complicate matters. We're working with color images on the <canvas> element, which means that each pixel needs to be composed of red, green, and blue. In order to display a monochrome image on a <canvas>, we'll need to convert each pixel from one channel, brightness, to three.

A convenient property of RGB images makes this transformation easy: any pixel made up of equal parts red, green, and blue will be gray. Therefore, to convert an image from monochrome to RGB, we simply use the brightness channel of each pixel in the monochrome image as all three channels in the RGB image.

Mixing it Up

Reversing the process, to create monochrome images from color ones, offers us a few more options. For every pixel, our task is to take three channels and condense them into one. The obvious thing to do is to simply average them; and indeed, this is exactly what happens by default in most photo editing programs when you convert an image to monochrome.

There's no reason that we need to limit ourselves to that transformation, though. We should consider other options because the average often produces flat, uninteresting pictures. Instead, we can mix the channels any way we want to produce the most interesting result possible.

I've put up a demo where you can play with the mixture of colors. Simply put numbers into each of the three inputs at the bottom of the page and hit "desaturate" to create a monochrome image where the channels have been weighted proportionally to the numbers you've entered. The histogram below the desaturate button will show you the effects of your mix.

Play with the values to find the result you find most pleasing, and notice how different the images that result from each mix can be.

Show Me The Goods

Here's the important parts of the code in the demo, with the fiddly bits stripped out:

function desaturate(rweight, gweight, bweight) {
  //normalize the color weights
  var scale = 1 / (rweight + gweight + bweight);
  rweight *= scale;
  gweight *= scale;
  bweight *= scale;

  each_pixel(image_data, function(r, g, b) {
    var brightness = r * rweight + g * gweight + b * bweight;

    //replace the r, g, and b values of the pixel with "brightness"
    return [brightness, brightness, brightness];
  });
}

//red channel only:
desaturate(1, 0, 0);

//green channel only:
desaturate(0, 1, 0);

//blue channel only:
desaturate(0, 0, 1);

//my favorite mix:
desaturate(5, 1, 4);

First we convert the color weights into percentages, then we multiply each component of each pixel times its weight to arrive at a new value for the pixel to take. Finally, we use the brightness value we've calculated as the value for all three channels to render the image in grayscale.

Conclusion

In this article, we've extended our definition of an image to include images with channels other than just red, green, and blue. We learned how to convert monochrome images for display in RGB, and looked at a couple different ways to convert back from RGB to monochrome. Finally, we looked at code to do the conversions we'd spent the whole article talking about.

Hopefully you have a pretty good grasp on what an image on a <canvas> is by now. If you want to download the code for the demos I've shown so far and play with it, go ahead and check it out at github.

Any comments, criticisms, or thoughts on what you'd like to see me write about, you can let me know by sending me an email.

Mar 05, 2009