shape.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. import copy
  2. import math
  3. import numpy as np
  4. import skimage.measure
  5. from qtpy import QtCore
  6. from qtpy import QtGui
  7. import labelme.utils
  8. from labelme.logger import logger
  9. # TODO(unknown):
  10. # - [opt] Store paths instead of creating new ones at each paint.
  11. class Shape(object):
  12. # Render handles as squares
  13. P_SQUARE = 0
  14. # Render handles as circles
  15. P_ROUND = 1
  16. # Flag for the handles we would move if dragging
  17. MOVE_VERTEX = 0
  18. # Flag for all other handles on the current shape
  19. NEAR_VERTEX = 1
  20. # The following class variables influence the drawing of all shape objects.
  21. line_color = None
  22. fill_color = None
  23. select_line_color = None
  24. select_fill_color = None
  25. vertex_fill_color = None
  26. hvertex_fill_color = None
  27. point_type = P_ROUND
  28. point_size = 8
  29. scale = 1.0
  30. def __init__(
  31. self,
  32. label=None,
  33. line_color=None,
  34. shape_type=None,
  35. flags=None,
  36. group_id=None,
  37. description=None,
  38. mask=None,
  39. ):
  40. self.label = label
  41. self.group_id = group_id
  42. self.points = []
  43. self.point_labels = []
  44. self.shape_type = shape_type
  45. self._shape_raw = None
  46. self._points_raw = []
  47. self._shape_type_raw = None
  48. self.fill = False
  49. self.selected = False
  50. self.shape_type = shape_type
  51. self.flags = flags
  52. self.description = description
  53. self.other_data = {}
  54. self.mask = mask
  55. self._highlightIndex = None
  56. self._highlightMode = self.NEAR_VERTEX
  57. self._highlightSettings = {
  58. self.NEAR_VERTEX: (4, self.P_ROUND),
  59. self.MOVE_VERTEX: (1.5, self.P_SQUARE),
  60. }
  61. self._closed = False
  62. if line_color is not None:
  63. # Override the class line_color attribute
  64. # with an object attribute. Currently this
  65. # is used for drawing the pending line a different color.
  66. self.line_color = line_color
  67. def setShapeRefined(self, shape_type, points, point_labels, mask=None):
  68. self._shape_raw = (self.shape_type, self.points, self.point_labels)
  69. self.shape_type = shape_type
  70. self.points = points
  71. self.point_labels = point_labels
  72. self.mask = mask
  73. def restoreShapeRaw(self):
  74. if self._shape_raw is None:
  75. return
  76. self.shape_type, self.points, self.point_labels = self._shape_raw
  77. self._shape_raw = None
  78. @property
  79. def shape_type(self):
  80. return self._shape_type
  81. @shape_type.setter
  82. def shape_type(self, value):
  83. if value is None:
  84. value = "polygon"
  85. if value not in [
  86. "polygon",
  87. "rectangle",
  88. "point",
  89. "line",
  90. "circle",
  91. "linestrip",
  92. "points",
  93. "mask",
  94. ]:
  95. raise ValueError("Unexpected shape_type: {}".format(value))
  96. self._shape_type = value
  97. def close(self):
  98. self._closed = True
  99. def addPoint(self, point, label=1):
  100. if self.points and point == self.points[0]:
  101. self.close()
  102. else:
  103. self.points.append(point)
  104. self.point_labels.append(label)
  105. def canAddPoint(self):
  106. return self.shape_type in ["polygon", "linestrip"]
  107. def popPoint(self):
  108. if self.points:
  109. if self.point_labels:
  110. self.point_labels.pop()
  111. return self.points.pop()
  112. return None
  113. def insertPoint(self, i, point, label=1):
  114. self.points.insert(i, point)
  115. self.point_labels.insert(i, label)
  116. def removePoint(self, i):
  117. if not self.canAddPoint():
  118. logger.warning(
  119. "Cannot remove point from: shape_type=%r",
  120. self.shape_type,
  121. )
  122. return
  123. if self.shape_type == "polygon" and len(self.points) <= 3:
  124. logger.warning(
  125. "Cannot remove point from: shape_type=%r, len(points)=%d",
  126. self.shape_type,
  127. len(self.points),
  128. )
  129. return
  130. if self.shape_type == "linestrip" and len(self.points) <= 2:
  131. logger.warning(
  132. "Cannot remove point from: shape_type=%r, len(points)=%d",
  133. self.shape_type,
  134. len(self.points),
  135. )
  136. return
  137. self.points.pop(i)
  138. self.point_labels.pop(i)
  139. def isClosed(self):
  140. return self._closed
  141. def setOpen(self):
  142. self._closed = False
  143. def getRectFromLine(self, pt1, pt2):
  144. x1, y1 = pt1.x(), pt1.y()
  145. x2, y2 = pt2.x(), pt2.y()
  146. return QtCore.QRectF(x1, y1, x2 - x1, y2 - y1)
  147. def paint(self, painter):
  148. if self.mask is None and not self.points:
  149. return
  150. color = self.select_line_color if self.selected else self.line_color
  151. pen = QtGui.QPen(color)
  152. # Try using integer sizes for smoother drawing(?)
  153. pen.setWidth(max(1, int(round(2.0 / self.scale))))
  154. painter.setPen(pen)
  155. if self.mask is not None:
  156. image_to_draw = np.zeros(self.mask.shape + (4,), dtype=np.uint8)
  157. fill_color = (
  158. self.select_fill_color.getRgb()
  159. if self.selected
  160. else self.fill_color.getRgb()
  161. )
  162. image_to_draw[self.mask] = fill_color
  163. qimage = QtGui.QImage.fromData(labelme.utils.img_arr_to_data(image_to_draw))
  164. painter.drawImage(
  165. int(round(self.points[0].x())),
  166. int(round(self.points[0].y())),
  167. qimage,
  168. )
  169. line_path = QtGui.QPainterPath()
  170. contours = skimage.measure.find_contours(np.pad(self.mask, pad_width=1))
  171. for contour in contours:
  172. contour += [self.points[0].y(), self.points[0].x()]
  173. line_path.moveTo(contour[0, 1], contour[0, 0])
  174. for point in contour[1:]:
  175. line_path.lineTo(point[1], point[0])
  176. painter.drawPath(line_path)
  177. if self.points:
  178. line_path = QtGui.QPainterPath()
  179. vrtx_path = QtGui.QPainterPath()
  180. negative_vrtx_path = QtGui.QPainterPath()
  181. if self.shape_type in ["rectangle", "mask"]:
  182. assert len(self.points) in [1, 2]
  183. if len(self.points) == 2:
  184. rectangle = self.getRectFromLine(*self.points)
  185. line_path.addRect(rectangle)
  186. if self.shape_type == "rectangle":
  187. for i in range(len(self.points)):
  188. self.drawVertex(vrtx_path, i)
  189. elif self.shape_type == "circle":
  190. assert len(self.points) in [1, 2]
  191. if len(self.points) == 2:
  192. rectangle = self.getCircleRectFromLine(self.points)
  193. line_path.addEllipse(rectangle)
  194. for i in range(len(self.points)):
  195. self.drawVertex(vrtx_path, i)
  196. elif self.shape_type == "linestrip":
  197. line_path.moveTo(self.points[0])
  198. for i, p in enumerate(self.points):
  199. line_path.lineTo(p)
  200. self.drawVertex(vrtx_path, i)
  201. elif self.shape_type == "points":
  202. assert len(self.points) == len(self.point_labels)
  203. for i, point_label in enumerate(self.point_labels):
  204. if point_label == 1:
  205. self.drawVertex(vrtx_path, i)
  206. else:
  207. self.drawVertex(negative_vrtx_path, i)
  208. else:
  209. line_path.moveTo(self.points[0])
  210. # Uncommenting the following line will draw 2 paths
  211. # for the 1st vertex, and make it non-filled, which
  212. # may be desirable.
  213. # self.drawVertex(vrtx_path, 0)
  214. for i, p in enumerate(self.points):
  215. line_path.lineTo(p)
  216. self.drawVertex(vrtx_path, i)
  217. if self.isClosed():
  218. line_path.lineTo(self.points[0])
  219. painter.drawPath(line_path)
  220. if vrtx_path.length() > 0:
  221. painter.drawPath(vrtx_path)
  222. painter.fillPath(vrtx_path, self._vertex_fill_color)
  223. if self.fill and self.mask is None:
  224. color = self.select_fill_color if self.selected else self.fill_color
  225. painter.fillPath(line_path, color)
  226. pen.setColor(QtGui.QColor(255, 0, 0, 255))
  227. painter.setPen(pen)
  228. painter.drawPath(negative_vrtx_path)
  229. painter.fillPath(negative_vrtx_path, QtGui.QColor(255, 0, 0, 255))
  230. def drawVertex(self, path, i):
  231. d = self.point_size / self.scale
  232. shape = self.point_type
  233. point = self.points[i]
  234. if i == self._highlightIndex:
  235. size, shape = self._highlightSettings[self._highlightMode]
  236. d *= size
  237. if self._highlightIndex is not None:
  238. self._vertex_fill_color = self.hvertex_fill_color
  239. else:
  240. self._vertex_fill_color = self.vertex_fill_color
  241. if shape == self.P_SQUARE:
  242. path.addRect(point.x() - d / 2, point.y() - d / 2, d, d)
  243. elif shape == self.P_ROUND:
  244. path.addEllipse(point, d / 2.0, d / 2.0)
  245. else:
  246. assert False, "unsupported vertex shape"
  247. def nearestVertex(self, point, epsilon):
  248. min_distance = float("inf")
  249. min_i = None
  250. for i, p in enumerate(self.points):
  251. dist = labelme.utils.distance(p - point)
  252. if dist <= epsilon and dist < min_distance:
  253. min_distance = dist
  254. min_i = i
  255. return min_i
  256. def nearestEdge(self, point, epsilon):
  257. min_distance = float("inf")
  258. post_i = None
  259. for i in range(len(self.points)):
  260. line = [self.points[i - 1], self.points[i]]
  261. dist = labelme.utils.distancetoline(point, line)
  262. if dist <= epsilon and dist < min_distance:
  263. min_distance = dist
  264. post_i = i
  265. return post_i
  266. def containsPoint(self, point):
  267. if self.mask is not None:
  268. y = np.clip(
  269. int(round(point.y() - self.points[0].y())),
  270. 0,
  271. self.mask.shape[0] - 1,
  272. )
  273. x = np.clip(
  274. int(round(point.x() - self.points[0].x())),
  275. 0,
  276. self.mask.shape[1] - 1,
  277. )
  278. return self.mask[y, x]
  279. return self.makePath().contains(point)
  280. def getCircleRectFromLine(self, line):
  281. """Computes parameters to draw with `QPainterPath::addEllipse`"""
  282. if len(line) != 2:
  283. return None
  284. (c, point) = line
  285. r = line[0] - line[1]
  286. d = math.sqrt(math.pow(r.x(), 2) + math.pow(r.y(), 2))
  287. rectangle = QtCore.QRectF(c.x() - d, c.y() - d, 2 * d, 2 * d)
  288. return rectangle
  289. def makePath(self):
  290. if self.shape_type in ["rectangle", "mask"]:
  291. path = QtGui.QPainterPath()
  292. if len(self.points) == 2:
  293. rectangle = self.getRectFromLine(*self.points)
  294. path.addRect(rectangle)
  295. elif self.shape_type == "circle":
  296. path = QtGui.QPainterPath()
  297. if len(self.points) == 2:
  298. rectangle = self.getCircleRectFromLine(self.points)
  299. path.addEllipse(rectangle)
  300. else:
  301. path = QtGui.QPainterPath(self.points[0])
  302. for p in self.points[1:]:
  303. path.lineTo(p)
  304. return path
  305. def boundingRect(self):
  306. return self.makePath().boundingRect()
  307. def moveBy(self, offset):
  308. self.points = [p + offset for p in self.points]
  309. def moveVertexBy(self, i, offset):
  310. self.points[i] = self.points[i] + offset
  311. def highlightVertex(self, i, action):
  312. """Highlight a vertex appropriately based on the current action
  313. Args:
  314. i (int): The vertex index
  315. action (int): The action
  316. (see Shape.NEAR_VERTEX and Shape.MOVE_VERTEX)
  317. """
  318. self._highlightIndex = i
  319. self._highlightMode = action
  320. def highlightClear(self):
  321. """Clear the highlighted point"""
  322. self._highlightIndex = None
  323. def copy(self):
  324. return copy.deepcopy(self)
  325. def __len__(self):
  326. return len(self.points)
  327. def __getitem__(self, key):
  328. return self.points[key]
  329. def __setitem__(self, key, value):
  330. self.points[key] = value