Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 8 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
GPU-accelerated stain normalization tools for histopathological images. Compatible with PyTorch, TensorFlow, and Numpy.
Normalization algorithms currently implemented:

- Macenko [\[1\]](#reference) (ported from [numpy implementation](https://github.com/schaugf/HEnorm_python))
- Reinhard [\[2\]](#reference)
- Modified Reinhard [\[3\]](#reference)
| Algorithm | numpy | torch | tensorflow |
|-|-|-|-|
| Macenko [\[1\]](#reference) | ✓ | ✓ | ✓ |
| Reinhard [\[2\]](#reference) | ✓ | ✓ | ✓ |
| Modified Reinhard [\[3\]](#reference) | ✓ | ✓ | ✓ |

## Installation

Expand Down Expand Up @@ -45,17 +47,9 @@ norm, H, E = normalizer.normalize(I=t_to_transform, stains=True)

![alt text](data/result.png)

## Implemented algorithms

| Algorithm | numpy | torch | tensorflow |
|-|-|-|-|
| Macenko | ✓ | ✓ | ✓ |
| Reinhard | ✓ | ✓ | ✓ |
| Modified Reinhard | ✓ | ✓ | ✓ |

## Backend comparison

Results with 10 runs per size on a Intel(R) Core(TM) i5-8365U CPU @ 1.60GHz
Macenko runtime results using different backends with 10 runs per image size on a Intel(R) Core(TM) i5-8365U CPU @ 1.60GHz.

| size | numpy avg. time | torch avg. time | tf avg. time |
|--------|-------------------|-------------------|------------------|
Expand All @@ -72,16 +66,15 @@ Results with 10 runs per size on a Intel(R) Core(TM) i5-8365U CPU @ 1.60GHz

- [1] 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.
- [2] Reinhard, Erik et al. "Color transfer between images." IEEE Computer Graphics and Applications. IEEE, 2001.
- [3] Roy, Santanu et al. "Modified Reinhard Algorithm for Color Normalization of Colorectal Cancer Histopathology Images". 2021 29th European Signal Processing Conference (EUSIPCO), IEEE, 2021.
- [3] Roy, Santanu et al. "Modified Reinhard Algorithm for Color Normalization of Colorectal Cancer Histopathology Images". 2021 29th European Signal Processing Conference (EUSIPCO). IEEE, 2021.

## Citing

If you find this software useful for your research, please cite it as:

```bibtex
@software{barbano2022torchstain,
author = {Carlo Alberto Barbano and
André Pedersen},
author = {Carlo Alberto Barbano and André Pedersen},
title = {EIDOSLAB/torchstain: v1.2.0-stable},
month = aug,
year = 2022,
Expand Down
1 change: 0 additions & 1 deletion compare.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import torch
from torchvision import transforms
import torchstain

import cv2
import matplotlib.pyplot as plt
import time
Expand Down
1 change: 0 additions & 1 deletion example.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from torchvision import transforms
import time


size = 1024
target = cv2.resize(cv2.cvtColor(cv2.imread("./data/target.png"), cv2.COLOR_BGR2RGB), (size, size))
to_transform = cv2.resize(cv2.cvtColor(cv2.imread("./data/source.png"), cv2.COLOR_BGR2RGB), (size, size))
Expand Down
32 changes: 16 additions & 16 deletions torchstain/numpy/normalizers/macenko.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ def __init__(self):
super().__init__()

self.HERef = np.array([[0.5626, 0.2159],
[0.7201, 0.8012],
[0.4062, 0.5581]])
[0.7201, 0.8012],
[0.4062, 0.5581]])
self.maxCRef = np.array([1.9705, 1.0308])

def __convert_rgb2od(self, I, Io=240, beta=0.15):
# calculate optical density
OD = -np.log((I.astype(float)+1)/Io)
OD = -np.log((I.astype(float) + 1) / Io)

# remove transparent pixels
ODhat = OD[~np.any(OD < beta, axis=1)]
Expand All @@ -26,22 +26,22 @@ def __convert_rgb2od(self, I, Io=240, beta=0.15):
def __find_HE(self, ODhat, eigvecs, alpha):
#project on the plane spanned by the eigenvectors corresponding to the two
# largest eigenvalues
That = ODhat.dot(eigvecs[:,1:3])
That = ODhat.dot(eigvecs[:, 1:3])

phi = np.arctan2(That[:,1],That[:,0])
phi = np.arctan2(That[:, 1], That[:, 0])

minPhi = np.percentile(phi, alpha)
maxPhi = np.percentile(phi, 100-alpha)
maxPhi = np.percentile(phi, 100 - alpha)

vMin = eigvecs[:,1:3].dot(np.array([(np.cos(minPhi), np.sin(minPhi))]).T)
vMax = eigvecs[:,1:3].dot(np.array([(np.cos(maxPhi), np.sin(maxPhi))]).T)
vMin = eigvecs[:, 1:3].dot(np.array([(np.cos(minPhi), np.sin(minPhi))]).T)
vMax = eigvecs[:, 1:3].dot(np.array([(np.cos(maxPhi), np.sin(maxPhi))]).T)

# a heuristic to make the vector corresponding to hematoxylin first and the
# one corresponding to eosin second
if vMin[0] > vMax[0]:
HE = np.array((vMin[:,0], vMax[:,0])).T
HE = np.array((vMin[:, 0], vMax[:, 0])).T
else:
HE = np.array((vMax[:,0], vMin[:,0])).T
HE = np.array((vMax[:, 0], vMin[:, 0])).T

return HE

Expand All @@ -55,7 +55,7 @@ def __find_concentration(self, OD, HE):
return C

def __compute_matrices(self, I, Io, alpha, beta):
I = I.reshape((-1,3))
I = I.reshape((-1, 3))

OD, ODhat = self.__convert_rgb2od(I, Io=Io, beta=beta)

Expand All @@ -67,7 +67,7 @@ def __compute_matrices(self, I, Io, alpha, beta):
C = self.__find_concentration(OD, HE)

# normalize stain concentrations
maxC = np.array([np.percentile(C[0,:], 99), np.percentile(C[1,:],99)])
maxC = np.array([np.percentile(C[0, :], 99), np.percentile(C[1, :],99)])

return HE, C, maxC

Expand All @@ -81,7 +81,7 @@ def normalize(self, I, Io=240, alpha=1, beta=0.15, stains=True):
''' Normalize staining appearence of H&E stained images

Example use:
see test.py
see example.py

Input:
I: RGB input image
Expand All @@ -97,7 +97,7 @@ def normalize(self, I, Io=240, alpha=1, beta=0.15, stains=True):
Macenko et al., ISBI 2009
'''
h, w, c = I.shape
I = I.reshape((-1,3))
I = I.reshape((-1, 3))

HE, C, maxC = self.__compute_matrices(I, Io, alpha, beta)

Expand All @@ -114,11 +114,11 @@ def normalize(self, I, Io=240, alpha=1, beta=0.15, stains=True):

if stains:
# unmix hematoxylin and eosin
H = np.multiply(Io, np.exp(np.expand_dims(-self.HERef[:,0], axis=1).dot(np.expand_dims(C2[0,:], axis=0))))
H = np.multiply(Io, np.exp(np.expand_dims(-self.HERef[:, 0], axis=1).dot(np.expand_dims(C2[0, :], axis=0))))
H[H > 255] = 255
H = np.reshape(H.T, (h, w, c)).astype(np.uint8)

E = np.multiply(Io, np.exp(np.expand_dims(-self.HERef[:,1], axis=1).dot(np.expand_dims(C2[1,:], axis=0))))
E = np.multiply(Io, np.exp(np.expand_dims(-self.HERef[:, 1], axis=1).dot(np.expand_dims(C2[1, :], axis=0))))
E[E > 255] = 255
E = np.reshape(E.T, (h, w, c)).astype(np.uint8)

Expand Down
7 changes: 3 additions & 4 deletions torchstain/tf/normalizers/macenko.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import numpy as np
import tensorflow.keras.backend as K


"""
Source code ported from: https://github.com/schaugf/HEnorm_python
Original implementation: https://github.com/mitkovetta/staining-normalization
Expand All @@ -14,8 +13,8 @@ def __init__(self):
super().__init__()

self.HERef = tf.constant([[0.5626, 0.2159],
[0.7201, 0.8012],
[0.4062, 0.5581]])
[0.7201, 0.8012],
[0.4062, 0.5581]])
self.maxCRef = tf.constant([1.9705, 1.0308])

def __convert_rgb2od(self, I, Io, beta):
Expand Down Expand Up @@ -78,7 +77,7 @@ def normalize(self, I, Io=240, alpha=1, beta=0.15, stains=True):
''' Normalize staining appearence of H&E stained images

Example use:
see test.py
see example.py

Input:
I: RGB input image: tensor of shape [C, H, W] and type uint8
Expand Down
4 changes: 2 additions & 2 deletions torchstain/torch/normalizers/macenko.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def __convert_rgb2od(self, I, Io, beta):
I = I.permute(1, 2, 0)

# calculate optical density
OD = -torch.log((I.reshape((-1, I.shape[-1])).float() + 1)/Io)
OD = -torch.log((I.reshape((-1, I.shape[-1])).float() + 1) / Io)

# remove transparent pixels
ODhat = OD[~torch.any(OD < beta, dim=1)]
Expand Down Expand Up @@ -79,7 +79,7 @@ def normalize(self, I, Io=240, alpha=1, beta=0.15, stains=True):
''' Normalize staining appearence of H&E stained images

Example use:
see test.py
see example.py

Input:
I: RGB input image: tensor of shape [C, H, W] and type uint8
Expand Down
4 changes: 2 additions & 2 deletions torchstain/torch/utils/rgb2lab.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

# constant conversion matrices between color spaces: https://gist.github.com/bikz05/6fd21c812ef6ebac66e1
_rgb2xyz = torch.tensor([[0.412453, 0.357580, 0.180423],
[0.212671, 0.715160, 0.072169],
[0.019334, 0.119193, 0.950227]])
[0.212671, 0.715160, 0.072169],
[0.019334, 0.119193, 0.950227]])

_white = torch.tensor([0.95047, 1., 1.08883])

Expand Down