Color replace, part 1

The dynamic color replace thing on the Chameleon page comes out of my long frustration with trying to do this sort of thing in a paint program such as Photoshop, even for simple things like shiny buttons on web pages. Photoshop's color replace tool can sometimes be effective, but most of the time it takes a lot of manual tweaking and still produces ugly (and static!) results. I'm curious if my technique is truly new (I sure haven't seen anyone doing it on the web using translucent PNG images), so I figured I'd describe the approach in some detail, and see if anyone tells me "it's been done."

If it seems that this approach is new enough to justify it, my plan is to build up the software and give it a nice interface usable by regular folk. The idea being that people can do it to their own images, either to make a fixed number of static images, or to make images that allow others to use the color picker to dynamically drag the color around, like in my demo (for instance, a designer might send a url to a client so they can play with the colors). The software for building these would probably work over the web, and would probably use the Canvas functionality available in every good browser (sorry Internet Explorer [1] users, but your browser sucks), lessening the number crunching my server will have to do.

First, let me describe what is actually happening here. What you are seeing is a four-channel PNG image, the channels being red, green, blue, and "alpha," or translucency. As you drag the sliders on the color picker, the color behind the image is being changed dynamically. Assuming you have a reasonable graphics chip, this is quite light on your CPU -- it isn't having to process every pixel individually -- so it happens really fast. And it should work on all modern browsers, without flash or java. Given that the color picker already exists, the code to make this work is trivial: basically one line of code.

What is less trivial is prepping the PNG image in the first place, at least if you want it to look "correct." I start with a 3 channel image, (red green and blue), such as a typical jpeg. Then, after picking a target hue (that is, the base color of an object in the photo that I want to make dynamically changeable), I convert the image to 4 channels, and then individually set the transparency and color of each pixel. Any pixel that is not close to the target hue will be unchanged in color, and 100% opaque. A pixel that is within range will have all four components calculated anew.

For this "create the PNG image" stage, of course, we do have to perform per-pixel calculations -- but this only has to happen once, so if it is a little slow processing all those pixels, not a huge deal. I found it convenient to do this in javascript, using the Canvas element in Firefox [2], but it could be done using an image library like GD or ImageMagick as well. Below I will describe the algorithm to do so.


The original image

Let me start off by simplifying the problem a bit for explanatory purposes. Imagine we have an image of a bright red car, like the corvette image above. Let's pretend that every pixel on the car body has a hue of exactly zero degrees, that is, red. It might be a shade or tint of red (red mixed with white or black) or a desaturated red (red mixed with gray), but still, its hue is exactly red, which by convention is considered to be zero degrees (yellow being 60 degrees, green 120, etc). Let's also assume that every pixel in the image that is not on the car body has a hue that is at least 60 degrees from red: further away from red than either yellow or magenta. In reality it's rare to find an image this pure, but for the sake of discussion, we'll pretend we've got one. Later we'll talk about how to deal with real world images, such as one where a blue sky and green trees are reflected in the red car.

Now we want to make an image that appears unchanged from the original when some of its pixels are partially transparent, and is in front of a pure red background. However, we don't necessarily want to make each pixel as transparent as possible...or things would go rather haywire as the original color decreases in saturation (which would allow us more freedom to form the color by mixing). For instance, we could reproduce a middle gray by making our pixel cyan (a.k.a. "turquoise") and making it 50% transparent. It should be rather obvious that this is probably not what we want. It makes more sense to show some restraint, and just make that pixel middle gray with no transparency.

So given our simplified case, we will want to end up with each pixel on the car body to be some shade of gray (or white or black), relying on the red background showing through to provide all of the redness of each pixel. This way, if we replace the background with white, black, blue, pink, olive, brown, or whatever...the car will look like it is painted that color. If done correctly, it should look as real as the original image of the red car, with reflections and shading looking correct.



The resulting PNG image has its colors modified from the original (as shown in the top image), and made partially transparent (represented by the bottom image, with white representing transparent and black meaning opaque)

Let's get a bit more specific. If the pixel in the original image is a very dark red, we will color the new pixel black and make it slightly transparent so whatever redness is there, comes from the background color. If the original pixel is a pastel red, we make the pixel white and partially transparent. If it is a desaturated red, we make it gray and partly transparent. In all cases -- in our simplified example -- a pixel on the car body will end up being a shade of gray, and the transparency will be determined by how saturated the original pixel is. Note that the shade of gray is not simply the original pixel color converted to gray scale: the top image above doesn't just look like a gray car (or a "black and white" photograph of a red car), but has a sort of strange metallicness to it. The more saturated the original pixel, the more we need to exagerrate its brightness or darkness to compensate for the increased transparency.

Now that we've covered the simple case, let's generalize a bit, to account for less pure images. What if there is an orange pixel in the image? Orange is only 30 degrees from red, so it might be affected. Pure orange might have red, green and blue components of 100%, 50%, and 0%. Our background color, red, is 100%, 0%, 0%. We can get orange by making the pixel yellow (100%, 100%, 0%), and mixing it 50/50 with the red background -- that is, 50% transparency. It's fairly obvious that if we make it any more transparent than 50%, we are not going to be able to get our orange by mixing with red, no matter what the pixel's new color is (assuming we are keeping our values between 0 and 100%, of course).

What about a dark orange, or brown? That might have RGB components of 50%, 25%, 0%. In theory, we could make our pixel a darkish green (0%, 50%, 0%) and make it 50% transparent, but again, we probably don't want to go there. It was impossible to shift the hue of our pixel all the way to green to make a bright orange, so it doesn't really make sense to do so to make a dark orange. Instead, let's only shift it to yellow. We can make it 25% transparent, and then give it RGB components of 33%, 33%, 0% (basically a dark yellow, which would probably look rather olive on your monitor). This case is shown below, where I drew it in 3d to help me visualize it.

The algorithm is fairly simple. First we calculate transparency of a pixel based on saturation and the proximity of the hue to red. Then calculate the new color of the pixel by simple extrapolation. The code to do so, in javascript, is below. Assume our target hue is red [3]. Assume pure red has a saturation of 1.0, and black, gray and white all have a saturation of zero. Alpha ranges from 0 (transparent) to 1 (opaque). RGB components also range from 0 to 1. Finally, we'll assume we have calculated the "hue shift", this is the number of degrees our pixel's hue differs from the target hue, divided by 60 (i.e. yellow and magenta would have a hue shift of 1, red would be 0).

if (hueShift < 1 && saturation > 0) {
 var alpha = 1 - (saturation * (1-hueShift));
 newRgba = {
  red: 1 - ((1 - origRgb.red) / alpha),
  green: origRgb.green / alpha,
  blue: origRgb.blue / alpha,
  alpha: alpha
  };
} else {
 newRgba = {
  red: origRgb.red,
  green: origRgb.green,
  blue: origRgb.blue,
  alpha: 1
  };
}

Now, there is a bit of a problem here. Do we really want to consider an dark orange pixel to be a mixture of red (our item's supposed original color) and yellow? (say, if there is a yellow object reflecting on the surface of a red car). Or maybe we really do want to consider it a mixture of red and a smaller amount of green. Or maybe it is a particularly strong reflection of a pure orange object, or, its an orange object that isn't the car body. Remember, we want it to look correct if we change the color to blue or black or white or whatever, and if we guess wrong, it will look wrong.

This is where things get yucky, and where some human intervention is necessary (and where the quality of the original photograph makes a big difference). I will go into these issues, and how best to deal with them, in part two.


  1. I know there is excanvas, which supports the canvas API in IE. For this kind of pixel level manipulation, though, I seriously doubt its performance would be acceptable. IE does support alpha transparency in PNGs, so the dynamic color-dragging will work, even in IE 5 and 6 via "filters." Prepping the images, however, will require a non-IE browser. Really, though...is the kind of person likely to want this still using IE?
  2. Although Safari and Chrome suppport Canvas, they do not allow you to save a canvas to a file. Firefox does, and it saves the alpha channel with it. I hear this capability is in the nightly WebKit builds, so maybe Safari and Chrome will have it soon.
  3. If the target hue is not red, this is easy to correct while keeping our the algorithm simple. First, convert the color to HSV (hue, saturation, value). Subtract the value of our target hue, and convert back to RGB. Perform our algorithm above. Convert to HSV again, and shift the hue back, then convert back to RGB. As you will see later, we already need to convert it to and from HSV for other reasons, so this is no big deal.