[Rod Stephens Books]
Index Books Python Examples About Rod Contact
[Mastodon] [Bluesky] [Facebook]
[Build Your Own Python Action Arcade!]

[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: Use a Scale widget to straighten images in Python

[This example lets you use a Scale widget to straighten images in Python]

Okay, this is my last image-straightening example, I promise!

My example Display a grid on the image-straightening tool in Python lets you draw horizontal or vertical lines to straighten an image. This example takes a completely different approach. It lets you use a Scale widget to adjust the image's angle of rotation. Combined with an alignment grid, it lets you rotate and image very precisely.

Many of the program's features are similar to those in the previous post. For example, this program lets display an alignment grid and lets you scale the image so it can fit on your screen. The following sections describe some of the more important differences between this program and the previous one.

Expanding the Image

Computer images are rectangular so when you them, they take up more space. A PIL image's rotate method has an expand parameter that lets you indicate whether you want to expand the image so the entire result can fit or whether you want to crop the image to fit in its original size. The following picture shows the possibilities.

[PIL's rotate method can expand the rotated image (or not)]

If you allow the image to expand, its size changes depending on the angle of rotation. If you display the result and the angle changes, the image would seem to wander around as its size changes and therefore the position of its center changes. To prevent that, this example resizes the original image so it is as large as it can be right after loading the image.

The following code shows how the program loads an image with the new code highlighted in blue.

def mnu_open_image(self): '''Open an image file.''' # Load the image. self.original_image = self.get_image_file('Load Image') # Resize to allow room for rotation. diameter = int(math.hypot(self.original_image.width, self.original_image.height)) self.pil_image = Image.new('RGBA', (diameter, diameter), color=None) # Copy the original centered on the enlarged image. x = (diameter - self.original_image.width) // 2 y = (diameter - self.original_image.height) // 2 self.pil_image.paste(self.original_image, (x, y)) # Reset the rotation angle. self.angle_scale.set(0) # Display the image at the desired scale. self.show_image()

[The rotated image's background is transparent] The code calls the get_image_file method, which lets the user pick a file and then loads it. It then calculates the maximum width and height the image could require during rotation. As the angle changes from 0° to 360°, the image's corners will trace out a circle with radius equal to the distance from the image's center to one of its corners. To figure out how big the image could be, the code uses math.hypot to find the length of the image's diagonal. It uses that as the rotated image's width and height.

Next, the code creates the image self.pil_image having that width and height. Notice that this example sets the new image's background color to None so the background is transparent. The picture on the right shows the Leaning Tower of Pisa at a scale that lets you see the transparent background. (Or really, it lets you not see the transparent background.)

Next, the code pastes the original image into the center of the enlarged image. It sets the initial scale to 0° and calls show_image to display self.pil_image as before.

Rotating the Image

When you adjust the Scale widget, it calls the show_image method to display the image. That method is almost the same as in the previous example. The following code shows the method with the changes highlighted in blue.

def show_image(self): '''Display the image at the desired scale.''' if self.pil_image is None: # No image. Hide the canvas. self.photo_image = None self.menu_bar.entryconfig(2, state=tk.DISABLED) # Scale self.file_menu.entryconfig('Save As...', state=tk.DISABLED) self.file_menu.entryconfig('Reset', state=tk.DISABLED) self.scrolled_frame.pack_forget() self.start_point = self.end_point = None self.new_line1 = self.new_line2 = None self.angle_scale.set(0) else: # Make a scaled version of the image. wid = int(self.pil_image.width * self.scale_var.get()) hgt = int(self.pil_image.height * self.scale_var.get()) scaled_image = self.pil_image.resize((wid, hgt)) # Rotate to the current angle. angle = self.angle_scale.get() rotated_image = scaled_image.rotate( angle, expand=False, resample=Image.Resampling.BICUBIC) # Convert into a PhotoImage. self.photo_image = ImageTk.PhotoImage(rotated_image) # Display it. self.canvas.delete(tk.ALL) self.image_id = self.canvas.create_image(0, 0, anchor=tk.NW, image=self.photo_image) wid = self.photo_image.width() hgt = self.photo_image.height() self.canvas.config(width=wid, height=hgt) # Draw the grid if desired. if self.grid_var.get(): spacing = int(self.grid_spacing * self.scale_var.get()) for y in range(spacing, hgt, spacing): self.canvas.create_line(0, y, wid, y, fill=self.grid_color) for x in range(spacing, wid, spacing): self.canvas.create_line(x, 0, x, hgt, fill=self.grid_color) # Reconfigure the frame. Do this after adding contained widgets. self.scrolled_frame.configure_frame() # Display the ScrolledFrame. self.scrolled_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True) # Enable the Scale menu. self.menu_bar.entryconfig(2, state=tk.NORMAL) # Scale # Enable the Save As and Reset menu items. self.file_menu.entryconfig('Save As...', state=tk.NORMAL) self.file_menu.entryconfig('Reset', state=tk.NORMAL)

The code creates a scaled version of the image as before. It then gets the rotation angle from the self.angle_scale widget and rotates the image. It then converts the image into a PhotoImage and displays it on the canvas as before.

Saving the Result

The only other code that's changed is the mnu_save_image method shown in the following code which saves the rotated image. As usual, the changes are highlighted in blue

def mnu_save_image(self): '''Save the rotated image.''' # Get the filename. filetypes = ( ('Image Files', '*.png;*.jpg;*.bmp;*.gif;*.tif'), ('All files', '*.*') ) filename = fd.asksaveasfilename(filetypes=filetypes, defaultextension='.png') if len(filename) > 0: # Rotate to straighten. rotated_image = self.pil_image.rotate( self.angle_scale.get(), expand=True, resample=Image.Resampling.BICUBIC) try: rotated_image.save(filename) except Exception as e: messagebox.showinfo('Save Error', f'Error saving image file.\n{e}') return None

This code displays a file save dialog as before to let the user select a file. If the user picks a file and doesn't just cancel the dialog, the code rotates the image. Notice that it uses self.angle_scale.get to get the rotation angle.

The code then saves the file as before.

Conclusion

This is the last post about image straightening. Like the previous example, it can draw an alignment grid on top of the image so you can see how straight it is. That combined with the fine control the Scale widget gives you makes it easy to straighten images very precisely. Note that you can click to the left or right of the Scale's thumb to adjust the angle one degree at a time, so you can have very precise control.

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

© 2025 - 2026 Rocky Mountain Computer Consulting, Inc. All rights reserved.