Skip to content
This repository has been archived by the owner on May 8, 2021. It is now read-only.

Commit

Permalink
tidying
Browse files Browse the repository at this point in the history
  • Loading branch information
Peter554 committed Aug 1, 2018
1 parent 0a1cfcc commit c287ac8
Show file tree
Hide file tree
Showing 28 changed files with 1,121 additions and 960 deletions.
26 changes: 0 additions & 26 deletions .travis.yml

This file was deleted.

33 changes: 24 additions & 9 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,36 @@ StainTools

Tools for tissue image stain normalization and augmentation in Python (tested on 3.5).

Latest build:

.. image:: https://travis-ci.org/Peter554/StainTools.svg?branch=master
:target: https://travis-ci.org/Peter554/StainTools


Install
========

``pip install staintools``

**NOTE:** StainTools requires the SPAMS (SPArse Modeling Software) package. Please find out about this `here <http://spams-devel.gforge.inria.fr>`__. This may be installed via conda. For example, see `here <https://github.com/conda-forge/python-spams-feedstock>`__.

Example usage
===============
Docs
====

Histology images are often stained with the Hematoxylin & Eosin (H&E) stains. These two chemicals typically stain: the nuclei a dark purple (Hematoxylin) and the cytoplasm a light pink (Eosin). Thus all pixels in a histology image are principally composed of two colors. These stain colors vary from image to image and may be summarised in a stain matrix:

.. image:: readme_pics/stain_matrix.png
:height: 200px



This package may be broken down as:

**Stain Extraction**

A stain extractor provides methods for estimating a stain matrix and a concentration matrix given an image. We implement:

- Macenko stain extractor. Stain matrix estimation via method of *M. Macenko et al.,“A method for normalizing histology slides for quantitative analysis,”*. This method considers the projection of pixels onto the 2D plane defined by the two principle eigenvectors of the optical density covariance matrix. It then considers the extreme directions (in terms of angular polar coordinate) in this plane. See the paper for details.

- Vahadane stain extractor. Stain matrix estimation via method of *A. Vahadane et al., “Structure-Preserving Color Normalization and Sparse Stain Separation for Histological Images,”*. This method takes a dictionary learning based approach to find the two basis stains that best fit the image. See the paper for details.

**Stain Normalizer**

A stain normalizer uses a stain extractor to transform one image to the staining of another target image.

Please see demo.ipynb `here <https://github.com/Peter554/StainTools/blob/master/demo.ipynb>`__.
For further examples of usage please see the demo notebooks (which serve also as tests by inspection).

Binary file modified data/i3.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified data/i4.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified data/i5.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed data/i6.png
Binary file not shown.
Binary file removed data/i7.png
Binary file not shown.
Binary file removed data/i8.png
Binary file not shown.
Binary file removed data/i9.png
Binary file not shown.
769 changes: 0 additions & 769 deletions demo.ipynb

This file was deleted.

135 changes: 135 additions & 0 deletions demo_brightness_standardizer.ipynb

Large diffs are not rendered by default.

259 changes: 259 additions & 0 deletions demo_macenko_stain_normalizer.ipynb

Large diffs are not rendered by default.

235 changes: 235 additions & 0 deletions demo_reinhard_color_normalizer.ipynb

Large diffs are not rendered by default.

104 changes: 104 additions & 0 deletions demo_stain_augmentor.ipynb

Large diffs are not rendered by default.

259 changes: 259 additions & 0 deletions demo_vahadane_stain_normalizer.ipynb

Large diffs are not rendered by default.

Empty file added make_readme_pics.py
Empty file.
Binary file added readme_pics/stain_matrix.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@

setup(
name='staintools',
version='0.1.1',
description='A package for stain stain_normalization, augmentation and more.',
version='0.9.9',
description='A package for tissue image stain normalization, augmentation and more.',
long_description=readme,
author='Peter Byfield',
author_email='byfield554@gmail.com',
url='https://github.com/Peter554/StainTools',
packages=find_packages(exclude=('tests', 'docs')),
packages=find_packages(exclude=('tests')),
install_requires=['numpy',
'opencv-python',
'matplotlib',
Expand Down
15 changes: 10 additions & 5 deletions staintools/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from . import standardization
from . import utils
from .stain_extractors.macenko_stain_extractor import MacenkoStainExtractor
from .stain_extractors.vahadane_stain_extractor import VahadaneStainExtractor

# For convenience
from staintools.reinhard_color_normalizer import ReinhardNormalizer
from staintools.utils.brightness_standardizer import BrightnessStandardizer
from .stain_normalizer import StainNormalizer
from .stain_augmentor import StainAugmentor

from .reinhard_color_normalizer import ReinhardColorNormalizer

from .utils.brightness_standardizer import BrightnessStandardizer
from .utils.visualization_utils import read_image, plot_image, \
plot_row_colors, make_image_stack, plot_image_stack
9 changes: 4 additions & 5 deletions staintools/reinhard_color_normalizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,17 @@
import numpy as np
import cv2 as cv

from staintools.stain_normalizer import Normaliser
from staintools.utils.misc_utils import is_uint8_image


class ReinhardNormalizer(Normaliser):
class ReinhardColorNormalizer(object):
"""
Normalize a patch stain to the target image using the method of:
E. Reinhard, M. Adhikhmin, B. Gooch, and P. Shirley, ‘Color transfer between images’, IEEE Computer Graphics and Applications, vol. 21, no. 5, pp. 34–41, Sep. 2001.
Normalize a patch color to the target image using the method of:
E. Reinhard, M. Adhikhmin, B. Gooch, and P. Shirley,
‘Color transfer between images’, IEEE Computer Graphics and Applications, vol. 21, no. 5, pp. 34–41, Sep. 2001.
"""

def __init__(self):
super().__init__()
self.target_means = None
self.target_stds = None

Expand Down
90 changes: 41 additions & 49 deletions staintools/stain_augmentor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,65 +3,57 @@
import numpy as np
import copy

from staintools.stain_extractors.ruifrok_johnston_stain_extractor import RuifrokJohnstonStainExtractor
from staintools.stain_extractors.macenko_stain_extractor import MacenkoStainExtractor
from staintools.stain_extractors.vahadane_stain_extractor import VahadaneStainExtractor
from staintools.utils.misc_utils import get_luminosity_mask


class Augmentor(object):
class StainAugmentor(object):

def __init__(self, method, sigma1, sigma2):
if method.lower() == 'rj':
self.extractor = RuifrokJohnstonStainExtractor
elif method.lower() == 'macenko':
def __init__(self, method, sigma1=0.2, sigma2=0.2, include_background=True):
if method.lower() == 'macenko':
self.extractor = MacenkoStainExtractor
elif method.lower() == 'vahadane':
self.extractor = VahadaneStainExtractor
else:
raise Exception('Method not recognized.')
self.sigma1 = sigma1
self.sigma2 = sigma2


def fit(self, I):
"""
Fit the augmentor to an image I.
:param I:
:return:
"""
self.Ishape = I.shape
self.tissue_mask = get_luminosity_mask(I).ravel()
self.stain_matrix, self.source_concentrations = self.fetcher.compute(I)


def augment(self, new_stain_mat=False, include_background=False):
"""
Return augmented image.
Optionally returns new stain matrix
:param new_stain_mat; type bool, if True computes & returns new stain matrix
:param include_background:
"""
channels = self.source_concentrations.shape[1]
source_concentrations = copy.deepcopy(self.source_concentrations)

for i in range(channels):
alpha = np.random.uniform(1 - self.sigma1, 1 + self.sigma1)
beta = np.random.uniform(-self.sigma2, self.sigma2)
if include_background:
source_concentrations[:, i] *= alpha
source_concentrations[:, i] += beta
else:
source_concentrations[self.not_white, i] *= alpha
source_concentrations[self.not_white, i] += beta

I_prime = np.clip((255 * np.exp(-1 * np.dot(source_concentrations, self.stain_matrix).reshape(self.Ishape))), 0,
255).astype(np.uint8)

if new_stain_mat:
stain_matrix = self.fetcher.compute(I_prime, just_stain=True)
return I_prime, stain_matrix
else:
return I_prime
self.include_background = include_background

def fit(self, I):
"""
Fit to an image I.
:param I:
:return:
"""
self.image_shape = I.shape
self.stain_matrix = self.extractor.get_stain_matrix(I)
self.source_concentrations = self.extractor.get_concentrations(I, self.stain_matrix)
self.n_stains = self.source_concentrations.shape[1]
self.tissue_mask = get_luminosity_mask(I).ravel()

def transform(self):
"""
Transform to produce an augmented version of the fitted image.
:return:
"""
augmented_concentrations = copy.deepcopy(self.source_concentrations)

for i in range(self.n_stains):
alpha = np.random.uniform(1 - self.sigma1, 1 + self.sigma1)
beta = np.random.uniform(-self.sigma2, self.sigma2)
if self.include_background:
augmented_concentrations[:, i] *= alpha
augmented_concentrations[:, i] += beta
else:
augmented_concentrations[self.tissue_mask, i] *= alpha
augmented_concentrations[self.tissue_mask, i] += beta

I_augmented = 255 * np.exp(-1 * np.dot(augmented_concentrations, self.stain_matrix))
I_augmented = I_augmented.reshape(self.image_shape)
I_augmented = np.clip(I_augmented, 0, 255)

return I_augmented
32 changes: 7 additions & 25 deletions staintools/stain_extractors/abc_stain_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,15 @@ class StainExtractor(ABC):

@abstractmethod
def get_stain_matrix(self, I, *args):
"""Estimate stain matrix given an image and relevant method parameters"""
"""
Estimate stain matrix given an image and relevant method parameters
"""

@staticmethod
def get_concentrations(I, stain_matrix, **kwargs):
"""
Estimate concentration matrix given an image, stain matrix and relevant method parameters
Get the concentration matrix. Suppose the input image is H x W x 3 (uint8). Define Npix = H * W.
Then the concentration matrix is Npix x 2 (or we could reshape to H x W x 2).
The first element of each row is the Hematoxylin concentration.
The second element of each row is the Eosin concentration.
We do this by 'solving' OD = C*S (Matrix product) where OD is optical density (Npix x 3),\
C is concentration (Npix x 2) and S is stain matrix (2 x 3).
See docs for spams.lasso.
We restrict the concentrations to be positive and penalise very large concentration values,\
so that background pixels (which can not easily be expressed in the Hematoxylin-Eosin basis) have \
low concentration and thus appear white.
Estimate concentration matrix given an image, stain matrix and relevant method parameters.
"""
n_stains = stain_matrix.shape[0]
if n_stains == 2:
OD = convert_RGB_to_OD(I).reshape((-1, 3))
lasso_regularizer = kwargs['lasso_regularizer'] if 'lasso_regularizer' in kwargs.keys() else 0.01
return spams.lasso(X=OD.T, D=stain_matrix.T, mode=2, lambda1=lasso_regularizer, pos=True).toarray().T
elif n_stains == 3:
OD = convert_RGB_to_OD(I).reshape((-1, 3))
return np.linalg.solve(stain_matrix.T, OD.T).T
else:
raise Exception("Number of stains must be 2 or 3.")
OD = convert_RGB_to_OD(I).reshape((-1, 3))
lasso_regularizer = kwargs['lasso_regularizer'] if 'lasso_regularizer' in kwargs.keys() else 0.01
return spams.lasso(X=OD.T, D=stain_matrix.T, mode=2, lambda1=lasso_regularizer, pos=True).toarray().T
9 changes: 5 additions & 4 deletions staintools/stain_extractors/macenko_stain_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,19 @@ class MacenkoStainExtractor(StainExtractor):
@staticmethod
def get_stain_matrix(I, luminosity_threshold=0.8, angular_percentile=99):
"""
Get the stain matrix (2x3). First row H and second row E.
See the original paper for details.
Stain matrix estimation via method of:
M. Macenko et al.,
“A method for normalizing histology slides for quantitative analysis,”
:param I: Image RGB uint8.
:param luminosity_threshold:
:param angular_percentile:
:return:
"""
# convert to OD and ignore background
mask = get_luminosity_mask(I, threshold=luminosity_threshold).reshape((-1,))
tissue_mask = get_luminosity_mask(I, threshold=luminosity_threshold).reshape((-1,))
OD = convert_RGB_to_OD(I).reshape((-1, 3))
OD = OD[mask]
OD = OD[tissue_mask]

# eigenvectors of cov in OD space (orthogonal as cov symmetric)
_, V = np.linalg.eigh(np.cov(OD, rowvar=False))
Expand Down
24 changes: 0 additions & 24 deletions staintools/stain_extractors/ruifrok_johnston_stain_extractor.py

This file was deleted.

10 changes: 5 additions & 5 deletions staintools/stain_extractors/vahadane_stain_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,19 @@ class VahadaneStainExtractor(StainExtractor):
@staticmethod
def get_stain_matrix(I, luminosity_threshold=0.8, dictionary_regularizer=0.1):
"""
Get the 2x3 stain matrix. First row H and second row E.
See the original paper for details.
Also see spams docs.
Stain matrix estimation via method of:
A. Vahadane et al.,
“Structure-Preserving Color Normalization and Sparse Stain Separation for Histological Images,”
:param I: Image RGB uint8.
:param luminosity_threshold:
:param dictionary_regularizer:
:return:
"""
# convert to OD and ignore background
mask = get_luminosity_mask(I, threshold=luminosity_threshold).reshape((-1,))
tissue_mask = get_luminosity_mask(I, threshold=luminosity_threshold).reshape((-1,))
OD = convert_RGB_to_OD(I).reshape((-1, 3))
OD = OD[mask]
OD = OD[tissue_mask]

# do the dictionary learning
dictionary = spams.trainDL(X=OD.T, K=2, lambda1=dictionary_regularizer, mode=2,
Expand Down
21 changes: 1 addition & 20 deletions staintools/stain_normalizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import numpy as np

from staintools.stain_extractors.ruifrok_johnston_stain_extractor import RuifrokJohnstonStainExtractor
from staintools.stain_extractors.macenko_stain_extractor import MacenkoStainExtractor
from staintools.stain_extractors.vahadane_stain_extractor import VahadaneStainExtractor
from staintools.utils.misc_utils import convert_OD_to_RGB
Expand All @@ -11,9 +10,7 @@
class StainNormalizer(object):

def __init__(self, method):
if method.lower() == 'rj':
self.extractor = RuifrokJohnstonStainExtractor
elif method.lower() == 'macenko':
if method.lower() == 'macenko':
self.extractor = MacenkoStainExtractor
elif method.lower() == 'vahadane':
self.extractor = VahadaneStainExtractor
Expand Down Expand Up @@ -41,23 +38,7 @@ def transform(self, I):
"""
stain_matrix_source = self.extractor.get_stain_matrix(I)
source_concentrations = self.extractor.get_concentrations(I, stain_matrix_source)
assert stain_matrix_source.min() >= 0, "Stain matrix has negative values."
assert source_concentrations.min() >= 0, "Concentration matrix has negative values."
maxC_source = np.percentile(source_concentrations, 99, axis=0).reshape((1, 2))
source_concentrations *= (self.maxC_target / maxC_source)
tmp = 255 * np.exp(-1 * np.dot(source_concentrations, self.stain_matrix_target))
return tmp.reshape(I.shape).astype(np.uint8)

def get_hematoxylin(self, I):
"""
Hematoxylin channel extraction.
:param I: Image RGB uint8.
:return:
"""
h, w, c = I.shape
stain_matrix_source = self.extractor.get_stain_matrix(I)
source_concentrations = self.extractor.get_concentrations(I, stain_matrix_source)
H = source_concentrations[:, 0].reshape(h, w)
H = np.exp(-1 * H)
return H

0 comments on commit c287ac8

Please sign in to comment.