labelimgplusplus 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
libs/canvas.py ADDED
@@ -0,0 +1,770 @@
1
+
2
+ try:
3
+ from PyQt5.QtGui import *
4
+ from PyQt5.QtCore import *
5
+ from PyQt5.QtWidgets import *
6
+ except ImportError:
7
+ from PyQt4.QtGui import *
8
+ from PyQt4.QtCore import *
9
+
10
+ # from PyQt4.QtOpenGL import *
11
+
12
+ from libs.shape import Shape
13
+ from libs.utils import distance
14
+
15
+ CURSOR_DEFAULT = Qt.ArrowCursor
16
+ CURSOR_POINT = Qt.PointingHandCursor
17
+ CURSOR_DRAW = Qt.CrossCursor
18
+ CURSOR_MOVE = Qt.ClosedHandCursor
19
+ CURSOR_GRAB = Qt.OpenHandCursor
20
+
21
+ # class Canvas(QGLWidget):
22
+
23
+
24
+ class Canvas(QWidget):
25
+ zoomRequest = pyqtSignal(int)
26
+ lightRequest = pyqtSignal(int)
27
+ scrollRequest = pyqtSignal(int, int)
28
+ newShape = pyqtSignal()
29
+ selectionChanged = pyqtSignal(bool)
30
+ shapeMoved = pyqtSignal()
31
+ drawingPolygon = pyqtSignal(bool)
32
+
33
+ CREATE, EDIT = list(range(2))
34
+
35
+ epsilon = 24.0
36
+
37
+ def __init__(self, *args, **kwargs):
38
+ super(Canvas, self).__init__(*args, **kwargs)
39
+ # Initialise local state.
40
+ self.mode = self.EDIT
41
+ self.shapes = []
42
+ self.current = None
43
+ self.selected_shape = None # save the selected shape here
44
+ self.selected_shape_copy = None
45
+ self.drawing_line_color = QColor(0, 0, 255)
46
+ self.drawing_rect_color = QColor(0, 0, 255)
47
+ self.line = Shape(line_color=self.drawing_line_color)
48
+ self.prev_point = QPointF()
49
+ self.offsets = QPointF(), QPointF()
50
+ self.scale = 1.0
51
+ self.overlay_color = None
52
+ self.label_font_size = 8
53
+ self.pixmap = QPixmap()
54
+ self.visible = {}
55
+ self._hide_background = False
56
+ self.hide_background = False
57
+ self.h_shape = None
58
+ self.h_vertex = None
59
+ self._painter = QPainter()
60
+ self._cursor = CURSOR_DEFAULT
61
+ # Menus:
62
+ self.menus = (QMenu(), QMenu())
63
+ # Set widget options.
64
+ self.setMouseTracking(True)
65
+ self.setFocusPolicy(Qt.WheelFocus)
66
+ self.verified = False
67
+ self.draw_square = False
68
+
69
+ # initialisation for panning
70
+ self.pan_initial_pos = QPoint()
71
+
72
+ def set_drawing_color(self, qcolor):
73
+ self.drawing_line_color = qcolor
74
+ self.drawing_rect_color = qcolor
75
+
76
+ def enterEvent(self, ev):
77
+ self.override_cursor(self._cursor)
78
+
79
+ def leaveEvent(self, ev):
80
+ self.restore_cursor()
81
+
82
+ def focusOutEvent(self, ev):
83
+ self.restore_cursor()
84
+
85
+ def isVisible(self, shape):
86
+ return self.visible.get(shape, True)
87
+
88
+ def drawing(self):
89
+ return self.mode == self.CREATE
90
+
91
+ def editing(self):
92
+ return self.mode == self.EDIT
93
+
94
+ def set_editing(self, value=True):
95
+ self.mode = self.EDIT if value else self.CREATE
96
+ if not value: # Create
97
+ self.un_highlight()
98
+ self.de_select_shape()
99
+ self.prev_point = QPointF()
100
+ self.repaint()
101
+
102
+ def un_highlight(self, shape=None):
103
+ if shape == None or shape == self.h_shape:
104
+ if self.h_shape:
105
+ self.h_shape.highlight_clear()
106
+ self.h_vertex = self.h_shape = None
107
+
108
+ def selected_vertex(self):
109
+ return self.h_vertex is not None
110
+
111
+ def mouseMoveEvent(self, ev):
112
+ """Update line with last point and current coordinates."""
113
+ pos = self.transform_pos(ev.pos())
114
+
115
+ # Update coordinates in status bar if image is opened
116
+ window = self.parent().window()
117
+ if window.file_path is not None:
118
+ self.parent().window().label_coordinates.setText(
119
+ 'X: %d; Y: %d' % (pos.x(), pos.y()))
120
+
121
+ # Polygon drawing.
122
+ if self.drawing():
123
+ self.override_cursor(CURSOR_DRAW)
124
+ if self.current:
125
+ # Display annotation width and height while drawing
126
+ current_width = abs(self.current[0].x() - pos.x())
127
+ current_height = abs(self.current[0].y() - pos.y())
128
+ self.parent().window().label_coordinates.setText(
129
+ 'Width: %d, Height: %d / X: %d; Y: %d' % (current_width, current_height, pos.x(), pos.y()))
130
+
131
+ color = self.drawing_line_color
132
+ if self.out_of_pixmap(pos):
133
+ # Don't allow the user to draw outside the pixmap.
134
+ # Clip the coordinates to 0 or max,
135
+ # if they are outside the range [0, max]
136
+ size = self.pixmap.size()
137
+ clipped_x = min(max(0, pos.x()), size.width())
138
+ clipped_y = min(max(0, pos.y()), size.height())
139
+ pos = QPointF(clipped_x, clipped_y)
140
+ elif len(self.current) > 1 and self.close_enough(pos, self.current[0]):
141
+ # Attract line to starting point and colorise to alert the
142
+ # user:
143
+ pos = self.current[0]
144
+ color = self.current.line_color
145
+ self.override_cursor(CURSOR_POINT)
146
+ self.current.highlight_vertex(0, Shape.NEAR_VERTEX)
147
+
148
+ if self.draw_square:
149
+ init_pos = self.current[0]
150
+ min_x = init_pos.x()
151
+ min_y = init_pos.y()
152
+ min_size = min(abs(pos.x() - min_x), abs(pos.y() - min_y))
153
+ direction_x = -1 if pos.x() - min_x < 0 else 1
154
+ direction_y = -1 if pos.y() - min_y < 0 else 1
155
+ self.line[1] = QPointF(min_x + direction_x * min_size, min_y + direction_y * min_size)
156
+ else:
157
+ self.line[1] = pos
158
+
159
+ self.line.line_color = color
160
+ self.prev_point = QPointF()
161
+ self.current.highlight_clear()
162
+ else:
163
+ self.prev_point = pos
164
+ self.repaint()
165
+ return
166
+
167
+ # Polygon copy moving.
168
+ if Qt.RightButton & ev.buttons():
169
+ if self.selected_shape_copy and self.prev_point:
170
+ self.override_cursor(CURSOR_MOVE)
171
+ self.bounded_move_shape(self.selected_shape_copy, pos)
172
+ self.repaint()
173
+ elif self.selected_shape:
174
+ self.selected_shape_copy = self.selected_shape.copy()
175
+ self.repaint()
176
+ return
177
+
178
+ # Polygon/Vertex moving.
179
+ if Qt.LeftButton & ev.buttons():
180
+ if self.selected_vertex():
181
+ self.bounded_move_vertex(pos)
182
+ self.shapeMoved.emit()
183
+ self.repaint()
184
+
185
+ # Display annotation width and height while moving vertex
186
+ point1 = self.h_shape[1]
187
+ point3 = self.h_shape[3]
188
+ current_width = abs(point1.x() - point3.x())
189
+ current_height = abs(point1.y() - point3.y())
190
+ self.parent().window().label_coordinates.setText(
191
+ 'Width: %d, Height: %d / X: %d; Y: %d' % (current_width, current_height, pos.x(), pos.y()))
192
+ elif self.selected_shape and self.prev_point:
193
+ self.override_cursor(CURSOR_MOVE)
194
+ self.bounded_move_shape(self.selected_shape, pos)
195
+ self.shapeMoved.emit()
196
+ self.repaint()
197
+
198
+ # Display annotation width and height while moving shape
199
+ point1 = self.selected_shape[1]
200
+ point3 = self.selected_shape[3]
201
+ current_width = abs(point1.x() - point3.x())
202
+ current_height = abs(point1.y() - point3.y())
203
+ self.parent().window().label_coordinates.setText(
204
+ 'Width: %d, Height: %d / X: %d; Y: %d' % (current_width, current_height, pos.x(), pos.y()))
205
+ else:
206
+ # pan
207
+ delta = ev.pos() - self.pan_initial_pos
208
+ self.scrollRequest.emit(delta.x(), Qt.Horizontal)
209
+ self.scrollRequest.emit(delta.y(), Qt.Vertical)
210
+ self.update()
211
+ return
212
+
213
+ # Just hovering over the canvas, 2 possibilities:
214
+ # - Highlight shapes
215
+ # - Highlight vertex
216
+ # Update shape/vertex fill and tooltip value accordingly.
217
+ self.setToolTip("Image")
218
+ priority_list = self.shapes + ([self.selected_shape] if self.selected_shape else [])
219
+ visible_shapes = [s for s in priority_list if self.isVisible(s)]
220
+
221
+ # First pass: check for nearby vertices (these take priority)
222
+ vertex_found = False
223
+ for shape in reversed(visible_shapes):
224
+ index = shape.nearest_vertex(pos, self.epsilon)
225
+ if index is not None:
226
+ if self.selected_vertex():
227
+ self.h_shape.highlight_clear()
228
+ self.h_vertex, self.h_shape = index, shape
229
+ shape.highlight_vertex(index, shape.MOVE_VERTEX)
230
+ self.override_cursor(CURSOR_POINT)
231
+ self.setToolTip("Click & drag to move point")
232
+ self.setStatusTip(self.toolTip())
233
+ self.update()
234
+ vertex_found = True
235
+ break
236
+
237
+ if not vertex_found:
238
+ # Second pass: find smallest shape containing point (for nested box support)
239
+ candidates = [s for s in visible_shapes if s.contains_point(pos)]
240
+ if candidates:
241
+ def shape_area(s):
242
+ rect = s.bounding_rect()
243
+ return rect.width() * rect.height()
244
+ shape = min(candidates, key=shape_area)
245
+
246
+ if self.selected_vertex():
247
+ self.h_shape.highlight_clear()
248
+ self.h_vertex, self.h_shape = None, shape
249
+ self.setToolTip(
250
+ "Click & drag to move shape '%s'" % shape.label)
251
+ self.setStatusTip(self.toolTip())
252
+ self.override_cursor(CURSOR_GRAB)
253
+ self.update()
254
+
255
+ # Display annotation width and height while hovering inside
256
+ point1 = self.h_shape[1]
257
+ point3 = self.h_shape[3]
258
+ current_width = abs(point1.x() - point3.x())
259
+ current_height = abs(point1.y() - point3.y())
260
+ self.parent().window().label_coordinates.setText(
261
+ 'Width: %d, Height: %d / X: %d; Y: %d' % (current_width, current_height, pos.x(), pos.y()))
262
+ else: # Nothing found, clear highlights, reset state.
263
+ if self.h_shape:
264
+ self.h_shape.highlight_clear()
265
+ self.update()
266
+ self.h_vertex, self.h_shape = None, None
267
+ self.override_cursor(CURSOR_DEFAULT)
268
+
269
+ def mousePressEvent(self, ev):
270
+ pos = self.transform_pos(ev.pos())
271
+
272
+ if ev.button() == Qt.LeftButton:
273
+ if self.drawing():
274
+ self.handle_drawing(pos)
275
+ else:
276
+ selection = self.select_shape_point(pos)
277
+ self.prev_point = pos
278
+
279
+ if selection is None:
280
+ # pan
281
+ QApplication.setOverrideCursor(QCursor(Qt.OpenHandCursor))
282
+ self.pan_initial_pos = ev.pos()
283
+
284
+ elif ev.button() == Qt.RightButton and self.editing():
285
+ self.select_shape_point(pos)
286
+ self.prev_point = pos
287
+ self.update()
288
+
289
+ def mouseReleaseEvent(self, ev):
290
+ if ev.button() == Qt.RightButton:
291
+ menu = self.menus[bool(self.selected_shape_copy)]
292
+ self.restore_cursor()
293
+ if not menu.exec_(self.mapToGlobal(ev.pos()))\
294
+ and self.selected_shape_copy:
295
+ # Cancel the move by deleting the shadow copy.
296
+ self.selected_shape_copy = None
297
+ self.repaint()
298
+ elif ev.button() == Qt.LeftButton and self.selected_shape:
299
+ if self.selected_vertex():
300
+ self.override_cursor(CURSOR_POINT)
301
+ else:
302
+ self.override_cursor(CURSOR_GRAB)
303
+ elif ev.button() == Qt.LeftButton:
304
+ pos = self.transform_pos(ev.pos())
305
+ if self.drawing():
306
+ self.handle_drawing(pos)
307
+ else:
308
+ # pan
309
+ QApplication.restoreOverrideCursor()
310
+
311
+ def end_move(self, copy=False):
312
+ assert self.selected_shape and self.selected_shape_copy
313
+ shape = self.selected_shape_copy
314
+ # del shape.fill_color
315
+ # del shape.line_color
316
+ if copy:
317
+ self.shapes.append(shape)
318
+ self.selected_shape.selected = False
319
+ self.selected_shape = shape
320
+ self.repaint()
321
+ else:
322
+ self.selected_shape.points = [p for p in shape.points]
323
+ self.selected_shape_copy = None
324
+
325
+ def hide_background_shapes(self, value):
326
+ self.hide_background = value
327
+ if self.selected_shape:
328
+ # Only hide other shapes if there is a current selection.
329
+ # Otherwise the user will not be able to select a shape.
330
+ self.set_hiding(True)
331
+ self.repaint()
332
+
333
+ def handle_drawing(self, pos):
334
+ if self.current and self.current.reach_max_points() is False:
335
+ init_pos = self.current[0]
336
+ min_x = init_pos.x()
337
+ min_y = init_pos.y()
338
+ target_pos = self.line[1]
339
+ max_x = target_pos.x()
340
+ max_y = target_pos.y()
341
+ self.current.add_point(QPointF(max_x, min_y))
342
+ self.current.add_point(target_pos)
343
+ self.current.add_point(QPointF(min_x, max_y))
344
+ self.finalise()
345
+ elif not self.out_of_pixmap(pos):
346
+ self.current = Shape()
347
+ self.current.add_point(pos)
348
+ self.line.points = [pos, pos]
349
+ self.set_hiding()
350
+ self.drawingPolygon.emit(True)
351
+ self.update()
352
+
353
+ def set_hiding(self, enable=True):
354
+ self._hide_background = self.hide_background if enable else False
355
+
356
+ def can_close_shape(self):
357
+ return self.drawing() and self.current and len(self.current) > 2
358
+
359
+ def mouseDoubleClickEvent(self, ev):
360
+ # We need at least 4 points here, since the mousePress handler
361
+ # adds an extra one before this handler is called.
362
+ if self.can_close_shape() and len(self.current) > 3:
363
+ self.current.pop_point()
364
+ self.finalise()
365
+
366
+ def select_shape(self, shape):
367
+ self.de_select_shape()
368
+ shape.selected = True
369
+ self.selected_shape = shape
370
+ self.set_hiding()
371
+ self.selectionChanged.emit(True)
372
+ self.update()
373
+
374
+ def select_shape_point(self, point):
375
+ """Select the smallest shape which contains this point."""
376
+ self.de_select_shape()
377
+ if self.selected_vertex(): # A vertex is marked for selection.
378
+ index, shape = self.h_vertex, self.h_shape
379
+ shape.highlight_vertex(index, shape.MOVE_VERTEX)
380
+ self.select_shape(shape)
381
+ return self.h_vertex
382
+
383
+ # Find all shapes containing the point
384
+ candidates = []
385
+ for shape in self.shapes:
386
+ if self.isVisible(shape) and shape.contains_point(point):
387
+ candidates.append(shape)
388
+
389
+ if candidates:
390
+ # Select the smallest shape (by bounding rect area) for nested box support
391
+ def shape_area(s):
392
+ rect = s.bounding_rect()
393
+ return rect.width() * rect.height()
394
+ smallest = min(candidates, key=shape_area)
395
+ self.select_shape(smallest)
396
+ self.calculate_offsets(smallest, point)
397
+ return self.selected_shape
398
+ return None
399
+
400
+ def calculate_offsets(self, shape, point):
401
+ rect = shape.bounding_rect()
402
+ x1 = rect.x() - point.x()
403
+ y1 = rect.y() - point.y()
404
+ x2 = (rect.x() + rect.width()) - point.x()
405
+ y2 = (rect.y() + rect.height()) - point.y()
406
+ self.offsets = QPointF(x1, y1), QPointF(x2, y2)
407
+
408
+ def snap_point_to_canvas(self, x, y):
409
+ """
410
+ Moves a point x,y to within the boundaries of the canvas.
411
+ :return: (x,y,snapped) where snapped is True if x or y were changed, False if not.
412
+ """
413
+ if x < 0 or x > self.pixmap.width() or y < 0 or y > self.pixmap.height():
414
+ x = max(x, 0)
415
+ y = max(y, 0)
416
+ x = min(x, self.pixmap.width())
417
+ y = min(y, self.pixmap.height())
418
+ return x, y, True
419
+
420
+ return x, y, False
421
+
422
+ def bounded_move_vertex(self, pos):
423
+ index, shape = self.h_vertex, self.h_shape
424
+ point = shape[index]
425
+ if self.out_of_pixmap(pos):
426
+ size = self.pixmap.size()
427
+ clipped_x = min(max(0, pos.x()), size.width())
428
+ clipped_y = min(max(0, pos.y()), size.height())
429
+ pos = QPointF(clipped_x, clipped_y)
430
+
431
+ if self.draw_square:
432
+ opposite_point_index = (index + 2) % 4
433
+ opposite_point = shape[opposite_point_index]
434
+
435
+ min_size = min(abs(pos.x() - opposite_point.x()), abs(pos.y() - opposite_point.y()))
436
+ direction_x = -1 if pos.x() - opposite_point.x() < 0 else 1
437
+ direction_y = -1 if pos.y() - opposite_point.y() < 0 else 1
438
+ shift_pos = QPointF(opposite_point.x() + direction_x * min_size - point.x(),
439
+ opposite_point.y() + direction_y * min_size - point.y())
440
+ else:
441
+ shift_pos = pos - point
442
+
443
+ shape.move_vertex_by(index, shift_pos)
444
+
445
+ left_index = (index + 1) % 4
446
+ right_index = (index + 3) % 4
447
+ left_shift = None
448
+ right_shift = None
449
+ if index % 2 == 0:
450
+ right_shift = QPointF(shift_pos.x(), 0)
451
+ left_shift = QPointF(0, shift_pos.y())
452
+ else:
453
+ left_shift = QPointF(shift_pos.x(), 0)
454
+ right_shift = QPointF(0, shift_pos.y())
455
+ shape.move_vertex_by(right_index, right_shift)
456
+ shape.move_vertex_by(left_index, left_shift)
457
+
458
+ def bounded_move_shape(self, shape, pos):
459
+ if self.out_of_pixmap(pos):
460
+ return False # No need to move
461
+ o1 = pos + self.offsets[0]
462
+ if self.out_of_pixmap(o1):
463
+ pos -= QPointF(min(0, o1.x()), min(0, o1.y()))
464
+ o2 = pos + self.offsets[1]
465
+ if self.out_of_pixmap(o2):
466
+ pos += QPointF(min(0, self.pixmap.width() - o2.x()),
467
+ min(0, self.pixmap.height() - o2.y()))
468
+ # The next line tracks the new position of the cursor
469
+ # relative to the shape, but also results in making it
470
+ # a bit "shaky" when nearing the border and allows it to
471
+ # go outside of the shape's area for some reason. XXX
472
+ # self.calculateOffsets(self.selectedShape, pos)
473
+ dp = pos - self.prev_point
474
+ if dp:
475
+ shape.move_by(dp)
476
+ self.prev_point = pos
477
+ return True
478
+ return False
479
+
480
+ def de_select_shape(self):
481
+ if self.selected_shape:
482
+ self.selected_shape.selected = False
483
+ self.selected_shape = None
484
+ self.set_hiding(False)
485
+ self.selectionChanged.emit(False)
486
+ self.update()
487
+
488
+ def delete_selected(self):
489
+ if self.selected_shape:
490
+ shape = self.selected_shape
491
+ self.un_highlight(shape)
492
+ self.shapes.remove(self.selected_shape)
493
+ self.selected_shape = None
494
+ self.update()
495
+ return shape
496
+
497
+ def copy_selected_shape(self):
498
+ if self.selected_shape:
499
+ shape = self.selected_shape.copy()
500
+ self.de_select_shape()
501
+ self.shapes.append(shape)
502
+ shape.selected = True
503
+ self.selected_shape = shape
504
+ self.bounded_shift_shape(shape)
505
+ return shape
506
+
507
+ def bounded_shift_shape(self, shape):
508
+ # Try to move in one direction, and if it fails in another.
509
+ # Give up if both fail.
510
+ point = shape[0]
511
+ offset = QPointF(2.0, 2.0)
512
+ self.calculate_offsets(shape, point)
513
+ self.prev_point = point
514
+ if not self.bounded_move_shape(shape, point - offset):
515
+ self.bounded_move_shape(shape, point + offset)
516
+
517
+ def paintEvent(self, event):
518
+ if not self.pixmap:
519
+ return super(Canvas, self).paintEvent(event)
520
+
521
+ p = self._painter
522
+ p.begin(self)
523
+ p.setRenderHint(QPainter.Antialiasing)
524
+ p.setRenderHint(QPainter.HighQualityAntialiasing)
525
+ p.setRenderHint(QPainter.SmoothPixmapTransform)
526
+
527
+ p.scale(self.scale, self.scale)
528
+ p.translate(self.offset_to_center())
529
+
530
+ temp = self.pixmap
531
+ if self.overlay_color:
532
+ temp = QPixmap(self.pixmap)
533
+ painter = QPainter(temp)
534
+ painter.setCompositionMode(painter.CompositionMode_Overlay)
535
+ painter.fillRect(temp.rect(), self.overlay_color)
536
+ painter.end()
537
+
538
+ p.drawPixmap(0, 0, temp)
539
+ Shape.scale = self.scale
540
+ Shape.label_font_size = self.label_font_size
541
+ for shape in self.shapes:
542
+ if (shape.selected or not self._hide_background) and self.isVisible(shape):
543
+ shape.fill = shape.selected or shape == self.h_shape
544
+ shape.paint(p)
545
+ if self.current:
546
+ self.current.paint(p)
547
+ self.line.paint(p)
548
+ if self.selected_shape_copy:
549
+ self.selected_shape_copy.paint(p)
550
+
551
+ # Paint rect
552
+ if self.current is not None and len(self.line) == 2:
553
+ left_top = self.line[0]
554
+ right_bottom = self.line[1]
555
+ rect_width = right_bottom.x() - left_top.x()
556
+ rect_height = right_bottom.y() - left_top.y()
557
+ p.setPen(self.drawing_rect_color)
558
+ brush = QBrush(Qt.BDiagPattern)
559
+ p.setBrush(brush)
560
+ p.drawRect(int(left_top.x()), int(left_top.y()), int(rect_width), int(rect_height))
561
+
562
+ if self.drawing() and not self.prev_point.isNull() and not self.out_of_pixmap(self.prev_point):
563
+ p.setPen(QColor(0, 0, 0))
564
+ p.drawLine(int(self.prev_point.x()), 0, int(self.prev_point.x()), int(self.pixmap.height()))
565
+ p.drawLine(0, int(self.prev_point.y()), int(self.pixmap.width()), int(self.prev_point.y()))
566
+
567
+ self.setAutoFillBackground(True)
568
+ if self.verified:
569
+ pal = self.palette()
570
+ pal.setColor(self.backgroundRole(), QColor(184, 239, 38, 128))
571
+ self.setPalette(pal)
572
+ else:
573
+ pal = self.palette()
574
+ pal.setColor(self.backgroundRole(), QColor(232, 232, 232, 255))
575
+ self.setPalette(pal)
576
+
577
+ p.end()
578
+
579
+ def transform_pos(self, point):
580
+ """Convert from widget-logical coordinates to painter-logical coordinates."""
581
+ return point / self.scale - self.offset_to_center()
582
+
583
+ def offset_to_center(self):
584
+ s = self.scale
585
+ area = super(Canvas, self).size()
586
+ w, h = self.pixmap.width() * s, self.pixmap.height() * s
587
+ aw, ah = area.width(), area.height()
588
+ x = (aw - w) / (2 * s) if aw > w else 0
589
+ y = (ah - h) / (2 * s) if ah > h else 0
590
+ return QPointF(x, y)
591
+
592
+ def out_of_pixmap(self, p):
593
+ w, h = self.pixmap.width(), self.pixmap.height()
594
+ return not (0 <= p.x() <= w and 0 <= p.y() <= h)
595
+
596
+ def finalise(self):
597
+ assert self.current
598
+ if self.current.points[0] == self.current.points[-1]:
599
+ self.current = None
600
+ self.drawingPolygon.emit(False)
601
+ self.update()
602
+ return
603
+
604
+ self.current.close()
605
+ self.shapes.append(self.current)
606
+ self.current = None
607
+ self.set_hiding(False)
608
+ self.newShape.emit()
609
+ self.update()
610
+
611
+ def close_enough(self, p1, p2):
612
+ # d = distance(p1 - p2)
613
+ # m = (p1-p2).manhattanLength()
614
+ # print "d %.2f, m %d, %.2f" % (d, m, d - m)
615
+ return distance(p1 - p2) < self.epsilon
616
+
617
+ # These two, along with a call to adjustSize are required for the
618
+ # scroll area.
619
+ def sizeHint(self):
620
+ return self.minimumSizeHint()
621
+
622
+ def minimumSizeHint(self):
623
+ if self.pixmap:
624
+ return self.scale * self.pixmap.size()
625
+ return super(Canvas, self).minimumSizeHint()
626
+
627
+ def wheelEvent(self, ev):
628
+ qt_version = 4 if hasattr(ev, "delta") else 5
629
+ if qt_version == 4:
630
+ if ev.orientation() == Qt.Vertical:
631
+ v_delta = ev.delta()
632
+ h_delta = 0
633
+ else:
634
+ h_delta = ev.delta()
635
+ v_delta = 0
636
+ else:
637
+ delta = ev.angleDelta()
638
+ h_delta = delta.x()
639
+ v_delta = delta.y()
640
+
641
+ mods = ev.modifiers()
642
+ if int(Qt.ControlModifier) | int(Qt.ShiftModifier) == int(mods) and v_delta:
643
+ self.lightRequest.emit(v_delta)
644
+ elif Qt.ControlModifier == int(mods) and v_delta:
645
+ self.zoomRequest.emit(v_delta)
646
+ else:
647
+ v_delta and self.scrollRequest.emit(v_delta, Qt.Vertical)
648
+ h_delta and self.scrollRequest.emit(h_delta, Qt.Horizontal)
649
+ ev.accept()
650
+
651
+ def keyPressEvent(self, ev):
652
+ key = ev.key()
653
+ if key == Qt.Key_Escape and self.current:
654
+ print('ESC press')
655
+ self.current = None
656
+ self.drawingPolygon.emit(False)
657
+ self.update()
658
+ elif key == Qt.Key_Return and self.can_close_shape():
659
+ self.finalise()
660
+ elif key == Qt.Key_Left and self.selected_shape:
661
+ self.move_one_pixel('Left')
662
+ elif key == Qt.Key_Right and self.selected_shape:
663
+ self.move_one_pixel('Right')
664
+ elif key == Qt.Key_Up and self.selected_shape:
665
+ self.move_one_pixel('Up')
666
+ elif key == Qt.Key_Down and self.selected_shape:
667
+ self.move_one_pixel('Down')
668
+
669
+ def move_one_pixel(self, direction):
670
+ # print(self.selectedShape.points)
671
+ if direction == 'Left' and not self.move_out_of_bound(QPointF(-1.0, 0)):
672
+ # print("move Left one pixel")
673
+ self.selected_shape.points[0] += QPointF(-1.0, 0)
674
+ self.selected_shape.points[1] += QPointF(-1.0, 0)
675
+ self.selected_shape.points[2] += QPointF(-1.0, 0)
676
+ self.selected_shape.points[3] += QPointF(-1.0, 0)
677
+ elif direction == 'Right' and not self.move_out_of_bound(QPointF(1.0, 0)):
678
+ # print("move Right one pixel")
679
+ self.selected_shape.points[0] += QPointF(1.0, 0)
680
+ self.selected_shape.points[1] += QPointF(1.0, 0)
681
+ self.selected_shape.points[2] += QPointF(1.0, 0)
682
+ self.selected_shape.points[3] += QPointF(1.0, 0)
683
+ elif direction == 'Up' and not self.move_out_of_bound(QPointF(0, -1.0)):
684
+ # print("move Up one pixel")
685
+ self.selected_shape.points[0] += QPointF(0, -1.0)
686
+ self.selected_shape.points[1] += QPointF(0, -1.0)
687
+ self.selected_shape.points[2] += QPointF(0, -1.0)
688
+ self.selected_shape.points[3] += QPointF(0, -1.0)
689
+ elif direction == 'Down' and not self.move_out_of_bound(QPointF(0, 1.0)):
690
+ # print("move Down one pixel")
691
+ self.selected_shape.points[0] += QPointF(0, 1.0)
692
+ self.selected_shape.points[1] += QPointF(0, 1.0)
693
+ self.selected_shape.points[2] += QPointF(0, 1.0)
694
+ self.selected_shape.points[3] += QPointF(0, 1.0)
695
+ self.shapeMoved.emit()
696
+ self.repaint()
697
+
698
+ def move_out_of_bound(self, step):
699
+ points = [p1 + p2 for p1, p2 in zip(self.selected_shape.points, [step] * 4)]
700
+ return True in map(self.out_of_pixmap, points)
701
+
702
+ def set_last_label(self, text, line_color=None, fill_color=None):
703
+ assert text
704
+ self.shapes[-1].label = text
705
+ if line_color:
706
+ self.shapes[-1].line_color = line_color
707
+
708
+ if fill_color:
709
+ self.shapes[-1].fill_color = fill_color
710
+
711
+ return self.shapes[-1]
712
+
713
+ def undo_last_line(self):
714
+ assert self.shapes
715
+ self.current = self.shapes.pop()
716
+ self.current.set_open()
717
+ self.line.points = [self.current[-1], self.current[0]]
718
+ self.drawingPolygon.emit(True)
719
+
720
+ def reset_all_lines(self):
721
+ assert self.shapes
722
+ self.current = self.shapes.pop()
723
+ self.current.set_open()
724
+ self.line.points = [self.current[-1], self.current[0]]
725
+ self.drawingPolygon.emit(True)
726
+ self.current = None
727
+ self.drawingPolygon.emit(False)
728
+ self.update()
729
+
730
+ def load_pixmap(self, pixmap):
731
+ self.pixmap = pixmap
732
+ self.shapes = []
733
+ self.repaint()
734
+
735
+ def load_shapes(self, shapes):
736
+ self.shapes = list(shapes)
737
+ self.current = None
738
+ self.repaint()
739
+
740
+ def set_shape_visible(self, shape, value):
741
+ self.visible[shape] = value
742
+ self.repaint()
743
+
744
+ def current_cursor(self):
745
+ cursor = QApplication.overrideCursor()
746
+ if cursor is not None:
747
+ cursor = cursor.shape()
748
+ return cursor
749
+
750
+ def override_cursor(self, cursor):
751
+ self._cursor = cursor
752
+ if self.current_cursor() is None:
753
+ QApplication.setOverrideCursor(cursor)
754
+ else:
755
+ QApplication.changeOverrideCursor(cursor)
756
+
757
+ def restore_cursor(self):
758
+ QApplication.restoreOverrideCursor()
759
+
760
+ def reset_state(self):
761
+ self.de_select_shape()
762
+ self.un_highlight()
763
+ self.selected_shape_copy = None
764
+
765
+ self.restore_cursor()
766
+ self.pixmap = None
767
+ self.update()
768
+
769
+ def set_drawing_shape_to_square(self, status):
770
+ self.draw_square = status