Source code for histolab.scorer

# encoding: utf-8

# ------------------------------------------------------------------------
# Copyright 2022 All Histolab Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ------------------------------------------------------------------------

import operator
from abc import abstractmethod

import numpy as np

from .filters import image_filters as imf
from .filters import morphological_filters as mof
from .filters.util import mask_difference
from .tile import Tile

try:
    from typing import Protocol, runtime_checkable
except ImportError:
    from typing_extensions import Protocol, runtime_checkable


[docs]@runtime_checkable class Scorer(Protocol): """General scorer object .. automethod:: __call__ """
[docs] @abstractmethod def __call__(self, tile: Tile) -> float: pass # pragma: no cover
[docs]class RandomScorer(Scorer): """Implement a Scorer that returns a random float score between 0 and 1. .. automethod:: __call__ """
[docs] def __call__(self, tile: Tile) -> float: """Return the random score associated with the tile. Parameters ---------- tile : Tile The tile to calculate the score from. Returns ------- float Random score ranging between 0 and 1. """ return np.random.random()
[docs]class CellularityScorer(Scorer): """Implement a Scorer that estimates the cellularity in an H&E-stained tile. This class deconvolves the hematoxylin channel and uses the fraction of tile occupied by hematoxylin as the cellularity score. Notice that this scorer is useful when tiles are extracted at a very low resolution with no artifacts; in this case, using the``NucleiScorer()`` instead would not work well as nuclei are no discernible at low magnification. .. automethod:: __call__ Parameters ---------- consider_tissue : bool, optional Whether the detected tissue on the tile should be considered to compute the cellularity score. Default is True Notes ----- If the tile presents artifacts (e.g., tissue folds, markers), the scorer cannot be fully trusted. """ def __init__(self, consider_tissue: bool = True) -> None: self.consider_tissue = consider_tissue
[docs] def __call__(self, tile: Tile) -> float: """Return the tile cellularity score. Parameters ---------- tile : Tile The tile to calculate the score from. consider_tissue : bool Whether the cellularity score should be computed by considering the tissue on the tile. Default is True Returns ------- float Cellularity score """ filters_cellularity = imf.Compose( [ imf.HematoxylinChannel(), imf.RgbToGrayscale(), imf.YenThreshold(operator.lt), ] ) mask_nuclei = np.array(tile.apply_filters(filters_cellularity).image) return ( np.count_nonzero(mask_nuclei) / np.count_nonzero(tile.tissue_mask) if self.consider_tissue else np.count_nonzero(mask_nuclei) / mask_nuclei.size )
[docs]class NucleiScorer(Scorer): r"""Implement a Scorer that estimates the presence of nuclei in an H&E-stained tile. This class implements an hybrid algorithm that combines thresholding and morphological operations to segment nuclei on H&E-stained histological images. The NucleiScorer class defines the score of a given tile t as: .. math:: s_t = N_t\cdot \mathrm{tanh}(T_t) \mathrm{, } \; 0\le s_t<1 where :math:`N_t` is the nuclei ratio on t, computed as number of white pixels on the segmented mask over the tile size, and :math:`T_t` the fraction of tissue in t. Notice that we introduced the hyperbolic tangent to bound the weight of the tissue ratio over the nuclei ratio. Notes ----- If the tile presents artifacts (e.g., tissue folds, markers), the scorer cannot be fully trusted. .. automethod:: __call__ """
[docs] def __call__(self, tile: Tile) -> float: """Return the nuclei score associated with the tile. Parameters ---------- tile : Tile The tile to calculate the score from. Returns ------- float Nuclei score """ filters_raw_nuclei = imf.Compose( [ imf.HematoxylinChannel(), imf.RgbToGrayscale(), imf.YenThreshold(operator.lt), ] ) filters_nuclei_cleaner = imf.Compose( [ imf.HematoxylinChannel(), imf.RgbToGrayscale(), imf.YenThreshold(operator.lt), mof.WhiteTopHat(), ] ) mask_raw_nuclei = np.array(tile.apply_filters(filters_raw_nuclei).image) mask_nuclei_clean = np.array(tile.apply_filters(filters_nuclei_cleaner).image) mask_nuclei = mask_difference(mask_raw_nuclei, mask_nuclei_clean) nuclei_ratio = np.count_nonzero(mask_nuclei) / mask_nuclei.size return nuclei_ratio * np.tanh(tile.tissue_ratio)