Overlay parts of images in C#

[overlay]

My previous post Brighten pixels in an image in C# lets you brighten certain pixels in an image. Unfortunately is often brightens pixels outside of the area that you want to modify.

For example, in the left side of the picture the raspberries are bright red, but so are parts of the box containing the cake. It would be nice if the previous example only brightened the raspberries and not the parts of the box. (I greatly exaggerated the redness to make the idea more obvious. In practice you would not want the raspberries so extremely red.)

One solution would be to let you select the parts of the image that you wanted to brighten. That would work, but it might be hard. You would need to be able to determine whether a pixel was inside the selected area, which could be hard of the area doesn’t have a simple shape like a rectangle. You might also want to be able to select multiple parts of the image to affect. Even if you did get that all to work the way you wanted it, you would need to apply the same technique to any other programs that you made that modify an image’s pixels.

Rather than taking that approach, I decided to make this example, which lets you overlay parts of one image on another. You load the modified overlay picture (in this case, the one with the reddened raspberries) on the left and the original image on the right. You select the area on the overlay image that you want to use and then you click the button to copy that area onto the picture on the right. You can repeat those steps several times to copy different pieces of the overlay image onto the original image.

This example was designed for exactly the purpose shown here: to copy an area in an overlay picture onto the corresponding area on the original picture. You can’t use it to move the overlay area somewhere else.

The following sections describe the most interesting pieces of this example.

Selecting the Area

The program lets you select an irregular area much as other programs do using MouseDown, MouseMove, and MouseUp event handlers. The big difference is that this example assumes that the image you are viewing may have been scaled. To keep track of the selected area, the program uses two lists of points, one at 1:1 scale and one at the currently selected scale.

The following code shows the variables that the program uses to select an area.

private bool Drawing = false;
private List<PointF> SelectedArea = new List<PointF>();
private List<PointF> ScaledSelectedArea = new List<PointF>();

The Drawing variable is true while you have the mouse down and are drawing to select an area. The SelectedArea list holds the area’s points in 1:1 coordinates. The ScaledSelectedArea list holds the area’s points at the current scale.

When you press the mouse down over the overlay picture, the following event handler executes.

// Start selecting an area.
private void picOverlay_MouseDown(object sender, MouseEventArgs e)
{
    if (picOverlay.Image == null) return;

    Drawing = true;

    ScaledSelectedArea = new List<PointF>();
    ScaledSelectedArea.Add(e.Location);

    SelectedArea = new List<PointF>();
    SelectedArea.Add(new PointF(e.X / ImageScale, e.Y / ImageScale));

    picOverlay.Refresh();
}

This code simply returns if you have not loaded a picture. If you have loaded a picture, it sets Drawing to true so the MouseMove event handler knows that you are drawing a selection area.

Next the code creates a new ScaledSelectionArea list and adds the current point to it. It then creates a new SelectionArea list, unscales the current point, and adds the result to the list.

For example, suppose the current scale is 0.5 so then the image is being displayed at half its true size. Then a pixel on the PictureBox represents two pixels on the full-scale image. If the point is at (10, 30) on the scaled image, then the corresponding point on the full-scale image is at (10 / 0.5, 30 / 0.5) = (20, 60).

After it saves the new point, the code refreshes the PictureBox so it can redraw itself. In particular, that removes any previous selection area from the picture.

When you move the mouse over the overlay picture, the following event handler executes.

// Continue selecting an area.
private void picOverlay_MouseMove(object sender, MouseEventArgs e)
{
    if (!Drawing) return;
    ScaledSelectedArea.Add(e.Location);
    SelectedArea.Add(new PointF(e.X / ImageScale, e.Y / ImageScale));
    picOverlay.Refresh();
}

If you are not correctly drawing a selection area, the event handler simply returns. If you are drawing a selection area, then the code adds the current point to the ScaledSelectionArea list and adds the unscaled point to the SelectedArea list.

The code finishes by refreshing the PictureBox to make it display the current selection area.

When you move the mouse over the overlay picture, the following event handler executes.

// Finish selecting an area.
private void picOverlay_MouseUp(object sender, MouseEventArgs e)
{
    Drawing = false;
    picOverlay.Refresh();
    btnCopy.Enabled = (SelectedArea.Count > 2);
}

This code sets Drawing to false so future MouseMove events know that you are not drawing a selection area. It then refreshes the PictureBox and, if the selection area is defined by at least three points (so it is (probably) not empty), the code enables the Copy button.

Those event handlers let you define the selection area. The following section explains how the program draws that area.

Drawing the Selection Area

The following code shows how the picOverlay control draws the selection area.

// Draw the selection area.
private void picOverlay_Paint(object sender, PaintEventArgs e)
{
    if (SelectedArea.Count < 3) return;

    // Draw the selection area.
    e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
    PointF[] points = ScaledSelectedArea.ToArray();
    using (Pen pen = new Pen(Color.Red, 2))
    {
        if (Drawing) e.Graphics.DrawLines(pen, points);
        else e.Graphics.DrawPolygon(pen, points);

        pen.Color = Color.Yellow;
        pen.DashPattern = new float[] { 5, 5 };

        if (Drawing) e.Graphics.DrawLines(pen, points);
        else e.Graphics.DrawPolygon(pen, points);
    }
}

If the SelectedArea list contains fewer than three points, then it cannot define an area so the code simply exits. Otherwise the program sets the Graphics object’s SmoothingMode.

Next the code converts the ScaledSelectedArea list into an array. It uses the scaled list because it is about to draw on the scaled picture.

The code then creates a thick red pen. If you are currently drawing the selection area, the program draws it as a sequence of lines. If you are not drawing a selection area, then the program draws the area as a polygon.

The code then changes the pen to a dashed yellow pen and draws the area again. The result is a thick red and yellow dashed outline of the area.

Notice that the Paint event handler does not clear the Graphics object and it does not draw the overlay image. The program sets the picOverlay object’s Image property to the overlay image, so it is automatically drawn before the Paint event handler executes. If the event handler called the Graphics object’s Clear method, it would erase the image.

Rescaling the Images

The code shown so far works as long as you don’t change the image’s scale. If you rescale the image, the SelectedArea list’s points are still valid because they are stored at full scale. However, the points in the ScaledSelectedArea list are no longer correct because they were saved at a different scale.

When you select one of the Scale menu’s commands, the following event handler executes to handle this problem.

private void mnuScale_Click(object sender, EventArgs e)
{
    // Get the scale factor.
    ToolStripMenuItem menu_item = sender as ToolStripMenuItem;
    string scale_text = menu_item.Text.Replace("&", "").Replace("%", "");
    ImageScale = float.Parse(scale_text) / 100f;
    ShowScaledImages();

    // Display the new scale.
    mnuScale.Text = "Scale (" + menu_item.Text.Replace("&", "") + ")";

    // Check the selected menu item.
    foreach (ToolStripMenuItem item in mnuScale.DropDownItems)
    {
        item.Checked = (item == menu_item);
    }

    // Scale the selected area.
    ScaleSelectionArea();
}

This code first gets the menu item that raised the Click event. It parses the menu item’s text to see learn the desired scale factor and saves it in variable ImageScale.

The code then calls the ShowScaledImages method to display the overlay and original images at the desired scale. That method is relatively straightforward, so I wont’ show it here. Download the example to see the details.

The Click event handler then loops through the Scale menu’s items to check the one that was selected and uncheck the others.

The event handler finishes by calling the following ScaleSelectionArea method to scale the points that define the selection area.

// Scale the selection area's points and
// save them in the ScaledSelectedArea list.
private void ScaleSelectionArea()
{
    ScaledSelectedArea = new List();
    foreach (PointF point in SelectedArea)
    {
        ScaledSelectedArea.Add(new PointF(
            point.X * ImageScale,
            point.Y * ImageScale));
    }
}

This method creates a new ScaledSelectionArea list. It then loops through the points in the SelectedArea list, scales them, and saves the scaled results into the new ScaledSelectionArea list.

Making the Overlay

The previous section explains how the example lets you select an overlay area. Now it’s time to use that area to perform the overlay.

You could loop through the pixels in the overlay picture, decide if a pixel is in the selected area, and then copy it onto the original picture. That would work, but it would be slow and difficult.

Fortunately there’s a much easier way. Simply use the overlay image to create a TextureBrush and then use that brush to fill the selected area on the original image.

The following code does that when you click the Copy button.

// Copy the selected area from the overlay
// image onto the background image.
private void btnCopy_Click(object sender, EventArgs e)
{
    if (SelectedArea.Count < 3) return;
    btnCopy.Enabled = false;
    mnuFileReset.Enabled = true;

    using (Graphics gr = Graphics.FromImage(MainImage))
    {
        using (TextureBrush brush = new TextureBrush(OverlayImage))
        {
            gr.FillPolygon(brush, SelectedArea.ToArray());
        }
    }

    ShowScaledImages();
}

If the SelectedArea list contains fewer than three points, then it cannot define an area so the method simply returns.

Next the code disables the Copy button because you don’t need to copy the same selection area onto the original image multiple times. That wouldn’t hurt anything, but disabling the button provides extra feedback that clicking it did something.

The code also enables the File menu’s Reset command. It doesn’t really make sense to let you reset the main image until after you have copied part of the overlay image onto it. Again, it wouldn’t hurt anything to reset the unmodified image, but this follows the rule of trying to prevent the user from doing things that don’t make sense.

The program then creates a Graphics object associated with the original image, uses the overlay image to make a TextureBrush, and fills the selection area with the brush. Notice that the code is using the unscaled selection area points to draw on the unscaled image MainImage.

Speaking of MainImage, I’ll briefly say a few words about image storage and let you download the example for the rest. The program stores the main image in three different variables.

private Bitmap OriginalMainImage = null;
private Bitmap MainImage = null;
private Bitmap ScaledMainImage = null;

The variable OriginalMainImage holds the original image as loaded from the file. When you select the File menu’s Reset command, the program uses this image to reset the MainImage value.

The value MainImage holds the current main image, which may have been modified by an overlay.

The variable ScaledMainImage holds the current possibly modified image but at the current scale factor.

The following code shows how the program initializes those variables when you select the File menu’s Open Main Image command.

private void mnuFileOpenMainImage_Click(object sender, EventArgs e)
{
    if (ofdImage.ShowDialog() == DialogResult.OK)
    {
        try
        {
            // Load the image.
            OriginalMainImage = LoadBitmapUnlocked(ofdImage.FileName);
            MainImage = new Bitmap(OriginalMainImage);
            mnuFileReset.Enabled = false;
            ShowScaledImages();
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message);
        }
    }
}

This code displays an OpenFileDialog. If you select an image file and click Open, the code loads the file into the OriginalMainImage variable. It then saves a a copy of that image in MainImage.

The code disables the File menu’s Reset command because it doesn’t make sense to reset the image if it has just been loaded and therefore has not changed.

Finally the code calls the ShowScaledImages method. That method simply displays the overlay and original images at the current scale. It’s fairly simple so it’s not shown here.

Conclusion

This example lets you overlay pieces of a modified version of an image on top of the original image. Combined with other programs that modify images, this program lets you make changes to selected areas of the image.

As is often the case, the example is complicated enough that I can’t include every detail here. Download the example program to see additional details and to experiment with it.


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.