canvas.py 22 KB

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