Selecting an area of an image with a mouse and recording the dimensions of the selection Selecting an area of an image with a mouse and recording the dimensions of the selection python-3.x python-3.x

Selecting an area of an image with a mouse and recording the dimensions of the selection


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 tkfrom PIL import Image, ImageTkclass 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:

screenshot of start of selection process

screenshot of end of selection process


Here's an example of doing something like that with tkinter. After the first mouse-button click, the coordinates of the current selection area rectangle are in the globals topx, topy, botx, boty (before then, the global rect_id variable value will be None).

To use the selection rectangle, you will need to add something to the GUI, like a button or menu, that uses the current selection rectangle's location & size to create the thumbnail — you can get the coordinates of the selection rectangle by calling canvas.coords(rect_id). Note that PIL.Image instances have a thumbnail() method that provides a simple way to create one.

import tkinter as tkfrom PIL import Image, ImageTkWIDTH, HEIGHT = 900, 900topx, topy, botx, boty = 0, 0, 0, 0rect_id = Nonepath = "Books.jpg"def get_mouse_posn(event):    global topy, topx    topx, topy = event.x, event.ydef update_sel_rect(event):    global rect_id    global topy, topx, botx, boty    botx, boty = event.x, event.y    canvas.coords(rect_id, topx, topy, botx, boty)  # Update selection rect.window = tk.Tk()window.title("Select Area")window.geometry('%sx%s' % (WIDTH, HEIGHT))window.configure(background='grey')img = ImageTk.PhotoImage(Image.open(path))canvas = tk.Canvas(window, width=img.width(), height=img.height(),                   borderwidth=0, highlightthickness=0)canvas.pack(expand=True)canvas.img = img  # Keep reference in case this code is put into a function.canvas.create_image(0, 0, image=img, anchor=tk.NW)# Create selection rectangle (invisible since corner points are equal).rect_id = canvas.create_rectangle(topx, topy, topx, topy,                                  dash=(2,2), fill='', outline='white')canvas.bind('<Button-1>', get_mouse_posn)canvas.bind('<B1-Motion>', update_sel_rect)window.mainloop()

Screenshot:

screenshot

You can download a copy of the Books.jpg image used by the code from here.