Coretex
human_segmentation_converter.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, Set
19 from pathlib import Path
20 
21 import os
22 
23 from PIL import Image
24 from shapely.geometry import Polygon
25 
26 import numpy as np
27 import skimage.measure
28 
29 from ..base_converter import BaseConverter
30 from ...annotation import CoretexImageAnnotation, CoretexSegmentationInstance, ImageDatasetClass, BBox
31 
32 
33 class HumanSegmentationConverter(BaseConverter):
34 
35  def __init__(self, datasetName: str, projectId: int, datasetPath: str) -> None:
36  super().__init__(datasetName, projectId, datasetPath)
37 
38  self.__imagesPath = os.path.join(datasetPath, "images")
39  self.__annotationsPath = os.path.join(datasetPath, "annotations")
40 
41  self.__imageNames = list(filter(
42  lambda path: path.endswith("jpg"),
43  os.listdir(self.__imagesPath))
44  )
45 
46  @property
47  def __backgroundClass(self) -> ImageDatasetClass:
48  coretexClass = self._dataset.classByName("background")
49  if coretexClass is None:
50  raise ValueError(f">> [Coretex] Class: (background) is not a part of dataset")
51 
52  return coretexClass
53 
54  @property
55  def __humanClass(self) -> ImageDatasetClass:
56  coretexClass = self._dataset.classByName("human")
57  if coretexClass is None:
58  raise ValueError(f">> [Coretex] Class: (human) is not a part of dataset")
59 
60  return coretexClass
61 
62  def _dataSource(self) -> List[str]:
63  return self.__imageNames
64 
65  def _extractLabels(self) -> Set[str]:
66  return set(["background", "human"])
67 
68  def __extractPolygons(self, annotationPath: str, imageWidth: int, imageHeight: int) -> List[List[int]]:
69  imageFile = Image.open(annotationPath)
70 
71  maskImage = imageFile.resize((imageWidth, imageHeight), Image.Resampling.LANCZOS)
72  subMaskArray = np.asarray(maskImage, dtype = np.uint8)
73 
74  # prevent segmented objects from being equal to image width/height
75  subMaskArray[:, 0] = 0
76  subMaskArray[0, :] = 0
77  subMaskArray[:, -1] = 0
78  subMaskArray[-1, :] = 0
79 
80  contours = skimage.measure.find_contours(subMaskArray, 0.5)
81 
82  segmentations: List[List[int]] = []
83  for contour in contours:
84  for i in range(len(contour)):
85  row, col = contour[i]
86  contour[i] = (col - 1, row - 1)
87 
88  # Make a polygon and simplify it
89  poly = Polygon(contour)
90 
91  if poly.geom_type == 'MultiPolygon':
92  # If MultiPolygon, take the smallest convex Polygon containing all the points in the object
93  poly = poly.convex_hull
94 
95  # Ignore if still not a Polygon (could be a line or point)
96  if poly.geom_type == 'Polygon':
97  segmentation = np.array(poly.exterior.coords).ravel().tolist()
98  segmentations.append(segmentation)
99 
100  # sorts polygons by size, descending
101  segmentations.sort(key=len, reverse=True)
102 
103  return segmentations
104 
105  def __extractInstances(self, annotationPath: str, imageWidth: int, imageHeight: int) -> List[CoretexSegmentationInstance]:
106  polygons = self.__extractPolygons(annotationPath, imageWidth, imageHeight)
107  if len(polygons) == 0:
108  return []
109 
110  instances: List[CoretexSegmentationInstance] = []
111 
112  largestBBox = BBox.fromPoly(polygons[0])
113  firstPolygon = Polygon(np.array(polygons[0]).reshape((-1, 2)))
114 
115  backgroundBoundingBox = BBox(0, 0, imageWidth, imageHeight)
116  instances.append(CoretexSegmentationInstance.create(
117  self.__backgroundClass.classIds[0],
118  backgroundBoundingBox,
119  [backgroundBoundingBox.polygon]
120  ))
121 
122  instances.append(CoretexSegmentationInstance.create(
123  self.__humanClass.classIds[0],
124  largestBBox,
125  [polygons[0]]
126  ))
127 
128  for index in range(1, len(polygons)):
129  currentBBox = BBox.fromPoly(polygons[index])
130  currentPolygon = Polygon(np.array(polygons[index]).reshape((-1, 2)))
131 
132  instances.append(CoretexSegmentationInstance.create(
133  self.__backgroundClass.classIds[0] if currentPolygon.intersects(firstPolygon) else self.__humanClass.classIds[0],
134  currentBBox,
135  [polygons[index]]
136  ))
137 
138  return instances
139 
140  def _extractSingleAnnotation(self, imageName: str) -> None:
141  imagePath = os.path.join(self.__imagesPath, imageName)
142  annotationPath = os.path.join(self.__annotationsPath, f"{Path(imagePath).stem}.png")
143 
144  image = Image.open(imagePath)
145  instances = self.__extractInstances(annotationPath, image.width, image.height)
146 
147  coretexAnnotation = CoretexImageAnnotation.create(imageName, image.width, image.height, instances)
148  self._saveImageAnnotationPair(imagePath, coretexAnnotation)