File size: 10,770 Bytes
907462b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# -*- coding: utf-8 -*-
# Plotting functions for preprocessing
#
# @ Fabian Hörst, fabian.hoerst@uk-essen.de
# Institute for Artifical Intelligence in Medicine,
# University Medicine Essen

import math
import os
import warnings
from typing import List, Tuple

import numpy as np
import rasterio
from PIL import Image, ImageDraw
from rasterio.mask import mask as rasterio_mask
from shapely.geometry import Polygon

from configs.python.config import COLOR_DEFINITIONS
from preprocessing.patch_extraction.src.utils.masking import get_filtered_polygons



def generate_polygon_overview(
    polygons: Tuple[List[Polygon], Polygon],
    region_labels: List[str],
    label_map: dict[str, int],
    reference_size: tuple[int],
    downsample: int = 1,
    image: Image = None,
    tissue_grid: Image = None,
) -> dict:
    """Generate a polygon overview.

    Creates overview images with annotation for each unique label region labels.
    Annotations with the same label are merged. Returns up to two figures for each unique annotation.
    One image is on white background, one overlay on optional background image (e.g., tissue image)

    Args:
        polygons (Tuple[List[Polygon], Polygon]): List of polygons to use.
        region_labels (List[str]): List of labels for the annotations provided as polygons parameter.
        label_map (dict[str, int]): Dictionary mapping the label names to an integer. Please ensure that background label has integer 0!
            Is used here to get the color from preprocessing.src.config.py
        reference_size (tuple[int]): Shape of resulting mask image. Shape should be (height, width, channels).
        downsample (int, optional):  Set the factor by which the polygon should be scaled down. Defaults to 1.
        image (Image, optional): Image as background. Defaults to None.
        tissue_grid (Image, optional): Image with tissue grid as background. Defaults to None.
    Returns:
        dict: Dictionary with annotation names as strings and PIL.Images as keys.
    """

    mask_container = {}
    image_container = {}
    areas = {}
    region_label_set = set(region_labels)

    # save white basic image
    white_bg = Image.fromarray(255 * np.ones(shape=reference_size, dtype=np.uint8))
    white_bg.save("tmp.tif")

    if image is None:
        src = 255 * np.ones(shape=reference_size, dtype=np.uint8)
        image = Image.fromarray(src)
    # draw individual images
    for label in region_label_set:
        label_image = image.copy()
        white_image = white_bg.copy()
        if tissue_grid is not None:
            label_tissue_grid = tissue_grid.copy()
        else:
            label_tissue_grid = None
        label_polygon = get_filtered_polygons(
            polygons, region_labels, label, downsample
        )
        with warnings.catch_warnings():
            warnings.filterwarnings("ignore")
            with rasterio.open("tmp.tif") as src:
                out_image, out_transform = rasterio_mask(src, label_polygon, crop=False)
                # check polygon draw
                label_polygon_list = []
                for poly in label_polygon:
                    if poly.type == "MultiPolygon":
                        labels = [x for x in poly.geoms]
                        label_polygon_list = label_polygon_list + labels
                    else:
                        label_polygon_list = label_polygon_list + [poly]
                poly_outline_image = label_image.copy()
                poly_outline_image_draw = ImageDraw.Draw(poly_outline_image)
                [
                    poly_outline_image_draw.polygon(
                        list(lp.exterior.coords),
                        outline=COLOR_DEFINITIONS[label_map[label]],
                        width=5,
                    )
                    for lp in label_polygon_list
                ]
                # [poly_outline_image_draw.polygon(list(lp.interiors), outline=COLOR_DEFINITIONS[label_map[label]] , width=5) for lp in label_polygon_list if len(list(lp.interiors)) > 2]
                # TODO: interiors are wrong, needs to be fixed (check file ID_1004_LOC_4_TIME_2_BUYUE2088_STATUS_0_UID_27 for an example with interiors)

                mask = out_image.transpose(1, 2, 0)
                mask = (mask / 255).astype(np.uint8)
                area = np.sum(mask[:, :, 0])

                opacity_mask = Image.fromarray(mask[:, :, 0] * 180)
                polygon_image = Image.fromarray(
                    (COLOR_DEFINITIONS[label_map[label]] * mask).astype(np.uint8)
                )
                label_image.paste(polygon_image, (0, 0), opacity_mask)
                white_image.paste(polygon_image, (0, 0), opacity_mask)
                mask_container[label] = {
                    "polygon_image": polygon_image,
                    "polygon_outline": label_polygon_list,
                    "mask": opacity_mask,
                }
                if label_tissue_grid is not None:
                    label_tissue_grid.paste(polygon_image, (0, 0), opacity_mask)
                    mask_container[label]["tissue_grid"] = label_tissue_grid
                    image_container[f"{label}_grid"] = label_tissue_grid
                image_container[label] = label_image
                image_container[f"{label}_clean"] = white_image
                image_container[f"{label}_ouline"] = poly_outline_image
                areas[area] = label

    os.remove("tmp.tif")

    # draw all masks on one image, sorted by areas
    sorted_labels = [areas[k] for k in sorted(areas, reverse=True)]

    final_image = image.copy()
    final_white = white_bg.copy()
    final_outline = image.copy()
    final_grid = tissue_grid.copy()
    final_outline_draw = ImageDraw.Draw(final_outline)
    for label in sorted_labels:
        polygon_image = mask_container[label]["polygon_image"]
        opacity_mask = mask_container[label]["mask"]
        polygon_outline = mask_container[label]["polygon_outline"]
        final_image.paste(polygon_image, (0, 0), opacity_mask)
        final_grid.paste(polygon_image, (0, 0), opacity_mask)
        final_white.paste(polygon_image, (0, 0), opacity_mask)
        [
            final_outline_draw.polygon(
                list(lp.exterior.coords),
                outline=COLOR_DEFINITIONS[label_map[label]],
                width=5,
            )
            for lp in polygon_outline
        ]
        # [final_outline_draw.polygon(list(lp.interiors), outline=COLOR_DEFINITIONS[label_map[label]] , width=5) for lp in polygon_outline if len(list(lp.interiors)) > 2]

    image_container["all_overlaid"] = final_image
    image_container["all_overlaid_clean"] = final_white
    image_container["all_overlaid_outline"] = final_outline
    image_container["all_overlaid_grid"] = final_grid
    
    return image_container


class DashedImageDraw(ImageDraw.ImageDraw):
    def thick_line(self, xy, direction, fill=None, width=0):
        # xy – Sequence of 2-tuples like [(x, y), (x, y), ...]
        # direction – Sequence of 2-tuples like [(x, y), (x, y), ...]
        if xy[0] != xy[1]:
            self.line(xy, fill=fill, width=width)
        else:
            x1, y1 = xy[0]
            dx1, dy1 = direction[0]
            dx2, dy2 = direction[1]
            if dy2 - dy1 < 0:
                x1 -= 1
            if dx2 - dx1 < 0:
                y1 -= 1
            if dy2 - dy1 != 0:
                if dx2 - dx1 != 0:
                    k = -(dx2 - dx1) / (dy2 - dy1)
                    a = 1 / math.sqrt(1 + k**2)
                    b = (width * a - 1) / 2
                else:
                    k = 0
                    b = (width - 1) / 2
                x3 = x1 - math.floor(b)
                y3 = y1 - int(k * b)
                x4 = x1 + math.ceil(b)
                y4 = y1 + int(k * b)
            else:
                x3 = x1
                y3 = y1 - math.floor((width - 1) / 2)
                x4 = x1
                y4 = y1 + math.ceil((width - 1) / 2)
            self.line([(x3, y3), (x4, y4)], fill=fill, width=1)
        return

    def dashed_line(self, xy, dash=(2, 2), fill=None, width=0):
        # xy – Sequence of 2-tuples like [(x, y), (x, y), ...]
        for i in range(len(xy) - 1):
            x1, y1 = xy[i]
            x2, y2 = xy[i + 1]
            x_length = x2 - x1
            y_length = y2 - y1
            length = math.sqrt(x_length**2 + y_length**2)
            dash_enabled = True
            postion = 0
            while postion <= length:
                for dash_step in dash:
                    if postion > length:
                        break
                    if dash_enabled:
                        start = postion / length
                        end = min((postion + dash_step - 1) / length, 1)
                        self.thick_line(
                            [
                                (
                                    round(x1 + start * x_length),
                                    round(y1 + start * y_length),
                                ),
                                (
                                    round(x1 + end * x_length),
                                    round(y1 + end * y_length),
                                ),
                            ],
                            xy,
                            fill,
                            width,
                        )
                    dash_enabled = not dash_enabled
                    postion += dash_step
        return

    def dashed_rectangle(self, xy, dash=(2, 2), outline=None, width=0):
        # xy - Sequence of [(x1, y1), (x2, y2)] where (x1, y1) is top left corner and (x2, y2) is bottom right corner
        x1, y1 = xy[0]
        x2, y2 = xy[1]
        halfwidth1 = math.floor((width - 1) / 2)
        halfwidth2 = math.ceil((width - 1) / 2)
        min_dash_gap = min(dash[1::2])
        end_change1 = halfwidth1 + min_dash_gap + 1
        end_change2 = halfwidth2 + min_dash_gap + 1
        odd_width_change = (width - 1) % 2
        self.dashed_line(
            [(x1 - halfwidth1, y1), (x2 - end_change1, y1)], dash, outline, width
        )
        self.dashed_line(
            [(x2, y1 - halfwidth1), (x2, y2 - end_change1)], dash, outline, width
        )
        self.dashed_line(
            [
                (x2 + halfwidth2, y2 + odd_width_change),
                (x1 + end_change2, y2 + odd_width_change),
            ],
            dash,
            outline,
            width,
        )
        self.dashed_line(
            [
                (x1 + odd_width_change, y2 + halfwidth2),
                (x1 + odd_width_change, y1 + end_change2),
            ],
            dash,
            outline,
            width,
        )
        return