canvas.py 36 KB

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