Make a blended composite image in C#

[composite image]

The other day I wanted to do something that all of us do from time to time: make a composite image with the Eye of Sauron on top of another picture. This turned out to be a bit more complicated than I had hoped it would be.


Desired Results

Just making a composite image isn’t too hard. For example, you can use the Graphics class’s DrawImage method to draw one image on top of another. The result is a rectangular image dropped on top of the background image as shown in the picture below.


[composite image]

This probably isn’t quite what you had in mind.

You can improve the result by using the foreground image to make a TextureBrush and then filling an ellipse with that brush. The result is an ellipse filled with part of the foreground image, as shown in the following picture.


[composite image]

This is much better, but it’s still not perfect because the top image still has a sharp edge, even if it is now elliptical.

What I really want is the top image to fade away as it approaches its edges. The picture on the left below shows the Eye of Sauron fading away near its edges. The picture on the right shows the result when you place this image on top of the background image.


[composite image] [composite image]

What we really need is a brush that fades away at its edges.

Alternatively, it would be nice to have a brush that only applies to an image’s alpha (transparency) channel. We could use that brush to make the alpha values of the foreground image fade away as shown in the picture on the left above. Then we could draw the result on top of the background image.

Unfortunately .NET doesn’t give us that kind of brush. However, we can create a brush that makes a solid color fade away at the edges of an ellipse. Then we can write a method that copies only the alpha values of the resulting image’s pixels onto the foreground image. The result is the foreground image fading toward the edges of the ellipse.

Fading To The Edges

When you select the File menu’s Foreground Image command, the following code lets you select a new foreground image.

private Bitmap BgImage = null;
private Bitmap FgImage = null;
private RectangleF FgRect = new RectangleF(-10, -10, 1, 1);

private void mnuFileForegroundImage_Click(object sender, EventArgs e)
{
    if (ofdImage.ShowDialog() == DialogResult.OK)
    {
        FgImage = LoadBitmapUnlocked(ofdImage.FileName);
        FgRect = new RectangleF(0, 0,
            FgImage.Width, FgImage.Height);
        AspectRatio = (float)FgImage.Width / (float)FgImage.Height;

        // Make a bitmap that fades from alpha = 255
        // at the center to alpha = 0 at the egdes.
        Bitmap ellipse_bm =
            new Bitmap(FgImage.Width, FgImage.Height);
        using (Graphics gr = Graphics.FromImage(ellipse_bm))
        {
            gr.Clear(Color.Transparent);
            using (GraphicsPath path = new GraphicsPath())
            {
                path.AddEllipse(FgRect);
                using (PathGradientBrush brush =
                    new PathGradientBrush(path))
                {
                    brush.CenterPoint = new PointF(
                        FgImage.Width / 2f,
                        FgImage.Height / 2f);
                    brush.CenterColor = Color.White;
                    brush.SurroundColors = new Color[] { Color.FromArgb(0, 0, 0, 0) };

                    Blend blend = new Blend();
                    blend.Positions = new float[] { 0.0f, 0.5f, 1.0f };
                    blend.Factors = new float[] { 0.0f, 1.0f, 1.0f };
                    brush.Blend = blend;

                    gr.FillPath(brush, path);
                }
            }
        }

        // Copy the alpha values from ellipse_bm to FgRect.
        CopyAlpha(FgImage, ellipse_bm);

        // Show the result.
        picImage.Refresh();
    }
}

Variables BgImage and FgImage hold the foreground and background images. The value FgRect holds the foreground image’s current location and size. When you resize the selection area, the program updates this value to hold the new selection.

The menu item’s Click event handler displays an OpenFileDialog to let you select the foreground image file and loads it into the FgImage variable. It then sets the FgRect value to place the image at its full size and at position (0, 0). The code also calculates the image’s aspect ratio.

Next, the code creates a new bitmap that has the same size as the foreground image to hold an ellipse that fades toward its edges. The code makes an associated Graphics object and clears it with the Transparent color.

The code then makes a GraphicsPath and adds to it an ellipse that fills the bitmap’s area.

It then creates a PathGradientBrush defined by the path. This creates a brush that makes colors fade from a defined center point (which need not actually be in the center of the path) to the points on the path’s edges.

The code sets the brush’s center point to be at the ellipse’s center and sets CenterColor to white so the brush starts with white at its center.

The program then sets the brush’s SurroundColors value to an array holding the colors that the brush should use for the points along the path. This example uses a single color, so the brush repeats that color as needed for the path’s points. This example uses the color black, but the only important thing here is that the surround color’s alpha component is 0 so the color is transparent.

After some experimentation, I decided that the image didn’t look its best if the colors blended linearly from white at the center to transparent at the ellipse’s edges. In that case the background image showed through starting at the image’s center.

Instead I wanted the color to remain solid until partway to the edge and then start to fall off into transparency. To do that, the code creates a Blend object.

The Positions array indicates the fraction of the distance from the path’s edge points to the center point where we want to define the colors. The Factors array indicates the fraction of the center and edge colors that should be used at the corresponding position.

For example, the position 0.0 means a point at the path’s edge. The factor 0.0 means those points should be 0.0 times the center color and 1.0 – 0.0 = 1.0 times the edge color.

The position 0.5 and factor 1.0 means that points halfway between the edge and center should be 1.0 times the center color and 1.0 – 1.0 = 0.0 times the edge color.

Finally the position 1.0 and factor 1.0 means that points at the center should be 1.0 times the center color and 0.0 times the edge color.

In this example, that means the brush is solid white from the center to halfway to the edge and then fades off to transparent at the edges of the path.

Having created the brush, the program simply fills the new image with the brush.

At this point we have an image that is the same size as the foreground image and that fades from white to transparent. Now the mnuFileForegroundImage_Click event handler calls the CopyAlpha method described next to copy the alpha values from this image onto the foreground image.

The event handler finishes by refreshing the picImage PictureBox control so its Paint event handler can draw the background image with the foreground image on top of it.

Combining Alpha Values

The CopyAlpha method shown below copies the alpha components of the pixels in one image to the pixels of another image.

// Copy the alpha values from mask to bm.
private void CopyAlpha(Bitmap bm, Bitmap mask)
{
    Bitmap32 bm32 = new Bitmap32(bm);
    Bitmap32 mask32 = new Bitmap32(mask);
    bm32.LockBitmap();
    mask32.LockBitmap();

    for (int x = 0; x < bm.Width; x++)
    {
        for (int y = 0; y < bm.Height; y++)
        {
            bm32.SetAlpha(x, y, mask32.GetAlpha(x, y));
        }
    }
    mask32.UnlockBitmap();
    bm32.UnlockBitmap();
}

The Bitmap class has GetPixel and SetPixel methods that you can use to do this. Unfortunately those methods are pretty slow. The CopyAlpha method uses the Bitmap32 class described in Use the Bitmap32 class to manipulate image pixels very quickly in C# to do the same thing much more quickly.

The method creates Bitmap32 objects representing the main image and the mask image that defines the alpha values. It then loops through the images’ pixels.

For each pixel, the method uses GetAlpha to get the mask pixel’s Alpha value. It then uses SetAlpha to set the main pixel’s alpha value.

Paint

The following code shows how the program’s Paint event handler draws the background image with the foreground image on top.

// Draw the selection rectangle.
private const int HandleRadius = 4;
private void picImage_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
    e.Graphics.InterpolationMode = InterpolationMode.High;

    try
    {
        // Get the scaled selection rectangle.
        RectangleF scaled_rect = ScaledSelectionRectangle();

        // Draw the foreground image.
        if (FgImage != null)
        {
            e.Graphics.DrawImage(FgImage, scaled_rect);
        }

        // Draw the selection rectangle.
        using (Pen pen = new Pen(Color.Red, 2))
        {
            e.Graphics.DrawRectangle(pen, scaled_rect);

            pen.Color = Color.Yellow;
            pen.DashPattern = new float[] { 5, 5 };
            e.Graphics.DrawRectangle(pen, scaled_rect);
        }

        PointF[] corners =
        {
            new PointF(scaled_rect.Left, scaled_rect.Top),
            new PointF(scaled_rect.Right, scaled_rect.Top),
            new PointF(scaled_rect.Left, scaled_rect.Bottom),
            new PointF(scaled_rect.Right, scaled_rect.Bottom),
        };
        foreach (PointF point in corners)
        {
            e.Graphics.DrawBox(Brushes.White, Pens.Black, point, HandleRadius);
        }
    }
    catch
    {
    }
}

After setting the Graphics object’s SmoothingMode and InterpolationMode properties, the code calls the ScaledSelectionRectangle method to get the currently selected area.

This example uses techniques described in the post Crop scaled images to a desired aspect ratio in C# to let the user select areas on the image even if the image is scaled. It’s a fairly involved technique so I don’t want to describe it again here. Download that example to see how it works.

After it has the scaled selection rectangle, the Paint event handler draws the draws the foreground image in that rectangle. There are a couple of things to note here. First, the image stored in FgImage has been modified by the CopyAlpha method so it fades from its center toward its edges. The program makes the image image fade when it loads the image, so it doesn’t need to worry about that again here.

Second, notice that the Paint event handler does not draw the background image. When you load the background image or change the scale, the program sets the picImage control’s Image property to the scaled background image. That means when the Paint event handler executes, the background image is already automatically drawn. The Paint event handler just needs to draw anything that should be on top of the background image.

After it draws the foreground image in the current selection rectangle, the code draws the rectangle. It makes a red pen and draws the rectangle. It then changes the pen to yellow, gives it a dash pattern, and redraws the rectangle. The result is a red and yellow dashed rectangle.

The code then creates an array holding the selection rectangle’s corners. It loops through those corners and uses the DrawBox extension method to draw white rectangles at the corners. The DrawBox extension method is relatively simple so I won’t show it here. Download the example to see how it works.

Conclusion

The most important technique used by this example is the way it prepares the foreground image when it is loaded. It creates an ellipse that shades from solid white to transparent at its edges. It then uses the CopyAlpha method to copy the alpha components from the ellipse to the foreground image. After that, using the foreground image is relatively simple.

I know that this example includes a ton of other details that I don’t cover here. In particular it uses techniques described in the post Crop scaled images to a desired aspect ratio in C# to let you select an area in a scaled image while preserving the foreground image’s aspect ratio. I’m sorry that I’ve left so much out of this post (and even the previous one), but this would be a very long post if I included every details.

Download the example program and look at the earlier example to see additional details. Overall I think you’ll find that this program works pretty well and lets you make some interesting images.


Download Example   Follow me on Twitter   RSS feed   Donate




About RodStephens

Rod Stephens is a software consultant and author who has written more than 30 books and 250 magazine articles covering C#, Visual Basic, Visual Basic for Applications, Delphi, and Java.
This entry was posted in algorithms, graphics, image processing and tagged , , , , , , , , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.