canvas.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. from qtpy import QtCore
  2. from qtpy import QtGui
  3. from qtpy import QtWidgets
  4. from labelme.lib import distance
  5. from labelme import QT5
  6. from labelme.shape import Shape
  7. # TODO(unknown):
  8. # - [maybe] Find optimal epsilon value.
  9. CURSOR_DEFAULT = QtCore.Qt.ArrowCursor
  10. CURSOR_POINT = QtCore.Qt.PointingHandCursor
  11. CURSOR_DRAW = QtCore.Qt.CrossCursor
  12. CURSOR_MOVE = QtCore.Qt.ClosedHandCursor
  13. CURSOR_GRAB = QtCore.Qt.OpenHandCursor
  14. class Canvas(QtWidgets.QWidget):
  15. zoomRequest = QtCore.Signal(int, QtCore.QPoint)
  16. scrollRequest = QtCore.Signal(int, int)
  17. newShape = QtCore.Signal()
  18. selectionChanged = QtCore.Signal(bool)
  19. shapeMoved = QtCore.Signal()
  20. drawingPolygon = QtCore.Signal(bool)
  21. edgeSelected = QtCore.Signal(bool)
  22. CREATE, EDIT = 0, 1
  23. # polygon, rectangle, or line
  24. _createMode = 'polygon'
  25. def __init__(self, *args, **kwargs):
  26. self.epsilon = kwargs.pop('epsilon', 11.0)
  27. super(Canvas, self).__init__(*args, **kwargs)
  28. # Initialise local state.
  29. self.mode = self.EDIT
  30. self.shapes = []
  31. self.shapesBackups = []
  32. self.current = None
  33. self.selectedShape = None # save the selected shape here
  34. self.selectedShapeCopy = None
  35. self.lineColor = QtGui.QColor(0, 0, 255)
  36. # self.line represents:
  37. # - createMode == 'polygon': edge from last point to current
  38. # - createMode == 'rectangle': diagonal line of the rectangle
  39. # - createMode == 'line': the line
  40. self.line = Shape(line_color=self.lineColor)
  41. self.prevPoint = QtCore.QPoint()
  42. self.prevMovePoint = QtCore.QPoint()
  43. self.offsets = QtCore.QPoint(), QtCore.QPoint()
  44. self.scale = 1.0
  45. self.pixmap = QtGui.QPixmap()
  46. self.visible = {}
  47. self._hideBackround = False
  48. self.hideBackround = False
  49. self.hShape = None
  50. self.hVertex = None
  51. self.hEdge = None
  52. self.movingShape = False
  53. self._painter = QtGui.QPainter()
  54. self._cursor = CURSOR_DEFAULT
  55. # Menus:
  56. self.menus = (QtWidgets.QMenu(), QtWidgets.QMenu())
  57. # Set widget options.
  58. self.setMouseTracking(True)
  59. self.setFocusPolicy(QtCore.Qt.WheelFocus)
  60. @property
  61. def createMode(self):
  62. return self._createMode
  63. @createMode.setter
  64. def createMode(self, value):
  65. if value not in ['polygon', 'rectangle', 'line']:
  66. raise ValueError('Unsupported createMode: %s' % value)
  67. self._createMode = value
  68. def storeShapes(self):
  69. shapesBackup = []
  70. for shape in self.shapes:
  71. shapesBackup.append(shape.copy())
  72. if len(self.shapesBackups) >= 10:
  73. self.shapesBackups = self.shapesBackups[-9:]
  74. self.shapesBackups.append(shapesBackup)
  75. @property
  76. def isShapeRestorable(self):
  77. if len(self.shapesBackups) < 2:
  78. return False
  79. return True
  80. def restoreShape(self):
  81. if not self.isShapeRestorable:
  82. return
  83. self.shapesBackups.pop() # latest
  84. shapesBackup = self.shapesBackups.pop()
  85. self.shapes = shapesBackup
  86. self.storeShapes()
  87. self.repaint()
  88. def enterEvent(self, ev):
  89. self.overrideCursor(self._cursor)
  90. def leaveEvent(self, ev):
  91. self.restoreCursor()
  92. def focusOutEvent(self, ev):
  93. self.restoreCursor()
  94. def isVisible(self, shape):
  95. return self.visible.get(shape, True)
  96. def drawing(self):
  97. return self.mode == self.CREATE
  98. def editing(self):
  99. return self.mode == self.EDIT
  100. def setEditing(self, value=True):
  101. self.mode = self.EDIT if value else self.CREATE
  102. if not value: # Create
  103. self.unHighlight()
  104. self.deSelectShape()
  105. def unHighlight(self):
  106. if self.hShape:
  107. self.hShape.highlightClear()
  108. self.hVertex = self.hShape = None
  109. def selectedVertex(self):
  110. return self.hVertex is not None
  111. def mouseMoveEvent(self, ev):
  112. """Update line with last point and current coordinates."""
  113. if QT5:
  114. pos = self.transformPos(ev.pos())
  115. else:
  116. pos = self.transformPos(ev.posF())
  117. self.prevMovePoint = pos
  118. self.restoreCursor()
  119. # Polygon drawing.
  120. if self.drawing():
  121. self.overrideCursor(CURSOR_DRAW)
  122. if not self.current:
  123. return
  124. color = self.lineColor
  125. if self.outOfPixmap(pos):
  126. # Don't allow the user to draw outside the pixmap.
  127. # Project the point to the pixmap's edges.
  128. pos = self.intersectionPoint(self.current[-1], pos)
  129. elif len(self.current) > 1 and \
  130. self.closeEnough(pos, self.current[0]):
  131. # Attract line to starting point and
  132. # colorise to alert the user.
  133. pos = self.current[0]
  134. color = self.current.line_color
  135. self.overrideCursor(CURSOR_POINT)
  136. self.current.highlightVertex(0, Shape.NEAR_VERTEX)
  137. if self.createMode == 'polygon':
  138. self.line[0] = self.current[-1]
  139. self.line[1] = pos
  140. elif self.createMode == 'rectangle':
  141. self.line.points = list(self.getRectangleFromLine(
  142. (self.current[0], pos)
  143. ))
  144. self.line.close()
  145. elif self.createMode == 'line':
  146. self.line.points = [self.current[0], pos]
  147. self.line.close()
  148. self.line.line_color = color
  149. self.repaint()
  150. self.current.highlightClear()
  151. return
  152. # Polygon copy moving.
  153. if QtCore.Qt.RightButton & ev.buttons():
  154. if self.selectedShapeCopy and self.prevPoint:
  155. self.overrideCursor(CURSOR_MOVE)
  156. self.boundedMoveShape(self.selectedShapeCopy, pos)
  157. self.repaint()
  158. elif self.selectedShape:
  159. self.selectedShapeCopy = self.selectedShape.copy()
  160. self.repaint()
  161. return
  162. # Polygon/Vertex moving.
  163. self.movingShape = False
  164. if QtCore.Qt.LeftButton & ev.buttons():
  165. if self.selectedVertex():
  166. self.boundedMoveVertex(pos)
  167. self.repaint()
  168. self.movingShape = True
  169. elif self.selectedShape and self.prevPoint:
  170. self.overrideCursor(CURSOR_MOVE)
  171. self.boundedMoveShape(self.selectedShape, pos)
  172. self.repaint()
  173. self.movingShape = True
  174. return
  175. # Just hovering over the canvas, 2 posibilities:
  176. # - Highlight shapes
  177. # - Highlight vertex
  178. # Update shape/vertex fill and tooltip value accordingly.
  179. self.setToolTip("Image")
  180. for shape in reversed([s for s in self.shapes if self.isVisible(s)]):
  181. # Look for a nearby vertex to highlight. If that fails,
  182. # check if we happen to be inside a shape.
  183. index = shape.nearestVertex(pos, self.epsilon)
  184. index_edge = shape.nearestEdge(pos, self.epsilon)
  185. if index is not None:
  186. if self.selectedVertex():
  187. self.hShape.highlightClear()
  188. self.hVertex = index
  189. self.hShape = shape
  190. self.hEdge = index_edge
  191. shape.highlightVertex(index, shape.MOVE_VERTEX)
  192. self.overrideCursor(CURSOR_POINT)
  193. self.setToolTip("Click & drag to move point")
  194. self.setStatusTip(self.toolTip())
  195. self.update()
  196. break
  197. elif shape.containsPoint(pos):
  198. if self.selectedVertex():
  199. self.hShape.highlightClear()
  200. self.hVertex = None
  201. self.hShape = shape
  202. self.hEdge = index_edge
  203. self.setToolTip(
  204. "Click & drag to move shape '%s'" % shape.label)
  205. self.setStatusTip(self.toolTip())
  206. self.overrideCursor(CURSOR_GRAB)
  207. self.update()
  208. break
  209. else: # Nothing found, clear highlights, reset state.
  210. if self.hShape:
  211. self.hShape.highlightClear()
  212. self.update()
  213. self.hVertex, self.hShape, self.hEdge = None, None, None
  214. self.edgeSelected.emit(self.hEdge is not None)
  215. def addPointToEdge(self):
  216. if (self.hShape is None and
  217. self.hEdge is None and
  218. self.prevMovePoint is None):
  219. return
  220. shape = self.hShape
  221. index = self.hEdge
  222. point = self.prevMovePoint
  223. shape.insertPoint(index, point)
  224. shape.highlightVertex(index, shape.MOVE_VERTEX)
  225. self.hShape = shape
  226. self.hVertex = index
  227. self.hEdge = None
  228. def getRectangleFromLine(self, line):
  229. pt1 = line[0]
  230. pt3 = line[1]
  231. pt2 = QtCore.QPoint(pt3.x(), pt1.y())
  232. pt4 = QtCore.QPoint(pt1.x(), pt3.y())
  233. return pt1, pt2, pt3, pt4
  234. def mousePressEvent(self, ev):
  235. if QT5:
  236. pos = self.transformPos(ev.pos())
  237. else:
  238. pos = self.transformPos(ev.posF())
  239. if ev.button() == QtCore.Qt.LeftButton:
  240. if self.drawing():
  241. if self.current:
  242. # Add point to existing shape.
  243. if self.createMode == 'polygon':
  244. self.current.addPoint(self.line[1])
  245. self.line[0] = self.current[-1]
  246. if self.current.isClosed():
  247. self.finalise()
  248. elif self.createMode in ['rectangle', 'line']:
  249. assert len(self.current.points) == 1
  250. self.current.points = self.line.points
  251. self.finalise()
  252. elif not self.outOfPixmap(pos):
  253. # Create new shape.
  254. self.current = Shape()
  255. self.current.addPoint(pos)
  256. self.line.points = [pos, pos]
  257. self.setHiding()
  258. self.drawingPolygon.emit(True)
  259. self.update()
  260. else:
  261. self.selectShapePoint(pos)
  262. self.prevPoint = pos
  263. self.repaint()
  264. elif ev.button() == QtCore.Qt.RightButton and self.editing():
  265. self.selectShapePoint(pos)
  266. self.prevPoint = pos
  267. self.repaint()
  268. def mouseReleaseEvent(self, ev):
  269. if ev.button() == QtCore.Qt.RightButton:
  270. menu = self.menus[bool(self.selectedShapeCopy)]
  271. self.restoreCursor()
  272. if not menu.exec_(self.mapToGlobal(ev.pos()))\
  273. and self.selectedShapeCopy:
  274. # Cancel the move by deleting the shadow copy.
  275. self.selectedShapeCopy = None
  276. self.repaint()
  277. elif ev.button() == QtCore.Qt.LeftButton and self.selectedShape:
  278. self.overrideCursor(CURSOR_GRAB)
  279. if self.movingShape:
  280. self.storeShapes()
  281. self.shapeMoved.emit()
  282. def endMove(self, copy=False):
  283. assert self.selectedShape and self.selectedShapeCopy
  284. shape = self.selectedShapeCopy
  285. # del shape.fill_color
  286. # del shape.line_color
  287. if copy:
  288. self.shapes.append(shape)
  289. self.selectedShape.selected = False
  290. self.selectedShape = shape
  291. self.repaint()
  292. else:
  293. shape.label = self.selectedShape.label
  294. self.deleteSelected()
  295. self.shapes.append(shape)
  296. self.storeShapes()
  297. self.selectedShapeCopy = None
  298. def hideBackroundShapes(self, value):
  299. self.hideBackround = value
  300. if self.selectedShape:
  301. # Only hide other shapes if there is a current selection.
  302. # Otherwise the user will not be able to select a shape.
  303. self.setHiding(True)
  304. self.repaint()
  305. def setHiding(self, enable=True):
  306. self._hideBackround = self.hideBackround if enable else False
  307. def canCloseShape(self):
  308. return self.drawing() and self.current and len(self.current) > 2
  309. def mouseDoubleClickEvent(self, ev):
  310. # We need at least 4 points here, since the mousePress handler
  311. # adds an extra one before this handler is called.
  312. if self.canCloseShape() and len(self.current) > 3:
  313. self.current.popPoint()
  314. self.finalise()
  315. def selectShape(self, shape):
  316. self.deSelectShape()
  317. shape.selected = True
  318. self.selectedShape = shape
  319. self.setHiding()
  320. self.selectionChanged.emit(True)
  321. self.update()
  322. def selectShapePoint(self, point):
  323. """Select the first shape created which contains this point."""
  324. self.deSelectShape()
  325. if self.selectedVertex(): # A vertex is marked for selection.
  326. index, shape = self.hVertex, self.hShape
  327. shape.highlightVertex(index, shape.MOVE_VERTEX)
  328. return
  329. for shape in reversed(self.shapes):
  330. if self.isVisible(shape) and shape.containsPoint(point):
  331. shape.selected = True
  332. self.selectedShape = shape
  333. self.calculateOffsets(shape, point)
  334. self.setHiding()
  335. self.selectionChanged.emit(True)
  336. return
  337. def calculateOffsets(self, shape, point):
  338. rect = shape.boundingRect()
  339. x1 = rect.x() - point.x()
  340. y1 = rect.y() - point.y()
  341. x2 = (rect.x() + rect.width()) - point.x()
  342. y2 = (rect.y() + rect.height()) - point.y()
  343. self.offsets = QtCore.QPoint(x1, y1), QtCore.QPoint(x2, y2)
  344. def boundedMoveVertex(self, pos):
  345. index, shape = self.hVertex, self.hShape
  346. point = shape[index]
  347. if self.outOfPixmap(pos):
  348. pos = self.intersectionPoint(point, pos)
  349. shape.moveVertexBy(index, pos - point)
  350. def boundedMoveShape(self, shape, pos):
  351. if self.outOfPixmap(pos):
  352. return False # No need to move
  353. o1 = pos + self.offsets[0]
  354. if self.outOfPixmap(o1):
  355. pos -= QtCore.QPoint(min(0, o1.x()), min(0, o1.y()))
  356. o2 = pos + self.offsets[1]
  357. if self.outOfPixmap(o2):
  358. pos += QtCore.QPoint(min(0, self.pixmap.width() - o2.x()),
  359. min(0, self.pixmap.height() - o2.y()))
  360. # XXX: The next line tracks the new position of the cursor
  361. # relative to the shape, but also results in making it
  362. # a bit "shaky" when nearing the border and allows it to
  363. # go outside of the shape's area for some reason.
  364. # self.calculateOffsets(self.selectedShape, pos)
  365. dp = pos - self.prevPoint
  366. if dp:
  367. shape.moveBy(dp)
  368. self.prevPoint = pos
  369. return True
  370. return False
  371. def deSelectShape(self):
  372. if self.selectedShape:
  373. self.selectedShape.selected = False
  374. self.selectedShape = None
  375. self.setHiding(False)
  376. self.selectionChanged.emit(False)
  377. self.update()
  378. def deleteSelected(self):
  379. if self.selectedShape:
  380. shape = self.selectedShape
  381. self.shapes.remove(self.selectedShape)
  382. self.storeShapes()
  383. self.selectedShape = None
  384. self.update()
  385. return shape
  386. def copySelectedShape(self):
  387. if self.selectedShape:
  388. shape = self.selectedShape.copy()
  389. self.deSelectShape()
  390. self.shapes.append(shape)
  391. self.storeShapes()
  392. shape.selected = True
  393. self.selectedShape = shape
  394. self.boundedShiftShape(shape)
  395. return shape
  396. def boundedShiftShape(self, shape):
  397. # Try to move in one direction, and if it fails in another.
  398. # Give up if both fail.
  399. point = shape[0]
  400. offset = QtCore.QPoint(2.0, 2.0)
  401. self.calculateOffsets(shape, point)
  402. self.prevPoint = point
  403. if not self.boundedMoveShape(shape, point - offset):
  404. self.boundedMoveShape(shape, point + offset)
  405. def paintEvent(self, event):
  406. if not self.pixmap:
  407. return super(Canvas, self).paintEvent(event)
  408. p = self._painter
  409. p.begin(self)
  410. p.setRenderHint(QtGui.QPainter.Antialiasing)
  411. p.setRenderHint(QtGui.QPainter.HighQualityAntialiasing)
  412. p.setRenderHint(QtGui.QPainter.SmoothPixmapTransform)
  413. p.scale(self.scale, self.scale)
  414. p.translate(self.offsetToCenter())
  415. p.drawPixmap(0, 0, self.pixmap)
  416. Shape.scale = self.scale
  417. for shape in self.shapes:
  418. if (shape.selected or not self._hideBackround) and \
  419. self.isVisible(shape):
  420. shape.fill = shape.selected or shape == self.hShape
  421. shape.paint(p)
  422. if self.current:
  423. self.current.paint(p)
  424. self.line.paint(p)
  425. if self.selectedShapeCopy:
  426. self.selectedShapeCopy.paint(p)
  427. p.end()
  428. def transformPos(self, point):
  429. """Convert from widget-logical coordinates to painter-logical ones."""
  430. return point / self.scale - self.offsetToCenter()
  431. def offsetToCenter(self):
  432. s = self.scale
  433. area = super(Canvas, self).size()
  434. w, h = self.pixmap.width() * s, self.pixmap.height() * s
  435. aw, ah = area.width(), area.height()
  436. x = (aw - w) / (2 * s) if aw > w else 0
  437. y = (ah - h) / (2 * s) if ah > h else 0
  438. return QtCore.QPoint(x, y)
  439. def outOfPixmap(self, p):
  440. w, h = self.pixmap.width(), self.pixmap.height()
  441. return not (0 <= p.x() <= w and 0 <= p.y() <= h)
  442. def finalise(self):
  443. assert self.current
  444. self.current.close()
  445. self.shapes.append(self.current)
  446. self.storeShapes()
  447. self.current = None
  448. self.setHiding(False)
  449. self.newShape.emit()
  450. self.update()
  451. def closeEnough(self, p1, p2):
  452. # d = distance(p1 - p2)
  453. # m = (p1-p2).manhattanLength()
  454. # print "d %.2f, m %d, %.2f" % (d, m, d - m)
  455. return distance(p1 - p2) < self.epsilon
  456. def intersectionPoint(self, p1, p2):
  457. # Cycle through each image edge in clockwise fashion,
  458. # and find the one intersecting the current line segment.
  459. # http://paulbourke.net/geometry/lineline2d/
  460. size = self.pixmap.size()
  461. points = [(0, 0),
  462. (size.width(), 0),
  463. (size.width(), size.height()),
  464. (0, size.height())]
  465. x1, y1 = p1.x(), p1.y()
  466. x2, y2 = p2.x(), p2.y()
  467. d, i, (x, y) = min(self.intersectingEdges((x1, y1), (x2, y2), points))
  468. x3, y3 = points[i]
  469. x4, y4 = points[(i + 1) % 4]
  470. if (x, y) == (x1, y1):
  471. # Handle cases where previous point is on one of the edges.
  472. if x3 == x4:
  473. return QtCore.QPoint(x3, min(max(0, y2), max(y3, y4)))
  474. else: # y3 == y4
  475. return QtCore.QPoint(min(max(0, x2), max(x3, x4)), y3)
  476. return QtCore.QPoint(x, y)
  477. def intersectingEdges(self, point1, point2, points):
  478. """Find intersecting edges.
  479. For each edge formed by `points', yield the intersection
  480. with the line segment `(x1,y1) - (x2,y2)`, if it exists.
  481. Also return the distance of `(x2,y2)' to the middle of the
  482. edge along with its index, so that the one closest can be chosen.
  483. """
  484. (x1, y1) = point1
  485. (x2, y2) = point2
  486. for i in range(4):
  487. x3, y3 = points[i]
  488. x4, y4 = points[(i + 1) % 4]
  489. denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
  490. nua = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)
  491. nub = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)
  492. if denom == 0:
  493. # This covers two cases:
  494. # nua == nub == 0: Coincident
  495. # otherwise: Parallel
  496. continue
  497. ua, ub = nua / denom, nub / denom
  498. if 0 <= ua <= 1 and 0 <= ub <= 1:
  499. x = x1 + ua * (x2 - x1)
  500. y = y1 + ua * (y2 - y1)
  501. m = QtCore.QPoint((x3 + x4) / 2, (y3 + y4) / 2)
  502. d = distance(m - QtCore.QPoint(x2, y2))
  503. yield d, i, (x, y)
  504. # These two, along with a call to adjustSize are required for the
  505. # scroll area.
  506. def sizeHint(self):
  507. return self.minimumSizeHint()
  508. def minimumSizeHint(self):
  509. if self.pixmap:
  510. return self.scale * self.pixmap.size()
  511. return super(Canvas, self).minimumSizeHint()
  512. def wheelEvent(self, ev):
  513. if QT5:
  514. mods = ev.modifiers()
  515. delta = ev.angleDelta()
  516. if QtCore.Qt.ControlModifier == int(mods):
  517. # with Ctrl/Command key
  518. # zoom
  519. self.zoomRequest.emit(delta.y(), ev.pos())
  520. else:
  521. # scroll
  522. self.scrollRequest.emit(delta.x(), QtCore.Qt.Horizontal)
  523. self.scrollRequest.emit(delta.y(), QtCore.Qt.Vertical)
  524. else:
  525. if ev.orientation() == QtCore.Qt.Vertical:
  526. mods = ev.modifiers()
  527. if QtCore.Qt.ControlModifier == int(mods):
  528. # with Ctrl/Command key
  529. self.zoomRequest.emit(ev.delta(), ev.pos())
  530. else:
  531. self.scrollRequest.emit(
  532. ev.delta(),
  533. QtCore.Qt.Horizontal
  534. if (QtCore.Qt.ShiftModifier == int(mods))
  535. else QtCore.Qt.Vertical)
  536. else:
  537. self.scrollRequest.emit(ev.delta(), QtCore.Qt.Horizontal)
  538. ev.accept()
  539. def keyPressEvent(self, ev):
  540. key = ev.key()
  541. if key == QtCore.Qt.Key_Escape and self.current:
  542. self.current = None
  543. self.drawingPolygon.emit(False)
  544. self.update()
  545. elif key == QtCore.Qt.Key_Return and self.canCloseShape():
  546. self.finalise()
  547. def setLastLabel(self, text):
  548. assert text
  549. self.shapes[-1].label = text
  550. self.shapesBackups.pop()
  551. self.storeShapes()
  552. return self.shapes[-1]
  553. def undoLastLine(self):
  554. assert self.shapes
  555. self.current = self.shapes.pop()
  556. self.current.setOpen()
  557. if self.createMode == 'polygon':
  558. self.line.points = [self.current[-1], self.current[0]]
  559. elif self.createMode in ['rectangle', 'line']:
  560. self.current.points = self.current.points[0:1]
  561. self.drawingPolygon.emit(True)
  562. def undoLastPoint(self):
  563. if not self.current or self.current.isClosed():
  564. return
  565. self.current.popPoint()
  566. if len(self.current) > 0:
  567. self.line[0] = self.current[-1]
  568. else:
  569. self.current = None
  570. self.drawingPolygon.emit(False)
  571. self.repaint()
  572. def loadPixmap(self, pixmap):
  573. self.pixmap = pixmap
  574. self.shapes = []
  575. self.repaint()
  576. def loadShapes(self, shapes):
  577. self.shapes = list(shapes)
  578. self.storeShapes()
  579. self.current = None
  580. self.repaint()
  581. def setShapeVisible(self, shape, value):
  582. self.visible[shape] = value
  583. self.repaint()
  584. def overrideCursor(self, cursor):
  585. self.restoreCursor()
  586. self._cursor = cursor
  587. QtWidgets.QApplication.setOverrideCursor(cursor)
  588. def restoreCursor(self):
  589. QtWidgets.QApplication.restoreOverrideCursor()
  590. def resetState(self):
  591. self.restoreCursor()
  592. self.pixmap = None
  593. self.shapesBackups = []
  594. self.update()