[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 crop images in Python and tkinter

[This program lets you interactively crop images in Python and tkinter]

This example is pretty long so I'm only going to describe the highlights here. You can download it to see the rest of the details.

I'll describe these pieces:

  • Building the User Interface
  • Opening Files
  • Displaying the Image
  • Tracking Mouse Events
  • Saving the Cropped Image

Building the User Interface

The program's main class, CropImageApp, creates the user interface in the following constructor.

class CropImageApp: def __init__(self): # Make the main interface. self.window = tk.Tk() self.window.title('crop_image') self.window.protocol('WM_DELETE_WINDOW', self.kill_callback) self.window.geometry('500x400') # Menus. self.build_menus() # Make a ScrolledFrame. self.scrolled_frame = ScrolledFrame(self.window) self.scrolled_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True) # Configure the frame. (Do this after adding all contained widgets.) self.scrolled_frame.configure_frame() # Currently there is no image or selection rectangle. self.image = None self.photo_image = None self.selection_rectangle = None self.selecting = False # Create the canvas where we will display images. self.canvas = tk.Canvas(self.scrolled_frame.frame, cursor='tcross') self.canvas.pack() self.scrolled_frame.configure_frame() # Watch for mouse events. self.canvas.bind('<Button-1>', self.mouse_down) self.canvas.bind('<B1-Motion>', self.mouse_move) self.canvas.bind('<ButtonRelease-1>', self.mouse_up) # Display the window. self.window.focus_force() self.window.mainloop()

This code initializes tkinter and creates the program's window. It calls self.build_menus (not shown here) to create the program's menus.

The program then creates a ScrolledFrame. See the post Make a scrolled frame that responds to the mouse wheel in tkinter and Python for information about that class. The program saves the ScrolledFrame so we can configure it later.

Next, the code sets some values to None so we know that no image is initially loaded.

The code then adds a Canvas widget to the ScrolledFrame's content frame.

This code binds some events and then enters the tkinter event loop.

Opening Files

When you invoke the File menu's Open command, the following event handler executes.

def mnu_open(self): '''Open a file.''' # Get the file name. filetypes = [ ('Image Files', '*.png *.jpg *.bmp *.gif'), ('All Files', '*,*') ] filename = askopenfilename(filetypes=filetypes) if len(filename) == 0: return # Load the selected image. self.image = Image.open(filename) self.photo_image = ImageTk.PhotoImage(self.image) # Remove the selection rectangle. self.selection_rectangle = None self.file_menu.entryconfig('Save As...', state=tk.DISABLED) # Size the canvas to fit the image. wid = self.photo_image.width() hgt = self.photo_image.height() self.canvas.config(width=wid, height=hgt) self.scrolled_frame.configure_frame() # Display the image. self.display_image()

This code displays an open file dialog. If you select a file, the code opens the file as a PIL Image object. It also converts that object into a PhotoImage so it can display it.

The code clears the current selection rectangle (if there is one) and disables the File menu's Save As command. It then resizes the Canvas widget so it fits the image and calls configure_frame to let that object rearrange itself to handle the new canvas size.

The method finishes by calling the display_image method described next.

Displaying the Image

The following display_image method displays the currently loaded image and selection rectangle (if there is one).

def display_image(self): '''Display the image with a selection rectangle if there is one.''' # Delete all canvas contents. self.canvas.delete(tk.ALL) # Create the new image. self.canvas.create_image(1, 1, anchor=tk.NW, image=self.photo_image) # Draw the selection rectangle. if self.selection_rectangle is not None: thickness = 1 self.canvas.create_rectangle(*self.selection_rectangle, width=thickness, outline='red') self.canvas.create_rectangle(*self.selection_rectangle, width=thickness, outline='yellow', dash=(3,5))

This method displays first deletes any objects that are currently drawn on the program's Canvas widget. (That includes the current image and selection rectangle.) It then uses the Canvas widget's create_image method to display the image.

Then, if the selection rectangle is not None, the code draws it. First it draws the rectangle in red and then draws it again with a dashed yellow outline. The result is an alternating red and yellow dashed outline that's easy to see when drawn on most images.

Tracking Mouse Events

The following code executes when you press the mouse down over the Canvas widget.

def mouse_down(self, event): '''Start selecting.''' self.selecting = True self.selection_rectangle = [event.x, event.y, event.x, event.y] self.display_image()

This method sets self.selecting to True so the next two event handlers know that we have started selecting an area. It then sets self.selection_rectangle to a list holding the mouse's position twice. That means initially the selection rectangle has zero width and height.

The method finishes by calling display_image to display the image with the zero-size selection rectangle.

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

def mouse_move(self, event): '''Update the selection rectangle.''' if not self.selecting: return self.selection_rectangle[2] = event.x self.selection_rectangle[3] = event.y self.display_image()

If self.selecting is False, we are not selecting a rectangle so the code simply returns.

Otherwise, the code updates the selection rectangle's last two coordinates and calls display_image to redisplay the image and rectangle.

The following code executes when you release the mouse.

def mouse_up(self, event): '''Stop selecting.''' self.selecting = False # Enable or disable the Save As menu item. disabled = \ self.photo_image is None or \ self.selection_rectangle is None or \ self.selection_rectangle[0] == self.selection_rectangle[2] or \ self.selection_rectangle[1] == self.selection_rectangle[3] if disabled: self.file_menu.entryconfig('Save As...', state=tk.DISABLED) else: self.file_menu.entryconfig('Save As...', state=tk.NORMAL)

This code sets self.selecting to False so we ignore future mouse movement. It then performs some tests to see if the File menu's Save As command should be enabled. It disables that item if there is no image loaded, if there is no selection rectangle, or if the rectangle has zero width or height.

Saving the Cropped Image

def mnu_save_as(self): '''Save the selected part of the image.''' # Get the file name. filetypes = [ ('Image Files', '*.png *.jpg *.bmp *.gif'), ('All Files', '*,*') ] filename = asksaveasfilename( filetypes=filetypes, defaultextension='.png') crop_rectangle = self.get_crop_rectangle() cropped_image = self.image.crop(crop_rectangle) cropped_image.save(filename)

This code saves the cropped image. First, it displays a Save As dialog to let you select the file where you want to save the file. If you pick a file, the code calls the get_crop_rectangle described shortly to get the cropping rectangle.

Next, the code calls the original PIL image's crop method to crop the image to the selection rectangle. It then calls the cropped image's save method to save the file.

The image's crop method takes as a parameter a tuple giving the cropping rectangle's upper left and lower right coordinates. Unfortunately, the selection rectangle doesn't ensure that the first set of X and Y coordinates are smaller than the second set. The following get_crop_rectangle method rearranges the selection rectangle's coordinates if necessary to satisfy crop.

def get_crop_rectangle(self): '''Return the selection rectangle with x1 < x2 and y1 < y2.''' x1 = min(self.selection_rectangle[0], self.selection_rectangle[2]) y1 = min(self.selection_rectangle[1], self.selection_rectangle[3]) x2 = max(self.selection_rectangle[0], self.selection_rectangle[2]) y2 = max(self.selection_rectangle[1], self.selection_rectangle[3]) return [x1, y1, x2, y2]

This code simply uses min and max to get the appropriate X and Y coordinates and return them in a new list.

Conclusion

The example only includes a few other pieces of code. For example, I haven't explained how the program builds its menus or how the ScrolledFrame works. To learn more about them, see these posts:

Download this example to see additional details and to experiment with it.

Postscript

[A volleyball scene generated by AI] To generate this example's image, I used Microsoft Designer, an artificial intelligence program that you can try out here. It's fun to play with, but it definitely does some weird things. This image was the least weird of the cartoon volleyball images I generated. If you look closely, you can see:
  • The blue and white volleyball's panels are pretty strange, although you could probably make a ball like that
  • The anthropomorphic volleyball's panels also seem unlikely, although it's a living volleyball so all bets are off
  • Most of the peoples' hands are basically just scribbles
  • The guy on the far left is diving through the net
  • The third and fourth people from the left should probably be behind the net given where their feet are
  • The jumping volleyball's left foot looks like its big toe is on the wrong side where the pinkie toe belongs
  • One person who looks male is wearing a bikini (but who am I to judge?)
The other images I generated were much stranger with one person having three legs (one with no foot), one person having four fingers on one hand and three on the other, people with missing hands, and hands with pinkies much longer than the other fingers. Pure nightmare fuel!
© 2024 Rocky Mountain Computer Consulting, Inc. All rights reserved.