canvas.py 36 KB

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