Coretex
coretex_format.py
1 # Copyright (C) 2023 Coretex LLC
2 
3 # This file is part of Coretex.ai
4 
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
9 
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU Affero General Public License for more details.
14 
15 # You should have received a copy of the GNU Affero General Public License
16 # along with this program. If not, see <https://www.gnu.org/licenses/>.
17 
18 from typing import List, Dict, Tuple, Optional
19 from typing_extensions import Self
20 from uuid import UUID
21 from math import cos, sin, radians
22 
23 from PIL import Image, ImageDraw
24 
25 import numpy as np
26 
27 from .bbox import BBox
28 from .classes_format import ImageDatasetClasses
29 from ....codable import Codable, KeyDescriptor
30 
31 
32 SegmentationType = List[int]
33 
34 
35 def toPoly(segmentation: List[int]) -> List[Tuple[int, int]]:
36  points: List[Tuple[int, int]] = []
37 
38  for index in range(0, len(segmentation) - 1, 2):
39  points.append((segmentation[index], segmentation[index + 1]))
40 
41  return points
42 
43 
45 
46  """
47  Segmentation Instance class
48 
49  Properties
50  ----------
51  classID : UUID
52  uuid of class
53  bbox : BBox
54  Bounding Box as a python class
55  segmentations : List[SegmentationType]
56  list of segmentations that define the precise boundaries of object
57  """
58 
59  classId: UUID
60  bbox: BBox
61  segmentations: List[SegmentationType]
62 
63  @classmethod
64  def _keyDescriptors(cls) -> Dict[str, KeyDescriptor]:
65  descriptors = super()._keyDescriptors()
66 
67  descriptors["classId"] = KeyDescriptor("class_id", UUID)
68  descriptors["bbox"] = KeyDescriptor("bbox", BBox)
69  descriptors["segmentations"] = KeyDescriptor("annotations")
70 
71  return descriptors
72 
73  @classmethod
74  def create(cls, classId: UUID, bbox: BBox, segmentations: List[SegmentationType]) -> Self:
75  """
76  Creates CoretexSegmentationInstance object with provided parameters
77 
78  Parameters
79  ----------
80  classID : UUID
81  uuid of class
82  bbox : BBox
83  Bounding Box as a python class
84  segmentations : List[SegmentationType]
85  list of segmentations that define the precise boundaries of object
86 
87  Returns
88  -------
89  The created CoretexSegmentationInstance object
90  """
91 
92  obj = cls()
93 
94  obj.classId = classId
95  obj.bbox = bbox
96  obj.segmentations = segmentations
97 
98  return obj
99 
100  def extractSegmentationMask(self, width: int, height: int) -> np.ndarray:
101  """
102  Generates segmentation mask based on provided
103  width and height of image\n
104  Pixel values are equal to class IDs
105 
106  Parameters
107  ----------
108  width : int
109  width of image in pixels
110  height : int
111  height of image in pixels
112 
113  Returns
114  -------
115  np.ndarray -> segmentation mask represented as np.ndarray
116 
117  Raises
118  ------
119  ValueError -> if segmentation has less then 4 values
120  """
121 
122  image = Image.new("L", (width, height))
123 
124  for segmentation in self.segmentationssegmentations:
125  if len(segmentation) < 4:
126  raise ValueError(f">> [Coretex] Segmentation has too few values ({len(segmentation)}. Minimum: 4)")
127 
128  draw = ImageDraw.Draw(image)
129  draw.polygon(toPoly(segmentation), fill = 1)
130 
131  return np.array(image)
132 
133  def extractBinaryMask(self, width: int, height: int) -> np.ndarray:
134  """
135  Works the same way as extractSegmentationMask function
136  Values that are > 0 are capped to 1
137 
138  Parameters
139  ----------
140  width : int
141  width of image in pixels
142  height : int
143  height of image in pixels
144 
145  Returns
146  -------
147  np.ndarray -> binary segmentation mask represented as np.ndarray
148  """
149 
150  binaryMask = self.extractSegmentationMaskextractSegmentationMask(width, height)
151  binaryMask[binaryMask > 0] = 1
152 
153  return binaryMask
154 
155  def centroid(self) -> Tuple[int, int]:
156  """
157  Calculates centroid of segmentations
158 
159  Returns
160  -------
161  Tuple[int, int] -> x, y coordinates of centroid
162  """
163 
164  flattenedSegmentations = [element for sublist in self.segmentationssegmentations for element in sublist]
165 
166  listCX = [value for index, value in enumerate(flattenedSegmentations) if index % 2 == 0]
167  centerX = sum(listCX) // len(listCX)
168 
169  listCY = [value for index, value in enumerate(flattenedSegmentations) if index % 2 != 0]
170  centerY = sum(listCY) // len(listCY)
171 
172  return centerX, centerY
173 
174  def centerSegmentations(self, newCentroid: Tuple[int, int]) -> None:
175  """
176  Centers segmentations to the specified center point
177 
178  Parameters
179  ----------
180  newCentroid : Tuple[int, int]
181  x, y coordinates of centroid
182  """
183 
184  newCenterX, newCenterY = newCentroid
185  oldCenterX, oldCenterY = self.centroidcentroid()
186 
187  modifiedSegmentations: List[List[int]] = []
188 
189  for segmentation in self.segmentationssegmentations:
190  modifiedSegmentation: List[int] = []
191 
192  for i in range(0, len(segmentation), 2):
193  x = segmentation[i] + (newCenterX - oldCenterX)
194  y = segmentation[i+1] + (newCenterY - oldCenterY)
195 
196  modifiedSegmentation.append(x)
197  modifiedSegmentation.append(y)
198 
199  modifiedSegmentations.append(modifiedSegmentation)
200 
201  self.segmentationssegmentations = modifiedSegmentations
202 
204  self,
205  degrees: int,
206  origin: Optional[Tuple[int, int]] = None
207  ) -> None:
208 
209  """
210  Rotates segmentations of CoretexSegmentationInstance object
211 
212  Parameters
213  ----------
214  degrees : int
215  degree of rotation
216  """
217 
218  if origin is None:
219  origin = self.centroidcentroid()
220 
221  rotatedSegmentations: List[List[int]] = []
222  centerX, centerY = origin
223 
224  # because rotations with image and segmentations doesn't go in same direction
225  # one of the rotations has to be inverted so they go in same direction
226  theta = radians(-degrees)
227  cosang, sinang = cos(theta), sin(theta)
228 
229  for segmentation in self.segmentationssegmentations:
230  rotatedSegmentation: List[int] = []
231 
232  for i in range(0, len(segmentation), 2):
233  x = segmentation[i] - centerX
234  y = segmentation[i + 1] - centerY
235 
236  newX = int(x * cosang - y * sinang) + centerX
237  newY = int(x * sinang + y * cosang) + centerY
238 
239  rotatedSegmentation.append(newX)
240  rotatedSegmentation.append(newY)
241 
242  rotatedSegmentations.append(rotatedSegmentation)
243 
244  self.segmentationssegmentations = rotatedSegmentations
245 
246 
248 
249  """
250  Image Annotation class
251 
252  Properties
253  ----------
254  name : str
255  name of annotation class
256  width : int
257  width of annotation
258  height : int
259  height of annotation
260  instances : List[CoretexSegmentationInstance]
261  list of SegmentationInstance objects
262  """
263 
264  name: str
265  width: int
266  height: int
267  instances: List[CoretexSegmentationInstance]
268 
269  @classmethod
270  def _keyDescriptors(cls) -> Dict[str, KeyDescriptor]:
271  descriptors = super()._keyDescriptors()
272  descriptors["instances"] = KeyDescriptor("instances", CoretexSegmentationInstance, list)
273 
274  return descriptors
275 
276  @classmethod
277  def create(
278  cls,
279  name: str,
280  width: int,
281  height: int,
282  instances: List[CoretexSegmentationInstance]
283  ) -> Self:
284  """
285  Creates CoretexImageAnnotation object with provided parameters
286 
287  Parameters
288  ----------
289  name : str
290  name of annotation class
291  width : int
292  width of annotation
293  height : int
294  height of annotation
295  instances : List[CoretexSegmentationInstance]
296  list of SegmentationInstance objects
297 
298  Returns
299  -------
300  The created CoretexImageAnnotation object
301  """
302 
303  obj = cls()
304 
305  obj.name = name
306  obj.width = width
307  obj.height = height
308  obj.instances = instances
309 
310  return obj
311 
312  def extractSegmentationMask(self, classes: ImageDatasetClasses) -> np.ndarray:
313  """
314  Generates segmentation mask of provided ImageDatasetClasses object
315 
316  Parameters
317  ----------
318  classes : ImageDatasetClasses
319  list of dataset classes
320 
321  Returns
322  -------
323  np.ndarray -> segmentation mask represented as np.ndarray
324  """
325 
326  image = Image.new("L", (self.width, self.height))
327 
328  for instance in self.instances:
329  labelId = classes.labelIdForClassId(instance.classId)
330  if labelId is None:
331  continue
332 
333  for segmentation in instance.segmentations:
334  if len(segmentation) < 4:
335  raise ValueError(f">> [Coretex] Segmentation has too few values ({len(segmentation)}. Minimum: 4)")
336 
337  draw = ImageDraw.Draw(image)
338  draw.polygon(toPoly(segmentation), fill = labelId + 1)
339 
340  return np.asarray(image)
Self create(cls, str name, int width, int height, List[CoretexSegmentationInstance] instances)
np.ndarray extractSegmentationMask(self, ImageDatasetClasses classes)
None rotateSegmentations(self, int degrees, Optional[Tuple[int, int]] origin=None)
Self create(cls, UUID classId, BBox bbox, List[SegmentationType] segmentations)