File size: 8,290 Bytes
8ad61d2
 
68b9a41
c858e30
54cb6c5
 
 
8ad61d2
d2ae102
 
 
 
 
 
8ad61d2
9faa18b
 
 
 
 
 
 
 
 
 
68b9a41
 
 
 
 
 
c858e30
7708b6c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c76c7ee
9faa18b
 
 
 
 
 
 
 
 
 
c76c7ee
 
 
 
 
 
 
 
 
 
68b9a41
 
 
 
7708b6c
 
 
 
a8828c1
 
 
 
 
 
68b9a41
 
7708b6c
 
68b9a41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9faa18b
 
7708b6c
0a92df1
 
8ad61d2
 
13c2a66
 
c858e30
0a92df1
 
 
 
8ad61d2
 
c858e30
8ad61d2
 
9faa18b
8ad61d2
7708b6c
9faa18b
68b9a41
7708b6c
 
c76c7ee
 
7708b6c
9faa18b
68b9a41
 
 
 
 
 
 
9faa18b
4fc760f
c858e30
 
54cb6c5
 
 
 
c76c7ee
0a92df1
54cb6c5
8ad61d2
7708b6c
 
 
 
 
0a92df1
 
7708b6c
9faa18b
 
 
68b9a41
9faa18b
68b9a41
 
 
 
8ad61d2
68b9a41
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
import cv2 as cv
import numpy as np
from polylabel import polylabel

from .config import default_config


class GenerateIslands:
    def __init__(self, indices_color_choices):
        """
        Args:
            indices_color_choices: 2D numpy array with the same shape as the image.
                Shows the color index chosen for each pixel in the image.
        """
        self.indices_color_choices = indices_color_choices
        
        # List of coordinates for each islands border
        self.island_borders = {}
        for color_index in np.unique(indices_color_choices):
            self.island_borders[color_index] = []
        
        # Images of the islands
        self.island_fills = {}
        for color_index in np.unique(indices_color_choices):
            self.island_fills[color_index] = []
        
        # Coordinate of centroids of islands
        self.island_centroids = {}
        for color_index in np.unique(indices_color_choices):
            self.island_centroids[color_index] = []

    
    def _is_valid_shape(self, contours, hierarchy, total_area, area_perc_threshold,
                        arc_length_area_ratio_threshold):
        holes_contours_ids = np.where(hierarchy[0,:,-1] != -1)[0]
        hole_areas_sum = 0
        for hole_contour_id in holes_contours_ids:
            hole_area = cv.contourArea(contours[hole_contour_id])
            hole_areas_sum += hole_area

        external_contours_ids = np.where(hierarchy[0,:,-1] == -1)[0]
        external_areas_sum = 0
        external_arc_length = 0
        for external_contour_id in external_contours_ids:
            external_areas_sum += cv.contourArea(contours[external_contour_id])
            external_arc_length += cv.arcLength(contours[external_contour_id],True)

        total_island_area = external_areas_sum - hole_areas_sum

        if total_island_area == 0:
            return False
        
        area_percentage = (total_island_area / total_area) * 100
        arc_length_area_ratio = (external_arc_length / total_island_area)

        if (area_percentage >= area_perc_threshold) \
            and (arc_length_area_ratio <= arc_length_area_ratio_threshold):
            return True
        else:
            return False


    def _get_cleaned_up_contours(self, island_fill, area_perc_threshold, 
                                 arc_length_area_ratio_threshold, check_shape_validity):
        contours_image = np.ones_like(island_fill)

        total_area = self.indices_color_choices.shape[0] * self.indices_color_choices.shape[1]

        contours, hierarchy = cv.findContours(
            island_fill, 
            mode = cv.RETR_TREE,
            method = cv.CHAIN_APPROX_NONE
        )

        if check_shape_validity:
            is_valid_shape = self._is_valid_shape(
                contours = contours, 
                hierarchy = hierarchy,
                total_area = total_area,
                area_perc_threshold = area_perc_threshold, 
                arc_length_area_ratio_threshold = arc_length_area_ratio_threshold
            )
        else:
            is_valid_shape = True
        
        contours_selected = []
        hierarchy_selected = []

        if is_valid_shape:
            for cntr_id, contour in enumerate(contours): 
                area_fraction_perc = (cv.contourArea(contour) / total_area) * 100
                if area_fraction_perc >= area_perc_threshold:
                    cv.drawContours(
                        image = contours_image, 
                        contours = [contour], 
                        contourIdx = 0, 
                        color = (0,255,0), 
                        thickness = 1)
                    contours_selected.append(contour)
                    hierarchy_selected.append(hierarchy[0][cntr_id])
        
        # If the shape is not valid, return a blank image
        return contours_image, \
            contours_selected, \
            np.array(hierarchy_selected)


    def _get_centroid_for_island(self, contours, hierarchy):
        if len(contours) == 0:
            return np.array([np.nan, np.nan])

        coordinates_for_polylabel = []

        external_contours_ids = np.where(hierarchy[:,-1] == -1)[0]
        for external_contour_id in external_contours_ids:
            epsilon = 0.01 * cv.arcLength(contours[external_contour_id],True)
            approx_contour = cv.approxPolyDP(contours[external_contour_id], epsilon, True)
            coordinates_for_polylabel.append(approx_contour.squeeze())

        holes_contours_ids = np.where(hierarchy[:,-1] != -1)[0]
        for hole_contour_id in holes_contours_ids:
            epsilon = 0.01 * cv.arcLength(contours[hole_contour_id],True)
            approx_contour = cv.approxPolyDP(contours[hole_contour_id], epsilon, True)
            coordinates_for_polylabel.append(approx_contour.squeeze())
        
        centroid_coords =  polylabel(coordinates_for_polylabel)
        return [int(centroid_coords[0]), int(centroid_coords[1])]


    def _get_islands_for_one_color(self, color_index, border_padding, area_perc_threshold, 
                                   arc_length_area_ratio_threshold, check_shape_validity,
                                   open_kernel_size):
        # Get a binary image with just the selected color
        this_color = (self.indices_color_choices == color_index).astype(np.uint8)
        # Pad the image to enable border detection on image boundaries
        this_color = np.pad(this_color, border_padding, mode='constant', constant_values=0)

        # Run the open morphological operation to remove small islands and isthmuses
        kernel = np.ones((open_kernel_size, open_kernel_size),np.uint8)
        this_color = cv.morphologyEx(this_color, cv.MORPH_OPEN, kernel)

        # Find connected components
        num_labels, labels_im = cv.connectedComponents(this_color)

        for component_id in range(1, num_labels):
            this_component = (labels_im == component_id).astype(np.uint8)
            self.island_fills[color_index].append(this_component)
            

            # Get cleaned up contours
            cleaned_up_contours, contours_selected, hierarchies_selected = self._get_cleaned_up_contours(
                island_fill = this_component, 
                area_perc_threshold = area_perc_threshold, 
                arc_length_area_ratio_threshold = arc_length_area_ratio_threshold,
                check_shape_validity = check_shape_validity
            )

            # Get the centroid of the island
            centroid_coords = self._get_centroid_for_island(
                contours_selected,
                hierarchies_selected
            )
            self.island_centroids[color_index].append(centroid_coords)

            contour_border_coords = np.where(cleaned_up_contours == 0)
            self.island_borders[color_index].append((color_index, contour_border_coords))

    
    def get_islands(self, config = default_config):
        border_padding = config["border_padding"]
        area_perc_threshold = config["area_perc_threshold"]
        arc_length_area_ratio_threshold = config["arc_length_area_ratio_threshold"]
        check_shape_validity = config["check_shape_validity"]
        open_kernel_size = config["open_kernel_size"]

        for color_index in np.unique(self.indices_color_choices):
            self._get_islands_for_one_color(
                color_index = color_index, 
                border_padding = border_padding, 
                area_perc_threshold = area_perc_threshold,
                arc_length_area_ratio_threshold = arc_length_area_ratio_threshold,
                check_shape_validity = check_shape_validity,
                open_kernel_size = open_kernel_size,
            )
        
        # Flatten the list of borders
        island_borders_list = []
        centroid_coords_list = []
        for color_id in self.island_borders:
            for idx, border_coords in enumerate(self.island_borders[color_id]):
                if len(border_coords[1][0]) > 0:
                    island_borders_list.append(self.island_borders[color_id][idx])
                    centroid_coords_list.append(self.island_centroids[color_id][idx])
        
        return island_borders_list, centroid_coords_list