Source code for tiatoolbox.tools.stainextract
"""Stain matrix extraction for stain normalization."""
from __future__ import annotations
import numpy as np
from sklearn.decomposition import DictionaryLearning
from tiatoolbox.utils.misc import get_luminosity_tissue_mask
from tiatoolbox.utils.transforms import rgb2od
[docs]
def vectors_in_correct_direction(e_vectors: np.ndarray) -> np.ndarray:
"""Points the eigen vectors in the right direction.
Args:
e_vectors (:class:`numpy.ndarray`):
Eigen vectors.
Returns:
:class:`numpy.ndarray`:
Pointing in the correct direction.
"""
if e_vectors[0, 0] < 0:
e_vectors[:, 0] *= -1
if e_vectors[0, 1] < 0:
e_vectors[:, 1] *= -1
return e_vectors
[docs]
def h_and_e_in_right_order(v1: np.ndarray, v2: np.ndarray) -> np.ndarray:
"""Rearrange input vectors for H&E in correct order with H as first output.
Args:
v1 (:class:`numpy.ndarray`):
Input vector for stain extraction.
v2 (:class:`numpy.ndarray`):
Input vector for stain extraction.
Returns:
:class:`numpy.ndarray`:
Input vectors in the correct order.
"""
if v1[0] > v2[0]:
return np.array([v1, v2])
return np.array([v2, v1])
[docs]
def dl_output_for_h_and_e(dictionary: np.ndarray) -> np.ndarray:
"""Return correct value for H and E from dictionary learning output.
Args:
dictionary (:class:`numpy.ndarray`):
:class:`sklearn.decomposition.DictionaryLearning` output
Returns:
:class:`numpy.ndarray`:
With correct values for H and E.
"""
if dictionary[0, 0] < dictionary[1, 0]:
return dictionary[[1, 0], :]
return dictionary
[docs]
class CustomExtractor:
"""Get the user-defined stain matrix.
This class contains code inspired by StainTools
[https://github.com/Peter554/StainTools] written by Peter Byfield.
Examples:
>>> from tiatoolbox.tools.stainextract import CustomExtractor
>>> from tiatoolbox.utils import imread
>>> extractor = CustomExtractor(stain_matrix)
>>> img = imread('path/to/image')
>>> stain_matrix = extractor.get_stain_matrix(img)
"""
def __init__(self: CustomExtractor, stain_matrix: np.ndarray) -> None:
"""Initialize :class:`CustomExtractor`."""
self.stain_matrix = stain_matrix
if self.stain_matrix.shape not in [(2, 3), (3, 3)]:
msg = "Stain matrix must have shape (2, 3) or (3, 3)."
raise ValueError(msg)
[docs]
def get_stain_matrix(self: CustomExtractor, _: np.ndarray) -> np.ndarray:
"""Get the user defined stain matrix.
Returns:
:class:`numpy.ndarray`:
User defined stain matrix.
"""
return self.stain_matrix
[docs]
class RuifrokExtractor:
"""Reuifrok stain extractor.
Get the stain matrix as defined in:
Ruifrok, Arnout C., and Dennis A. Johnston. "Quantification of
histochemical staining by color deconvolution." Analytical and
quantitative cytology and histology 23.4 (2001): 291-299.
This class contains code inspired by StainTools
[https://github.com/Peter554/StainTools] written by Peter Byfield.
Examples:
>>> from tiatoolbox.tools.stainextract import RuifrokExtractor
>>> from tiatoolbox.utils import imread
>>> extractor = RuifrokExtractor()
>>> img = imread('path/to/image')
>>> stain_matrix = extractor.get_stain_matrix(img)
"""
def __init__(self: RuifrokExtractor) -> None:
"""Initialize :class:`RuifrokExtractor`."""
self.__stain_matrix = np.array([[0.65, 0.70, 0.29], [0.07, 0.99, 0.11]])
[docs]
def get_stain_matrix(self: RuifrokExtractor, _: np.ndarray) -> np.ndarray:
"""Get the pre-defined stain matrix.
Returns:
:class:`numpy.ndarray`:
Pre-defined stain matrix.
"""
return self.__stain_matrix.copy()
[docs]
class MacenkoExtractor:
"""Macenko stain extractor.
Get the stain matrix as defined in:
Macenko, Marc, et al. "A method for normalizing histology
slides for quantitative analysis." 2009 IEEE International
Symposium on Biomedical Imaging: From Nano to Macro. IEEE, 2009.
This class contains code inspired by StainTools
[https://github.com/Peter554/StainTools] written by Peter Byfield.
Args:
luminosity_threshold (float):
Threshold used for tissue area selection
angular_percentile (int):
Percentile of angular coordinates to be selected
with respect to the principle, orthogonal eigenvectors.
Examples:
>>> from tiatoolbox.tools.stainextract import MacenkoExtractor
>>> from tiatoolbox.utils import imread
>>> extractor = MacenkoExtractor()
>>> img = imread('path/to/image')
>>> stain_matrix = extractor.get_stain_matrix(img)
"""
def __init__(
self: MacenkoExtractor,
luminosity_threshold: float = 0.8,
angular_percentile: float = 99,
) -> None:
"""Initialize :class:`MacenkoExtractor`."""
self.__luminosity_threshold = luminosity_threshold
self.__angular_percentile = angular_percentile
[docs]
def get_stain_matrix(self: MacenkoExtractor, img: np.ndarray) -> np.ndarray:
"""Stain matrix estimation.
Args:
img (:class:`numpy.ndarray`):
Input image used for stain matrix estimation.
Returns:
:class:`numpy.ndarray`:
Estimated stain matrix.
"""
img = img.astype("uint8") # ensure input image is uint8
luminosity_threshold = self.__luminosity_threshold
angular_percentile = self.__angular_percentile
# convert to OD and ignore background
tissue_mask = get_luminosity_tissue_mask(
img,
threshold=luminosity_threshold,
).reshape((-1,))
img_od = rgb2od(img).reshape((-1, 3))
img_od = img_od[tissue_mask]
# eigenvectors of covariance in OD space (orthogonal as covariance symmetric)
_, eigen_vectors = np.linalg.eigh(np.cov(img_od, rowvar=False))
# the two principle eigenvectors
eigen_vectors = eigen_vectors[:, [2, 1]]
# make sure vectors are pointing the right way
eigen_vectors = vectors_in_correct_direction(e_vectors=eigen_vectors)
# project on this basis.
proj = np.dot(img_od, eigen_vectors)
# angular coordinates with respect to the principle, orthogonal eigenvectors
phi = np.arctan2(proj[:, 1], proj[:, 0])
# min and max angles
min_phi = np.percentile(phi, 100 - angular_percentile)
max_phi = np.percentile(phi, angular_percentile)
# the two principle colors
v1 = np.dot(eigen_vectors, np.array([np.cos(min_phi), np.sin(min_phi)]))
v2 = np.dot(eigen_vectors, np.array([np.cos(max_phi), np.sin(max_phi)]))
# order of H&E - H first row
he = h_and_e_in_right_order(v1, v2)
return he / np.linalg.norm(he, axis=1)[:, None]
[docs]
class VahadaneExtractor:
"""Vahadane stain extractor.
Get the stain matrix as defined in:
Vahadane, Abhishek, et al. "Structure-preserving color normalization
and sparse stain separation for histological images."
IEEE transactions on medical imaging 35.8 (2016): 1962-1971.
This class contains code inspired by StainTools
[https://github.com/Peter554/StainTools] written by Peter Byfield.
Args:
luminosity_threshold (float):
Threshold used for tissue area selection.
regularizer (float):
Regularizer used in dictionary learning.
Examples:
>>> from tiatoolbox.tools.stainextract import VahadaneExtractor
>>> from tiatoolbox.utils import imread
>>> extractor = VahadaneExtractor()
>>> img = imread('path/to/image')
>>> stain_matrix = extractor.get_stain_matrix(img)
"""
def __init__(
self: VahadaneExtractor,
luminosity_threshold: float = 0.8,
regularizer: float = 0.1,
) -> None:
"""Initialize :class:`VahadaneExtractor`."""
self.__luminosity_threshold = luminosity_threshold
self.__regularizer = regularizer
[docs]
def get_stain_matrix(self: VahadaneExtractor, img: np.ndarray) -> np.ndarray:
"""Stain matrix estimation.
Args:
img (:class:`numpy.ndarray`):
Input image used for stain matrix estimation
Returns:
:class:`numpy.ndarray`:
Estimated stain matrix.
"""
img = img.astype("uint8") # ensure input image is uint8
luminosity_threshold = self.__luminosity_threshold
regularizer = self.__regularizer
# convert to OD and ignore background
tissue_mask = get_luminosity_tissue_mask(
img,
threshold=luminosity_threshold,
).reshape((-1,))
img_od = rgb2od(img).reshape((-1, 3))
img_od = img_od[tissue_mask]
# do the dictionary learning
dl = DictionaryLearning(
n_components=2,
alpha=regularizer,
transform_alpha=regularizer,
fit_algorithm="lars",
transform_algorithm="lasso_lars",
positive_dict=True,
verbose=False,
max_iter=3,
transform_max_iter=1000,
)
dictionary = dl.fit_transform(X=img_od.T).T
# order H and E.
# H on first row.
dictionary = dl_output_for_h_and_e(dictionary)
return dictionary / np.linalg.norm(dictionary, axis=1)[:, None]