Title: Transparentify JPG images in C#
This example lets you make some pixels in an image part of a transparent background. Use the File menu to open an image file. When you click on the original image on the left, the program converts pixels that have the same color as the one you clicked into transparent pixels. After you have made areas of the image transparent, click Expand Transparency to blend the edges of the transparent background into the adjacent non-transparent pixels.
The following sections explain the key parts of the program.
Overview
The Bitmap class's MakeTransparent method gives an image a transparent background, but it has some major disadvantages. First, the method only affects pixels of the exact color specified and won't change pixels that are close to that color even if they should also be part of the transparent background. Take a look at the middle picture at the top of this post. Many pixels that look white around the edges of the logo are not transparent even though they should be part of the transparent background. Those pixels are not exactly white, so when I clicked on a white pixel in the picture on the left, those pixels were not changed. This is a particular problem with lossy compression formats such as JPG that may slightly alter pixel colors to save space.
The MakeTransparent method also makes all pixels of a given color transparent, even if they should not be part of the transparent background. If you look carefully at the middle picture, you can see that some of the pixels in the chef's hat and in the heart in "Oven" have been made transparent. Those pixels are not outside of the logo so they should not be part of the transparent background, but they were white so the method made them transparent.
This example uses a different method to handle those problems. It performs two main tasks: making pixels transparent and expanding the transparent background.
Making Pixels Transparent
The MakeTransparent method makes all pixels of a given color transparent. It changes pixels even if they are not in a contiguous region and it doesn't change pixels if they are even the tiniest bit different from the target color.
The following Transparentify method colors pixels that match a color approximately and that are contiguous to a given pixel.
// Make the indicated pixel's color transparent.
private Bitmap Transparentify(Bitmap bm_input,
int x, int y, int dr, int dg, int db)
{
// Get the target color's components.
Color target_color = bm_input.GetPixel(x, y);
byte r = target_color.R;
byte g = target_color.G;
byte b = target_color.B;
// Make a copy of the original bitmap.
Bitmap bm = new Bitmap(bm_input);
// Make a stack of points that we need to visit.
Stack<Point> points = new Stack<Point>();
// Make an array to keep track of where we've been.
int width = bm_input.Width;
int height = bm_input.Height;
bool[,] added_to_stack = new bool[width, height];
// Start at the target point.
points.Push(new Point(x, y));
added_to_stack[x, y] = true;
bm.SetPixel(x, y, Color.Transparent);
// Repeat until the stack is empty.
while (points.Count > 0)
{
// Process the top point.
Point point = points.Pop();
// Examine its neighbors.
for (int i = point.X - 1; i <= point.X + 1; i++)
{
for (int j = point.Y - 1; j <= point.Y + 1; j++)
{
// If the point (i, j) is outside
// of the bitmap, skip it.
if ((i < 0) || (i >= width) ||
(j < 0) || (j >= height)) continue;
// If we have already considred
// this point, skip it.
if (added_to_stack[i, j]) continue;
// Get this point's color.
Color color = bm_input.GetPixel(i, j);
// See if this point's RGB vlues are with
// the allowed ranges.
if (Math.Abs(r - color.R) > dr) continue;
if (Math.Abs(g - color.G) > dg) continue;
if (Math.Abs(b - color.B) > db) continue;
// Add the point to the stack.
points.Push(new Point(i, j));
added_to_stack[i, j] = true;
bm.SetPixel(i, j, Color.Transparent);
}
}
}
// Return the new bitmap.
return bm;
}
The method first gets the color of the target pixel at position [x, y]. It then gets the color's red, green, and blue components.
Next, the code creates a bitmap named bm to hold its final result. It then creates a Stack of Point object to keep track of the pixels that it needs to visit. It also makes a Boolean array added_to_stack to keep track of the pixels that have previously been added to the stack. The code pushes the starting point onto the stack, sets its added_to_stack value to true, and makes it transparent.
The method then enters a loop that executes until the stack is empty. Within the loop, the code gets the first point from the stack. It then loops through that point's neighboring pixels. The code checks whether the neighboring pixel:
- Lies within the image
- Has not already been added to the stack
- Has red, green and blue color components that are close to those of the target pixel's color
If the neighbor meets all of those requirements, then the code adds the neighbor to the stack, sets its added_to_stack value to true, and makes the neighbor transparent.
The method continues processing the stack until it has processed all of the pixels that are reachable from the initial target pixel and that have acceptable colors. After it finishes processing all of those pixels, the method returns the result bitmap.
Expanding the Transparent Background
The following picture shows the sample image after I clicked on one of the pixels in the white outer area on the original image.
The result has made most of the appropriate pixels part of the transparent background. The pixels nearest to the edges of the logo are still not transparent because they differed from the target color by too much. They are mostly white, but not white enough.
Notice that the white pixels inside the chef's hat and the heart in "Oven" were not converted into transparent background pixels. They were not reachable from the target point that I clicked, so they keep their original white color.
One problem with the result so far is those almost-white pixels near the edge of the logo. If you draw the result on a white a background, it blends smoothly into the background and produces a nice result. Unfortunately, it you draw the logo on top of some other background, such as the blue and yellow background shown here, the almost-white pixels are clearly visible.
The next step is to expand the transparent background so the almost-white pixels blend into the non-transparent pixels that are next to them. The example program uses the following ExpandTransparency method to do that.
// Make pixels that are near transparent ones partly transparent.
private Bitmap ExpandTransparency(Bitmap input_bm, float max_dist)
{
Bitmap result_bm = new Bitmap(input_bm);
float[,] distances =
GetDistancesToTransparent(input_bm, max_dist);
int width = input_bm.Width;
int height = input_bm.Height;
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
// If this pixel is transparent, skip it.
if (input_bm.GetPixel(x, y).A == 0)
continue;
// See if this pixel is near a transparent one.
float distance = distances[x, y];
if (distance > max_dist) continue;
float scale = distance / max_dist;
Color color = input_bm.GetPixel(x, y);
int r = color.R;
int g = color.G;
int b = color.B;
int a = (int)(255 * scale);
color = Color.FromArgb(a, r, g, b);
result_bm.SetPixel(x, y, color);
}
}
return result_bm;
}
This method first creates a copy of the input bitmap. It then calls the GetDistancesToTransparent method described shortly to find the distance from each pixel to the transparent pixel that is closest to it.
The code then loops through the pixels in the image. If the current pixel is already transparent, the loop skips it. The program also skips the pixel if the distance to the nearest transparent pixel is greater than the maximum distance max_dist.
The idea for the remaining pixels is to scale the pixel's alpha (opacity) component so those that are closest to a transparent pixel are mostly transparent. To do get a pixel's scale, the code divides the distance from the pixel to a transparent pixel by the maximum distance that we care about. The code sets the pixel's alpha component to that scale times the maximum possible component value 255.
For example, suppose dx and dy are five as shown in the pictures above. Then max_dist is dx + dy = 10.0.
Now suppose a particular pixel is one pixel away from a transparent background pixel. In that case, the scale is 1 / 10.0 = 0.1. The code sets that pixel's alpha component to 0.1 * 255 = 25, so the pixel is mostly transparent. The pixel is close to a transparent pixel, so that makes sense.
For another example, consider a pixel that is nine pixels away from a transparent pixel. In that case, the scale factor is 9 / 10.0 = 0.9, so the code sets its alpha component to 0.9 * 255 = 229. This pixel is far from the transparent pixel, so it is mostly opaque.
After it has finished processing all of the image's pixels, the method returns the result bitmap.
Handling Edges
This method is pretty good at smoothing the edges of large areas of transparent pixels, but it does not treat the edges of the image as transparent. For example, consider the picture on the right in the following figure. (It's the same picture as the previous one.)
The pixels along the edge of the logo are almost white. Now suppose the edges of the image come right up to those almost-white pixels. In that case, the almost white pixels along the edges of the image are adjacent to the image's edges, but they are not adjacent to any transparent pixels. That means their opacities will not be adjusted.
You could modify the code to treat the edges of the image as transparent. That would work, but what if you don't want to treat all of the edges as transparent?
An alternative approach is to ensure that the image has a border of at least one transparent pixel along the edges that you want to blend. That's what I've done in this example. The example image has a thin border of white pixels along its edges so the first step can make them transparent.
GetDistancesToTransparent
The following GetDistancesToTransparent method builds an array giving the distances from each pixel in the image to a transparent pixel.
// Return an array showing how far each
// pixel is from a transparent one.
private float[,] GetDistancesToTransparent(
Bitmap bm, float max_dist)
{
int width = bm.Width;
int height = bm.Height;
float[,] distances = new float[width, height];
for (int x = 0; x < width; x++)
for (int y = 0; y < height; y++)
distances[x, y] = float.PositiveInfinity;
// Examine pixels.
int dxmax = (int)max_dist;
if (dxmax < max_dist) dxmax++;
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
// See if this pixel is transparent.
if (bm.GetPixel(x, y).A == 0)
{
for (int dx = -dxmax; dx <= dxmax; dx++)
{
int px = x + dx;
if ((px < 0) || (px >= width)) continue;
for (int dy = -dxmax; dy <= dxmax; dy++)
{
int py = y + dy;
if ((py < 0) || (py >= height)) continue;
float dist = (float)Math.Sqrt(dx * dx + dy * dy);
if (distances[px, py] > dist)
distances[px, py] = dist;
}
}
}
}
}
return distances;
}
The method first creates an array to hold the distances and initializes it so every entry is float.PositiveInfinity. It then loops through all of the image's pixels.
If a pixel is transparent, the code loops through that pixel's neighbors. The code calculates the distance between the transparent pixel and its neighbor. If the calculated distance is less than the neighbor's current distance in the distances array, then the code updates the array.
Summary
This program probably isn't perfect, but it's a lot better than the MakeTransparent method alone. It only makes pixels transparent that are connected to an initial starting pixel. It also catches pixels that have color close to but not exactly the same as the initial pixel. Finally, it allows you to expand from transparent pixels into the adjacent pixels to blend the image's transparent background so it will sit nicely on any other background where you draw it.
Download the example to experiment with it and to see additional details.
|