Source code for tiatoolbox.tools.stainextract

# ***** BEGIN GPL LICENSE BLOCK *****
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# This file contains code inspired by StainTools
# [https://github.com/Peter554/StainTools] written by Peter Byfield.
#
# The Original Code is Copyright (C) 2021, TIA Centre, University of Warwick
# All rights reserved.
# ***** END GPL LICENSE BLOCK *****

"""Stain matrix extraction for stain normalization."""
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): """Points the eigen vectors in the right direction. Args: e_vectors (:class:`numpy.ndarray`): eigen vectors Returns: 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, v2): """Rearranges 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: input vectors in the correct order. """ if v1[0] > v2[0]: he = np.array([v1, v2]) else: he = np.array([v2, v1]) return he
[docs]def dl_output_for_h_and_e(dictionary): """Return correct value for H and E from dictionary learning output. Args: dictionary (:class:`numpy.ndarray`): :class:`sklearn.decomposition.DictionaryLearning` output Returns: ndarray with correct values for H and E """ if dictionary[0, 0] < dictionary[1, 0]: dictionary = 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.misc import imread >>> extractor = CustomExtractor(stain_matrix) >>> img = imread('path/to/image') >>> stain_matrix = extractor.get_stain_matrix(img) """ def __init__(self, stain_matrix): self.stain_matrix = stain_matrix if self.stain_matrix.shape not in [(2, 3), (3, 3)]: raise ValueError("Stain matrix must be either (2,3) or (3,3)")
[docs] def get_stain_matrix(self, _): """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.misc import imread >>> extractor = RuifrokExtractor() >>> img = imread('path/to/image') >>> stain_matrix = extractor.get_stain_matrix(img) """
[docs] @staticmethod def get_stain_matrix(_): """Get the pre-defined stain matrix. Returns: ndarray: pre-defined stain matrix. """ return np.array([[0.65, 0.70, 0.29], [0.07, 0.99, 0.11]])
[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. Examples: >>> from tiatoolbox.tools.stainextract import MacenkoExtractor() >>> from tiatoolbox.utils.misc import imread >>> extractor = MacenkoExtractor() >>> img = imread('path/to/image') >>> stain_matrix = extractor.get_stain_matrix(img) """
[docs] @staticmethod def get_stain_matrix(img, luminosity_threshold=0.8, angular_percentile=99): """Stain matrix estimation. Args: img (:class:`numpy.ndarray`): input image used for stain matrix estimation luminosity_threshold (float): threshold used for tissue area selection angular_percentile (int): Returns: :class:`numpy.ndarray`: estimated stain matrix. """ img = img.astype("uint8") # ensure input image is uint8 # 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 cov in OD space (orthogonal as cov symmetric) _, e_vects = np.linalg.eigh(np.cov(img_od, rowvar=False)) # the two principle eigenvectors e_vects = e_vects[:, [2, 1]] # make sure vectors are pointing the right way e_vects = vectors_in_correct_direction(e_vectors=e_vects) # project on this basis. proj = np.dot(img_od, e_vects) # angular coordinates with repect to the prinicple, 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(e_vects, np.array([np.cos(min_phi), np.sin(min_phi)])) v2 = np.dot(e_vects, 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) normalized_rows = he / np.linalg.norm(he, axis=1)[:, None] return normalized_rows
[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. Examples: >>> from tiatoolbox.tools.stainextract import VahadaneExtractor() >>> from tiatoolbox.utils.misc import imread >>> extractor = VahadaneExtractor() >>> img = imread('path/to/image') >>> stain_matrix = extractor.get_stain_matrix(img) """
[docs] @staticmethod def get_stain_matrix(img, luminosity_threshold=0.8, regulariser=0.1): """Stain matrix estimation. Args: img (:class:`numpy.ndarray`): input image used for stain matrix estimation luminosity_threshold (float): threshold used for tissue area selection regulariser (float): regulariser used in dictionary learning Returns: :class:`numpy.ndarray`: estimated stain matrix. """ img = img.astype("uint8") # ensure input image is uint8 # 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=regulariser, transform_alpha=regulariser, 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) normalized_rows = dictionary / np.linalg.norm(dictionary, axis=1)[:, None] return normalized_rows