Spaces:
No application file
No application file
| # -*- 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 | |