canvas.py 36 KB

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