canvas.py 39 KB

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