canvas.py 22 KB

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