canvas.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959
  1. import numpy as np
  2. from qtpy import QtCore
  3. from qtpy import QtGui
  4. from qtpy import QtWidgets
  5. import labelme.ai
  6. from labelme import QT5
  7. from labelme.shape import Shape
  8. import labelme.utils
  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. MOVE_SPEED = 5.0
  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(list)
  22. shapeMoved = QtCore.Signal()
  23. drawingPolygon = QtCore.Signal(bool)
  24. vertexSelected = QtCore.Signal(bool)
  25. CREATE, EDIT = 0, 1
  26. # polygon, rectangle, line, or point
  27. _createMode = "polygon"
  28. _fill_drawing = False
  29. def __init__(self, *args, **kwargs):
  30. self.epsilon = kwargs.pop("epsilon", 10.0)
  31. self.double_click = kwargs.pop("double_click", "close")
  32. if self.double_click not in [None, "close"]:
  33. raise ValueError(
  34. "Unexpected value for double_click event: {}".format(
  35. self.double_click
  36. )
  37. )
  38. self.num_backups = kwargs.pop("num_backups", 10)
  39. self._crosshair = kwargs.pop(
  40. "crosshair",
  41. {
  42. "polygon": False,
  43. "rectangle": True,
  44. "circle": False,
  45. "line": False,
  46. "point": False,
  47. "linestrip": False,
  48. "ai_polygon": False,
  49. },
  50. )
  51. super(Canvas, self).__init__(*args, **kwargs)
  52. # Initialise local state.
  53. self.mode = self.EDIT
  54. self.shapes = []
  55. self.shapesBackups = []
  56. self.current = None
  57. self.selectedShapes = [] # save the selected shapes here
  58. self.selectedShapesCopy = []
  59. # self.line represents:
  60. # - createMode == 'polygon': edge from last point to current
  61. # - createMode == 'rectangle': diagonal line of the rectangle
  62. # - createMode == 'line': the line
  63. # - createMode == 'point': the point
  64. self.line = Shape()
  65. self.prevPoint = QtCore.QPoint()
  66. self.prevMovePoint = QtCore.QPoint()
  67. self.offsets = QtCore.QPoint(), QtCore.QPoint()
  68. self.scale = 1.0
  69. self.pixmap = QtGui.QPixmap()
  70. self.visible = {}
  71. self._hideBackround = False
  72. self.hideBackround = False
  73. self.hShape = None
  74. self.prevhShape = None
  75. self.hVertex = None
  76. self.prevhVertex = None
  77. self.hEdge = None
  78. self.prevhEdge = None
  79. self.movingShape = False
  80. self.snapping = True
  81. self.hShapeIsSelected = False
  82. self._painter = QtGui.QPainter()
  83. self._cursor = CURSOR_DEFAULT
  84. # Menus:
  85. # 0: right-click without selection and dragging of shapes
  86. # 1: right-click with selection and dragging of shapes
  87. self.menus = (QtWidgets.QMenu(), QtWidgets.QMenu())
  88. # Set widget options.
  89. self.setMouseTracking(True)
  90. self.setFocusPolicy(QtCore.Qt.WheelFocus)
  91. self._ai_callback = None
  92. def setAiCallback(self, ai_callback):
  93. self._ai_callback = ai_callback
  94. def fillDrawing(self):
  95. return self._fill_drawing
  96. def setFillDrawing(self, value):
  97. self._fill_drawing = value
  98. @property
  99. def createMode(self):
  100. return self._createMode
  101. @createMode.setter
  102. def createMode(self, value):
  103. if value not in [
  104. "polygon",
  105. "rectangle",
  106. "circle",
  107. "line",
  108. "point",
  109. "linestrip",
  110. "ai_polygon",
  111. ]:
  112. raise ValueError("Unsupported createMode: %s" % value)
  113. self._createMode = value
  114. def storeShapes(self):
  115. shapesBackup = []
  116. for shape in self.shapes:
  117. shapesBackup.append(shape.copy())
  118. if len(self.shapesBackups) > self.num_backups:
  119. self.shapesBackups = self.shapesBackups[-self.num_backups - 1 :]
  120. self.shapesBackups.append(shapesBackup)
  121. @property
  122. def isShapeRestorable(self):
  123. # We save the state AFTER each edit (not before) so for an
  124. # edit to be undoable, we expect the CURRENT and the PREVIOUS state
  125. # to be in the undo stack.
  126. if len(self.shapesBackups) < 2:
  127. return False
  128. return True
  129. def restoreShape(self):
  130. # This does _part_ of the job of restoring shapes.
  131. # The complete process is also done in app.py::undoShapeEdit
  132. # and app.py::loadShapes and our own Canvas::loadShapes function.
  133. if not self.isShapeRestorable:
  134. return
  135. self.shapesBackups.pop() # latest
  136. # The application will eventually call Canvas.loadShapes which will
  137. # push this right back onto the stack.
  138. shapesBackup = self.shapesBackups.pop()
  139. self.shapes = shapesBackup
  140. self.selectedShapes = []
  141. for shape in self.shapes:
  142. shape.selected = False
  143. self.update()
  144. def enterEvent(self, ev):
  145. self.overrideCursor(self._cursor)
  146. def leaveEvent(self, ev):
  147. self.unHighlight()
  148. self.restoreCursor()
  149. def focusOutEvent(self, ev):
  150. self.restoreCursor()
  151. def isVisible(self, shape):
  152. return self.visible.get(shape, True)
  153. def drawing(self):
  154. return self.mode == self.CREATE
  155. def editing(self):
  156. return self.mode == self.EDIT
  157. def setEditing(self, value=True):
  158. self.mode = self.EDIT if value else self.CREATE
  159. if self.mode == self.EDIT:
  160. # CREATE -> EDIT
  161. self.repaint() # clear crosshair
  162. else:
  163. # EDIT -> CREATE
  164. self.unHighlight()
  165. self.deSelectShape()
  166. def unHighlight(self):
  167. if self.hShape:
  168. self.hShape.highlightClear()
  169. self.update()
  170. self.prevhShape = self.hShape
  171. self.prevhVertex = self.hVertex
  172. self.prevhEdge = self.hEdge
  173. self.hShape = self.hVertex = self.hEdge = None
  174. def selectedVertex(self):
  175. return self.hVertex is not None
  176. def selectedEdge(self):
  177. return self.hEdge is not None
  178. def mouseMoveEvent(self, ev):
  179. """Update line with last point and current coordinates."""
  180. try:
  181. if QT5:
  182. pos = self.transformPos(ev.localPos())
  183. else:
  184. pos = self.transformPos(ev.posF())
  185. except AttributeError:
  186. return
  187. self.prevMovePoint = pos
  188. self.restoreCursor()
  189. # Polygon drawing.
  190. if self.drawing():
  191. if self.createMode == "ai_polygon":
  192. self.line.shape_type = "points"
  193. else:
  194. self.line.shape_type = self.createMode
  195. self.overrideCursor(CURSOR_DRAW)
  196. if not self.current:
  197. self.repaint() # draw crosshair
  198. return
  199. if self.outOfPixmap(pos):
  200. # Don't allow the user to draw outside the pixmap.
  201. # Project the point to the pixmap's edges.
  202. pos = self.intersectionPoint(self.current[-1], pos)
  203. elif (
  204. self.snapping
  205. and len(self.current) > 1
  206. and self.createMode == "polygon"
  207. and self.closeEnough(pos, self.current[0])
  208. ):
  209. # Attract line to starting point and
  210. # colorise to alert the user.
  211. pos = self.current[0]
  212. self.overrideCursor(CURSOR_POINT)
  213. self.current.highlightVertex(0, Shape.NEAR_VERTEX)
  214. if self.createMode in ["polygon", "linestrip", "ai_polygon"]:
  215. self.line[0] = self.current[-1]
  216. self.line[1] = pos
  217. elif self.createMode == "rectangle":
  218. self.line.points = [self.current[0], pos]
  219. self.line.close()
  220. elif self.createMode == "circle":
  221. self.line.points = [self.current[0], pos]
  222. self.line.shape_type = "circle"
  223. elif self.createMode == "line":
  224. self.line.points = [self.current[0], pos]
  225. self.line.close()
  226. elif self.createMode == "point":
  227. self.line.points = [self.current[0]]
  228. self.line.close()
  229. self.repaint()
  230. self.current.highlightClear()
  231. return
  232. # Polygon copy moving.
  233. if QtCore.Qt.RightButton & ev.buttons():
  234. if self.selectedShapesCopy and self.prevPoint:
  235. self.overrideCursor(CURSOR_MOVE)
  236. self.boundedMoveShapes(self.selectedShapesCopy, pos)
  237. self.repaint()
  238. elif self.selectedShapes:
  239. self.selectedShapesCopy = [
  240. s.copy() for s in self.selectedShapes
  241. ]
  242. self.repaint()
  243. return
  244. # Polygon/Vertex moving.
  245. if QtCore.Qt.LeftButton & ev.buttons():
  246. if self.selectedVertex():
  247. self.boundedMoveVertex(pos)
  248. self.repaint()
  249. self.movingShape = True
  250. elif self.selectedShapes and self.prevPoint:
  251. self.overrideCursor(CURSOR_MOVE)
  252. self.boundedMoveShapes(self.selectedShapes, pos)
  253. self.repaint()
  254. self.movingShape = True
  255. return
  256. # Just hovering over the canvas, 2 possibilities:
  257. # - Highlight shapes
  258. # - Highlight vertex
  259. # Update shape/vertex fill and tooltip value accordingly.
  260. self.setToolTip(self.tr("Image"))
  261. for shape in reversed([s for s in self.shapes if self.isVisible(s)]):
  262. # Look for a nearby vertex to highlight. If that fails,
  263. # check if we happen to be inside a shape.
  264. index = shape.nearestVertex(pos, self.epsilon / self.scale)
  265. index_edge = shape.nearestEdge(pos, self.epsilon / self.scale)
  266. if index is not None:
  267. if self.selectedVertex():
  268. self.hShape.highlightClear()
  269. self.prevhVertex = self.hVertex = index
  270. self.prevhShape = self.hShape = shape
  271. self.prevhEdge = self.hEdge
  272. self.hEdge = None
  273. shape.highlightVertex(index, shape.MOVE_VERTEX)
  274. self.overrideCursor(CURSOR_POINT)
  275. self.setToolTip(self.tr("Click & drag to move point"))
  276. self.setStatusTip(self.toolTip())
  277. self.update()
  278. break
  279. elif index_edge is not None and shape.canAddPoint():
  280. if self.selectedVertex():
  281. self.hShape.highlightClear()
  282. self.prevhVertex = self.hVertex
  283. self.hVertex = None
  284. self.prevhShape = self.hShape = shape
  285. self.prevhEdge = self.hEdge = index_edge
  286. self.overrideCursor(CURSOR_POINT)
  287. self.setToolTip(self.tr("Click to create point"))
  288. self.setStatusTip(self.toolTip())
  289. self.update()
  290. break
  291. elif shape.containsPoint(pos):
  292. if self.selectedVertex():
  293. self.hShape.highlightClear()
  294. self.prevhVertex = self.hVertex
  295. self.hVertex = None
  296. self.prevhShape = self.hShape = shape
  297. self.prevhEdge = self.hEdge
  298. self.hEdge = None
  299. self.setToolTip(
  300. self.tr("Click & drag to move shape '%s'") % shape.label
  301. )
  302. self.setStatusTip(self.toolTip())
  303. self.overrideCursor(CURSOR_GRAB)
  304. self.update()
  305. break
  306. else: # Nothing found, clear highlights, reset state.
  307. self.unHighlight()
  308. self.vertexSelected.emit(self.hVertex is not None)
  309. def addPointToEdge(self):
  310. shape = self.prevhShape
  311. index = self.prevhEdge
  312. point = self.prevMovePoint
  313. if shape is None or index is None or point is None:
  314. return
  315. shape.insertPoint(index, point)
  316. shape.highlightVertex(index, shape.MOVE_VERTEX)
  317. self.hShape = shape
  318. self.hVertex = index
  319. self.hEdge = None
  320. self.movingShape = True
  321. def removeSelectedPoint(self):
  322. shape = self.prevhShape
  323. index = self.prevhVertex
  324. if shape is None or index is None:
  325. return
  326. shape.removePoint(index)
  327. shape.highlightClear()
  328. self.hShape = shape
  329. self.prevhVertex = None
  330. self.movingShape = True # Save changes
  331. def mousePressEvent(self, ev):
  332. if QT5:
  333. pos = self.transformPos(ev.localPos())
  334. else:
  335. pos = self.transformPos(ev.posF())
  336. if ev.button() == QtCore.Qt.LeftButton:
  337. if self.drawing():
  338. if self.current:
  339. # Add point to existing shape.
  340. if self.createMode == "polygon":
  341. self.current.addPoint(self.line[1])
  342. self.line[0] = self.current[-1]
  343. if self.current.isClosed():
  344. self.finalise()
  345. elif self.createMode in ["rectangle", "circle", "line"]:
  346. assert len(self.current.points) == 1
  347. self.current.points = self.line.points
  348. self.finalise()
  349. elif self.createMode in ["linestrip", "ai_polygon"]:
  350. self.current.addPoint(self.line[1])
  351. self.line[0] = self.current[-1]
  352. if int(ev.modifiers()) == QtCore.Qt.ControlModifier:
  353. self.finalise()
  354. elif not self.outOfPixmap(pos):
  355. # Create new shape.
  356. self.current = Shape(
  357. shape_type="points"
  358. if self.createMode == "ai_polygon"
  359. else self.createMode
  360. )
  361. self.current.addPoint(pos)
  362. if self.createMode == "point":
  363. self.finalise()
  364. else:
  365. if self.createMode == "circle":
  366. self.current.shape_type = "circle"
  367. self.line.points = [pos, pos]
  368. self.setHiding()
  369. self.drawingPolygon.emit(True)
  370. self.update()
  371. elif self.editing():
  372. if self.selectedEdge():
  373. self.addPointToEdge()
  374. elif (
  375. self.selectedVertex()
  376. and int(ev.modifiers()) == QtCore.Qt.ShiftModifier
  377. ):
  378. # Delete point if: left-click + SHIFT on a point
  379. self.removeSelectedPoint()
  380. group_mode = int(ev.modifiers()) == QtCore.Qt.ControlModifier
  381. self.selectShapePoint(pos, multiple_selection_mode=group_mode)
  382. self.prevPoint = pos
  383. self.repaint()
  384. elif ev.button() == QtCore.Qt.RightButton and self.editing():
  385. group_mode = int(ev.modifiers()) == QtCore.Qt.ControlModifier
  386. if not self.selectedShapes or (
  387. self.hShape is not None
  388. and self.hShape not in self.selectedShapes
  389. ):
  390. self.selectShapePoint(pos, multiple_selection_mode=group_mode)
  391. self.repaint()
  392. self.prevPoint = pos
  393. def mouseReleaseEvent(self, ev):
  394. if ev.button() == QtCore.Qt.RightButton:
  395. menu = self.menus[len(self.selectedShapesCopy) > 0]
  396. self.restoreCursor()
  397. if (
  398. not menu.exec_(self.mapToGlobal(ev.pos()))
  399. and self.selectedShapesCopy
  400. ):
  401. # Cancel the move by deleting the shadow copy.
  402. self.selectedShapesCopy = []
  403. self.repaint()
  404. elif ev.button() == QtCore.Qt.LeftButton:
  405. if self.editing():
  406. if (
  407. self.hShape is not None
  408. and self.hShapeIsSelected
  409. and not self.movingShape
  410. ):
  411. self.selectionChanged.emit(
  412. [x for x in self.selectedShapes if x != self.hShape]
  413. )
  414. if self.movingShape and self.hShape:
  415. index = self.shapes.index(self.hShape)
  416. if (
  417. self.shapesBackups[-1][index].points
  418. != self.shapes[index].points
  419. ):
  420. self.storeShapes()
  421. self.shapeMoved.emit()
  422. self.movingShape = False
  423. def endMove(self, copy):
  424. assert self.selectedShapes and self.selectedShapesCopy
  425. assert len(self.selectedShapesCopy) == len(self.selectedShapes)
  426. if copy:
  427. for i, shape in enumerate(self.selectedShapesCopy):
  428. self.shapes.append(shape)
  429. self.selectedShapes[i].selected = False
  430. self.selectedShapes[i] = shape
  431. else:
  432. for i, shape in enumerate(self.selectedShapesCopy):
  433. self.selectedShapes[i].points = shape.points
  434. self.selectedShapesCopy = []
  435. self.repaint()
  436. self.storeShapes()
  437. return True
  438. def hideBackroundShapes(self, value):
  439. self.hideBackround = value
  440. if self.selectedShapes:
  441. # Only hide other shapes if there is a current selection.
  442. # Otherwise the user will not be able to select a shape.
  443. self.setHiding(True)
  444. self.update()
  445. def setHiding(self, enable=True):
  446. self._hideBackround = self.hideBackround if enable else False
  447. def canCloseShape(self):
  448. return self.drawing() and self.current and len(self.current) > 2
  449. def mouseDoubleClickEvent(self, ev):
  450. # We need at least 4 points here, since the mousePress handler
  451. # adds an extra one before this handler is called.
  452. if (
  453. self.double_click == "close"
  454. and self.canCloseShape()
  455. and len(self.current) > 3
  456. ):
  457. self.current.popPoint()
  458. self.finalise()
  459. def selectShapes(self, shapes):
  460. self.setHiding()
  461. self.selectionChanged.emit(shapes)
  462. self.update()
  463. def selectShapePoint(self, point, multiple_selection_mode):
  464. """Select the first shape created which contains this point."""
  465. if self.selectedVertex(): # A vertex is marked for selection.
  466. index, shape = self.hVertex, self.hShape
  467. shape.highlightVertex(index, shape.MOVE_VERTEX)
  468. else:
  469. for shape in reversed(self.shapes):
  470. if self.isVisible(shape) and shape.containsPoint(point):
  471. self.setHiding()
  472. if shape not in self.selectedShapes:
  473. if multiple_selection_mode:
  474. self.selectionChanged.emit(
  475. self.selectedShapes + [shape]
  476. )
  477. else:
  478. self.selectionChanged.emit([shape])
  479. self.hShapeIsSelected = False
  480. else:
  481. self.hShapeIsSelected = True
  482. self.calculateOffsets(point)
  483. return
  484. self.deSelectShape()
  485. def calculateOffsets(self, point):
  486. left = self.pixmap.width() - 1
  487. right = 0
  488. top = self.pixmap.height() - 1
  489. bottom = 0
  490. for s in self.selectedShapes:
  491. rect = s.boundingRect()
  492. if rect.left() < left:
  493. left = rect.left()
  494. if rect.right() > right:
  495. right = rect.right()
  496. if rect.top() < top:
  497. top = rect.top()
  498. if rect.bottom() > bottom:
  499. bottom = rect.bottom()
  500. x1 = left - point.x()
  501. y1 = top - point.y()
  502. x2 = right - point.x()
  503. y2 = bottom - point.y()
  504. self.offsets = QtCore.QPointF(x1, y1), QtCore.QPointF(x2, y2)
  505. def boundedMoveVertex(self, pos):
  506. index, shape = self.hVertex, self.hShape
  507. point = shape[index]
  508. if self.outOfPixmap(pos):
  509. pos = self.intersectionPoint(point, pos)
  510. shape.moveVertexBy(index, pos - point)
  511. def boundedMoveShapes(self, shapes, pos):
  512. if self.outOfPixmap(pos):
  513. return False # No need to move
  514. o1 = pos + self.offsets[0]
  515. if self.outOfPixmap(o1):
  516. pos -= QtCore.QPointF(min(0, o1.x()), min(0, o1.y()))
  517. o2 = pos + self.offsets[1]
  518. if self.outOfPixmap(o2):
  519. pos += QtCore.QPointF(
  520. min(0, self.pixmap.width() - o2.x()),
  521. min(0, self.pixmap.height() - o2.y()),
  522. )
  523. # XXX: The next line tracks the new position of the cursor
  524. # relative to the shape, but also results in making it
  525. # a bit "shaky" when nearing the border and allows it to
  526. # go outside of the shape's area for some reason.
  527. # self.calculateOffsets(self.selectedShapes, pos)
  528. dp = pos - self.prevPoint
  529. if dp:
  530. for shape in shapes:
  531. shape.moveBy(dp)
  532. self.prevPoint = pos
  533. return True
  534. return False
  535. def deSelectShape(self):
  536. if self.selectedShapes:
  537. self.setHiding(False)
  538. self.selectionChanged.emit([])
  539. self.hShapeIsSelected = False
  540. self.update()
  541. def deleteSelected(self):
  542. deleted_shapes = []
  543. if self.selectedShapes:
  544. for shape in self.selectedShapes:
  545. self.shapes.remove(shape)
  546. deleted_shapes.append(shape)
  547. self.storeShapes()
  548. self.selectedShapes = []
  549. self.update()
  550. return deleted_shapes
  551. def deleteShape(self, shape):
  552. if shape in self.selectedShapes:
  553. self.selectedShapes.remove(shape)
  554. if shape in self.shapes:
  555. self.shapes.remove(shape)
  556. self.storeShapes()
  557. self.update()
  558. def duplicateSelectedShapes(self):
  559. if self.selectedShapes:
  560. self.selectedShapesCopy = [s.copy() for s in self.selectedShapes]
  561. self.boundedShiftShapes(self.selectedShapesCopy)
  562. self.endMove(copy=True)
  563. return self.selectedShapes
  564. def boundedShiftShapes(self, shapes):
  565. # Try to move in one direction, and if it fails in another.
  566. # Give up if both fail.
  567. point = shapes[0][0]
  568. offset = QtCore.QPointF(2.0, 2.0)
  569. self.offsets = QtCore.QPoint(), QtCore.QPoint()
  570. self.prevPoint = point
  571. if not self.boundedMoveShapes(shapes, point - offset):
  572. self.boundedMoveShapes(shapes, point + offset)
  573. def paintEvent(self, event):
  574. if not self.pixmap:
  575. return super(Canvas, self).paintEvent(event)
  576. p = self._painter
  577. p.begin(self)
  578. p.setRenderHint(QtGui.QPainter.Antialiasing)
  579. p.setRenderHint(QtGui.QPainter.HighQualityAntialiasing)
  580. p.setRenderHint(QtGui.QPainter.SmoothPixmapTransform)
  581. p.scale(self.scale, self.scale)
  582. p.translate(self.offsetToCenter())
  583. p.drawPixmap(0, 0, self.pixmap)
  584. # draw crosshair
  585. if (
  586. self._crosshair[self._createMode]
  587. and self.drawing()
  588. and self.prevMovePoint
  589. and not self.outOfPixmap(self.prevMovePoint)
  590. ):
  591. p.setPen(QtGui.QColor(0, 0, 0))
  592. p.drawLine(
  593. 0,
  594. int(self.prevMovePoint.y()),
  595. self.width() - 1,
  596. int(self.prevMovePoint.y()),
  597. )
  598. p.drawLine(
  599. int(self.prevMovePoint.x()),
  600. 0,
  601. int(self.prevMovePoint.x()),
  602. self.height() - 1,
  603. )
  604. Shape.scale = self.scale
  605. for shape in self.shapes:
  606. if (shape.selected or not self._hideBackround) and self.isVisible(
  607. shape
  608. ):
  609. shape.fill = shape.selected or shape == self.hShape
  610. shape.paint(p)
  611. if self.current:
  612. self.current.paint(p)
  613. self.line.paint(p)
  614. if self.selectedShapesCopy:
  615. for s in self.selectedShapesCopy:
  616. s.paint(p)
  617. if (
  618. self.fillDrawing()
  619. and self.createMode == "polygon"
  620. and self.current is not None
  621. and len(self.current.points) >= 2
  622. ):
  623. drawing_shape = self.current.copy()
  624. drawing_shape.addPoint(self.line[1])
  625. drawing_shape.fill = True
  626. drawing_shape.paint(p)
  627. p.end()
  628. def transformPos(self, point):
  629. """Convert from widget-logical coordinates to painter-logical ones."""
  630. return point / self.scale - self.offsetToCenter()
  631. def offsetToCenter(self):
  632. s = self.scale
  633. area = super(Canvas, self).size()
  634. w, h = self.pixmap.width() * s, self.pixmap.height() * s
  635. aw, ah = area.width(), area.height()
  636. x = (aw - w) / (2 * s) if aw > w else 0
  637. y = (ah - h) / (2 * s) if ah > h else 0
  638. return QtCore.QPointF(x, y)
  639. def outOfPixmap(self, p):
  640. w, h = self.pixmap.width(), self.pixmap.height()
  641. return not (0 <= p.x() <= w - 1 and 0 <= p.y() <= h - 1)
  642. def finalise(self):
  643. assert self.current
  644. if self.createMode == "ai_polygon":
  645. # convert points to polygon by an AI model
  646. assert self.current.shape_type == "points"
  647. points = self._ai_callback(
  648. points=np.array(
  649. [[point.x(), point.y()] for point in self.current.points],
  650. dtype=np.float32,
  651. )
  652. )
  653. self.current.setShapeRefined(
  654. points=[
  655. QtCore.QPointF(point[0], point[1]) for point in points
  656. ],
  657. shape_type="polygon",
  658. )
  659. self.current.close()
  660. self.shapes.append(self.current)
  661. self.storeShapes()
  662. self.current = None
  663. self.setHiding(False)
  664. self.newShape.emit()
  665. self.update()
  666. def closeEnough(self, p1, p2):
  667. # d = distance(p1 - p2)
  668. # m = (p1-p2).manhattanLength()
  669. # print "d %.2f, m %d, %.2f" % (d, m, d - m)
  670. # divide by scale to allow more precision when zoomed in
  671. return labelme.utils.distance(p1 - p2) < (self.epsilon / self.scale)
  672. def intersectionPoint(self, p1, p2):
  673. # Cycle through each image edge in clockwise fashion,
  674. # and find the one intersecting the current line segment.
  675. # http://paulbourke.net/geometry/lineline2d/
  676. size = self.pixmap.size()
  677. points = [
  678. (0, 0),
  679. (size.width() - 1, 0),
  680. (size.width() - 1, size.height() - 1),
  681. (0, size.height() - 1),
  682. ]
  683. # x1, y1 should be in the pixmap, x2, y2 should be out of the pixmap
  684. x1 = min(max(p1.x(), 0), size.width() - 1)
  685. y1 = min(max(p1.y(), 0), size.height() - 1)
  686. x2, y2 = p2.x(), p2.y()
  687. d, i, (x, y) = min(self.intersectingEdges((x1, y1), (x2, y2), points))
  688. x3, y3 = points[i]
  689. x4, y4 = points[(i + 1) % 4]
  690. if (x, y) == (x1, y1):
  691. # Handle cases where previous point is on one of the edges.
  692. if x3 == x4:
  693. return QtCore.QPointF(x3, min(max(0, y2), max(y3, y4)))
  694. else: # y3 == y4
  695. return QtCore.QPointF(min(max(0, x2), max(x3, x4)), y3)
  696. return QtCore.QPointF(x, y)
  697. def intersectingEdges(self, point1, point2, points):
  698. """Find intersecting edges.
  699. For each edge formed by `points', yield the intersection
  700. with the line segment `(x1,y1) - (x2,y2)`, if it exists.
  701. Also return the distance of `(x2,y2)' to the middle of the
  702. edge along with its index, so that the one closest can be chosen.
  703. """
  704. (x1, y1) = point1
  705. (x2, y2) = point2
  706. for i in range(4):
  707. x3, y3 = points[i]
  708. x4, y4 = points[(i + 1) % 4]
  709. denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
  710. nua = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)
  711. nub = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)
  712. if denom == 0:
  713. # This covers two cases:
  714. # nua == nub == 0: Coincident
  715. # otherwise: Parallel
  716. continue
  717. ua, ub = nua / denom, nub / denom
  718. if 0 <= ua <= 1 and 0 <= ub <= 1:
  719. x = x1 + ua * (x2 - x1)
  720. y = y1 + ua * (y2 - y1)
  721. m = QtCore.QPointF((x3 + x4) / 2, (y3 + y4) / 2)
  722. d = labelme.utils.distance(m - QtCore.QPointF(x2, y2))
  723. yield d, i, (x, y)
  724. # These two, along with a call to adjustSize are required for the
  725. # scroll area.
  726. def sizeHint(self):
  727. return self.minimumSizeHint()
  728. def minimumSizeHint(self):
  729. if self.pixmap:
  730. return self.scale * self.pixmap.size()
  731. return super(Canvas, self).minimumSizeHint()
  732. def wheelEvent(self, ev):
  733. if QT5:
  734. mods = ev.modifiers()
  735. delta = ev.angleDelta()
  736. if QtCore.Qt.ControlModifier == int(mods):
  737. # with Ctrl/Command key
  738. # zoom
  739. self.zoomRequest.emit(delta.y(), ev.pos())
  740. else:
  741. # scroll
  742. self.scrollRequest.emit(delta.x(), QtCore.Qt.Horizontal)
  743. self.scrollRequest.emit(delta.y(), QtCore.Qt.Vertical)
  744. else:
  745. if ev.orientation() == QtCore.Qt.Vertical:
  746. mods = ev.modifiers()
  747. if QtCore.Qt.ControlModifier == int(mods):
  748. # with Ctrl/Command key
  749. self.zoomRequest.emit(ev.delta(), ev.pos())
  750. else:
  751. self.scrollRequest.emit(
  752. ev.delta(),
  753. QtCore.Qt.Horizontal
  754. if (QtCore.Qt.ShiftModifier == int(mods))
  755. else QtCore.Qt.Vertical,
  756. )
  757. else:
  758. self.scrollRequest.emit(ev.delta(), QtCore.Qt.Horizontal)
  759. ev.accept()
  760. def moveByKeyboard(self, offset):
  761. if self.selectedShapes:
  762. self.boundedMoveShapes(
  763. self.selectedShapes, self.prevPoint + offset
  764. )
  765. self.repaint()
  766. self.movingShape = True
  767. def keyPressEvent(self, ev):
  768. modifiers = ev.modifiers()
  769. key = ev.key()
  770. if self.drawing():
  771. if key == QtCore.Qt.Key_Escape and self.current:
  772. self.current = None
  773. self.drawingPolygon.emit(False)
  774. self.update()
  775. elif key == QtCore.Qt.Key_Return and self.canCloseShape():
  776. self.finalise()
  777. elif modifiers == QtCore.Qt.AltModifier:
  778. self.snapping = False
  779. elif self.editing():
  780. if key == QtCore.Qt.Key_Up:
  781. self.moveByKeyboard(QtCore.QPointF(0.0, -MOVE_SPEED))
  782. elif key == QtCore.Qt.Key_Down:
  783. self.moveByKeyboard(QtCore.QPointF(0.0, MOVE_SPEED))
  784. elif key == QtCore.Qt.Key_Left:
  785. self.moveByKeyboard(QtCore.QPointF(-MOVE_SPEED, 0.0))
  786. elif key == QtCore.Qt.Key_Right:
  787. self.moveByKeyboard(QtCore.QPointF(MOVE_SPEED, 0.0))
  788. def keyReleaseEvent(self, ev):
  789. modifiers = ev.modifiers()
  790. if self.drawing():
  791. if int(modifiers) == 0:
  792. self.snapping = True
  793. elif self.editing():
  794. if self.movingShape and self.selectedShapes:
  795. index = self.shapes.index(self.selectedShapes[0])
  796. if (
  797. self.shapesBackups[-1][index].points
  798. != self.shapes[index].points
  799. ):
  800. self.storeShapes()
  801. self.shapeMoved.emit()
  802. self.movingShape = False
  803. def setLastLabel(self, text, flags):
  804. assert text
  805. self.shapes[-1].label = text
  806. self.shapes[-1].flags = flags
  807. self.shapesBackups.pop()
  808. self.storeShapes()
  809. return self.shapes[-1]
  810. def undoLastLine(self):
  811. assert self.shapes
  812. self.current = self.shapes.pop()
  813. self.current.setOpen()
  814. self.current.restoreShapeRaw()
  815. if self.createMode in ["polygon", "linestrip"]:
  816. self.line.points = [self.current[-1], self.current[0]]
  817. elif self.createMode in ["rectangle", "line", "circle"]:
  818. self.current.points = self.current.points[0:1]
  819. elif self.createMode == "point":
  820. self.current = None
  821. self.drawingPolygon.emit(True)
  822. def undoLastPoint(self):
  823. if not self.current or self.current.isClosed():
  824. return
  825. self.current.popPoint()
  826. if len(self.current) > 0:
  827. self.line[0] = self.current[-1]
  828. else:
  829. self.current = None
  830. self.drawingPolygon.emit(False)
  831. self.update()
  832. def loadPixmap(self, pixmap, clear_shapes=True):
  833. self.pixmap = pixmap
  834. if clear_shapes:
  835. self.shapes = []
  836. self.update()
  837. def loadShapes(self, shapes, replace=True):
  838. if replace:
  839. self.shapes = list(shapes)
  840. else:
  841. self.shapes.extend(shapes)
  842. self.storeShapes()
  843. self.current = None
  844. self.hShape = None
  845. self.hVertex = None
  846. self.hEdge = None
  847. self.update()
  848. def setShapeVisible(self, shape, value):
  849. self.visible[shape] = value
  850. self.update()
  851. def overrideCursor(self, cursor):
  852. self.restoreCursor()
  853. self._cursor = cursor
  854. QtWidgets.QApplication.setOverrideCursor(cursor)
  855. def restoreCursor(self):
  856. QtWidgets.QApplication.restoreOverrideCursor()
  857. def resetState(self):
  858. self.restoreCursor()
  859. self.pixmap = None
  860. self.shapesBackups = []
  861. self.update()