Source code for wsipipe.preprocess.tissue_detection.tissue_detector

"""
Tissue Detectors create a 2d array of booleans indicating if that area contains
tissue or not. 

The input is an RGB numpy array representing the slide.
Usually a downsampled thumbnail image as whole slide images as level 0
are often too large to store in memory.

"""

from abc import ABCMeta, abstractmethod
from typing import List, Union

import numpy as np
from skimage.color import rgb2hsv, rgb2gray
from skimage.filters import threshold_otsu

from wsipipe.preprocess.tissue_detection.filters import PreFilter, NullBlur
from wsipipe.preprocess.tissue_detection.morphology_transforms import MorphologyTransform, NullTransform

[docs]class TissueDetector(metaclass=ABCMeta): """Generic tissue detector class Args: pre_filter: Any filters or transforms that are to be applied before the tissue detection. Can be lists of filters or individual filters. Defaults to NullBlur morph_transform (): Any filters or transforms that are to be applied after the tissue detection. Can be lists of transforms or individual transforms. Defaults to NullTransform Returns: An ndarray of booleans with the same dimensions as the input image True means foreground, False means background """ def __init__(self, pre_filter: Union[PreFilter, List[PreFilter]] = NullBlur(), morph_transform: Union[MorphologyTransform, List[MorphologyTransform]] = NullTransform() ) -> None: # assign values if not isinstance(pre_filter, list): self.pre_filter = [pre_filter] else: self.pre_filter = pre_filter if not isinstance(morph_transform, list): self.morph_transform = [morph_transform] else: self.morph_transform = morph_transform @abstractmethod def __call__(self, image: np.ndarray) -> np.array: raise NotImplementedError
[docs]class TissueDetectorOTSU(TissueDetector): def __call__(self, image: np.ndarray) -> np.ndarray: """Applies Otsu thresholding as a tissue detector 1. Convert from RGB to HSV 2. Perform automatic thresholding using Otsu's method on the H and S channels 3. Combine the thresholded H and S channels Also applies prefilters and post transforms if specified Args: image: A scaled down WSI image. Must be r,g,b. Returns: An ndarray of booleans with the same dimensions as the input image True means foreground, False means background """ # convert the image into the hsv colour space image_hsv = rgb2hsv(image) # apply filter for pf in self.pre_filter: image_hsv = pf(image_hsv) # use Otsu's method to find the thresholds for hue and saturation thresh_h = threshold_otsu(image_hsv[:, :, 0]) thresh_s = threshold_otsu(image_hsv[:, :, 1]) # mask the image to get determine which pixels with hue and saturation above their thresholds mask_h = image_hsv[:, :, 1] > thresh_h mask_s = image_hsv[:, :, 1] > thresh_s # combine the masks with an OR so any pixel above either threshold counts as foreground np_mask = np.logical_or(mask_h, mask_s) # apply morphological transforms for mt in self.morph_transform: np_mask = mt(np_mask) return np_mask
[docs]class TissueDetectorGreyScale(TissueDetector): def __init__(self, pre_filter = NullBlur(), morph_transform = NullTransform(), grey_level: float = 0.8) -> None: """ Greyscale tissue detector In addition to prefilter and morph_transform from generic class Args: grey_level (float): Value between 0 and 1 for greyscale threshold. Defaults to 0.8 """ super().__init__(pre_filter, morph_transform) self.grey_level = grey_level def __call__(self, image: np.ndarray) -> np.ndarray: """Applies greyscale thresholding as a tissue detector 1. Convert PIL image to numpy array 2. Convert from RGB to gray scale 3. Get masks, any pixel that is less than a threshold (e.g. 0.8) Also applies prefilters and post transforms if specified Args: image: A scaled down WSI image. Must be r,g,b. Returns: An ndarray of booleans with the same dimensions as the input image True means foreground, False means background """ # convert PIL image to numpy array image = np.asarray(image) # change pure black to pure white imager = image[:, :, 0] == 0 imageg = image[:, :, 1] == 0 imageb = image[:, :, 2] == 0 image_mask = np.expand_dims(np.logical_and(np.logical_and(imager, imageg), imageb), axis=-1) image = np.where(image_mask, [255,255,255], image) image = np.array(image, dtype=np.uint8) # convert to gray-scale image_grey = rgb2gray(image) # apply filter for pf in self.pre_filter: image = pf(image) # get masks, any pixel that is less than 0.8 np_mask = np.less_equal(image_grey, self.grey_level) # apply morphological transforms for mt in self.morph_transform: np_mask = mt(np_mask) return np_mask
[docs]class TissueDetectorAll(TissueDetector): def __call__(self, image: np.ndarray) -> np.ndarray: """A Null tissue detector Just returns the whole image as True everything is foreground to be kept Args: image: A scaled down WSI image. Must be r,g,b. Returns: An ndarray of booleans with the same dimensions as the input image True means foreground """ # convert PIL image to numpy array image = np.asarray(image) # get masks, all pixels np_mask = np.array(np.ones(image.shape[0:2]), dtype=bool) return np_mask