canvas.py 39 KB

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