canvas.py 40 KB

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