Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
89eabfb
started making submodules for tf and torch
andreped Aug 5, 2022
631e3ce
made subpackages
andreped Aug 5, 2022
a1ea833
updated tests to work with new structure
andreped Aug 5, 2022
537a6aa
keep imports in each subpackage separate
andreped Aug 5, 2022
a520b10
separation complete, linking with inits work
andreped Aug 5, 2022
3af9479
bug fix in tests, updated example
andreped Aug 5, 2022
e3e6493
test if backend install selection works with CI
andreped Aug 5, 2022
9de07ec
major rework on backend logic - seems to work locally
andreped Aug 5, 2022
7eef272
changed pip install in CI
andreped Aug 5, 2022
ae01195
find-links not compatible with [] pattern
andreped Aug 5, 2022
e8baefe
typo T not T1 lambda, [] pip stuff
andreped Aug 5, 2022
23ef481
from module in test
andreped Aug 5, 2022
20556a2
debugging - forgot to wrap test in def?
andreped Aug 5, 2022
d0c7f6a
reduced python version for tf test
andreped Aug 5, 2022
4448536
Merge pull request #13 from andreped/backends
carloalbertobarbano Aug 5, 2022
cd9d1cc
remove unnecessary import
carloalbertobarbano Aug 5, 2022
85e0d32
remove unnecessary lines
carloalbertobarbano Aug 5, 2022
8d1438a
add numpy module
carloalbertobarbano Aug 5, 2022
05500a9
move numpy_macenko_normalizer to numpy module
carloalbertobarbano Aug 5, 2022
45f3df2
simplify normalizers filename
carloalbertobarbano Aug 5, 2022
0f2a5c9
fix for simplifying imports, some refactoring
andreped Aug 5, 2022
26b2f17
Updated README given proper TF support
andreped Aug 5, 2022
ac632d7
renamed 'base' to 'core'. Refactored
andreped Aug 5, 2022
29dc815
refactor imports
carloalbertobarbano Aug 5, 2022
bf00cfc
update tests
carloalbertobarbano Aug 5, 2022
2e5d8cc
keep base package (renamed from core); resolve conflicts
carloalbertobarbano Aug 8, 2022
4753b7d
Merge branch 'andreped-backends' into backends
carloalbertobarbano Aug 8, 2022
2de9b5e
rename torch_backend and tf_backend to torch and tf
carloalbertobarbano Aug 8, 2022
9aa9021
fix import typo
carloalbertobarbano Aug 8, 2022
347ebef
fix class path
carloalbertobarbano Aug 8, 2022
b797388
fix import (prevent tf from being loaded)
carloalbertobarbano Aug 8, 2022
6b05312
fix typo
carloalbertobarbano Aug 8, 2022
dcac28b
fix conflicts
carloalbertobarbano Aug 8, 2022
3cb9980
use interpolation
carloalbertobarbano Aug 8, 2022
e044373
Merge pull request #16 from EIDOSLAB/backends
carloalbertobarbano Aug 8, 2022
0cf63f4
update README
carloalbertobarbano Aug 8, 2022
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
76 changes: 69 additions & 7 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
python-version: 3.6

- name: Install dependencies
run: pip install wheel setuptools torch tensorflow numpy
run: pip install wheel setuptools

- name: Build wheel
run: python setup.py bdist_wheel
Expand All @@ -46,20 +46,82 @@ jobs:
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: pip install wheel setuptools torch tensorflow numpy
- name: Download artifact
uses: actions/download-artifact@master
with:
name: "Python wheel"

- name: Install deps and wheel
run: |
pip install tensorflow torch
pip install --find-links=${{github.workspace}} torchstain

- name: Install test dependencies
run: pip install opencv-python matplotlib torchvision scikit-image pytest
run: pip install opencv-python torchvision scikit-image pytest

- name: Run tests
run: |
pytest -v tests/test_utils.py
pytest -v tests/test_normalizers.py

test-tf:
needs: build
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ windows-2019, ubuntu-18.04, macos-11 ]
python-version: [ 3.7 ]

steps:
- uses: actions/checkout@v1
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}

- name: Download artifact
uses: actions/download-artifact@master
with:
name: "Python wheel"

- name: Install wheel
run: pip install --find-links=${{github.workspace}} torchstain
- name: Install deps and wheel
run: |
pip install tensorflow
pip install --find-links=${{github.workspace}} torchstain

- name: Install test dependencies
run: pip install opencv-python scikit-image pytest

- name: Run tests
run: pytest -v tests/test_tf_normalizer.py

test-torch:
needs: build
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ windows-2019, ubuntu-18.04, macos-11 ]
python-version: [ 3.7 ]

steps:
- uses: actions/checkout@v1
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}

- name: Download artifact
uses: actions/download-artifact@master
with:
name: "Python wheel"

- name: Install deps and wheel wheel
run: |
pytest -v tests
pip install torch
pip install --find-links=${{github.workspace}} torchstain

- name: Install test dependencies
run: pip install opencv-python torchvision scikit-image pytest

- name: Run tests
run: pytest -v tests/test_torch_normalizer.py
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
[![tests](https://github.com/EIDOSLAB/torchstain/workflows/tests/badge.svg)](https://github.com/EIDOSLAB/torchstain/actions)
[![Pip Downloads](https://img.shields.io/pypi/dm/torchstain?label=pip%20downloads&logo=python)](https://pypi.org/project/torchstain/)

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

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

## Installation

```bash
pip3 install torchstain
pip install torchstain
```

## Example Usage
Expand All @@ -31,7 +31,7 @@ T = transforms.Compose([
transforms.Lambda(lambda x: x*255)
])

torch_normalizer = torchstain.MacenkoNormalizer(backend='torch')
torch_normalizer = torchstain.normalizers.MacenkoNormalizer(backend='torch')
torch_normalizer.fit(T(target))

t_to_transform = T(to_transform)
Expand Down
6 changes: 3 additions & 3 deletions example.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@
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))

normalizer = torchstain.MacenkoNormalizer(backend='numpy')
normalizer = torchstain.normalizers.MacenkoNormalizer(backend='numpy')
normalizer.fit(target)

T = transforms.Compose([
transforms.ToTensor(),
transforms.Lambda(lambda x: x*255)
])

torch_normalizer = torchstain.MacenkoNormalizer(backend='torch')
torch_normalizer = torchstain.normalizers.MacenkoNormalizer(backend='torch')
torch_normalizer.fit(T(target))

tf_normalizer = torchstain.MacenkoNormalizer(backend='tensorflow')
tf_normalizer = torchstain.normalizers.MacenkoNormalizer(backend='tensorflow')
tf_normalizer.fit(T(target))

t_to_transform = T(to_transform)
Expand Down
10 changes: 6 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import pathlib
from setuptools import setup, find_packages
from setuptools import setup, find_packages, find_namespace_packages

HERE = pathlib.Path(__file__).parent
README = (HERE / "README.md").read_text()
Expand All @@ -17,10 +17,12 @@
packages=find_packages(exclude=('tests')),
zip_safe=False,
install_requires=[
'torch',
'numpy',
'tensorflow'
'numpy'
],
extras_require={
"tf": ["tensorflow"],
"torch": ["torch"],
},
classifiers=[
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
Expand Down
73 changes: 36 additions & 37 deletions tests/test_normalizers.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,45 @@
import os
import cv2
import matplotlib.pyplot as plt
import torchstain
import torch
from torchvision import transforms
import time
from skimage.metrics import structural_similarity as ssim
import numpy as np


size = 1024
curr_file_path = os.path.dirname(os.path.realpath(__file__))
target = cv2.resize(cv2.cvtColor(cv2.imread(os.path.join(curr_file_path, "../data/target.png")), cv2.COLOR_BGR2RGB), (size, size))
to_transform = cv2.resize(cv2.cvtColor(cv2.imread(os.path.join(curr_file_path, "../data/source.png")), cv2.COLOR_BGR2RGB), (size, size))

# setup preprocessing and preprocess image to be normalized
T = transforms.Compose([
transforms.ToTensor(),
transforms.Lambda(lambda x: x*255)
])
t_to_transform = T(to_transform)

# initialize normalizers for each backend and fit to target image
normalizer = torchstain.MacenkoNormalizer(backend='numpy')
normalizer.fit(target)

torch_normalizer = torchstain.MacenkoNormalizer(backend='torch')
torch_normalizer.fit(T(target))

tf_normalizer = torchstain.MacenkoNormalizer(backend='tensorflow')
tf_normalizer.fit(T(target))

# transform
result_numpy, _, _ = normalizer.normalize(I=to_transform, stains=True)
result_torch, _, _ = torch_normalizer.normalize(I=t_to_transform, stains=True)
result_tf, _, _ = tf_normalizer.normalize(I=t_to_transform, stains=True)

# convert to numpy and set dtype
result_numpy = result_numpy.astype("float32")
result_torch = result_torch.numpy().astype("float32")
result_tf = result_tf.numpy().astype("float32")

# assess whether the normalized images are identical across backends
np.testing.assert_almost_equal(ssim(result_numpy.flatten(), result_torch.flatten()), 1.0, decimal=4, verbose=True)
np.testing.assert_almost_equal(ssim(result_numpy.flatten(), result_tf.flatten()), 1.0, decimal=4, verbose=True)
def test_normalize_all():
size = 1024
curr_file_path = os.path.dirname(os.path.realpath(__file__))
target = cv2.resize(cv2.cvtColor(cv2.imread(os.path.join(curr_file_path, "../data/target.png")), cv2.COLOR_BGR2RGB), (size, size))
to_transform = cv2.resize(cv2.cvtColor(cv2.imread(os.path.join(curr_file_path, "../data/source.png")), cv2.COLOR_BGR2RGB), (size, size))

# setup preprocessing and preprocess image to be normalized
T = transforms.Compose([
transforms.ToTensor(),
transforms.Lambda(lambda x: x*255)
])
t_to_transform = T(to_transform)

# initialize normalizers for each backend and fit to target image
normalizer = torchstain.normalizers.MacenkoNormalizer(backend='numpy')
normalizer.fit(target)

torch_normalizer = torchstain.normalizers.MacenkoNormalizer(backend='torch')
torch_normalizer.fit(T(target))

tf_normalizer = torchstain.normalizers.MacenkoNormalizer(backend='tensorflow')
tf_normalizer.fit(T(target))

# transform
result_numpy, _, _ = normalizer.normalize(I=to_transform, stains=True)
result_torch, _, _ = torch_normalizer.normalize(I=t_to_transform, stains=True)
result_tf, _, _ = tf_normalizer.normalize(I=t_to_transform, stains=True)

# convert to numpy and set dtype
result_numpy = result_numpy.astype("float32")
result_torch = result_torch.numpy().astype("float32")
result_tf = result_tf.numpy().astype("float32")

# assess whether the normalized images are identical across backends
np.testing.assert_almost_equal(ssim(result_numpy.flatten(), result_torch.flatten()), 1.0, decimal=4, verbose=True)
np.testing.assert_almost_equal(ssim(result_numpy.flatten(), result_tf.flatten()), 1.0, decimal=4, verbose=True)
35 changes: 35 additions & 0 deletions tests/test_tf_normalizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import os
import cv2
import torchstain
import tensorflow as tf
import time
from skimage.metrics import structural_similarity as ssim
import numpy as np

def test_normalize_tf():
size = 1024
curr_file_path = os.path.dirname(os.path.realpath(__file__))
target = cv2.resize(cv2.cvtColor(cv2.imread(os.path.join(curr_file_path, "../data/target.png")), cv2.COLOR_BGR2RGB), (size, size))
to_transform = cv2.resize(cv2.cvtColor(cv2.imread(os.path.join(curr_file_path, "../data/source.png")), cv2.COLOR_BGR2RGB), (size, size))

# setup preprocessing and preprocess image to be normalized
T = lambda x: tf.convert_to_tensor(np.moveaxis(x, -1, 0).astype("float32")) # * 255
t_to_transform = T(to_transform)

# initialize normalizers for each backend and fit to target image
normalizer = torchstain.normalizers.MacenkoNormalizer(backend='numpy')
normalizer.fit(target)

tf_normalizer = torchstain.normalizers.MacenkoNormalizer(backend='tensorflow')
tf_normalizer.fit(T(target))

# transform
result_numpy, _, _ = normalizer.normalize(I=to_transform, stains=True)
result_tf, _, _ = tf_normalizer.normalize(I=t_to_transform, stains=True)

# convert to numpy and set dtype
result_numpy = result_numpy.astype("float32")
result_tf = result_tf.numpy().astype("float32")

# assess whether the normalized images are identical across backends
np.testing.assert_almost_equal(ssim(result_numpy.flatten(), result_tf.flatten()), 1.0, decimal=4, verbose=True)
39 changes: 39 additions & 0 deletions tests/test_torch_normalizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import os
import cv2
import torchstain
import torch
from torchvision import transforms
import time
from skimage.metrics import structural_similarity as ssim
import numpy as np

def test_normalize_torch():
size = 1024
curr_file_path = os.path.dirname(os.path.realpath(__file__))
target = cv2.resize(cv2.cvtColor(cv2.imread(os.path.join(curr_file_path, "../data/target.png")), cv2.COLOR_BGR2RGB), (size, size))
to_transform = cv2.resize(cv2.cvtColor(cv2.imread(os.path.join(curr_file_path, "../data/source.png")), cv2.COLOR_BGR2RGB), (size, size))

# setup preprocessing and preprocess image to be normalized
T = transforms.Compose([
transforms.ToTensor(),
transforms.Lambda(lambda x: x*255)
])
t_to_transform = T(to_transform)

# initialize normalizers for each backend and fit to target image
normalizer = torchstain.normalizers.MacenkoNormalizer(backend='numpy')
normalizer.fit(target)

torch_normalizer = torchstain.normalizers.MacenkoNormalizer(backend='torch')
torch_normalizer.fit(T(target))

# transform
result_numpy, _, _ = normalizer.normalize(I=to_transform, stains=True)
result_torch, _, _ = torch_normalizer.normalize(I=t_to_transform, stains=True)

# convert to numpy and set dtype
result_numpy = result_numpy.astype("float32")
result_torch = result_torch.numpy().astype("float32")

# assess whether the normalized images are identical across backends
np.testing.assert_almost_equal(ssim(result_numpy.flatten(), result_torch.flatten()), 1.0, decimal=4, verbose=True)
8 changes: 4 additions & 4 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import torch
import torchstain
import torchstain.torch
import numpy as np


def test_cov():
x = np.random.randn(10, 10)
cov_np = np.cov(x)
cov_t = torchstain.utils.cov(torch.tensor(x))
cov_t = torchstain.torch.utils.cov(torch.tensor(x))

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

def test_percentile():
x = np.random.randn(10, 10)
p = 20
p_np = np.percentile(x, p, method='nearest')
p_t = torchstain.utils.percentile(torch.tensor(x), p)
p_np = np.percentile(x, p, interpolation='nearest')
p_t = torchstain.torch.utils.percentile(torch.tensor(x), p)

np.testing.assert_almost_equal(p_np, p_t)
2 changes: 1 addition & 1 deletion torchstain/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__version__ = '1.1.0'

from torchstain.normalizers.macenko_normalizer import MacenkoNormalizer
from torchstain.base import normalizers
1 change: 1 addition & 0 deletions torchstain/base/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from torchstain.base import normalizers
2 changes: 2 additions & 0 deletions torchstain/base/normalizers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .he_normalizer import HENormalizer
from .macenko import MacenkoNormalizer
12 changes: 12 additions & 0 deletions torchstain/base/normalizers/macenko.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
def MacenkoNormalizer(backend='torch'):
if backend == 'numpy':
from torchstain.numpy.normalizers import NumpyMacenkoNormalizer
return NumpyMacenkoNormalizer()
elif backend == "torch":
from torchstain.torch.normalizers.macenko import TorchMacenkoNormalizer
return TorchMacenkoNormalizer()
elif backend == "tensorflow":
from torchstain.tf.normalizers.macenko import TensorFlowMacenkoNormalizer
return TensorFlowMacenkoNormalizer()
else:
raise Exception(f'Unknown backend {backend}')
1 change: 0 additions & 1 deletion torchstain/normalizers/__init__.py

This file was deleted.

13 changes: 0 additions & 13 deletions torchstain/normalizers/macenko_normalizer.py

This file was deleted.

Empty file added torchstain/numpy/__init__.py
Empty file.
1 change: 1 addition & 0 deletions torchstain/numpy/normalizers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .macenko import NumpyMacenkoNormalizer
Loading