Skip to content

Commit e044373

Browse files
Merge pull request #16 from EIDOSLAB/backends
Backends
2 parents 5e88229 + 3cb9980 commit e044373

32 files changed

+253
-110
lines changed

.github/workflows/tests.yml

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
python-version: 3.6
2020

2121
- name: Install dependencies
22-
run: pip install wheel setuptools torch tensorflow numpy
22+
run: pip install wheel setuptools
2323

2424
- name: Build wheel
2525
run: python setup.py bdist_wheel
@@ -46,20 +46,82 @@ jobs:
4646
with:
4747
python-version: ${{ matrix.python-version }}
4848

49-
- name: Install dependencies
50-
run: pip install wheel setuptools torch tensorflow numpy
49+
- name: Download artifact
50+
uses: actions/download-artifact@master
51+
with:
52+
name: "Python wheel"
53+
54+
- name: Install deps and wheel
55+
run: |
56+
pip install tensorflow torch
57+
pip install --find-links=${{github.workspace}} torchstain
5158
5259
- name: Install test dependencies
53-
run: pip install opencv-python matplotlib torchvision scikit-image pytest
60+
run: pip install opencv-python torchvision scikit-image pytest
61+
62+
- name: Run tests
63+
run: |
64+
pytest -v tests/test_utils.py
65+
pytest -v tests/test_normalizers.py
66+
67+
test-tf:
68+
needs: build
69+
runs-on: ${{ matrix.os }}
70+
strategy:
71+
matrix:
72+
os: [ windows-2019, ubuntu-18.04, macos-11 ]
73+
python-version: [ 3.7 ]
74+
75+
steps:
76+
- uses: actions/checkout@v1
77+
- name: Set up Python ${{ matrix.python-version }}
78+
uses: actions/setup-python@v2
79+
with:
80+
python-version: ${{ matrix.python-version }}
5481

5582
- name: Download artifact
5683
uses: actions/download-artifact@master
5784
with:
5885
name: "Python wheel"
5986

60-
- name: Install wheel
61-
run: pip install --find-links=${{github.workspace}} torchstain
87+
- name: Install deps and wheel
88+
run: |
89+
pip install tensorflow
90+
pip install --find-links=${{github.workspace}} torchstain
91+
92+
- name: Install test dependencies
93+
run: pip install opencv-python scikit-image pytest
6294

6395
- name: Run tests
96+
run: pytest -v tests/test_tf_normalizer.py
97+
98+
test-torch:
99+
needs: build
100+
runs-on: ${{ matrix.os }}
101+
strategy:
102+
matrix:
103+
os: [ windows-2019, ubuntu-18.04, macos-11 ]
104+
python-version: [ 3.7 ]
105+
106+
steps:
107+
- uses: actions/checkout@v1
108+
- name: Set up Python ${{ matrix.python-version }}
109+
uses: actions/setup-python@v2
110+
with:
111+
python-version: ${{ matrix.python-version }}
112+
113+
- name: Download artifact
114+
uses: actions/download-artifact@master
115+
with:
116+
name: "Python wheel"
117+
118+
- name: Install deps and wheel wheel
64119
run: |
65-
pytest -v tests
120+
pip install torch
121+
pip install --find-links=${{github.workspace}} torchstain
122+
123+
- name: Install test dependencies
124+
run: pip install opencv-python torchvision scikit-image pytest
125+
126+
- name: Run tests
127+
run: pytest -v tests/test_torch_normalizer.py

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
[![tests](https://github.com/EIDOSLAB/torchstain/workflows/tests/badge.svg)](https://github.com/EIDOSLAB/torchstain/actions)
55
[![Pip Downloads](https://img.shields.io/pypi/dm/torchstain?label=pip%20downloads&logo=python)](https://pypi.org/project/torchstain/)
66

7-
Pytorch-compatible normalization tools for histopathological images.
7+
GPU-accelerated stain normalization tools for histopathological images. Compatible with PyTorch, TensorFlow, and Numpy.
88
Normalization algorithms currently implemented:
99

1010
- Macenko et al. [\[1\]](#reference) (ported from [numpy implementation](https://github.com/schaugf/HEnorm_python))
1111

1212
## Installation
1313

1414
```bash
15-
pip3 install torchstain
15+
pip install torchstain
1616
```
1717

1818
## Example Usage

example.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,18 @@
1010
target = cv2.resize(cv2.cvtColor(cv2.imread("./data/target.png"), cv2.COLOR_BGR2RGB), (size, size))
1111
to_transform = cv2.resize(cv2.cvtColor(cv2.imread("./data/source.png"), cv2.COLOR_BGR2RGB), (size, size))
1212

13-
normalizer = torchstain.MacenkoNormalizer(backend='numpy')
13+
normalizer = torchstain.normalizers.MacenkoNormalizer(backend='numpy')
1414
normalizer.fit(target)
1515

1616
T = transforms.Compose([
1717
transforms.ToTensor(),
1818
transforms.Lambda(lambda x: x*255)
1919
])
2020

21-
torch_normalizer = torchstain.MacenkoNormalizer(backend='torch')
21+
torch_normalizer = torchstain.normalizers.MacenkoNormalizer(backend='torch')
2222
torch_normalizer.fit(T(target))
2323

24-
tf_normalizer = torchstain.MacenkoNormalizer(backend='tensorflow')
24+
tf_normalizer = torchstain.normalizers.MacenkoNormalizer(backend='tensorflow')
2525
tf_normalizer.fit(T(target))
2626

2727
t_to_transform = T(to_transform)

setup.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import pathlib
2-
from setuptools import setup, find_packages
2+
from setuptools import setup, find_packages, find_namespace_packages
33

44
HERE = pathlib.Path(__file__).parent
55
README = (HERE / "README.md").read_text()
@@ -17,10 +17,12 @@
1717
packages=find_packages(exclude=('tests')),
1818
zip_safe=False,
1919
install_requires=[
20-
'torch',
21-
'numpy',
22-
'tensorflow'
20+
'numpy'
2321
],
22+
extras_require={
23+
"tf": ["tensorflow"],
24+
"torch": ["torch"],
25+
},
2426
classifiers=[
2527
'Development Status :: 4 - Beta',
2628
'Intended Audience :: Developers',

tests/test_normalizers.py

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,45 @@
11
import os
22
import cv2
3-
import matplotlib.pyplot as plt
43
import torchstain
54
import torch
65
from torchvision import transforms
76
import time
87
from skimage.metrics import structural_similarity as ssim
98
import numpy as np
109

11-
12-
size = 1024
13-
curr_file_path = os.path.dirname(os.path.realpath(__file__))
14-
target = cv2.resize(cv2.cvtColor(cv2.imread(os.path.join(curr_file_path, "../data/target.png")), cv2.COLOR_BGR2RGB), (size, size))
15-
to_transform = cv2.resize(cv2.cvtColor(cv2.imread(os.path.join(curr_file_path, "../data/source.png")), cv2.COLOR_BGR2RGB), (size, size))
16-
17-
# setup preprocessing and preprocess image to be normalized
18-
T = transforms.Compose([
19-
transforms.ToTensor(),
20-
transforms.Lambda(lambda x: x*255)
21-
])
22-
t_to_transform = T(to_transform)
23-
24-
# initialize normalizers for each backend and fit to target image
25-
normalizer = torchstain.MacenkoNormalizer(backend='numpy')
26-
normalizer.fit(target)
27-
28-
torch_normalizer = torchstain.MacenkoNormalizer(backend='torch')
29-
torch_normalizer.fit(T(target))
30-
31-
tf_normalizer = torchstain.MacenkoNormalizer(backend='tensorflow')
32-
tf_normalizer.fit(T(target))
33-
34-
# transform
35-
result_numpy, _, _ = normalizer.normalize(I=to_transform, stains=True)
36-
result_torch, _, _ = torch_normalizer.normalize(I=t_to_transform, stains=True)
37-
result_tf, _, _ = tf_normalizer.normalize(I=t_to_transform, stains=True)
38-
39-
# convert to numpy and set dtype
40-
result_numpy = result_numpy.astype("float32")
41-
result_torch = result_torch.numpy().astype("float32")
42-
result_tf = result_tf.numpy().astype("float32")
43-
44-
# assess whether the normalized images are identical across backends
45-
np.testing.assert_almost_equal(ssim(result_numpy.flatten(), result_torch.flatten()), 1.0, decimal=4, verbose=True)
46-
np.testing.assert_almost_equal(ssim(result_numpy.flatten(), result_tf.flatten()), 1.0, decimal=4, verbose=True)
10+
def test_normalize_all():
11+
size = 1024
12+
curr_file_path = os.path.dirname(os.path.realpath(__file__))
13+
target = cv2.resize(cv2.cvtColor(cv2.imread(os.path.join(curr_file_path, "../data/target.png")), cv2.COLOR_BGR2RGB), (size, size))
14+
to_transform = cv2.resize(cv2.cvtColor(cv2.imread(os.path.join(curr_file_path, "../data/source.png")), cv2.COLOR_BGR2RGB), (size, size))
15+
16+
# setup preprocessing and preprocess image to be normalized
17+
T = transforms.Compose([
18+
transforms.ToTensor(),
19+
transforms.Lambda(lambda x: x*255)
20+
])
21+
t_to_transform = T(to_transform)
22+
23+
# initialize normalizers for each backend and fit to target image
24+
normalizer = torchstain.normalizers.MacenkoNormalizer(backend='numpy')
25+
normalizer.fit(target)
26+
27+
torch_normalizer = torchstain.normalizers.MacenkoNormalizer(backend='torch')
28+
torch_normalizer.fit(T(target))
29+
30+
tf_normalizer = torchstain.normalizers.MacenkoNormalizer(backend='tensorflow')
31+
tf_normalizer.fit(T(target))
32+
33+
# transform
34+
result_numpy, _, _ = normalizer.normalize(I=to_transform, stains=True)
35+
result_torch, _, _ = torch_normalizer.normalize(I=t_to_transform, stains=True)
36+
result_tf, _, _ = tf_normalizer.normalize(I=t_to_transform, stains=True)
37+
38+
# convert to numpy and set dtype
39+
result_numpy = result_numpy.astype("float32")
40+
result_torch = result_torch.numpy().astype("float32")
41+
result_tf = result_tf.numpy().astype("float32")
42+
43+
# assess whether the normalized images are identical across backends
44+
np.testing.assert_almost_equal(ssim(result_numpy.flatten(), result_torch.flatten()), 1.0, decimal=4, verbose=True)
45+
np.testing.assert_almost_equal(ssim(result_numpy.flatten(), result_tf.flatten()), 1.0, decimal=4, verbose=True)

tests/test_tf_normalizer.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import os
2+
import cv2
3+
import torchstain
4+
import tensorflow as tf
5+
import time
6+
from skimage.metrics import structural_similarity as ssim
7+
import numpy as np
8+
9+
def test_normalize_tf():
10+
size = 1024
11+
curr_file_path = os.path.dirname(os.path.realpath(__file__))
12+
target = cv2.resize(cv2.cvtColor(cv2.imread(os.path.join(curr_file_path, "../data/target.png")), cv2.COLOR_BGR2RGB), (size, size))
13+
to_transform = cv2.resize(cv2.cvtColor(cv2.imread(os.path.join(curr_file_path, "../data/source.png")), cv2.COLOR_BGR2RGB), (size, size))
14+
15+
# setup preprocessing and preprocess image to be normalized
16+
T = lambda x: tf.convert_to_tensor(np.moveaxis(x, -1, 0).astype("float32")) # * 255
17+
t_to_transform = T(to_transform)
18+
19+
# initialize normalizers for each backend and fit to target image
20+
normalizer = torchstain.normalizers.MacenkoNormalizer(backend='numpy')
21+
normalizer.fit(target)
22+
23+
tf_normalizer = torchstain.normalizers.MacenkoNormalizer(backend='tensorflow')
24+
tf_normalizer.fit(T(target))
25+
26+
# transform
27+
result_numpy, _, _ = normalizer.normalize(I=to_transform, stains=True)
28+
result_tf, _, _ = tf_normalizer.normalize(I=t_to_transform, stains=True)
29+
30+
# convert to numpy and set dtype
31+
result_numpy = result_numpy.astype("float32")
32+
result_tf = result_tf.numpy().astype("float32")
33+
34+
# assess whether the normalized images are identical across backends
35+
np.testing.assert_almost_equal(ssim(result_numpy.flatten(), result_tf.flatten()), 1.0, decimal=4, verbose=True)

tests/test_torch_normalizer.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import os
2+
import cv2
3+
import torchstain
4+
import torch
5+
from torchvision import transforms
6+
import time
7+
from skimage.metrics import structural_similarity as ssim
8+
import numpy as np
9+
10+
def test_normalize_torch():
11+
size = 1024
12+
curr_file_path = os.path.dirname(os.path.realpath(__file__))
13+
target = cv2.resize(cv2.cvtColor(cv2.imread(os.path.join(curr_file_path, "../data/target.png")), cv2.COLOR_BGR2RGB), (size, size))
14+
to_transform = cv2.resize(cv2.cvtColor(cv2.imread(os.path.join(curr_file_path, "../data/source.png")), cv2.COLOR_BGR2RGB), (size, size))
15+
16+
# setup preprocessing and preprocess image to be normalized
17+
T = transforms.Compose([
18+
transforms.ToTensor(),
19+
transforms.Lambda(lambda x: x*255)
20+
])
21+
t_to_transform = T(to_transform)
22+
23+
# initialize normalizers for each backend and fit to target image
24+
normalizer = torchstain.normalizers.MacenkoNormalizer(backend='numpy')
25+
normalizer.fit(target)
26+
27+
torch_normalizer = torchstain.normalizers.MacenkoNormalizer(backend='torch')
28+
torch_normalizer.fit(T(target))
29+
30+
# transform
31+
result_numpy, _, _ = normalizer.normalize(I=to_transform, stains=True)
32+
result_torch, _, _ = torch_normalizer.normalize(I=t_to_transform, stains=True)
33+
34+
# convert to numpy and set dtype
35+
result_numpy = result_numpy.astype("float32")
36+
result_torch = result_torch.numpy().astype("float32")
37+
38+
# assess whether the normalized images are identical across backends
39+
np.testing.assert_almost_equal(ssim(result_numpy.flatten(), result_torch.flatten()), 1.0, decimal=4, verbose=True)

tests/test_utils.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import torch
22
import torchstain
3+
import torchstain.torch
34
import numpy as np
45

5-
66
def test_cov():
77
x = np.random.randn(10, 10)
88
cov_np = np.cov(x)
9-
cov_t = torchstain.utils.cov(torch.tensor(x))
9+
cov_t = torchstain.torch.utils.cov(torch.tensor(x))
1010

1111
np.testing.assert_almost_equal(cov_np, cov_t.numpy())
1212

1313
def test_percentile():
1414
x = np.random.randn(10, 10)
1515
p = 20
16-
p_np = np.percentile(x, p, method='nearest')
17-
p_t = torchstain.utils.percentile(torch.tensor(x), p)
16+
p_np = np.percentile(x, p, interpolation='nearest')
17+
p_t = torchstain.torch.utils.percentile(torch.tensor(x), p)
1818

1919
np.testing.assert_almost_equal(p_np, p_t)

torchstain/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
__version__ = '1.1.0'
22

3-
from torchstain.normalizers.macenko_normalizer import MacenkoNormalizer
3+
from torchstain.base import normalizers

torchstain/base/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from torchstain.base import normalizers

0 commit comments

Comments
 (0)