Source code for tiatoolbox.tools.stainaugment

"""Stain augmentation."""

from __future__ import annotations

import copy

import numpy as np

from tiatoolbox.tools.stainnorm import get_normalizer
from tiatoolbox.utils.misc import get_luminosity_tissue_mask


[docs] class StainAugmentor: """Stain augmentation using predefined stain matrices or stain extraction methods. This class performs stain augmentation on RGB histology images by modifying their stain concentration values. It supports two stain extraction methods: *Vahadane* and *Macenko*. The augmentation can operate either by extracting a stain matrix from the input image or by using a user-provided stain matrix. Providing a pre-extracted `stain_matrix` allows faster augmentation and enables the augmentor to behave like a stain normalizer when the augmentation parameters (`sigma1`, `sigma2`) are set to zero. This avoids the need for dictionary learning during stain matrix extraction and is suitable for efficient on-the-fly stain augmentation or normalization. Args: method (str): Stain extraction method to use. Supported values are `"vahadane"` (default) and `"macenko"`. stain_matrix (numpy.ndarray): Optional pre-extracted stain matrix for a target image. When supplied, the augmentor uses this matrix directly for stain normalization or faster augmentation. If `None`, the stain matrix is extracted from the input image using the selected method. sigma1 (float): Controls the scaling of stain concentrations. The scale factor `alpha` is sampled from the range `[1 - sigma1, 1 + sigma1]`. Default is `0.4`. sigma2 (float): Controls the additive shift of stain concentrations. The shift `beta` is sampled from the range `[-sigma2, sigma2]`. Default is `0.2`. augment_background (bool): Whether to apply stain augmentation to background pixels. If `False` (default), augmentation is applied only to tissue regions determined using a luminosity-based tissue mask. p (float): Probability of applying stain augmentation. If a random draw exceeds `p`, the input image is returned unchanged. This provides a simple way to control how often augmentation occurs during training. Attributes: stain_normalizer: Internal stain normalization object used for matrix and concentration extraction. stain_matrix (numpy.ndarray): Extracted or user-provided stain matrix. source_concentrations (numpy.ndarray): Stain concentration values extracted from the input image. n_stains (int): Number of stain channels in the concentration matrix. Typically `2` for H&E images. tissue_mask (numpy.ndarray): Boolean mask indicating tissue regions when background augmentation is disabled. Examples: >>> from tiatoolbox.tools.stainaugment import StainAugmentor >>> import numpy as np >>> >>> stain_matrix = np.array([ ... [0.91633014, -0.20408072, -0.34451435], ... [0.17669817, 0.92528011, 0.33561059], ... ]) >>> >>> augmentor = StainAugmentor(stain_matrix=stain_matrix, p=0.5) >>> augmentor.fit(img) >>> img_aug = augmentor.augment() """ def __init__( self, method: str = "vahadane", stain_matrix: np.ndarray | None = None, sigma1: float = 0.4, sigma2: float = 0.2, p: float = 1.0, *, augment_background: bool = False, ) -> None: """Initialize StainAugmentor object.""" self.method = method.lower() if self.method not in {"macenko", "vahadane"}: msg = ( f"Unsupported stain extractor method {self.method!r}. " f"Choose either 'vahadane' or 'macenko'." ) raise ValueError( msg, ) self.stain_matrix = stain_matrix self.sigma1 = sigma1 self.sigma2 = sigma2 self.augment_background = augment_background self.p = p self.stain_normalizer = get_normalizer(self.method) self.rng = np.random.default_rng() # Populated during fit() self.alpha: float self.beta: float self.img_shape: tuple[int, ...] self.tissue_mask: np.ndarray self.n_stains: int self.source_concentrations: np.ndarray self._original_img: np.ndarray
[docs] def fit(self, img: np.ndarray, threshold: float = 0.85) -> None: """Extract stain matrix and concentrations from the input image.""" self._original_img = img # store original image for probability logic if self.stain_matrix is None: self.stain_normalizer.fit(img) self.stain_matrix = self.stain_normalizer.stain_matrix_target self.source_concentrations = self.stain_normalizer.target_concentrations else: self.source_concentrations = self.stain_normalizer.get_concentrations( img, self.stain_matrix ) self.n_stains = self.source_concentrations.shape[1] if not self.augment_background: self.tissue_mask = get_luminosity_tissue_mask( img, threshold=threshold ).ravel() self.img_shape = img.shape
[docs] def augment(self) -> np.ndarray: """Generate a stain-augmented image, applied with probability p.""" if self.rng.random() > self.p: return self._original_img augmented_concentrations = copy.deepcopy(self.source_concentrations) for i in range(self.n_stains): self._sample_params() if self.augment_background: augmented_concentrations[:, i] *= self.alpha augmented_concentrations[:, i] += self.beta else: augmented_concentrations[self.tissue_mask, i] *= self.alpha augmented_concentrations[self.tissue_mask, i] += self.beta self.stain_matrix = np.asarray(self.stain_matrix) img_augmented = 255 * np.exp( -np.dot(augmented_concentrations, self.stain_matrix) ) img_augmented = img_augmented.reshape(self.img_shape) img_augmented = np.clip(img_augmented, 0, 255) return img_augmented.astype(np.uint8)
[docs] def __call__(self, img: np.ndarray) -> np.ndarray: """Convenience wrapper: fit + augment.""" self.fit(img) return self.augment()
def _sample_params(self) -> None: """Generate random alpha/beta parameters.""" rng = np.random.default_rng() self.alpha = rng.uniform(1 - self.sigma1, 1 + self.sigma1) self.beta = rng.uniform(-self.sigma2, self.sigma2)