text_based.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. import typing
  2. import numpy as np
  3. from skimage.draw import circle_perimeter, polygon
  4. from abc import ABC, abstractmethod
  5. from view.python_core.rois.base_classes import BaseROIData
  6. class BaseTextROIData(BaseROIData, ABC):
  7. """
  8. Abstract class to define API for text based ROI Data
  9. """
  10. _splitter = '\t'
  11. def __init__(self, label: str, basic_text_description="A text based ROI"):
  12. super().__init__(label, basic_text_description)
  13. @classmethod
  14. @abstractmethod
  15. def read_from_text_line(cls, text_line):
  16. pass
  17. @abstractmethod
  18. def write_to_text_line(self) -> str:
  19. pass
  20. @classmethod
  21. def split_line(cls, text_line) -> list:
  22. return text_line.split(cls._splitter)
  23. def compose_line(self, text_line_parts) -> str:
  24. return self._splitter.join(text_line_parts)
  25. def get_text_description(self, frame_size):
  26. return \
  27. f'File: {self.roi_file}; Pixels: {self.get_boolean_mask(frame_size).sum()}; ' \
  28. f'Label;{self.label};{self.basic_text_description}'
  29. class CircleILTISROIData(BaseTextROIData):
  30. def __init__(self, label: str, x: float, y: float, d: float, basic_text_description="A circular ILTIS ROI"):
  31. """
  32. Initialize the circle ROI data with the X and Y coordinates of the center and the diameter of the circle
  33. :param label: str, unique identifier for the ROI
  34. :param x: float, X coordinates of the center
  35. :param y: float, Y coordinates of the center
  36. :param d: float, diameter of the circle
  37. """
  38. super().__init__(label, basic_text_description)
  39. self.x, self.y, self.d = x, y, d
  40. @classmethod
  41. def read_from_text_line(cls, text_line):
  42. """
  43. Initialize a circle ROI object from a line of text
  44. :param text_line: str, possibly ending with a '\n'
  45. :return: CircleILTISROIData object
  46. """
  47. text_line = text_line.rstrip("\n")
  48. text_parts = cls.split_line(text_line)
  49. label = text_parts[1]
  50. x, y, d = (float(temp) for temp in text_parts[2:5])
  51. return cls(label, x, y, d, text_line)
  52. def write_to_text_line(self) -> str:
  53. """
  54. Returns ROI information formatted as a line of text. Output lines are tab delimited and
  55. always start with the keyword 'circle', followed by the X and Y coordinates of the circle, followed by the
  56. diameter of the circle. The line is terminated with a '\n'
  57. :return: str
  58. """
  59. to_write_parts = ["circle", str(self.label),
  60. f"{self.x:.2f}", f"{self.y:.2f}", f"{self.d:.2f}"]
  61. return self.compose_line(to_write_parts) + "\n"
  62. def get_boolean_mask(self, frame_size: typing.Iterable[int]) -> np.ndarray:
  63. """
  64. Returns a boolean numpy array of shape <unexcluded_frame_size>, with all pixels within the circle set to True,
  65. and all pixels outside to False
  66. :param frame_size: 2-member tuple of ints
  67. :return: numpy.ndarray
  68. """
  69. mask = np.zeros(frame_size, dtype=bool)
  70. rr, cc = circle_perimeter(
  71. r=int(self.x), c=int(self.y), radius=int(self.d / 2), shape=frame_size)
  72. mask[rr, cc] = True
  73. return mask
  74. class PolygonILTISROIData(BaseTextROIData):
  75. def __init__(self, label: str, list_of_vertices: list, basic_text_description: str = 'An ILTIS polygon ROI'):
  76. """
  77. Initialize a polygon ROI object
  78. :param label: str, unique identifier for the ROI
  79. :param list_of_vertices: list of tuples, where each tuple represents a vertex, in the form of two ints,
  80. the X and Y coordinates of the vertex
  81. """
  82. super().__init__(label, basic_text_description)
  83. self.list_of_vertices = np.array(list_of_vertices, dtype=int)
  84. @classmethod
  85. def read_from_text_line(cls, text_line):
  86. """
  87. Initialize a polygon ROI object from a line of text
  88. :param text_line: str, possibly ending with a '\n'
  89. :return: PolygonILTISROIData object
  90. """
  91. text_line = text_line.rstrip("\n")
  92. text_parts = cls.split_line(text_line)
  93. label = text_parts[1]
  94. n_remaining_parts = len(text_parts) - 2
  95. n_vertices = int(np.floor(n_remaining_parts / 2))
  96. list_of_vertices = []
  97. for ind in range(n_vertices):
  98. x, y = round(float(text_parts[2 + 2 * ind])), round(float(text_parts[3 + 2 * ind]))
  99. list_of_vertices.append((x, y))
  100. return cls(label, list_of_vertices, text_line)
  101. def write_to_text_line(self) -> str:
  102. """
  103. Returns ROI information formatted as a line of text. Output lines are tab delimited and
  104. always start with the keyword 'polygon', followed by X1, Y1, X2, Y2, ... where (X1, Y1), (X2, Y2),.. represent
  105. vertices such every consecutive pair of vertices form a side of the polygon. The line is terminated with a '\n'
  106. :return: str
  107. """
  108. to_write_parts = ["polygon", str(self.label)]
  109. for vertex in self.list_of_vertices:
  110. to_write_parts += [f"{vertex[0]:.2f}", f"{vertex[1]:.2f}"]
  111. return self.compose_line(to_write_parts) + "\n"
  112. def get_boolean_mask(self, frame_size: typing.Iterable[int]) -> np.ndarray:
  113. """
  114. Returns a boolean numpy array of shape <unexcluded_frame_size>, with all pixels within the polygon set to True,
  115. and all pixels outside to False
  116. :param frame_size: 2-member tuple of ints
  117. :return: numpy.ndarray
  118. """
  119. mask = np.zeros(frame_size, dtype=bool)
  120. x_coors, y_coors = zip(*self.list_of_vertices)
  121. rr, cc = polygon(r=x_coors, c=y_coors)
  122. mask[rr, cc] = True
  123. return mask