canvas.py 20 KB

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