-
Notifications
You must be signed in to change notification settings - Fork 101
NEW: Add stainaugment feature for stain augmentation #181
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 16 commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
5183a45
[skip travis] NEW: Add stainaugment feature for stain augmentation
mostafajahanifar bfb3975
[skip travis] MAINT: add licence and docstring placeholders
mostafajahanifar 0adee96
[skip travis] MAINT: correct StainAugmentation misspelt
mostafajahanifar 7ee8424
[skip travis] UPD: corrected tools init
mostafajahanifar bbe0888
ENH: add albumentations and multiprocessing compatibility to stainaug
mostafajahanifar bfe1041
ENH: enhance the class to support faster stand-alone augmentation
mostafajahanifar 1aa1bad
[skip travis] ENH: improve class structure and functions
mostafajahanifar 150abd2
[skip travis] DOC: add docstrings
mostafajahanifar ae12b22
[skip travis] MAINT: correct for deepsource errors
mostafajahanifar 01881fa
[skip travis] MAINT: correct for deepsource errors
mostafajahanifar 7f55783
TST: add tests for StainAugmentation
mostafajahanifar a289083
TST: update the StainAugmentation tests
mostafajahanifar b029e1d
TST: improve coverage
mostafajahanifar e5506d8
Merge branch 'develop' of https://github.com/TIA-Lab/tiatoolbox into …
mostafajahanifar 57ca5c7
NULL: empty commit
mostafajahanifar 6ddac4d
TST: improve coverage
mostafajahanifar 9f2dd5a
Merge branch 'develop' of https://github.com/TIA-Lab/tiatoolbox into …
mostafajahanifar ff0e65c
MAINT: apply reviewers comments
mostafajahanifar 3727594
UPD: improve randomness in augmentation
mostafajahanifar 83295a9
DOC: add stainaugment to the usage.rst
mostafajahanifar 26dcf3f
[skip travis] MAINT: correct spacing for docstrings
mostafajahanifar File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ channels: | |
| - defaults | ||
| dependencies: | ||
| - python=3.7 | ||
| - albumentations | ||
| - Click | ||
| - cython | ||
| - defusedxml | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ channels: | |
| - pytorch | ||
| - defaults | ||
| dependencies: | ||
| - albumentations | ||
| - black | ||
| - Click | ||
| - cython | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| albumentations>0.5.0 | ||
| Click>=7.0 | ||
| defusedxml | ||
| glymur | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ channels: | |
| - defaults | ||
| dependencies: | ||
| - python=3.7 | ||
| - albumentations | ||
| - Click | ||
| - cython | ||
| - defusedxml | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| albumentations | ||
| black | ||
| bump2version==0.5.11 | ||
| Click>=7.0 | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| """Tests for stain augmentation code.""" | ||
|
|
||
| import pathlib | ||
|
|
||
| import albumentations as alb | ||
| import numpy as np | ||
| import pytest | ||
|
|
||
| from tiatoolbox.data import stainnorm_target | ||
| from tiatoolbox.tools.stainaugment import StainAugmentation | ||
| from tiatoolbox.tools.stainnorm import get_normaliser | ||
| from tiatoolbox.utils.misc import imread | ||
|
|
||
|
|
||
| def test_stainaugment(source_image, norm_vahadane): | ||
| """Test functionality of the StainAugmentation class.""" | ||
| source_img = imread(pathlib.Path(source_image)) | ||
| target_img = stainnorm_target() | ||
| vahadane_img = imread(pathlib.Path(norm_vahadane)) | ||
|
|
||
| # Test invalid method in the input | ||
| with pytest.raises(ValueError, match=r".*Unsupported stain extractor method.*"): | ||
| _ = StainAugmentation(method="mosi") | ||
mostafajahanifar marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| # 1. Testing without stain matrix. | ||
| # Test with Vahadane stain extractor | ||
| augmentor = StainAugmentation( | ||
mostafajahanifar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| method="macenko", sigma1=3.0, sigma2=3.0, augment_background=True | ||
| ) | ||
| augmentor.fit(source_img) | ||
| source_img_aug = augmentor.augment() | ||
| assert source_img_aug.dtype == source_img.dtype | ||
| assert np.shape(source_img_aug) == np.shape(source_img) | ||
| assert np.mean(np.absolute(source_img_aug / 255.0 - source_img / 255.0)) > 1e-2 | ||
|
|
||
| # 2. Testing with predefined stain matrix | ||
| # We first extract the stain matrix of the target image and try to augment the | ||
| # source image with respect to that image. | ||
| norm = get_normaliser("vahadane") | ||
| norm.fit(target_img) | ||
| target_stain_matrix = norm.stain_matrix_target | ||
|
|
||
| # Now we augment the the source image with sigma1=0, sigma2=0 to force the augmentor | ||
| # to act like a normalizer | ||
| augmentor = StainAugmentation( | ||
| method="vahadane", | ||
| stain_matrix=target_stain_matrix, | ||
| sigma1=0.0, | ||
| sigma2=0.0, | ||
| augment_background=False, | ||
| ) | ||
| augmentor.fit(source_img, threshold=0.8) | ||
| source_img_aug = augmentor.augment() | ||
| assert np.mean(np.absolute(vahadane_img / 255.0 - source_img_aug / 255.0)) < 1e-1 | ||
|
|
||
| # 3. Test in albumentation framework | ||
| # Using the same trick as before, augment the image with pre-defined stain matrix | ||
| # and sigma1,2 equal to 0. The output should be equal to stain normalized image. | ||
| aug_pipeline = alb.Compose( | ||
| [ | ||
| StainAugmentation( | ||
| method="vahadane", | ||
| stain_matrix=target_stain_matrix, | ||
| sigma1=0.0, | ||
| sigma2=0.0, | ||
| always_apply=True, | ||
| ) | ||
| ], | ||
| p=1, | ||
| ) | ||
| source_img_aug = aug_pipeline(image=source_img)["image"] | ||
| assert np.mean(np.absolute(vahadane_img / 255.0 - source_img_aug / 255.0)) < 1e-1 | ||
|
|
||
| # Test for albumentation helper functions | ||
| params = augmentor.get_transform_init_args_names() | ||
| augmentor.get_params_dependent_on_targets(params) | ||
| assert params == ( | ||
| "method", | ||
| "stain_matrix", | ||
| "sigma1", | ||
| "sigma2", | ||
| "augment_background", | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,214 @@ | ||
| # ***** 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, TIACentre, University of Warwick | ||
| # All rights reserved. | ||
| # ***** END GPL LICENSE BLOCK ***** | ||
|
|
||
| """Staing augmentation""" | ||
mostafajahanifar marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| import copy | ||
| import random | ||
|
|
||
| import numpy as np | ||
| from albumentations.core.transforms_interface import ImageOnlyTransform | ||
|
|
||
| from tiatoolbox.tools.stainnorm import get_normaliser | ||
| from tiatoolbox.utils.misc import get_luminosity_tissue_mask | ||
|
|
||
|
|
||
| class StainAugmentation(ImageOnlyTransform): | ||
| """Stain augmentation using predefined stain matrix or stain extraction methods. | ||
|
|
||
| This stain augmentation class can be used in 'albumentations' augmentation pipelines | ||
| as well as stand alone. There is an option to use predefined `stain_matrix` in the | ||
| input which enables the `StainAugmentation` to generate augmented images faster or | ||
| do stain normalization to a specific target `stain_matrix`. Having stain matrix | ||
| beforhand, we don't need to do dictionary learning for stain matrix extraction, | ||
| hence,speed up the stain augmentation/normalization process which makes it more | ||
| appropriate for one-the-fly stain augmentation/normalization. | ||
|
|
||
| Args: | ||
| method (str): The method to use for stain matrix and stain concentration | ||
| extraction. Can be either "vahadane" (default) or "macenko". | ||
| stain_matrix (:class:`numpy.ndarray`): Pre-extracted stain matrix of a target | ||
| image. This can be used for both on-the-fly stain normalization and faster | ||
| stain augmentation. User can use tools in `tiatoolbox.tools.stainextract` | ||
| to extract this information. If None (default), the stain matrix will be | ||
| automatically extracted using the method specified by user. | ||
| sigma1 (float): Controls the extent of the stain concentrations scale parameter | ||
| (`alpha` belonging to [1-sigma1, 1+sigma1] range). Default is 0.5. | ||
| sigma2 (float): Controls the extent of the stain concentrations shift parameter | ||
| (`beta` belonging to [-sigma2, sigma2] range). Default is 0.25. | ||
| augment_background (bool): Specifies whether to apply stain augmentation on the | ||
| background or not. Default is False, which indicates that only tissue region | ||
| will be stain augmented. | ||
| always_apply (False): For use with 'albumentations' pipeline. Please refer to | ||
| albumentations documentations for more information. | ||
| p (0.5): For use with 'albumentations' pipeline which specifies the probability | ||
| of using the augmentation in a 'albumentations' pipeline. . Please refer to | ||
| albumentations documentations for more information. | ||
| Attributes: | ||
mostafajahanifar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| stain_normaliser: Fitted stain normalization class. | ||
| stain_matrix (:class:`numpy.ndarray`): extracted stain matrix from the image | ||
| source_concentrations (:class:`numpy.ndarray`): extracted stain | ||
| concentrations from the input image. | ||
| n_stains (int): number of stain channels in the stain concentrations. | ||
| Expected to be 2 for H&E stained images. | ||
| tissue_mask (:class:`numpy.ndarray`): tissue region mask in the image. | ||
| Examples: | ||
| >>> '''Using the stain augmentor in the 'albumentations' pipeline''' | ||
| >>> from tiatoolbox.tools.stainaugment import StainAugmentaiton | ||
| >>> import albumentations as A | ||
| >>> # Defining an examplar stain matrix as refrence | ||
| >>> stain_matrix = np.array([[0.91633014, -0.20408072, -0.34451435], | ||
| ... [0.17669817, 0.92528011, 0.33561059]]) | ||
| >>> # Define albumentations pipeline | ||
| >>> aug_pipline = A.Compose([ | ||
| ... A.RandomRotate90(), | ||
| ... A.Flip(), | ||
| ... StainAugmentaiton(stain_matrix=stain_matrix) | ||
| ... ]) | ||
| >>> # apply the albumentations pipeline on an image (RGB numpy unit8 type) | ||
| >>> img_aug = aug(image=img)['image'] | ||
|
|
||
| >>> '''Using the stain augmentor stand alone''' | ||
| >>> from tiatoolbox.tools.stainaugment import StainAugmentaiton | ||
| >>> # Defining an examplar stain matrix as refrence | ||
| >>> stain_matrix = np.array([[0.91633014, -0.20408072, -0.34451435], | ||
| ... [0.17669817, 0.92528011, 0.33561059]]) | ||
| >>> # Instantiate the stain augmentor and fit it on an image | ||
| >>> stain_augmentor = StainAugmentation(stain_matrix=stain_matrix) | ||
| >>> stain_augmentor.fit(img) | ||
| >>> # Now using the fitted `stain_augmentor` in a loop to generate | ||
| >>> # several augmented instances from the same image. | ||
| >>> for i in range(10): | ||
| ... img_aug = stain_augmentor.augment() | ||
| """ | ||
mostafajahanifar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| def __init__( | ||
| self, | ||
| method: str = "vahadane", | ||
| stain_matrix: np.ndarray = None, | ||
| sigma1: float = 0.5, | ||
| sigma2: float = 0.25, | ||
| augment_background: bool = False, | ||
| always_apply=False, | ||
| p=0.5, | ||
| ) -> np.ndarray: | ||
| super().__init__(always_apply=always_apply, p=p) | ||
|
|
||
| self.augment_background = augment_background | ||
| self.sigma1 = sigma1 | ||
| self.sigma2 = sigma2 | ||
| self.method = method | ||
| self.stain_matrix = stain_matrix | ||
|
|
||
| if self.method.lower() not in {"macenko", "vahadane"}: | ||
| raise ValueError( | ||
| f"Unsupported stain extractor method '{self.method}' for " | ||
| "StainAugmentation. Choose either 'vahadane' or 'macenko'." | ||
| ) | ||
| self.stain_normaliser = get_normaliser(self.method.lower()) | ||
|
|
||
| self.alpha = None | ||
| self.beta = None | ||
| self.img_shape = None | ||
| self.tissue_mask = None | ||
| self.n_stains = None | ||
| self.source_concentrations = None | ||
|
|
||
| def fit(self, img, threshold=0.85): | ||
| """Fit function to extract information needed for stain augmentation. | ||
|
|
||
| The `fit` function uses either 'Macenko' or 'Vahadane' stain extraction methods | ||
| to extract stain matrix and stain concentrations of the input image to be used | ||
| in the `augment` function. | ||
|
|
||
| Args: | ||
| img (:class:`numpy.ndarray`): RGB image in the form of uint8 numpy array. | ||
| threshold (float): The threshold value used to find tissue mask from the | ||
| luminosity component of the image. The found `tissue_mask` will be used | ||
| to filter out background area in stain augmentation process upon user | ||
| setting `augment_background=False`. | ||
| """ | ||
mostafajahanifar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if self.stain_matrix is None: | ||
| self.stain_normaliser.fit(img) | ||
| self.stain_matrix = self.stain_normaliser.stain_matrix_target | ||
| self.source_concentrations = self.stain_normaliser.target_concentrations | ||
| else: | ||
| self.source_concentrations = self.stain_normaliser.get_concentrations( | ||
| img, self.stain_matrix | ||
| ) | ||
| self.n_stains = self.source_concentrations.shape[1] | ||
| self.tissue_mask = get_luminosity_tissue_mask(img, threshold=threshold).ravel() | ||
| self.img_shape = img.shape | ||
|
|
||
| def augment(self): | ||
| """Return an augmented instance based on source stain concentrations. | ||
|
|
||
| Stain concentrations of the source image are altered (scaled and shifted) | ||
| based on the random alpha and beta paramters, and then an augmented image is | ||
| reconstructed from the altered concentrations. | ||
| All parameters needed for this part are calculated when calling fit() function. | ||
|
|
||
| Returns: | ||
| img_augmented (:class:`numpy.ndarray`): stain augmented image. | ||
| """ | ||
mostafajahanifar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| self.get_params() | ||
| augmented_concentrations = copy.deepcopy(self.source_concentrations) | ||
| for i in range(self.n_stains): | ||
| 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 | ||
| img_augmented = 255 * np.exp( | ||
| -1 * np.dot(augmented_concentrations, self.stain_matrix) | ||
| ) | ||
| img_augmented = img_augmented.reshape(self.img_shape) | ||
| img_augmented = np.clip(img_augmented, 0, 255) | ||
| return np.uint8(img_augmented) | ||
|
|
||
| def apply(self, img, **params): # alpha=None, beta=None, | ||
| """Call the `fit` and `augment` functions to generate an stain augmented image. | ||
|
|
||
| Args: | ||
| img (:class:`numpy.ndarray`): Input RGB image in the form of unit8 numpy | ||
| array. | ||
| Returns: | ||
| :class:`numpy.ndarray`: Stain augmented image with the same | ||
| size and format as the input img. | ||
| """ | ||
| self.fit(img, threshold=0.85) | ||
| return self.augment() | ||
|
|
||
| def get_params(self): | ||
| """Returns randomly generated parameters based on input arguments.""" | ||
| self.alpha = random.uniform(1 - self.sigma1, 1 + self.sigma1) | ||
| self.beta = random.uniform(-self.sigma2, self.sigma2) | ||
| return {} | ||
|
|
||
| def get_params_dependent_on_targets(self, params): | ||
| """Does nothing, added to resolve flake 8 error""" | ||
| return {} | ||
|
|
||
| def get_transform_init_args_names(self): | ||
| """Return the argument names for albumentations use.""" | ||
| return ("method", "stain_matrix", "sigma1", "sigma2", "augment_background") | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.