canvas.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912
  1. # Copyright (c) <2015-Present> Tzutalin
  2. # Copyright (C) 2013 MIT, Computer Science and Artificial Intelligence Laboratory. Bryan Russell, Antonio Torralba,
  3. # William T. Freeman. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
  4. # associated documentation files (the "Software"), to deal in the Software without restriction, including without
  5. # limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
  6. # Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
  7. # The above copyright notice and this permission notice shall be included in all copies or substantial portions of
  8. # the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
  9. # NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
  10. # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
  11. # CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  12. # THE SOFTWARE.
  13. import copy
  14. from PyQt5.QtCore import Qt, pyqtSignal, QPointF, QPoint
  15. from PyQt5.QtGui import QPainter, QBrush, QColor, QPixmap
  16. from PyQt5.QtWidgets import QWidget, QMenu, QApplication
  17. from libs.shape import Shape
  18. from libs.utils import distance
  19. CURSOR_DEFAULT = Qt.ArrowCursor
  20. CURSOR_POINT = Qt.PointingHandCursor
  21. CURSOR_DRAW = Qt.CrossCursor
  22. CURSOR_MOVE = Qt.ClosedHandCursor
  23. CURSOR_GRAB = Qt.OpenHandCursor
  24. class Canvas(QWidget):
  25. zoomRequest = pyqtSignal(int)
  26. scrollRequest = pyqtSignal(int, int)
  27. newShape = pyqtSignal()
  28. # selectionChanged = pyqtSignal(bool)
  29. selectionChanged = pyqtSignal(list)
  30. shapeMoved = pyqtSignal()
  31. drawingPolygon = pyqtSignal(bool)
  32. CREATE, EDIT = list(range(2))
  33. _fill_drawing = False # draw shadows
  34. epsilon = 5.0
  35. def __init__(self, *args, **kwargs):
  36. super(Canvas, self).__init__(*args, **kwargs)
  37. # Initialise local state.
  38. self.mode = self.EDIT
  39. self.shapes = []
  40. self.shapesBackups = []
  41. self.current = None
  42. self.selectedShapes = []
  43. self.selectedShape = None # save the selected shape here
  44. self.selectedShapesCopy = []
  45. self.drawingLineColor = QColor(0, 0, 255)
  46. self.drawingRectColor = QColor(0, 0, 255)
  47. self.line = Shape(line_color=self.drawingLineColor)
  48. self.prevPoint = QPointF()
  49. self.offsets = QPointF(), QPointF()
  50. self.scale = 1.0
  51. self.pixmap = QPixmap()
  52. self.visible = {}
  53. self._hideBackround = False
  54. self.hideBackround = False
  55. self.hShape = None
  56. self.hVertex = None
  57. self._painter = QPainter()
  58. self._cursor = CURSOR_DEFAULT
  59. # Menus:
  60. self.menus = (QMenu(), QMenu())
  61. # Set widget options.
  62. self.setMouseTracking(True)
  63. self.setFocusPolicy(Qt.WheelFocus)
  64. self.verified = False
  65. self.drawSquare = False
  66. self.fourpoint = True # ADD
  67. self.pointnum = 0
  68. self.movingShape = False
  69. self.selectCountShape = False
  70. #initialisation for panning
  71. self.pan_initial_pos = QPoint()
  72. #lockedshapes related
  73. self.lockedShapes = []
  74. self.isInTheSameImage = False
  75. def setDrawingColor(self, qColor):
  76. self.drawingLineColor = qColor
  77. self.drawingRectColor = qColor
  78. def enterEvent(self, ev):
  79. self.overrideCursor(self._cursor)
  80. def leaveEvent(self, ev):
  81. self.restoreCursor()
  82. def focusOutEvent(self, ev):
  83. self.restoreCursor()
  84. def isVisible(self, shape):
  85. return self.visible.get(shape, True)
  86. def drawing(self):
  87. return self.mode == self.CREATE
  88. def editing(self):
  89. return self.mode == self.EDIT
  90. def setEditing(self, value=True):
  91. self.mode = self.EDIT if value else self.CREATE
  92. if not value: # Create
  93. self.unHighlight()
  94. self.deSelectShape()
  95. self.prevPoint = QPointF()
  96. self.repaint()
  97. def unHighlight(self):
  98. if self.hShape:
  99. self.hShape.highlightClear()
  100. self.hVertex = self.hShape = None
  101. def selectedVertex(self):
  102. return self.hVertex is not None
  103. def mouseMoveEvent(self, ev):
  104. """Update line with last point and current coordinates."""
  105. pos = self.transformPos(ev.pos())
  106. # Update coordinates in status bar if image is opened
  107. window = self.parent().window()
  108. if window.filePath is not None:
  109. self.parent().window().labelCoordinates.setText(
  110. 'X: %d; Y: %d' % (pos.x(), pos.y()))
  111. # Polygon drawing.
  112. if self.drawing():
  113. self.overrideCursor(CURSOR_DRAW) # ?
  114. if self.current:
  115. # Display annotation width and height while drawing
  116. currentWidth = abs(self.current[0].x() - pos.x())
  117. currentHeight = abs(self.current[0].y() - pos.y())
  118. self.parent().window().labelCoordinates.setText(
  119. 'Width: %d, Height: %d / X: %d; Y: %d' % (currentWidth, currentHeight, pos.x(), pos.y()))
  120. color = self.drawingLineColor
  121. if self.outOfPixmap(pos):
  122. # Don't allow the user to draw outside the pixmap.
  123. # Clip the coordinates to 0 or max,
  124. # if they are outside the range [0, max]
  125. size = self.pixmap.size()
  126. clipped_x = min(max(0, pos.x()), size.width())
  127. clipped_y = min(max(0, pos.y()), size.height())
  128. pos = QPointF(clipped_x, clipped_y)
  129. elif len(self.current) > 1 and self.closeEnough(pos, self.current[0]):
  130. # Attract line to starting point and colorise to alert the
  131. # user:
  132. pos = self.current[0]
  133. color = self.current.line_color
  134. self.overrideCursor(CURSOR_POINT)
  135. self.current.highlightVertex(0, Shape.NEAR_VERTEX)
  136. if self.drawSquare:
  137. self.line.points = [self.current[0], pos]
  138. self.line.close()
  139. elif self.fourpoint:
  140. self.line[0] = self.current[-1]
  141. self.line[1] = pos
  142. else:
  143. self.line[1] = pos # pos is the mouse's current position
  144. self.line.line_color = color
  145. self.prevPoint = QPointF() # ?
  146. self.current.highlightClear()
  147. else:
  148. self.prevPoint = pos
  149. self.repaint()
  150. return
  151. # Polygon copy moving.
  152. if Qt.RightButton & ev.buttons():
  153. if self.selectedShapesCopy and self.prevPoint:
  154. self.overrideCursor(CURSOR_MOVE)
  155. self.boundedMoveShape(self.selectedShapesCopy, pos)
  156. self.repaint()
  157. elif self.selectedShapes:
  158. self.selectedShapesCopy = [
  159. s.copy() for s in self.selectedShapes
  160. ]
  161. self.repaint()
  162. return
  163. # Polygon/Vertex moving.
  164. if Qt.LeftButton & ev.buttons():
  165. if self.selectedVertex():
  166. self.boundedMoveVertex(pos)
  167. self.shapeMoved.emit()
  168. self.repaint()
  169. self.movingShape = True
  170. elif self.selectedShapes and self.prevPoint:
  171. self.overrideCursor(CURSOR_MOVE)
  172. self.boundedMoveShape(self.selectedShapes, pos)
  173. self.shapeMoved.emit()
  174. self.repaint()
  175. self.movingShape = True
  176. else:
  177. #pan
  178. delta_x = pos.x() - self.pan_initial_pos.x()
  179. delta_y = pos.y() - self.pan_initial_pos.y()
  180. self.scrollRequest.emit(delta_x, Qt.Horizontal)
  181. self.scrollRequest.emit(delta_y, Qt.Vertical)
  182. self.update()
  183. return
  184. # Just hovering over the canvas, 2 posibilities:
  185. # - Highlight shapes
  186. # - Highlight vertex
  187. # Update shape/vertex fill and tooltip value accordingly.
  188. self.setToolTip("Image")
  189. for shape in reversed([s for s in self.shapes if self.isVisible(s)]):
  190. # Look for a nearby vertex to highlight. If that fails,
  191. # check if we happen to be inside a shape.
  192. index = shape.nearestVertex(pos, self.epsilon)
  193. if index is not None:
  194. if self.selectedVertex():
  195. self.hShape.highlightClear()
  196. self.hVertex, self.hShape = index, shape
  197. shape.highlightVertex(index, shape.MOVE_VERTEX)
  198. self.overrideCursor(CURSOR_POINT)
  199. self.setToolTip("Click & drag to move point")
  200. self.setStatusTip(self.toolTip())
  201. self.update()
  202. break
  203. elif shape.containsPoint(pos):
  204. if self.selectedVertex():
  205. self.hShape.highlightClear()
  206. self.hVertex, self.hShape = None, shape
  207. self.setToolTip(
  208. "Click & drag to move shape '%s'" % shape.label)
  209. self.setStatusTip(self.toolTip())
  210. self.overrideCursor(CURSOR_GRAB)
  211. self.update()
  212. break
  213. else: # Nothing found, clear highlights, reset state.
  214. if self.hShape:
  215. self.hShape.highlightClear()
  216. self.update()
  217. self.hVertex, self.hShape = None, None
  218. self.overrideCursor(CURSOR_DEFAULT)
  219. def mousePressEvent(self, ev):
  220. pos = self.transformPos(ev.pos())
  221. if ev.button() == Qt.LeftButton:
  222. if self.drawing():
  223. # self.handleDrawing(pos) # OLD
  224. if self.current:
  225. if self.fourpoint: # ADD IF
  226. # Add point to existing shape.
  227. # print('Adding points in mousePressEvent is ', self.line[1])
  228. self.current.addPoint(self.line[1])
  229. self.line[0] = self.current[-1]
  230. if self.current.isClosed():
  231. # print('1111')
  232. self.finalise()
  233. elif self.drawSquare:
  234. assert len(self.current.points) == 1
  235. self.current.points = self.line.points
  236. self.finalise()
  237. elif not self.outOfPixmap(pos):
  238. # Create new shape.
  239. self.current = Shape()
  240. self.current.addPoint(pos)
  241. self.line.points = [pos, pos]
  242. self.setHiding()
  243. self.drawingPolygon.emit(True)
  244. self.update()
  245. else:
  246. group_mode = int(ev.modifiers()) == Qt.ControlModifier
  247. self.selectShapePoint(pos, multiple_selection_mode=group_mode)
  248. self.prevPoint = pos
  249. self.pan_initial_pos = pos
  250. elif ev.button() == Qt.RightButton and self.editing():
  251. group_mode = int(ev.modifiers()) == Qt.ControlModifier
  252. self.selectShapePoint(pos, multiple_selection_mode=group_mode)
  253. self.prevPoint = pos
  254. self.update()
  255. def mouseReleaseEvent(self, ev):
  256. if ev.button() == Qt.RightButton:
  257. menu = self.menus[bool(self.selectedShapesCopy)]
  258. self.restoreCursor()
  259. if not menu.exec_(self.mapToGlobal(ev.pos()))\
  260. and self.selectedShapesCopy:
  261. # Cancel the move by deleting the shadow copy.
  262. # self.selectedShapeCopy = None
  263. self.selectedShapesCopy = []
  264. self.repaint()
  265. elif ev.button() == Qt.LeftButton and self.selectedShapes:
  266. if self.selectedVertex():
  267. self.overrideCursor(CURSOR_POINT)
  268. else:
  269. self.overrideCursor(CURSOR_GRAB)
  270. elif ev.button() == Qt.LeftButton and not self.fourpoint:
  271. pos = self.transformPos(ev.pos())
  272. if self.drawing():
  273. self.handleDrawing(pos)
  274. else:
  275. #pan
  276. QApplication.restoreOverrideCursor() # ?
  277. if self.movingShape and self.hShape:
  278. if self.hShape in self.shapes:
  279. index = self.shapes.index(self.hShape)
  280. if (
  281. self.shapesBackups[-1][index].points
  282. != self.shapes[index].points
  283. ):
  284. self.storeShapes()
  285. self.shapeMoved.emit() # connect to updateBoxlist in PPOCRLabel.py
  286. self.movingShape = False
  287. def endMove(self, copy=False):
  288. assert self.selectedShapes and self.selectedShapesCopy
  289. assert len(self.selectedShapesCopy) == len(self.selectedShapes)
  290. if copy:
  291. for i, shape in enumerate(self.selectedShapesCopy):
  292. shape.idx = len(self.shapes) # add current box index
  293. self.shapes.append(shape)
  294. self.selectedShapes[i].selected = False
  295. self.selectedShapes[i] = shape
  296. else:
  297. for i, shape in enumerate(self.selectedShapesCopy):
  298. self.selectedShapes[i].points = shape.points
  299. self.selectedShapesCopy = []
  300. self.repaint()
  301. self.storeShapes()
  302. return True
  303. def hideBackroundShapes(self, value):
  304. self.hideBackround = value
  305. if self.selectedShapes:
  306. # Only hide other shapes if there is a current selection.
  307. # Otherwise the user will not be able to select a shape.
  308. self.setHiding(True)
  309. self.repaint()
  310. def handleDrawing(self, pos):
  311. if self.current and self.current.reachMaxPoints() is False:
  312. if self.fourpoint:
  313. targetPos = self.line[self.pointnum]
  314. self.current.addPoint(targetPos)
  315. print('current points in handleDrawing is ', self.line[self.pointnum])
  316. self.update()
  317. if self.pointnum == 3:
  318. self.finalise()
  319. else:
  320. initPos = self.current[0]
  321. print('initPos', self.current[0])
  322. minX = initPos.x()
  323. minY = initPos.y()
  324. targetPos = self.line[1]
  325. maxX = targetPos.x()
  326. maxY = targetPos.y()
  327. self.current.addPoint(QPointF(maxX, minY))
  328. self.current.addPoint(targetPos)
  329. self.current.addPoint(QPointF(minX, maxY))
  330. self.finalise()
  331. elif not self.outOfPixmap(pos):
  332. print('release')
  333. self.current = Shape()
  334. self.current.addPoint(pos)
  335. self.line.points = [pos, pos]
  336. self.setHiding()
  337. self.drawingPolygon.emit(True)
  338. self.update()
  339. def setHiding(self, enable=True):
  340. self._hideBackround = self.hideBackround if enable else False
  341. def canCloseShape(self):
  342. return self.drawing() and self.current and len(self.current) > 2
  343. def mouseDoubleClickEvent(self, ev):
  344. # We need at least 4 points here, since the mousePress handler
  345. # adds an extra one before this handler is called.
  346. if self.canCloseShape() and len(self.current) > 3:
  347. if not self.fourpoint:
  348. self.current.popPoint()
  349. self.finalise()
  350. def selectShapes(self, shapes):
  351. for s in shapes: s.seleted = True
  352. self.setHiding()
  353. self.selectionChanged.emit(shapes)
  354. self.update()
  355. def selectShapePoint(self, point, multiple_selection_mode):
  356. """Select the first shape created which contains this point."""
  357. if self.selectedVertex(): # A vertex is marked for selection.
  358. index, shape = self.hVertex, self.hShape
  359. shape.highlightVertex(index, shape.MOVE_VERTEX)
  360. return self.hVertex
  361. else:
  362. for shape in reversed(self.shapes):
  363. if self.isVisible(shape) and shape.containsPoint(point):
  364. self.calculateOffsets(shape, point)
  365. self.setHiding()
  366. if multiple_selection_mode:
  367. if shape not in self.selectedShapes: # list
  368. self.selectionChanged.emit(
  369. self.selectedShapes + [shape]
  370. )
  371. else:
  372. self.selectionChanged.emit([shape])
  373. return
  374. self.deSelectShape()
  375. def calculateOffsets(self, shape, point):
  376. rect = shape.boundingRect()
  377. x1 = rect.x() - point.x()
  378. y1 = rect.y() - point.y()
  379. x2 = (rect.x() + rect.width()) - point.x()
  380. y2 = (rect.y() + rect.height()) - point.y()
  381. self.offsets = QPointF(x1, y1), QPointF(x2, y2)
  382. def snapPointToCanvas(self, x, y):
  383. """
  384. Moves a point x,y to within the boundaries of the canvas.
  385. :return: (x,y,snapped) where snapped is True if x or y were changed, False if not.
  386. """
  387. if x < 0 or x > self.pixmap.width() or y < 0 or y > self.pixmap.height():
  388. x = max(x, 0)
  389. y = max(y, 0)
  390. x = min(x, self.pixmap.width())
  391. y = min(y, self.pixmap.height())
  392. return x, y, True
  393. return x, y, False
  394. def boundedMoveVertex(self, pos):
  395. index, shape = self.hVertex, self.hShape
  396. point = shape[index]
  397. if self.outOfPixmap(pos):
  398. size = self.pixmap.size()
  399. clipped_x = min(max(0, pos.x()), size.width())
  400. clipped_y = min(max(0, pos.y()), size.height())
  401. pos = QPointF(clipped_x, clipped_y)
  402. if self.drawSquare:
  403. opposite_point_index = (index + 2) % 4
  404. opposite_point = shape[opposite_point_index]
  405. min_size = min(abs(pos.x() - opposite_point.x()), abs(pos.y() - opposite_point.y()))
  406. directionX = -1 if pos.x() - opposite_point.x() < 0 else 1
  407. directionY = -1 if pos.y() - opposite_point.y() < 0 else 1
  408. shiftPos = QPointF(opposite_point.x() + directionX * min_size - point.x(),
  409. opposite_point.y() + directionY * min_size - point.y())
  410. else:
  411. shiftPos = pos - point
  412. if [shape[0].x(), shape[0].y(), shape[2].x(), shape[2].y()] \
  413. == [shape[3].x(),shape[1].y(),shape[1].x(),shape[3].y()]:
  414. shape.moveVertexBy(index, shiftPos)
  415. lindex = (index + 1) % 4
  416. rindex = (index + 3) % 4
  417. lshift = None
  418. rshift = None
  419. if index % 2 == 0:
  420. rshift = QPointF(shiftPos.x(), 0)
  421. lshift = QPointF(0, shiftPos.y())
  422. else:
  423. lshift = QPointF(shiftPos.x(), 0)
  424. rshift = QPointF(0, shiftPos.y())
  425. shape.moveVertexBy(rindex, rshift)
  426. shape.moveVertexBy(lindex, lshift)
  427. else:
  428. shape.moveVertexBy(index, shiftPos)
  429. def boundedMoveShape(self, shapes, pos):
  430. if type(shapes).__name__ != 'list': shapes = [shapes]
  431. if self.outOfPixmap(pos):
  432. return False # No need to move
  433. o1 = pos + self.offsets[0]
  434. if self.outOfPixmap(o1):
  435. pos -= QPointF(min(0, o1.x()), min(0, o1.y()))
  436. o2 = pos + self.offsets[1]
  437. if self.outOfPixmap(o2):
  438. pos += QPointF(min(0, self.pixmap.width() - o2.x()),
  439. min(0, self.pixmap.height() - o2.y()))
  440. # The next line tracks the new position of the cursor
  441. # relative to the shape, but also results in making it
  442. # a bit "shaky" when nearing the border and allows it to
  443. # go outside of the shape's area for some reason. XXX
  444. #self.calculateOffsets(self.selectedShape, pos)
  445. dp = pos - self.prevPoint
  446. if dp:
  447. for shape in shapes:
  448. shape.moveBy(dp)
  449. shape.close()
  450. self.prevPoint = pos
  451. return True
  452. return False
  453. def deSelectShape(self):
  454. if self.selectedShapes:
  455. for shape in self.selectedShapes: shape.selected=False
  456. self.setHiding(False)
  457. self.selectionChanged.emit([])
  458. self.update()
  459. def deleteSelected(self):
  460. deleted_shapes = []
  461. if self.selectedShapes:
  462. for shape in self.selectedShapes:
  463. self.shapes.remove(shape)
  464. deleted_shapes.append(shape)
  465. self.storeShapes()
  466. self.selectedShapes = []
  467. self.update()
  468. self.updateShapeIndex()
  469. return deleted_shapes
  470. def storeShapes(self):
  471. shapesBackup = []
  472. for shape in self.shapes:
  473. shapesBackup.append(shape.copy())
  474. if len(self.shapesBackups) >= 10:
  475. self.shapesBackups = self.shapesBackups[-9:]
  476. self.shapesBackups.append(shapesBackup)
  477. def copySelectedShape(self):
  478. if self.selectedShapes:
  479. self.selectedShapesCopy = [s.copy() for s in self.selectedShapes]
  480. self.boundedShiftShapes(self.selectedShapesCopy)
  481. self.endMove(copy=True)
  482. return self.selectedShapes
  483. def boundedShiftShapes(self, shapes):
  484. # Try to move in one direction, and if it fails in another.
  485. # Give up if both fail.
  486. for shape in shapes:
  487. point = shape[0]
  488. offset = QPointF(5.0, 5.0)
  489. self.calculateOffsets(shape, point)
  490. self.prevPoint = point
  491. if not self.boundedMoveShape(shape, point - offset):
  492. self.boundedMoveShape(shape, point + offset)
  493. def paintEvent(self, event):
  494. if not self.pixmap:
  495. return super(Canvas, self).paintEvent(event)
  496. p = self._painter
  497. p.begin(self)
  498. p.setRenderHint(QPainter.Antialiasing)
  499. p.setRenderHint(QPainter.HighQualityAntialiasing)
  500. p.setRenderHint(QPainter.SmoothPixmapTransform)
  501. p.scale(self.scale, self.scale)
  502. p.translate(self.offsetToCenter())
  503. p.drawPixmap(0, 0, self.pixmap)
  504. Shape.scale = self.scale
  505. for shape in self.shapes:
  506. if (shape.selected or not self._hideBackround) and self.isVisible(shape):
  507. shape.fill = shape.selected or shape == self.hShape
  508. shape.paint(p)
  509. if self.current:
  510. self.current.paint(p)
  511. self.line.paint(p)
  512. if self.selectedShapesCopy:
  513. for s in self.selectedShapesCopy:
  514. s.paint(p)
  515. # Paint rect
  516. if self.current is not None and len(self.line) == 2 and not self.fourpoint:
  517. # print('Drawing rect')
  518. leftTop = self.line[0]
  519. rightBottom = self.line[1]
  520. rectWidth = rightBottom.x() - leftTop.x()
  521. rectHeight = rightBottom.y() - leftTop.y()
  522. p.setPen(self.drawingRectColor)
  523. brush = QBrush(Qt.BDiagPattern)
  524. p.setBrush(brush)
  525. p.drawRect(leftTop.x(), leftTop.y(), rectWidth, rectHeight)
  526. # ADD:
  527. if (
  528. self.fillDrawing()
  529. and self.fourpoint
  530. and self.current is not None
  531. and len(self.current.points) >= 2
  532. ):
  533. print('paint event')
  534. drawing_shape = self.current.copy()
  535. drawing_shape.addPoint(self.line[1])
  536. drawing_shape.fill = True
  537. drawing_shape.paint(p)
  538. if self.drawing() and not self.prevPoint.isNull() and not self.outOfPixmap(self.prevPoint):
  539. p.setPen(QColor(0, 0, 0))
  540. p.drawLine(int(self.prevPoint.x()), 0, int(self.prevPoint.x()), self.pixmap.height())
  541. p.drawLine(0, int(self.prevPoint.y()), self.pixmap.width(), int(self.prevPoint.y()))
  542. self.setAutoFillBackground(True)
  543. if self.verified:
  544. pal = self.palette()
  545. pal.setColor(self.backgroundRole(), QColor(184, 239, 38, 128))
  546. self.setPalette(pal)
  547. else:
  548. pal = self.palette()
  549. pal.setColor(self.backgroundRole(), QColor(232, 232, 232, 255))
  550. self.setPalette(pal)
  551. # adaptive BBOX label & index font size
  552. if self.pixmap:
  553. h, w = self.pixmap.size().height(), self.pixmap.size().width()
  554. fontszie = int(max(h, w) / 48)
  555. for s in self.shapes:
  556. s.fontsize = fontszie
  557. p.end()
  558. def fillDrawing(self):
  559. return self._fill_drawing
  560. def transformPos(self, point):
  561. """Convert from widget-logical coordinates to painter-logical coordinates."""
  562. return point / self.scale - self.offsetToCenter()
  563. def offsetToCenter(self):
  564. s = self.scale
  565. area = super(Canvas, self).size()
  566. w, h = self.pixmap.width() * s, self.pixmap.height() * s
  567. aw, ah = area.width(), area.height()
  568. x = (aw - w) / (2 * s) if aw > w else 0
  569. y = (ah - h) / (2 * s) if ah > h else 0
  570. return QPointF(x, y)
  571. def outOfPixmap(self, p):
  572. w, h = self.pixmap.width(), self.pixmap.height()
  573. return not (0 <= p.x() <= w and 0 <= p.y() <= h)
  574. def finalise(self):
  575. assert self.current
  576. if self.current.points[0] == self.current.points[-1]:
  577. # print('finalse')
  578. self.current = None
  579. self.drawingPolygon.emit(False)
  580. self.update()
  581. return
  582. self.current.close()
  583. self.current.idx = len(self.shapes) # add current box index
  584. self.shapes.append(self.current)
  585. self.current = None
  586. self.setHiding(False)
  587. self.newShape.emit()
  588. self.update()
  589. def closeEnough(self, p1, p2):
  590. #d = distance(p1 - p2)
  591. #m = (p1-p2).manhattanLength()
  592. # print "d %.2f, m %d, %.2f" % (d, m, d - m)
  593. return distance(p1 - p2) < self.epsilon
  594. # These two, along with a call to adjustSize are required for the
  595. # scroll area.
  596. def sizeHint(self):
  597. return self.minimumSizeHint()
  598. def minimumSizeHint(self):
  599. if self.pixmap:
  600. return self.scale * self.pixmap.size()
  601. return super(Canvas, self).minimumSizeHint()
  602. def wheelEvent(self, ev):
  603. qt_version = 4 if hasattr(ev, "delta") else 5
  604. if qt_version == 4:
  605. if ev.orientation() == Qt.Vertical:
  606. v_delta = ev.delta()
  607. h_delta = 0
  608. else:
  609. h_delta = ev.delta()
  610. v_delta = 0
  611. else:
  612. delta = ev.angleDelta()
  613. h_delta = delta.x()
  614. v_delta = delta.y()
  615. mods = ev.modifiers()
  616. if Qt.ControlModifier == int(mods) and v_delta:
  617. self.zoomRequest.emit(v_delta)
  618. else:
  619. v_delta and self.scrollRequest.emit(v_delta, Qt.Vertical)
  620. h_delta and self.scrollRequest.emit(h_delta, Qt.Horizontal)
  621. ev.accept()
  622. def keyPressEvent(self, ev):
  623. key = ev.key()
  624. shapesBackup = copy.deepcopy(self.shapes)
  625. if len(shapesBackup) == 0:
  626. return
  627. self.shapesBackups.pop()
  628. self.shapesBackups.append(shapesBackup)
  629. if key == Qt.Key_Escape and self.current:
  630. print('ESC press')
  631. self.current = None
  632. self.drawingPolygon.emit(False)
  633. self.update()
  634. elif key == Qt.Key_Return and self.canCloseShape():
  635. self.finalise()
  636. elif key == Qt.Key_Left and self.selectedShapes:
  637. self.moveOnePixel('Left')
  638. elif key == Qt.Key_Right and self.selectedShapes:
  639. self.moveOnePixel('Right')
  640. elif key == Qt.Key_Up and self.selectedShapes:
  641. self.moveOnePixel('Up')
  642. elif key == Qt.Key_Down and self.selectedShapes:
  643. self.moveOnePixel('Down')
  644. elif key == Qt.Key_X and self.selectedShapes:
  645. for i in range(len(self.selectedShapes)):
  646. self.selectedShape = self.selectedShapes[i]
  647. if self.rotateOutOfBound(0.01):
  648. continue
  649. self.selectedShape.rotate(0.01)
  650. self.shapeMoved.emit()
  651. self.update()
  652. elif key == Qt.Key_C and self.selectedShapes:
  653. for i in range(len(self.selectedShapes)):
  654. self.selectedShape = self.selectedShapes[i]
  655. if self.rotateOutOfBound(-0.01):
  656. continue
  657. self.selectedShape.rotate(-0.01)
  658. self.shapeMoved.emit()
  659. self.update()
  660. def rotateOutOfBound(self, angle):
  661. for shape in range(len(self.selectedShapes)):
  662. self.selectedShape = self.selectedShapes[shape]
  663. for i, p in enumerate(self.selectedShape.points):
  664. if self.outOfPixmap(self.selectedShape.rotatePoint(p, angle)):
  665. return True
  666. return False
  667. def moveOnePixel(self, direction):
  668. # print(self.selectedShape.points)
  669. self.selectCount = len(self.selectedShapes)
  670. self.selectCountShape = True
  671. for i in range(len(self.selectedShapes)):
  672. self.selectedShape = self.selectedShapes[i]
  673. if direction == 'Left' and not self.moveOutOfBound(QPointF(-1.0, 0)):
  674. # print("move Left one pixel")
  675. self.selectedShape.points[0] += QPointF(-1.0, 0)
  676. self.selectedShape.points[1] += QPointF(-1.0, 0)
  677. self.selectedShape.points[2] += QPointF(-1.0, 0)
  678. self.selectedShape.points[3] += QPointF(-1.0, 0)
  679. elif direction == 'Right' and not self.moveOutOfBound(QPointF(1.0, 0)):
  680. # print("move Right one pixel")
  681. self.selectedShape.points[0] += QPointF(1.0, 0)
  682. self.selectedShape.points[1] += QPointF(1.0, 0)
  683. self.selectedShape.points[2] += QPointF(1.0, 0)
  684. self.selectedShape.points[3] += QPointF(1.0, 0)
  685. elif direction == 'Up' and not self.moveOutOfBound(QPointF(0, -1.0)):
  686. # print("move Up one pixel")
  687. self.selectedShape.points[0] += QPointF(0, -1.0)
  688. self.selectedShape.points[1] += QPointF(0, -1.0)
  689. self.selectedShape.points[2] += QPointF(0, -1.0)
  690. self.selectedShape.points[3] += QPointF(0, -1.0)
  691. elif direction == 'Down' and not self.moveOutOfBound(QPointF(0, 1.0)):
  692. # print("move Down one pixel")
  693. self.selectedShape.points[0] += QPointF(0, 1.0)
  694. self.selectedShape.points[1] += QPointF(0, 1.0)
  695. self.selectedShape.points[2] += QPointF(0, 1.0)
  696. self.selectedShape.points[3] += QPointF(0, 1.0)
  697. shapesBackup = []
  698. shapesBackup = copy.deepcopy(self.shapes)
  699. self.shapesBackups.append(shapesBackup)
  700. self.shapeMoved.emit()
  701. self.repaint()
  702. def moveOutOfBound(self, step):
  703. points = [p1+p2 for p1, p2 in zip(self.selectedShape.points, [step]*4)]
  704. return True in map(self.outOfPixmap, points)
  705. def setLastLabel(self, text, line_color=None, fill_color=None, key_cls=None):
  706. assert text
  707. self.shapes[-1].label = text
  708. if line_color:
  709. self.shapes[-1].line_color = line_color
  710. if fill_color:
  711. self.shapes[-1].fill_color = fill_color
  712. if key_cls:
  713. self.shapes[-1].key_cls = key_cls
  714. self.storeShapes()
  715. return self.shapes[-1]
  716. def undoLastLine(self):
  717. assert self.shapes
  718. self.current = self.shapes.pop()
  719. self.current.setOpen()
  720. self.line.points = [self.current[-1], self.current[0]]
  721. self.drawingPolygon.emit(True)
  722. def undoLastPoint(self):
  723. if not self.current or self.current.isClosed():
  724. return
  725. self.current.popPoint()
  726. if len(self.current) > 0:
  727. self.line[0] = self.current[-1]
  728. else:
  729. self.current = None
  730. self.drawingPolygon.emit(False)
  731. self.repaint()
  732. def resetAllLines(self):
  733. assert self.shapes
  734. self.current = self.shapes.pop()
  735. self.current.setOpen()
  736. self.line.points = [self.current[-1], self.current[0]]
  737. self.drawingPolygon.emit(True)
  738. self.current = None
  739. self.drawingPolygon.emit(False)
  740. self.update()
  741. def loadPixmap(self, pixmap):
  742. self.pixmap = pixmap
  743. self.shapes = []
  744. self.repaint()
  745. def loadShapes(self, shapes, replace=True):
  746. if replace:
  747. self.shapes = list(shapes)
  748. else:
  749. self.shapes.extend(shapes)
  750. self.current = None
  751. self.hShape = None
  752. self.hVertex = None
  753. # self.hEdge = None
  754. self.storeShapes()
  755. self.updateShapeIndex()
  756. self.repaint()
  757. def setShapeVisible(self, shape, value):
  758. self.visible[shape] = value
  759. self.repaint()
  760. def currentCursor(self):
  761. cursor = QApplication.overrideCursor()
  762. if cursor is not None:
  763. cursor = cursor.shape()
  764. return cursor
  765. def overrideCursor(self, cursor):
  766. self._cursor = cursor
  767. if self.currentCursor() is None:
  768. QApplication.setOverrideCursor(cursor)
  769. else:
  770. QApplication.changeOverrideCursor(cursor)
  771. def restoreCursor(self):
  772. QApplication.restoreOverrideCursor()
  773. def resetState(self):
  774. self.restoreCursor()
  775. self.pixmap = None
  776. self.update()
  777. self.shapesBackups = []
  778. def setDrawingShapeToSquare(self, status):
  779. self.drawSquare = status
  780. def restoreShape(self):
  781. if not self.isShapeRestorable:
  782. return
  783. self.shapesBackups.pop() # latest
  784. shapesBackup = self.shapesBackups.pop()
  785. self.shapes = shapesBackup
  786. self.selectedShapes = []
  787. for shape in self.shapes:
  788. shape.selected = False
  789. self.updateShapeIndex()
  790. self.repaint()
  791. @property
  792. def isShapeRestorable(self):
  793. if len(self.shapesBackups) < 2:
  794. return False
  795. return True
  796. def updateShapeIndex(self):
  797. for i in range(len(self.shapes)):
  798. self.shapes[i].idx = i
  799. self.update()