canvas.py 37 KB

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