Вот еще один, к сожалению, гораздо более сложный способ сделать это (потому что он делает несколько вещей, которые вы упомянули, также желая сделать в комментариях к моему первому ответу ).Он затеняет область за пределами выделения и делает это, используя возможности векторной графики tkinter
(не PIL
обработки изображений), что, я думаю, делает его более легким, а может и быстрее,подход, поскольку он не включает обработку относительно больших объемов данных изображения и их передачу.
Первоначально я пытался нарисовать заштрихованную внешнюю область как один непрерывный многоугольник, но это не сработало, потому что tkinter
не поддерживает такие вогнутые многоугольники, поэтому вместо этого рисуются четыре прямоугольника без рамки - плюс пустой прямоугольник с рамкой для выделения выделенной области (рисунки ниже).
Я позаимствовал несколько интересных идейиспользуется в ActiveState Code »Рецепт под названием ᴘʏᴛʜᴏɴ ᴛᴋɪɴᴛᴇʀ ᴄᴀɴᴠᴀs ʀᴇᴄᴛᴀɴɢʟᴇ sᴇʟᴇᴄᴛɪᴏɴ ʙᴏx от Sunjay Varma.
Код является объектно-ориентированным, что, надеюсь, облегчит егопонять (и расширить).Обратите внимание, что вы можете получить текущий прямоугольник выделения в виде двух точек, вызвав метод cur_selection()
экземпляра класса MousePositionTracker
, чтобы его можно было использовать для получения информации, необходимой для фактического обрезания изображения (что, вероятно, будет использовать 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 a selection objects for updating.
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()
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()
Вот некоторые изображения, показывающие это в действии:
![screenshot of start of selection process](https://i.stack.imgur.com/ohQ6x.png)
![screenshot of end of selection process](https://i.stack.imgur.com/UmbqR.png)