[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: Crop an area to a certain aspect ratio in Python and tkinter

[This program lets you crop an area to a certain aspect ratio in Python and tkinter]

My post Use drag handles to select an area for cropping in Python and tkinter shows how you can use drag handles to select part of an image for cropping. This example adds the ability to select an area that has a specific aspect ratio. It also ensures that the selected area lies completely on the image, which makes it easier to select an area that goes right up to the image's edges.

Getting the Aspect Ratio

I often need to crop images and sometimes I want a specific aspect ratio.

In case you haven't heard this term before, the aspect ratio is the ratio of the image's width to its height. As an equation, it's width / height. For example, depending on the project, I might want a square 1:1 aspect ratio, the 4:3 aspect ratio that phones produce, or the 3:4 aspect ratio for a vertically oriented image. (Most of these pictures are for my web sites and the Facebook feed for my bakery The Enchanted Oven, although I have not cropped all of those pictures to specific aspect ratios.)

The following method gets the aspect ratio entered in the window's text box.

def get_aspect_ratio(self): '''Return the current aspect ratio.''' try: text = self.aspect_var.get() wid = float(text.split(':')[0]) hgt = float(text.split(':')[1]) aspect_ratio = wid / hgt if math.isinf(aspect_ratio): return None else: return aspect_ratio except: return None

This code gets the text from the entry widget's textvariable. It splits the text at the colon character, converts the pieces into floats, and saves the results in variables wid and hgt. It then calculates the aspect ratio width / height. If the result is infinity or if anything goes wrong, the method returns None.

Adjusting the Box

When you press the left mouse button and move the mouse, the b1_mouse_move event handler adjusts the selection box so it has the desired aspect ratio. This method is long but it's not too complicated, it just needs to consider a bunch of cases depending on which part of the image you're moving with the mouse.

The following code shows the b1_mouse_move event handler. Take a quick look now and I'll describe the main pieces in the following sections.

def b1_mouse_move(self, event): '''Process a drag.''' # Copy the box to work with. box = self.box.copy() # Get the current aspect ratio. aspect_ratio = self.get_aspect_ratio() if aspect_ratio is None: # Bail if there's a problem. return # Update the appropriate values. match self.mouse_is_over: case 'nw': wid = box[2] - event.x hgt = box[3] - event.y wid, hgt = get_enlarged_size(wid, hgt, aspect_ratio) box[0] = box[2] - wid box[1] = box[3] - hgt case 'se': wid = event.x - box[0] hgt = event.y - box[1] wid, hgt = get_enlarged_size(wid, hgt, aspect_ratio) box[2] = box[0] + wid box[3] = box[1] + hgt case 'ne': wid = event.x - box[0] hgt = box[3] - event.y wid, hgt = get_enlarged_size(wid, hgt, aspect_ratio) box[2] = box[0] + wid box[1] = box[3] - hgt case 'sw': wid = box[2] - event.x hgt = event.y - box[1] wid, hgt = get_enlarged_size(wid, hgt, aspect_ratio) box[0] = box[2] - wid box[3] = box[1] + hgt case 'n': hgt = box[3] - event.y wid = hgt * aspect_ratio if hgt < 10: hgt = 10 if wid < 10: wid = 10 cx = (box[0] + box[2]) / 2 box[0] = cx - wid / 2 box[2] = box[0] + wid box[1] = box[3] - hgt case 's': hgt = event.y - box[1] wid = hgt * aspect_ratio if hgt < 10: hgt = 10 if wid < 10: wid = 10 cx = (box[0] + box[2]) / 2 box[0] = cx - wid / 2 box[2] = box[0] + wid box[3] = box[1] + hgt case 'w': wid = box[2] - event.x hgt = wid / aspect_ratio if hgt < 10: hgt = 10 if wid < 10: wid = 10 cy = (box[1] + box[3]) / 2 box[1] = cy - hgt / 2 box[3] = box[1] + hgt box[0] = box[2] - wid case 'e': wid = event.x - box[0] hgt = wid / aspect_ratio if hgt < 10: hgt = 10 if wid < 10: wid = 10 cy = (box[1] + box[3]) / 2 box[1] = cy - hgt / 2 box[3] = box[1] + hgt box[2] = box[0] + wid case 'body': # See how much the mouse has moved. dx = event.x - self.start_x dy = event.y - self.start_y box = [ box[0] + dx, box[1] + dy, box[2] + dx, box[3] + dy, ] self.start_x = event.x self.start_y = event.y case _: pass # See if the new box fits on the image. scale = self.selected_scale.get() wid = int(self.image.width * scale) hgt = int(self.image.height * scale) if box[0] >= 0 and \ box[1] >= 0 and \ box[2] < wid and \ box[3] < hgt: # Allow this change. self.box = box self.display_image()

The method first copies the selection box to make it easier to work with and so it can discard changes if necessary. It then gets the current aspect ratio.

Next, the code uses a match case block to consider each of the cases where you're dragging different drag handles. There are three main cases: dragging a corner, dragging a side, and dragging the body.

Dragging a Corner

Consider the first case where you're dragging the box's northwest corner. In that case, the box's opposite corner in the southeast should remain unmoved. Here's the code that handles this case.

case 'nw': wid = box[2] - event.x hgt = box[3] - event.y wid, hgt = get_enlarged_size(wid, hgt, aspect_ratio) box[0] = box[2] - wid box[1] = box[3] - hgt

The code first gets the width and height of the box if this move is allowed. Because the southeast corner isn't moving, the desired box width is that corner's X coordinate minus the mouse's current X position. Similarly, the desired box height is the southeast corner's Y coordinate minus the mouse's Y position.

Next, the code calls the get_enlarged_size method (described a bit later) so adjust the desired width and height so they have the desired aspect ratio. The code then subtracts the adjusted width and height from the southeast corner's coordinates to get the new coordinates for the box's northwest corner.

The code that moves the other corners is similar. They calculate the desired width and height with respect to the opposite corner, call get_enlarged_size so the box has the correct aspect ratio, and then update the corner being dragged by adding or subtracting the adjusted box width and height from the fixed corner's coordinates.

Dragging a Side

Dragging one of the box's sides is a bit different for a couple of reasons.

First, when you drag a side, you are changing either the box's width or height and you can calculate its other dimension from the width/height and the aspect ratio. That means you don't need to use the mouse's position to find both the width and height.

Second, there is no fixed opposite corner because there is no opposite corner. Instead when you change a dimension, the box should remain centered in the other dimension. For example, suppose you're dragging the box's north edge. If you drag upward, you're making the box taller so its width should grow scaled by the aspect ratio. The box's west and east corners should remain centered horizontally around the box's midpoint.

The following snippet shows the code that deals with dragging the box's north edge.

case 'n': hgt = box[3] - event.y wid = hgt * aspect_ratio if hgt < 10: hgt = 10 if wid < 10: wid = 10 cx = (box[0] + box[2]) / 2 box[0] = cx - wid / 2 box[2] = box[0] + wid box[1] = box[3] - hgt

This code subtracts the mouse's Y position from the box's south (bottom) Y coordinate to get the box's new height. It then calculates the corresponding width for the current aspect ratio.

Next, the code calculates the X coordinate cx of the box's horizontal midpoint. It adjusts the box's X coordinates so it is centered at cx and sets the box's new upper Y coordinate to the mouse's position.

Dragging the Body

The following code shows how the method drags the box's body.

case 'body': # See how much the mouse has moved. dx = event.x - self.start_x dy = event.y - self.start_y box = [ box[0] + dx, box[1] + dy, box[2] + dx, box[3] + dy, ] self.start_x = event.x self.start_y = event.y

This code sees how much the mouse has moved since the last time this method was called and adjusts all of the box's coordinates accordingly.

Unlike the other cases, this code updates the saved start_x and start_y values to the mouse's current position so it can perform the same calculation the next time the mouse moves.

Checking the Box

When you drag part of the selection box, other parts of it may also move. For example, if you drag the north edge, the east and west edges will also move. In particular, they may move off of the image.

To prevent that, the b1_mouse_move event handler uses the following code.

# See if the new box fits on the image. scale = self.selected_scale.get() wid = int(self.image.width * scale) hgt = int(self.image.height * scale) if box[0] >= 0 and \ box[1] >= 0 and \ box[2] < wid and \ box[3] < hgt: # Allow this change. self.box = box self.display_image()

This code gets the current image scale and multiplies the image's dimensions by that scale. If all of the selection box's corners lie on the image, the code copies the modified selection box into self.box to accept the change.

The method finishes by calling self.display_image to redraw the image and the updated selection box.

Getting the Enlarged Size

The following get_enlarged_size function enlarges one of the dimensions to make wid / hgt = aspect_ratio.

def get_enlarged_size(wid, hgt, aspect_ratio): '''Enlarge one of the dimensions to make wid / hgt = aspect_ratio.''' if wid < 10: wid = 10 if hgt < 10: hgt = 10 if wid / hgt > aspect_ratio: # Too short and wide. Increase the height. hgt = wid / aspect_ratio else: # Too tall and thin. Increase the width. wid = hgt * aspect_ratio return wid, hgt

First, the code ensures that the width and height are both at least 10. That prevents you from selecting very small areas and avoids some weird side effects when either of those values goes negative.

Next, the code compares wid / hgt to the desired aspect ratio. If wid / hgt is bigger than the aspect ratio, the box is too short and wide so the code calculates a larger height to fix it. Conversely, if wid / hgt is smaller than the aspect ratio, the box is too tall and thin so the code calculates a larger width to fix it.

After adjusting the width or height, the function returns the two values.

Conclusion

Download the example to see additional details and to experiment with it. I think you'll find it very intuitive and easy to use.
© 2024 - 2026 Rocky Mountain Computer Consulting, Inc. All rights reserved.