Shortcuts

Source code for slideflow.slide.qc.otsu

"""Otsu's thresholding QC algorithm."""

import cv2
import numpy as np
import slideflow as sf
import rasterio
import shapely.affinity as sa
from slideflow import errors
from typing import Union, Optional


def _apply_mask(image, mask):
    resized_mask = cv2.resize(
        (~mask).astype(np.uint8),
        (image.shape[1], image.shape[0]),
        interpolation=cv2.INTER_NEAREST
    )
    return cv2.bitwise_or(image, image, mask=resized_mask)


def _get_level_for_otsu(wsi: "sf.WSI", min_size: int = 500) -> int:
    """Find the smallest downsample level of a minimum size."""
    smallest_dim = np.array([min(L['dimensions']) for L in wsi.levels])
    level_ids = np.array([L['level'] for L in wsi.levels])
    sorted_idx = np.argsort(smallest_dim)
    try:
        best_idx = np.where(smallest_dim[sorted_idx] > min_size)[0][0]
    except IndexError:
        # If the slide is smaller than the target minimum dimension,
        # use the full slide image
        best_idx = sorted_idx[-1]
    return level_ids[sorted_idx][best_idx]

# -----------------------------------------------------------------------------

[docs]class Otsu: def __init__(self, slide_level: Optional[int] = None): """Prepare Otsu's thresholding algorithm for filtering a slide. This method is used to detect areas of tissue and remove background. This QC method works by obtaining a thumbnail of a slide, and converting the image into the HSV color space. The HSV image undergoes a median blur using OpenCV with a kernel size of 7, and the image is thresholded using ``cv2.THRESH_OTSU``. This results in a binary mask, which is then applied to the slide for filtering. Original paper: https://ieeexplore.ieee.org/document/4310076 .. warning:: Otsu's thresholding may give unexpected results with slides that have many large pen marks, erroneously identifying pen marks as tissue and removing the actual tissue as background. This behavior can be circumvented by applying a Gaussian filter before Otsu's thresholding. .. code-block:: python import slideflow as sf from slideflow.slide import qc wsi = sf.WSI(...) gaussian = qc.GaussianV2() otsu = qc.Otsu() wsi.qc([gaussian, otsu]) Examples Apply Otsu's thresholding to a slide. .. code-block:: python import slideflow as sf from slideflow.slide import qc wsi = sf.WSI(...) otsu = qc.Otsu() wsi.qc(otsu) Args: level (int): Slide pyramid level at which to perform filtering. Defaults to second-lowest available level. """ self.level = slide_level def __repr__(self): return "Otsu(slide_level={!r})".format( self.level ) def _thumb_from_slide( self, wsi: "sf.WSI", ) -> np.ndarray: """Get a thumbnail from the given slide. Args: wsi (sf.WSI): Whole-slide image. Returns: np.ndarray: RGB thumbnail of the whole-slide image. """ if self.level is None: # Otsu's thresholding can be done on the smallest downsample level, # with the smallest dimension being at least 500 pixels level = _get_level_for_otsu(wsi, min_size=500) else: level = self.level try: if wsi.slide.has_levels: sf.log.debug("Applying Otsu's thresholding at level={}".format(level)) thumb = wsi.slide.read_level(level=level, to_numpy=True) else: sf.log.debug("Applying Otsu's thresholding at level=None") thumb = wsi.slide.read_level(to_numpy=True) except Exception as e: raise errors.QCError( f"Thumbnail error for slide {wsi.shortname}, QC failed: {e}" ) if thumb.shape[-1] == 4: thumb = thumb[:, :, :3] # Only apply Otsu thresholding within ROI, if present # If ROI is the ROI_issues, invert it if wsi.has_rois(): ofact = 1 / wsi.slide.level_downsamples[level] roi_mask = np.zeros((thumb.shape[0], thumb.shape[1])) # Scale ROIs to thumbnail size scaled_polys = wsi._scale_polys( [roi.poly for roi in wsi.get_rois(ignore_artifact=True)], xfact=ofact, yfact=ofact, ) scaled_issues_polys = wsi._scale_polys( [roi.invert(*wsi.dimensions).poly for roi in wsi.get_artifacts()], xfact=ofact, yfact=ofact, ) # Rasterize scaled ROIs if len(scaled_polys) > 0: roi_mask = rasterio.features.rasterize( scaled_polys, out_shape=thumb.shape[:2] ) if len(scaled_issues_polys) > 0: roi_mask_issues = rasterio.features.rasterize( scaled_issues_polys, out_shape=thumb.shape[:2] ) # If there are artifacts, remove them from the ROI mask if len(scaled_polys) > 0: roi_mask = np.minimum(roi_mask_issues, roi_mask) else: roi_mask = roi_mask_issues if wsi.roi_method == 'outside': roi_mask = ~roi_mask thumb = cv2.bitwise_or( thumb, thumb, mask=roi_mask.astype(np.uint8) ) # Only apply Otsu thresholding within areas not already removed # with other QC methods. if wsi.has_non_roi_qc(): thumb = _apply_mask(thumb, wsi.get_qc_mask(roi=False)) return thumb def __call__( self, wsi: Union["sf.WSI", np.ndarray], mask: Optional[np.ndarray] = None, ) -> np.ndarray: """Perform Otsu's thresholding on the given slide or image. Args: slide (sf.WSI, np.ndarray): Either a Slideflow WSI or a numpy array, with shape (h, w, c) and type np.uint8. mask (np.ndarray): Restrict Otsu's threshold to the area of the image indicated by this boolean mask. Defaults to None. Returns: np.ndarray: QC boolean mask, where True = filtered out. """ if isinstance(wsi, sf.WSI): thumb = self._thumb_from_slide(wsi) else: thumb = wsi if mask is not None: thumb = _apply_mask(thumb, mask) hsv_img = cv2.cvtColor(thumb, cv2.COLOR_RGB2HSV) img_med = cv2.medianBlur(hsv_img[:, :, 1], 7) flags = cv2.THRESH_OTSU+cv2.THRESH_BINARY_INV _, otsu_mask = cv2.threshold(img_med, 0, 255, flags) return otsu_mask.astype(bool)