[Rod Stephens Books]
Index Books Python Examples About Rod Contact
[Mastodon] [Bluesky]
[Build Your Own Ray Tracer With Python]

[Beginning Database Design Solutions, Second Edition]

[Beginning Software Engineering, Second Edition]

[Essential Algorithms, Second Edition]

[The Modern C# Challenge]

[WPF 3d, Three-Dimensional Graphics with WPF and C#]

[The C# Helper Top 100]

[Interview Puzzles Dissected]

Title: Interactively overlay parts of one image on another with Python and PIL

[This program overlays parts of the picture on the left onto the picture on the right]

This is a pretty complicated example and I don't want to cover every details. Download the example to see thhe details. In this post, I'll touch on some of the highlights.

Using the Program

To use the program, invoke the File menu's Background Image and Overlay Image commands to load the background (right) and overlay (left) images that you want to use. Then use the mouse to select an area on the overlay image. The progam draws the selected area in yellow so you can see what you're doing. After you select an area on the left, click the > button to copy that area onto the result picture on the right.

If you select an area and then decide that you don't want to copy it, just use the mouse to select a new area.

In the picture at the top of the post, the upper left and leftmost raspberries have already been overlaid. The raspberry in the lower left has been selected on the left but not yet copied onto the result image on the right.

To get better control over the selected area, it's often useful to zoom in on the images. Use the Scale menu to scale the pictures by factors between 10% and 300%.

The program displays the images in canvas widgets placed inside ScrolledFrame objects. See my post Make a scrolled frame that responds to the mouse wheel in tkinter and Python for information about those.

If you decide you've messed up the result, you can use the File menu's Revert command to remove all of your changes and start over with the original background image.

When you're finished overlaying parts of the image, use the File menu's Save As command to save the result.

Managing Images

The program uses four images to keep track of what's happening.
  • self.background_image - The original unmodified background image.
  • self.overlay_image - The original unmodified overlay image.
  • self.result_image - The background image with pieces overlaid on it.
  • self.current_overlay_image - The overlay image with a selection area drawn on it.
If you keep the imeges' purposes in mind as you read the code, it's not hard to understand what's happening. For example, when you move the mouse, the mouse move event handler updates the selection area's points and then the program draws them on self.current_overlay_image. (More on that in the next section.)

One oddity of tkinter is that it can only display images that are saved in non-volatile variables. If you create an image in a method-local variable, display the image, and then the method ends, the image is not visible.

To make sure the images stick around, the program uses a dictionary called self.photo_images. It uses the canvas widget that is displaying the image as its key in the dictionary, so a canvas's image is replaced when you display a new one on that canvas.

Tracking Mouse Events

When you press the mouse button down on the left canvas widget, the following event handler executes.

def mouse_down(self, event): '''Save the drawing start point.''' if self.result_image is None: return # Bail if no images loaded. scale = self.selected_scale.get() x = int(event.x / scale) y = int(event.y / scale) self.selection_points = [(x, y)] self.selection_in_progress = True

This code first checks whether we have loaded both a background image and an overlay image and, if they are not both loaded, it exits.

If both images are loaded, the code gets the currently selected scale factor, scales the mouse point's coordinates, and saves the current point in the self.selection_points list.

The code also sets self.selection_in_progress = True so the mouse_move and mouse_up event handlers know that we are selecting an area.

The following code executes when you move the mouse over the left canvas widget.

def mouse_move(self, event): '''Add a drawing point.''' if not self.selection_in_progress: return # Bail if not selecting. scale = self.selected_scale.get() x = int(event.x / scale) y = int(event.y / scale) self.selection_points.append((x, y)) # Draw the selection curve on the result image and display it. self.draw_selection()

This code checks self.selection_in_progress and returns if we are not currently selecting an area.

If we are selecting an area, the code gets the mouse's coordiates, scales them, and appends them to the self.selection_points list.

The code then calls the draw_selection method to redraw the current overlay image with the selected area shown in yellow.

When you release the mouse button, the following event handler executes.

def mouse_up(self, event): '''Finish drawing.''' if not self.selection_in_progress: return # Bail if not selecting. # We are no longer selecting. self.selection_in_progress = True # If we have at least three selection points, enable the button. if len(self.selection_points) >= 2: self.overlay_button.config(state='normal') else: # We no longer have a selection. self.overlay_button.config(state='disabled') self.selection_points = None self.draw_selection()

Like the mouse_move event handler, this code first checks whether a selection is in progress and returns if one is not.

If a selection is in progress, the code sets selection_in_progress to False so we ignore future mouse move and mouse release events.

Next, the code checks whether whether we have at least two points in the selection list. Later we will pass those points to PIL's polygon method and that method throws a hissy fit if you pass it fewer than two points. (In fact, you need at least three non-colinear points to define a non-trivial polygon, so we could check for those conditions but that would be more work. If the points are colinear, the polygon method essentially draws a line segment, which probably isn't what you want but it doesn't seem to hurt anything.)

Anyway, if we have at least two points, the code enables the > button. If we have only one point, the code disables the > button, clears the selecxtion point list, and redraws the overlay image without a selection area.

Performing the Overlay

When you click the > button, the following code executes.

def perform_overlay(self): '''Perform the overlay.''' # Copy the selected part of the overlay image onto the result. self.overlay_selected_area() # We no longer have a selection. self.selection_points = None self.overlay_button.config(state='disabled') self.draw_selection()

This method calls overlay_selected_area (described next) to do all of the real work. It then clears the selection_points list, disables the > button, and calls draw_selection to redraw the overlay image without the selection area.

The following code shows the overlay_selected_area method that actually performs the overlay.

def overlay_selected_area(self): '''Overlay the selected area onto the result image.''' # Make a mask image. wid = self.overlay_image.width hgt = self.overlay_image.height mask_image = Image.new('1', (wid, hgt), 'black') # White out the selected area. dr = ImageDraw.Draw(mask_image) dr.polygon(self.selection_points, fill='white', outline='white') # Paste the selected area from the overlay onto the background image. self.result_image.paste(self.overlay_image, mask=mask_image)

This code first makes a mask image that's the same size as the overlay image and filled with black. It gives the image the 1 image format so the image is black and white and uses 1 bit for each pixel. (This is one of PIL's allowed mask image formats.)

Next, the code uses the polygon method to fill the selected area wiht white on the mask image.

The method finishes by calling the result image's paste method, passing it the overlay image and the mask image. That copies the pieces of the overlay image corresponding to white pixels in the mask image onto the result image. (See my post Use a mask to overlay one image on another with Python and PIL for more information about performing an overlay with a mask.)

Conclusion

Download the example to experiment with it.

This post only describes some of the program's most important image processing pieces and there are plenty of other pieces to explore. For example, the show_image_in_canvas method displays an image on a canvas widget at a specified scale and then updates the ScrolledFrame that contains it so it can adjust its scroll bars properly. Download the example to see that and other details.

For more information image processing in Python, see my Manning liveProject Algorithm Projects with Python: Image Processing. It explain how to do things like:
• Rotation• Scaling• Stretching
• Brightness enhancement• Contrast enhancement• Cropping
• Remapping colors• Image sharpening• Embossing
• Color enhancement• Sharpening• Gray scale
• Black and white• Sepia tone• Other color scales
© 2024 Rocky Mountain Computer Consulting, Inc. All rights reserved.