Avishkar Autar · Aug 14 2009 · Graphics and Rendering
From time to time, I’ve run across the problem of trying to get rid of a white background of an image with a design that’s mostly the same shade throughout. The hard part is not getting rid of the white background, but getting all the “transition pixels” (i.e. those that allow the design’s edges to gradually fade into the background) to have a somewhat accurate alpha (i.e. transparency) value, so that the design can then be taken and blended nicely atop an arbitrary background without the very common halo effect; this is shown below, trying to remove a white background with Photoshop’s magic wand.
There are ways to mitigate the issue shown above in Photoshop (see post), but none that are truly simple to the point where they can be done with a click of the mouse. This isn’t really a problem with Photoshop; after all, PS is made to be a general solution and what I’m presenting here is a very specific case.
Anyways, I finally stumbled across the idea of using a user-defined background color and foreground color, and using some bit of magic to interpolate between the values to generate a valid alpha channel. My first instinct was to compute the saturation value of a pixel and use it to find an alpha value for the pixel. However, after a bit of investigation, I realized this wasn’t the correct approach. From Wikipedia’s article on the HSL and HSV color spaces,
There are two factors that determine the “strength” of the saturation. The first is the distances between the different RGB values. The closer they are together, the more the RGB values neutralize each other, and the less the hue is emphasized, thus lowering the saturation. (R = G = B means no saturation at all.) The second factor is the distance the RGB values are from the midpoint. This is because the closer they are to 0, the darker they are until they are totally black (and thus no saturation); and, the closer they get to MAX value, the brighter they are until they are until totally white (and once again no saturation).
Note that grayscale pixels (R = G = B) are considered totally unsaturated, and this could easily lead to problematic cases – definately cases of a black design on a white background.
Moving on, I realized that lightness was a better indicator and it was surprisingly easy to calculate: l = 0.5(max + min). I decided to use a fixed background color of white, just to make things easier, so based upon the lightness of the background (1.0) and foreground color (supplied by the user), I computed the minimum (minL = lightness of the darker, foreground color) and computed the lightness value of each pixel in the image. In general, lightness values should increase as you go from the foreground color to the background color and they should be in the range [minL, 1]. I then did a simple linear scale to the [0,1] range, and did a few checks for pixels that were outside the [0,1] range (caused rogue pixels that were darker than the foreground color). I then computed the alpha value, alpha = 1.0 – l, and that was it. You can see the result below.
It’s not perfect. If you zoom in, you will see a white-ish halo, but it’s certainly good enough for many cases. The algorithm could also be refined by replacing the simple linear scaling from [0, minL] to [0,1] with a more “aggressive” function, skewing the lightness values toward 1.0, which could minimize or possibly eliminate the halo effect.
Will post code and application soon. Hopefully, I can also spare some time to work on improving this.