canvas.py 20 KB

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