I want to be able to drag rectangular selection with a mouse cursor over the image displayed in my program and to read the dimensions of the selection, so that I could use them to crop that image later. How do I do it in Python 3?
UPD:
Suppose I am doing it this way:
import tkinter as tk
from PIL import ImageTk, Image
#This creates the main window of an application
window = tk.Tk()
window.title("Join")
window.geometry("900x900")
window.configure(background='grey')
path = "Book.jpg"
#Creates a Tkinter-compatible photo image, which can be used everywhere Tkinter expects an image object.
img = ImageTk.PhotoImage(Image.open(path))
#The Label widget is a standard Tkinter widget used to display a text or image on the screen.
panel = tk.Label(window, image = img)
#The Pack geometry manager packs widgets in rows or columns.
panel.pack(side = "bottom", fill = "both", expand = "yes")
#Start the GUI
window.mainloop()
Here's another, unfortunately much more involved way to do it (because it does several of the things you mentioned also wanting to do in comments to my first answer). It shades the area outside of the select, and does so using tkinter
's vector-graphic (not PIL
's image-processing) capabilities, which I think makes it the lighter-weight, and maybe faster, too, approach since it doesn't involve processing relatively-large amounts of image data and transferring it.
Originally I tried to draw the shaded outside area as a single continuous polygon, but that didn't work because tkinter
doesn't support such concave polygonal shapes, so four border-less rectangles are drawn instead—plus an empty one with a just a border to outline the selected region (pictures below).
I borrowed a few interesting ideas used in a ActiveState Code » Recipe titled Pʏᴛʜᴏɴ Tᴋɪɴᴛᴇʀ Cᴀɴᴠᴀs Rᴇᴄᴛᴀɴɢʟᴇ Sᴇʟᴇᴄᴛɪᴏɴ Bᴏx by Sunjay Varma.
The code is object-oriented, which hopefully will make it easier to understand (and extend). Note you can get the current selection rectangle as two points by calling the MousePositionTracker
class instance's cur_selection()
method, so that could be used to get the information needed to do the actual image cropping (which likely will involve using PIL
).
import tkinter as tk
from PIL import Image, ImageTk
class MousePositionTracker(tk.Frame):
""" Tkinter Canvas mouse position widget. """
def __init__(self, canvas):
self.canvas = canvas
self.canv_width = self.canvas.cget('width')
self.canv_height = self.canvas.cget('height')
self.reset()
# Create canvas cross-hair lines.
xhair_opts = dict(dash=(3, 2), fill='white', state=tk.HIDDEN)
self.lines = (self.canvas.create_line(0, 0, 0, self.canv_height, **xhair_opts),
self.canvas.create_line(0, 0, self.canv_width, 0, **xhair_opts))
def cur_selection(self):
return (self.start, self.end)
def begin(self, event):
self.hide()
self.start = (event.x, event.y) # Remember position (no drawing).
def update(self, event):
self.end = (event.x, event.y)
self._update(event)
self._command(self.start, (event.x, event.y)) # User callback.
def _update(self, event):
# Update cross-hair lines.
self.canvas.coords(self.lines[0], event.x, 0, event.x, self.canv_height)
self.canvas.coords(self.lines[1], 0, event.y, self.canv_width, event.y)
self.show()
def reset(self):
self.start = self.end = None
def hide(self):
self.canvas.itemconfigure(self.lines[0], state=tk.HIDDEN)
self.canvas.itemconfigure(self.lines[1], state=tk.HIDDEN)
def show(self):
self.canvas.itemconfigure(self.lines[0], state=tk.NORMAL)
self.canvas.itemconfigure(self.lines[1], state=tk.NORMAL)
def autodraw(self, command=lambda *args: None):
"""Setup automatic drawing; supports command option"""
self.reset()
self._command = command
self.canvas.bind("<Button-1>", self.begin)
self.canvas.bind("<B1-Motion>", self.update)
self.canvas.bind("<ButtonRelease-1>", self.quit)
def quit(self, event):
self.hide() # Hide cross-hairs.
self.reset()
class SelectionObject:
""" Widget to display a rectangular area on given canvas defined by two points
representing its diagonal.
"""
def __init__(self, canvas, select_opts):
# Create attributes needed to display selection.
self.canvas = canvas
self.select_opts1 = select_opts
self.width = self.canvas.cget('width')
self.height = self.canvas.cget('height')
# Options for areas outside rectanglar selection.
select_opts1 = self.select_opts1.copy() # Avoid modifying passed argument.
select_opts1.update(state=tk.HIDDEN) # Hide initially.
# Separate options for area inside rectanglar selection.
select_opts2 = dict(dash=(2, 2), fill='', outline='white', state=tk.HIDDEN)
# Initial extrema of inner and outer rectangles.
imin_x, imin_y, imax_x, imax_y = 0, 0, 1, 1
omin_x, omin_y, omax_x, omax_y = 0, 0, self.width, self.height
self.rects = (
# Area *outside* selection (inner) rectangle.
self.canvas.create_rectangle(omin_x, omin_y, omax_x, imin_y, **select_opts1),
self.canvas.create_rectangle(omin_x, imin_y, imin_x, imax_y, **select_opts1),
self.canvas.create_rectangle(imax_x, imin_y, omax_x, imax_y, **select_opts1),
self.canvas.create_rectangle(omin_x, imax_y, omax_x, omax_y, **select_opts1),
# Inner rectangle.
self.canvas.create_rectangle(imin_x, imin_y, imax_x, imax_y, **select_opts2)
)
def update(self, start, end):
# Current extrema of inner and outer rectangles.
imin_x, imin_y, imax_x, imax_y = self._get_coords(start, end)
omin_x, omin_y, omax_x, omax_y = 0, 0, self.width, self.height
# Update coords of all rectangles based on these extrema.
self.canvas.coords(self.rects[0], omin_x, omin_y, omax_x, imin_y),
self.canvas.coords(self.rects[1], omin_x, imin_y, imin_x, imax_y),
self.canvas.coords(self.rects[2], imax_x, imin_y, omax_x, imax_y),
self.canvas.coords(self.rects[3], omin_x, imax_y, omax_x, omax_y),
self.canvas.coords(self.rects[4], imin_x, imin_y, imax_x, imax_y),
for rect in self.rects: # Make sure all are now visible.
self.canvas.itemconfigure(rect, state=tk.NORMAL)
def _get_coords(self, start, end):
""" Determine coords of a polygon defined by the start and
end points one of the diagonals of a rectangular area.
"""
return (min((start[0], end[0])), min((start[1], end[1])),
max((start[0], end[0])), max((start[1], end[1])))
def hide(self):
for rect in self.rects:
self.canvas.itemconfigure(rect, state=tk.HIDDEN)
class Application(tk.Frame):
# Default selection object options.
SELECT_OPTS = dict(dash=(2, 2), stipple='gray25', fill='red',
outline='')
def __init__(self, parent, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
path = "Books.jpg"
img = ImageTk.PhotoImage(Image.open(path))
self.canvas = tk.Canvas(root, width=img.width(), height=img.height(),
borderwidth=0, highlightthickness=0)
self.canvas.pack(expand=True)
self.canvas.create_image(0, 0, image=img, anchor=tk.NW)
self.canvas.img = img # Keep reference.
# Create selection object to show current selection boundaries.
self.selection_obj = SelectionObject(self.canvas, self.SELECT_OPTS)
# Callback function to update it given two points of its diagonal.
def on_drag(start, end, **kwarg): # Must accept these arguments.
self.selection_obj.update(start, end)
# Create mouse position tracker that uses the function.
self.posn_tracker = MousePositionTracker(self.canvas)
self.posn_tracker.autodraw(command=on_drag) # Enable callbacks.
if __name__ == '__main__':
WIDTH, HEIGHT = 900, 900
BACKGROUND = 'grey'
TITLE = 'Image Cropper'
root = tk.Tk()
root.title(TITLE)
root.geometry('%sx%s' % (WIDTH, HEIGHT))
root.configure(background=BACKGROUND)
app = Application(root, background=BACKGROUND)
app.pack(side=tk.TOP, fill=tk.BOTH, expand=tk.TRUE)
app.mainloop()
Here's some images showing it in action:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With