forked from foobar167/junkyard
-
Notifications
You must be signed in to change notification settings - Fork 0
/
polygon_drawing.py
executable file
·406 lines (367 loc) · 21 KB
/
polygon_drawing.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
# Drawing polygon on the image
import tkinter as tk
from tkinter import ttk
from datetime import datetime
from PIL import Image, ImageTk
class AutoScrollbar(ttk.Scrollbar):
""" A scrollbar that hides itself if it's not needed.
Works only if you use the grid geometry manager """
def set(self, lo, hi):
if float(lo) <= 0.0 and float(hi) >= 1.0:
self.grid_remove()
else:
self.grid()
ttk.Scrollbar.set(self, lo, hi)
def pack(self, **kw):
raise tk.TclError('Cannot use pack with the widget ' + self.__class__.__name__)
def place(self, **kw):
raise tk.TclError('Cannot use place with the widget ' + self.__class__.__name__)
class CanvasImage:
""" Image on the canvas """
def __init__(self, parent, impath):
""" Initialize the canvas """
# Create frame container for the canvas and scrolling bars. Make it expandable
self.imframe = ttk.Frame(parent) # parent of the CanvasImage objects
# Vertical and horizontal scrollbars for canvas
vbar = AutoScrollbar(self.imframe, orient='vertical')
hbar = AutoScrollbar(self.imframe, orient='horizontal')
vbar.grid(row=0, column=1, sticky='ns')
hbar.grid(row=1, column=0, sticky='we')
# Create canvas and put image on it
self.canvas = tk.Canvas(self.imframe, highlightthickness=0,
xscrollcommand=hbar.set, yscrollcommand=vbar.set)
self.canvas.grid(row=0, column=0, sticky='nswe')
self.canvas.update() # wait till canvas is created
vbar.configure(command=self.scroll_y) # bind scrollbars to the canvas
hbar.configure(command=self.scroll_x)
# Bind events to the Canvas
self.canvas.bind('<Configure>', self.show_image) # canvas is resized
self.canvas.bind('<ButtonPress-3>', self.move_from)
self.canvas.bind('<B3-Motion>', self.move_to)
self.canvas.bind('<MouseWheel>', self.wheel) # with Windows and MacOS, but not Linux
self.canvas.bind('<Button-5>', self.wheel) # only with Linux, wheel scroll down
self.canvas.bind('<Button-4>', self.wheel) # only with Linux, wheel scroll up
self.image = Image.open(impath) # open image
self.im_width, self.im_height = self.image.size
self.imscale = 1.0 # scale for the canvaas image
self.delta = 1.3 # zoom magnitude
# Put image into container rectangle and use it to set proper coordinates to the image
self.container = self.canvas.create_rectangle(0, 0, self.im_width, self.im_height, width=0)
self.show_image() # show image on the screen
self.canvas.focus_set() # set focus on the canvas
def scroll_y(self, *args):
""" Scroll canvas vertically and redraw the image """
self.canvas.yview(*args) # scroll vertically
self.show_image() # redraw the image
def scroll_x(self, *args):
""" Scroll canvas horizontally and redraw the image """
self.canvas.xview(*args) # scroll horizontally
self.show_image() # redraw the image
def show_image(self, event=None):
""" Show image on the canvas """
box_image = self.canvas.coords(self.container) # get image area
box_canvas = (self.canvas.canvasx(0), # get visible area of the canvas
self.canvas.canvasy(0),
self.canvas.canvasx(self.canvas.winfo_width()),
self.canvas.canvasy(self.canvas.winfo_height()))
box_img_int = tuple(map(round, box_image)) # convert to integer or it will not work properly
# Get scroll region box
box_scroll = [min(box_img_int[0], box_canvas[0]), min(box_img_int[1], box_canvas[1]),
max(box_img_int[2], box_canvas[2]), max(box_img_int[3], box_canvas[3])]
# Horizontal part of the image is in the visible area
if box_scroll[0] == box_canvas[0] and box_scroll[2] == box_canvas[2]:
box_scroll[0] = box_img_int[0]
box_scroll[2] = box_img_int[2]
# Vertical part of the image is in the visible area
if box_scroll[1] == box_canvas[1] and box_scroll[3] == box_canvas[3]:
box_scroll[1] = box_img_int[1]
box_scroll[3] = box_img_int[3]
# Convert scroll region to tuple and to integer
self.canvas.configure(scrollregion=tuple(map(round, box_scroll))) # set scroll region
x1 = max(box_canvas[0] - box_image[0], 0) # get coordinates (x1,y1,x2,y2) of the image tile
y1 = max(box_canvas[1] - box_image[1], 0)
x2 = min(box_canvas[2], box_image[2]) - box_image[0]
y2 = min(box_canvas[3], box_image[3]) - box_image[1]
if round(x2 - x1) > 0 and round(y2 - y1) > 0: # show image if it in the visible area
image = self.image.crop((round(x1 / self.imscale), round(y1 / self.imscale),
round(x2 / self.imscale), round(y2 / self.imscale)))
imagetk = ImageTk.PhotoImage(image.resize((round(x2 - x1), round(y2 - y1))))
imageid = self.canvas.create_image(max(box_canvas[0], box_img_int[0]),
max(box_canvas[1], box_img_int[1]),
anchor='nw', image=imagetk)
self.canvas.lower(imageid) # set image into background
self.canvas.imagetk = imagetk # keep an extra reference to prevent garbage-collection
def move_from(self, event):
""" Remember previous coordinates for scrolling with the mouse """
self.canvas.scan_mark(event.x, event.y)
def move_to(self, event):
""" Drag (move) canvas to the new position """
self.canvas.scan_dragto(event.x, event.y, gain=1)
self.show_image() # redraw the image
def outside(self, x, y):
""" Checks if the point (x,y) is outside the image area """
bbox = self.canvas.coords(self.container) # get image area
if bbox[0] < x < bbox[2] and bbox[1] < y < bbox[3]:
return False # point (x,y) is inside the image area
else:
return True # point (x,y) is outside the image area
def wheel(self, event):
""" Zoom with mouse wheel """
x = self.canvas.canvasx(event.x)
y = self.canvas.canvasy(event.y)
if self.outside(x, y): return # zoom only inside image area
scale = 1.0
# Respond to Linux (event.num) or Windows (event.delta) wheel event
if event.num == 5 or event.delta == -120: # scroll down
i = min(self.im_width, self.im_height)
if round(i * self.imscale) < 30: return # image is less than 30 pixels
self.imscale /= self.delta
scale /= self.delta
if event.num == 4 or event.delta == 120: # scroll up
i = min(self.canvas.winfo_width(), self.canvas.winfo_height())
if i < self.imscale: return # 1 pixel is bigger than the visible area
self.imscale *= self.delta
scale *= self.delta
self.canvas.scale('all', x, y, scale, scale) # rescale all canvas objects
# Redraw some figures before showing image on the screen
self.redraw_figures()
self.show_image()
def redraw_figures(self):
""" Dummy function to redraw figures in the children classes """
pass
def grid(self, **kw):
""" Put CanvasImage on the parent widget """
self.imframe.grid(**kw) # place CanvasImage widget on the grid
self.imframe.grid(sticky='nswe') # make frame container sticky
self.imframe.rowconfigure(0, weight=1) # make canvas expandable
self.imframe.columnconfigure(0, weight=1)
def pack(self, **kw):
""" Exception: cannot use pack with this widget """
raise Exception('Cannot use pack with the widget ' + self.__class__.__name__)
def place(self, **kw):
""" Exceiption: cannot use place with this widget """
raise Exception('Cannot use place with the widget ' + self.__class__.__name__)
class Polygons(CanvasImage):
""" Class of Polygons. Inherit CanvasImage class """
def __init__(self, parent, impath):
""" Initialize the Polygons """
CanvasImage.__init__(self, parent, impath) # call __init__ of the CanvasImage class
self.canvas.bind('<ButtonPress-1>', self.set_edge) # set new edge
self.canvas.bind('<Motion>', self.motion) # handle mouse motion
self.canvas.bind('<Delete>', self.delete_roi) # delete selected polygon
# Polygon parameters
self.width_line = 2 # lines width
self.dash = (1, 1) # dash pattern
self.color_draw = 'red' # color to draw
self.color_active = 'yellow' # color of active figures
self.color_point = 'blue' # color of pointed figures
self.color_back = '#808080' # background color
self.stipple = 'gray12' # value of stipple
self.tag_edge_start = '1st_edge' # starting edge of the polygon
self.tag_edge = 'edge' # edges of the polygon
self.tag_edge_id = 'edge_id' # part of unique ID of the edge
self.tag_poly = 'polygon' # polygon tag
self.tag_const = 'poly' # constant tag for polygon
self.tag_poly_line = 'poly_line' # edge of the polygon
self.tag_circle = 'circle' # sticking circle tag
self.radius_stick = 10 # distance where line sticks to the polygon's staring point
self.radius_circle = 3 # radius of the sticking circle
self.edge = None # current edge of the new polygon
self.polygon = [] # vertices of the polygon
self.selected_poly = [] # selected polygons
def set_edge(self, event):
""" Set edge of the polygon """
if self.edge and ' '.join(map(str, self.dash)) == self.canvas.itemcget(self.edge, 'dash'):
return # the edge is out of scope or self-crossing with other edges
x = self.canvas.canvasx(event.x) # get coordinates of the event on the canvas
y = self.canvas.canvasy(event.y)
if not self.edge: # start drawing polygon
self.draw_edge(x, y, self.tag_edge_start)
# Draw sticking circle
self.canvas.create_oval(x - self.radius_circle, y - self.radius_circle,
x + self.radius_circle, y + self.radius_circle,
width=0, fill=self.color_draw,
tags=(self.tag_edge, self.tag_circle))
else: # continue drawing polygon
x1, y1, x2, y2 = self.canvas.coords(self.tag_edge_start) # get coords of the 1st edge
x3, y3, x4, y4 = self.canvas.coords(self.edge) # get coordinates of the current edge
if x4 == x1 and y4 == y1: # finish drawing polygon
if len(self.polygon) > 2: # draw polygon on the zoomed image canvas
bbox = self.canvas.coords(self.container) # get image area
vertices = list(map((lambda i: (i[0] * self.imscale + bbox[0],
i[1] * self.imscale + bbox[1])), self.polygon))
# Create identification tag
# [:-3] means microseconds to milliseconds, anyway there are zeros on Windows OS
tag_id = datetime.now().strftime('%Y-%m-%d_%H-%M-%S.%f')[:-3]
# Create polygon. 2nd tag is ALWAYS a unique tag ID + constant string.
self.canvas.create_polygon(vertices, fill=self.color_point,
stipple=self.stipple, width=0, state='hidden',
tags=(self.tag_poly, tag_id + self.tag_const))
# Create polyline. 2nd tag is ALWAYS a unique tag ID.
for j in range(len(vertices)-1):
self.canvas.create_line(vertices[j], vertices[j+1], width=self.width_line,
fill=self.color_back, tags=(self.tag_poly_line, tag_id))
self.canvas.create_line(vertices[-1], vertices[0], width=self.width_line,
fill=self.color_back, tags=(self.tag_poly_line, tag_id))
self.delete_edges() # delete edges of drawn polygon
else:
self.draw_edge(x, y) # continue drawing polygon, set new edge
def draw_edge(self, x, y, tags=None):
""" Draw edge of the polygon """
if len(self.polygon) > 1:
x1, y1, x2, y2 = self.canvas.coords(self.edge)
if x1 == x2 and y1 == y2:
return # don't draw edge in the same point, otherwise it'll be self-intersection
edge_id = self.tag_edge_id + str(len(self.polygon))
self.edge = self.canvas.create_line(x, y, x, y, fill=self.color_draw, width=self.width_line,
tags=(tags, self.tag_edge, edge_id,))
bbox = self.canvas.coords(self.container) # get image area
x1 = round((x - bbox[0]) / self.imscale) # get (x,y) on the image without zoom
y1 = round((y - bbox[1]) / self.imscale)
self.polygon.append((x1, y1)) # add new vertex to the list of polygon vertices
def motion(self, event):
""" Track mouse position over the canvas """
if self.edge: # relocate edge of the polygon
x = self.canvas.canvasx(event.x) # get coordinates of the event on the canvas
y = self.canvas.canvasy(event.y)
x1, y1, x2, y2 = self.canvas.coords(self.tag_edge_start) # get coordinates of the 1st edge
x3, y3, x4, y4 = self.canvas.coords(self.edge) # get coordinates of the current edge
dx = x - x1
dy = y - y1
# Set new coordinates of the edge
if self.radius_stick * self.radius_stick > dx * dx + dy * dy:
self.canvas.coords(self.edge, x3, y3, x1, y1) # stick to the beginning
self.set_dash(x1, y1) # set dash for edge segment
else:
self.canvas.coords(self.edge, x3, y3, x, y) # follow the mouse movements
self.set_dash(x, y) # set dash for edge segment
# Handle polygons on the canvas
self.deselect_roi() # change color and zeroize selected roi polygon
self.select_roi() # change color and select roi polygon
def set_dash(self, x, y):
""" Set dash for edge segment """
# If outside of the image or polygon self-intersection is occurred
if self.outside(x, y) or self.polygon_selfintersection():
self.canvas.itemconfigure(self.edge, dash=self.dash) # set dashed line
else:
self.canvas.itemconfigure(self.edge, dash='') # set solid line
def deselect_roi(self):
""" Deselect current roi object """
if not self.selected_poly: return # selected polygons list is empty
for i in self.selected_poly:
self.canvas.itemconfigure(i, fill=self.color_back) # deselect lines
self.canvas.itemconfigure(i + self.tag_const, state='hidden') # hide polygon
self.selected_poly.clear() # clear the list
def select_roi(self):
""" Select and change color of the current roi object """
if self.edge: return # new polygon is being created now
i = self.canvas.find_withtag('current') # id of the current object
tags = self.canvas.gettags(i) # get tags of the current object
if self.tag_poly_line in tags: # if it's a roi polygon. 2nd tag is ALWAYS a unique tag ID
self.canvas.itemconfigure(tags[1], fill=self.color_point) # select lines
self.canvas.itemconfigure(tags[1] + self.tag_const, state='normal') # show polygon
self.selected_poly.append(tags[1]) # remember 2nd unique tag_id
def redraw_figures(self):
""" Overwritten method. Redraw sticking circle """
bbox = self.canvas.coords(self.tag_circle)
if bbox: # radius of sticky circle is unchanged
cx = (bbox[0] + bbox[2]) / 2 # center of the circle
cy = (bbox[1] + bbox[3]) / 2
self.canvas.coords(self.tag_circle,
cx - self.radius_circle, cy - self.radius_circle,
cx + self.radius_circle, cy + self.radius_circle)
def delete_edges(self):
""" Delete edges of drawn polygon """
self.edge = None # delete all edges and set current edge to None
self.canvas.delete(self.tag_edge) # delete all edges
self.polygon.clear() # remove all items from vertices list
def delete_roi(self, event=None):
""" Delete selected polygon """
if self.edge: # if polygon is being drawing, delete it
self.delete_edges() # delete edges of drawn polygon
elif self.selected_poly: # delete selected polygon
for i in self.selected_poly:
self.canvas.delete(i) # delete lines
self.canvas.delete(i + self.tag_const) # delete polygon
self.selected_poly.clear() # clear the list
@staticmethod
def orientation(p1, p2, p3):
""" Find orientation of ordered triplet (p1, p2, p3). Returns following values:
0 --> p1, p2 and p3 are collinear
-1 --> clockwise
1 --> counterclockwise """
val = (p2[0] - p1[0]) * (p3[1] - p2[1]) - (p2[1] - p1[1]) * (p3[0] - p2[0])
if val < 0: return -1 # clockwise
elif val > 0: return 1 # counterclockwise
else: return 0 # collinear
@staticmethod
def on_segment(p1, p2, p3):
""" Given three collinear points p1, p2, p3, the function checks
if point p2 lies on line segment p1-p3 """
# noinspection PyChainedComparisons
if p2[0] <= max(p1[0], p3[0]) and p2[0] >= min(p1[0], p3[0]) and \
p2[1] <= max(p1[1], p3[1]) and p2[1] >= min(p1[1], p3[1]):
return True
return False
def intersect(self, p1, p2, p3, p4):
""" Return True if line segments p1-p2 and p3-p4 intersect, otherwise return False """
# Find 4 orientations
o1 = self.orientation(p1, p2, p3)
o2 = self.orientation(p1, p2, p4)
o3 = self.orientation(p3, p4, p1)
o4 = self.orientation(p3, p4, p2)
# General case
if o1 != o2 and o3 != o4: return True # segments intersect
# Segments p1-p2 and p3-p4 are collinear
if o1 == o2 == 0:
# p3 lies on segment p1-p2
if self.on_segment(p1, p3, p2): return True
# p4 lies on segment p1-p2
if self.on_segment(p1, p4, p2): return True
# p1 lies on segment p3-p4
if self.on_segment(p3, p1, p4): return True
return False # doesn't intersect
def penultimate_intersect(self, p1, p2, p3):
""" Check penultimate (last but one) edge,
where p1 and p4 coincide with the current edge """
if self.orientation(p1, p2, p3) == 0 and not self.on_segment(p3, p1, p2):
return True
else:
return False
def first_intersect(self, p1, p2, p3, p4):
""" Check the 1st edge, where points p2 and p3 CAN coincide """
if p2[0] == p3[0] and p2[1] == p3[1]: return False # p2 and p3 coincide -- this is OK
if p1[0] == p3[0] and p1[1] == p3[1]: return False # there is only 1 edge
# There is only 2 edges
if p1[0] == p4[0] and p1[1] == p4[1]: return self.penultimate_intersect(p1, p2, p3)
return self.intersect(p1, p2, p3, p4) # General case
def polygon_selfintersection(self):
""" Check if polygon has self-intersections """
x1, y1, x2, y2 = self.canvas.coords(self.edge) # get coords of the current edge
for i in range(1, len(self.polygon)-2): # don't include the 1st ant the last 2 edges
x3, y3, x4, y4 = self.canvas.coords(self.tag_edge_id + str(i))
if self.intersect((x1, y1), (x2, y2), (x3, y3), (x4, y4)): return True
# Check penultimate (last but one) edge, where points p1 and p4 coincide
j = len(self.polygon) - 2
if j > 0: # 2 or more edges
x3, y3, x4, y4 = self.canvas.coords(self.tag_edge_id + str(j))
if self.penultimate_intersect((x1, y1), (x2, y2), (x3, y3)): return True
# Check the 1st edge, where points p2 and p3 can coincide
x3, y3, x4, y4 = self.canvas.coords(self.tag_edge_start)
if self.first_intersect((x1, y1), (x2, y2), (x3, y3), (x4, y4)): return True
return False # there is no self-intersections in the polygon
class MainWindow(ttk.Frame):
""" Main window class """
def __init__(self, mainframe, path):
""" Initialize the main Frame """
ttk.Frame.__init__(self, master=mainframe)
self.master.title('Draw polygons')
self.master.geometry('800x600')
self.master.rowconfigure(0, weight=1) # make the CanvasImage expandable
self.master.columnconfigure(0, weight=1)
polygons = Polygons(self.master, path)
polygons.grid(row=0, column=0)
filename = './data/doge.jpg' # place path to your image here
root = tk.Tk()
app = MainWindow(root, path=filename)
root.mainloop()