canvas.py 40 KB

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