Spaces:
No application file
No application file
File size: 10,620 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 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 | # -*- coding: utf-8 -*-
# Masking function to generate tissue masks
#
# @ Fabian Hörst, fabian.hoerst@uk-essen.de
# Institute for Artifical Intelligence in Medicine,
# University Medicine Essen
import os
import warnings
from typing import List, Tuple, Union
import cv2
import numpy as np
import rasterio
import skimage.color as sk_color
import skimage.filters as sk_filters
import skimage.morphology as sk_morphology
from histolab.filters.image_filters import BluePenFilter, GreenPenFilter, RedPenFilter
from PIL import Image
from rasterio.mask import mask as rasterio_mask
from shapely.affinity import scale
from shapely.geometry import Polygon
from preprocessing.patch_extraction import logger
def generate_tissue_mask(
tissue_tile: np.ndarray,
mask_otsu: bool = False,
polygons: List[Polygon] = None,
region_labels: List[str] = None,
otsu_annotation: Union[List[str], str] = "object",
downsample: int = 1,
apply_prefilter: bool = False,
) -> np.ndarray:
"""Generate a tissue mask using otsu thresholding.
Per Default, otsu-thresholding is performed. If mask_otsu is true, first a masked image is calculate
using the annotation matching the otsu_annotation label.
Args:
tissue_tile (np.ndarray): Tissue tile as numpy array with shape (height, width, 3)
mask_otsu (bool, optional): If masking is applied before thresholding. Defaults to False.
polygons (List[Polygon], optional): Annotations of this WSI as a list of polygons (referenced to highest level of WSI). Defaults to None.
region_labels (List[str], optional): List of labels for the annotations provided as polygons parameter. Defaults to None.
otsu_annotation (Union[List[str], str], optional): List with annotation names or string with annotation name to use for a masked otsu thresholding.
Defaults to "object".
downsample (int, optional): Downsampling of the tissue tile compared to highest WSI level. Used for matching annotations with tissue-tile size.
Defaults to 1.
apply_prefilter (bool, optional): If a prefilter should be used to remove markers before applying otsu. Defaults to False.
Returns:
np.ndarray: Binary tissue mask with shape (height, width)
"""
if polygons is not None:
assert len(polygons) == len(
region_labels
), "Polygon list and polygon labels are not having the same length"
if mask_otsu:
# filter
otsu_polgyon = get_filtered_polygons(
polygons=polygons,
region_labels=region_labels,
filter_labels=otsu_annotation,
downsample=downsample,
)
if len(otsu_polgyon) != 0:
logger.debug(
"Mask tissue thumbnail with region before applying Otsu thresholding"
)
tissue_tile = mask_tile_with_region(tile=tissue_tile, polygons=otsu_polgyon)
else:
logger.error("ValueError:")
logger.error(
"Annotation with given label does not exist. Using unmasked thresholding"
)
# apply otsu thresholding
if apply_prefilter:
tissue_tile = remove_marker_filters(tile=tissue_tile)
tissue_mask = apply_otsu_thresholding(tile=tissue_tile)
assert len(np.unique(tissue_mask)) <= 2, "Mask is not binary"
return tissue_mask
def convert_polygons_to_mask(
polygons: Tuple[List[Polygon], Polygon],
reference_size: tuple[int],
downsample: int = 1,
) -> np.ndarray:
"""Convert a polygon to a mask
The function is assuming that polygons have already been filtered (see get_filtered_polygon).
Args:
polygons (Tuple[List[Polygon], Polygon]): List of polygons converted to a mask. Can work with Polygons with holes inside
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.
Returns:
np.ndarray: Binary mask with shape (height, width)
"""
if type(polygons) is not List:
polygons = list(polygons)
polygons_downsampled = [
scale(
poly,
xfact=1 / downsample,
yfact=1 / downsample,
origin=(0, 0),
)
for poly in polygons
]
src = 255 * np.ones(shape=reference_size, dtype=np.uint8)
im = Image.fromarray(src)
im.save("tmp.tif")
with warnings.catch_warnings():
warnings.filterwarnings("ignore")
with rasterio.open("tmp.tif") as src:
out_image, _ = rasterio_mask(src, polygons_downsampled, crop=False)
mask = out_image.transpose(1, 2, 0)
mask = np.invert(mask)
os.remove("tmp.tif")
mask = (mask / 255).astype(np.uint8)
assert len(np.unique(mask)) <= 2, "Mask is not binary"
return mask[:, :, 0]
def get_filtered_polygons(
polygons: List[Polygon],
region_labels: List[str],
filter_labels: List[str],
downsample: int = 1,
) -> List[Polygon]:
"""Filter Polygons by a list of filter labels
Returns a list with filtered polygons containing just the polygons with
the label specified in filter_labels
Args:
polygons (List[Polygon]): Annotations as a list of polygons.
region_labels (List[str]): List of labels
filter_labels (List[str]): List of labels to filter
downsample (int, optional): Scaling factor to downscale polygon. Defaults to 1.
Returns:
List[Polygon]: List with filtered polygons
"""
logger.debug(
f"Filter polygons for label: {filter_labels} and downsample results to {downsample}"
)
filtered_polygons = []
for poly, region_label in zip(polygons, region_labels):
if region_label in filter_labels:
filtered_polygons.append(
scale(poly, xfact=1 / downsample, yfact=1 / downsample, origin=(0, 0))
)
if len(filtered_polygons) == 0:
logger.debug(
"ValueError: Annotation with given label does not exist or Annotation has a non-valid Type."
)
return filtered_polygons
def mask_tile_with_region(
tile: np.ndarray, polygons: Union[List[Polygon], Polygon]
) -> np.ndarray:
"""Mask a tile with a region and return the masked tile
Args:
tile (np.ndarray): Tile which should be masked
polygons (Union[List[Polygon], Polygon]): List of mask polygons or a polygon to mask
Returns:
np.ndarray: Masked tile
"""
if type(polygons) is not List:
polygons = list(polygons)
# create temp file for rasterio
src = 255 * np.ones(shape=(tile.shape[0:2]), dtype=np.uint8)
im = Image.fromarray(src)
im.save("tmp.tif")
# get mask out of polygon
with warnings.catch_warnings():
warnings.filterwarnings("ignore")
with rasterio.open("tmp.tif") as src:
out_image, out_transform = rasterio_mask(src, polygons, crop=False)
mask = out_image.transpose(1, 2, 0)
# remove temp file
os.remove("tmp.tif")
# create masked figure
fg = cv2.bitwise_or(tile, tile, mask=mask)
inverse_mask = cv2.bitwise_not(mask)
background = np.full(tile.shape, 255, dtype=np.uint8)
bk = cv2.bitwise_or(background, background, mask=inverse_mask)
return cv2.bitwise_or(fg, bk)
def apply_otsu_thresholding(tile: np.ndarray) -> np.ndarray:
"""Generate a binary tissue mask by using Otsu thresholding
Args:
tile (np.ndarray): Tile with tissue with shape (height, width, 3)
Returns:
np.ndarray: Binary mask with shape (height, width)
"""
hsv_img = cv2.cvtColor(tile.astype(np.uint8), cv2.COLOR_RGB2HSV)
gray_mask = cv2.inRange(hsv_img, (0, 0, 70), (180, 10, 255))
black_mask = cv2.inRange(hsv_img, (0, 0, 0), (180, 255, 85))
# Set all grey/black pixels to white
full_tile_bg = np.copy(tile)
full_tile_bg[np.where(gray_mask | black_mask)] = 255
# apply otsu mask first time for removing larger artifacts
masked_image_gray = 255 * sk_color.rgb2gray(full_tile_bg)
thresh = sk_filters.threshold_otsu(masked_image_gray)
otsu_masking = masked_image_gray < thresh
# improving mask
otsu_masking = sk_morphology.remove_small_objects(otsu_masking, 60)
otsu_masking = sk_morphology.dilation(otsu_masking, sk_morphology.square(12))
otsu_masking = sk_morphology.closing(otsu_masking, sk_morphology.square(5))
otsu_masking = sk_morphology.remove_small_holes(otsu_masking, 250)
tile = mask_rgb(tile, otsu_masking).astype(np.uint8)
# apply otsu mask second time for removing small artifacts
masked_image_gray = 255 * sk_color.rgb2gray(tile)
thresh = sk_filters.threshold_otsu(masked_image_gray)
otsu_masking = masked_image_gray < thresh
otsu_masking = sk_morphology.remove_small_holes(otsu_masking, 5000)
otsu_thr = ~otsu_masking
otsu_thr = otsu_thr.astype(np.uint8)
return otsu_thr
def mask_rgb(rgb: np.ndarray, mask: np.ndarray) -> np.ndarray:
"""Mask an RGB image
Args:
rgb (np.ndarray): RGB image to mask with shape (height, width, 3)
mask (np.ndarray): Binary mask with shape (height, width)
Returns:
np.ndarray: Masked image
"""
assert (
rgb.shape[:-1] == mask.shape
), "Mask and RGB shape are different. Cannot mask when source and mask have different dimension."
mask_positive = np.dstack([mask, mask, mask])
mask_negative = np.dstack([~mask, ~mask, ~mask])
positive = rgb * mask_positive
negative = rgb * mask_negative
negative = 255 * (negative > 0.0001).astype(int)
masked_image = positive + negative
return np.clip(masked_image, a_min=0, a_max=255)
def remove_marker_filters(tile: np.ndarray) -> np.ndarray:
"""Generate a binary tissue mask by using Otsu thresholding
Args:
tile (np.ndarray): Tile with tissue with shape (height, width, 3)
Returns:
np.ndarray: Binary mask with shape (height, width)
"""
red_pen_filter = RedPenFilter()
green_pen_filter = GreenPenFilter()
blue_pen_filter = BluePenFilter()
tile = Image.fromarray(tile.astype(np.uint8))
tile = blue_pen_filter(tile)
tile = green_pen_filter(tile)
tile = red_pen_filter(tile)
image_rgb_np = np.array(tile)
black_pixels = (
(image_rgb_np[:, :, 0] == 0)
& (image_rgb_np[:, :, 1] == 0)
& (image_rgb_np[:, :, 2] == 0)
)
image_rgb_np[black_pixels] = [255, 255, 255]
return image_rgb_np
|